mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-12 00:19:36 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71216bc247 | |||
| e07ac59aee | |||
| baa30bfba9 | |||
| f210f51e17 | |||
| b09821a0b1 | |||
| f835ad4e42 | |||
| 659e27bbf6 | |||
| 7726be1aed | |||
| 2e1ca3584d | |||
| 54d24ff59d | |||
| a1742e9aa5 | |||
| cb385d1595 | |||
| 1ebe3c4d65 | |||
| 5260c34f8e | |||
| 9437aebabe | |||
| 68526ddfd4 | |||
| 9f9e36efa9 | |||
| cdd2a2a2c6 | |||
| 5b171b2317 | |||
| 427ed49d62 | |||
| 9150b25227 | |||
| 8b8a389cc3 | |||
| 839e211790 | |||
| ae9a44033b | |||
| dc9e0906fd | |||
| 016374722d | |||
| 7e503a70fd | |||
| 75270008dc | |||
| 3e0dffb898 | |||
| 3eed8b24c4 | |||
| 71589f93f1 | |||
| 50fde94e13 | |||
| 8bf7a279a5 | |||
| 08cc0f9942 | |||
| 771724bfee | |||
| f69b03d12c | |||
| 82b0004cc6 | |||
| 4a2ce95dfa | |||
| 53933f218b | |||
| 306139fcef | |||
| ab703d331e | |||
| a2986dfc1a | |||
| cb862ae4b1 | |||
| e28da35ca4 | |||
| 8bdc151c7e | |||
| dfd3b02014 | |||
| 6f6d1afcd4 | |||
| a24e6c8c4d | |||
| d141fe3c04 | |||
| 162c4acd7c | |||
| fde78a4ece | |||
| b1ffffd545 | |||
| 977554dd49 | |||
| 4ca8ce5751 | |||
| de55444012 | |||
| 3ec1c37f23 | |||
| eb9821dc3f | |||
| 3467cc5be0 | |||
| b10a28bf52 | |||
| 1b1656c4b5 | |||
| b29733e435 | |||
| f8a7b8ad83 | |||
| 43c62d85dd | |||
| 43b7ab7a77 | |||
| d0c883a418 | |||
| 33fc370ff5 | |||
| 0a1fb50906 | |||
| f348c07b60 | |||
| 60b2f217d0 | |||
| f7babe93d9 |
@@ -1,4 +1,4 @@
|
||||
name: Test and release
|
||||
name: Autoupdate go.mod and go.sum
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -34,15 +34,16 @@ jobs:
|
||||
# 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]
|
||||
|
||||
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
|
||||
@@ -62,12 +63,11 @@ jobs:
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
CI_RUN=${CI} make test
|
||||
git config --global --add safe.directory /__w/graphql-monitoring-proxy/graphql-monitoring-proxy
|
||||
|
||||
# if go.mod or go.sum have changed then commit the changes to the repository
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git add go.mod go.sum
|
||||
git commit -m "Update go.mod and go.sum"
|
||||
git push
|
||||
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"
|
||||
|
||||
@@ -15,6 +15,12 @@ on:
|
||||
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.
|
||||
@@ -78,7 +84,26 @@ jobs:
|
||||
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"
|
||||
|
||||
@@ -10,6 +10,15 @@ on:
|
||||
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
|
||||
@@ -18,3 +27,46 @@ jobs:
|
||||
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"
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
graphql-proxy
|
||||
test.sh
|
||||
banned.json*
|
||||
banned.json*
|
||||
dist/
|
||||
@@ -1,4 +1,6 @@
|
||||
CI_RUN?=false
|
||||
TIMESTAMP := $(shell date +%Y%m%d-%H%M%S)
|
||||
|
||||
# ADDITIONAL_BUILD_FLAGS=""
|
||||
|
||||
# ifeq ($(CI_RUN), true)
|
||||
@@ -32,3 +34,25 @@ all: test-packages test
|
||||
update: ## update dependencies
|
||||
@go get -u -v ./...
|
||||
@go mod tidy -v
|
||||
|
||||
.PHONY: build-amd64
|
||||
build-amd64: ## build the Linux AMD64 binary
|
||||
GOOS=linux GOARCH=amd64 go build -o graphql-proxy-amd64 *.go
|
||||
|
||||
.PHONY: build-arm64
|
||||
build-arm64: ## build the Linux ARM64 binary
|
||||
GOOS=linux GOARCH=arm64 go build -o graphql-proxy-arm64 *.go
|
||||
|
||||
.PHONY: build-all
|
||||
build-all: build-amd64 build-arm64 ## build both AMD64 and ARM64 binaries
|
||||
|
||||
.PHONY: docker
|
||||
docker: build-all ## build multi-arch (AMD64 and ARM64) docker image
|
||||
@mkdir -p dist
|
||||
@mv graphql-proxy-amd64 dist/bot-linux-amd64
|
||||
@mv graphql-proxy-arm64 dist/bot-linux-arm64
|
||||
@docker buildx build --push \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t ghcr.io/lukaszraczylo/graphql-monitoring-proxy:local-test-build-$(TIMESTAMP) \
|
||||
.
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
Creates a passthrough proxy to a graphql endpoint(s), allowing you to analyse the queries and responses, producing the Prometheus metrics at a fraction of the cost - because, as we know - $0 is a fair price.
|
||||
|
||||
This project is in active use by [telegram-bot.app](https://telegram-bot.app), and was tested with 30k queries per second on a single instance, consuming 10 MB of RAM and 0.1% CPU.
|
||||
This project is in active use by [telegram-bot.app](https://telegram-bot.app), and was tested with 30k queries per second on a single instance, consuming 10 MB of RAM and 0.1% CPU. [Benchmarks](https://lukaszraczylo.github.io/graphql-monitoring-proxy/dev/bench/) are available.
|
||||
|
||||

|
||||
|
||||
- [graphql monitoring proxy](#graphql-monitoring-proxy)
|
||||
- [Why this project exists](#why-this-project-exists)
|
||||
- [Important releases](#important-releases)
|
||||
- [How to deploy](#how-to-deploy)
|
||||
- [Note on websocket support](#note-on-websocket-support)
|
||||
- [Endpoints](#endpoints)
|
||||
@@ -28,7 +29,6 @@ This project is in active use by [telegram-bot.app](https://telegram-bot.app), a
|
||||
- [Cache operations](#cache-operations)
|
||||
- [General](#general)
|
||||
- [Metrics which matter](#metrics-which-matter)
|
||||
- [Tracing](#tracing)
|
||||
- [Healthcheck](#healthcheck)
|
||||
- [Monitoring endpoint](#monitoring-endpoint)
|
||||
|
||||
@@ -36,13 +36,21 @@ This project is in active use by [telegram-bot.app](https://telegram-bot.app), a
|
||||
|
||||
I wanted to monitor the queries and responses of our graphql endpoint. Still, we didn't want to pay the price of the graphql server itself ( and I will not point fingers at a particular well-known project), as monitoring and basic security features should be a standard, free functionality.
|
||||
|
||||
### Important releases
|
||||
|
||||
You should always try to stick to the latest and greatest version of the graphql-proxy to ensure that it's as much bug-free as possible. Following list will be kept to the maximum of five "most important" bugs and enhancements included in the latest versions.
|
||||
|
||||
* **20/08/2024 - 0.23.21+** - Fixes the bug when timeouts were not respected on proxy-graphql line. Affected versions before that were timeouting after 30 seconds which was set as default ( thanks to Jurica Železnjak for reporting ). It also provides a temporary fix for running within kubernetes deployment, when graphql server ( for example - hasura ) took more time to start than the proxy, causing avalanche of errors with "can't proxy the request".
|
||||
|
||||
* **19/08/2024 - 0.21.82+** - Fixed the issue when proxy failed to start if global cache was disabled, therefore not initialized and proxy tried to perform the cache operations during normal query operations.
|
||||
|
||||
### How to deploy
|
||||
|
||||
You can find the example of the Kubernetes manifest in the [example standalone deployment](static/kubernetes-deployment.yaml) or [example combined deployment](static/kubernetes-single-deployment.yaml) files. Observed advantage of multideployment is that it allows the network requests to travel via localhost, without leaving the deployment which brings quite significant network performance boost.
|
||||
|
||||
#### Note on websocket support
|
||||
|
||||
Proxy in its current version 0.5.30 does not support websockets. If you need to proxy the websocket requests - you can use following trick whilst setting up the proxy. As I'm a big fan of Traefik - there's an example which works with the mentioned above combined deployment.
|
||||
Proxy in its current version 0.23.3 does not support websockets. If you need to proxy the websocket requests - you can use following trick whilst setting up the proxy. As I'm a big fan of Traefik - there's an example which works with the mentioned above combined deployment.
|
||||
|
||||
<details>
|
||||
<summary>Click to show working Traefik Ingress Route example.</summary>
|
||||
@@ -104,7 +112,7 @@ In this case, both proxy and websockets will be available under the `/v1/graphql
|
||||
| security | Blocking mutations in read-only mode |
|
||||
| security | Allow access only to listed URLs |
|
||||
| security | Ban / unban specific user from accessing the application |
|
||||
| maintenance | Hasura event cleaner |
|
||||
| maintenance | Hasura events cleaner |
|
||||
|
||||
|
||||
### Configuration
|
||||
@@ -145,8 +153,6 @@ You can still use the non-prefixed environment variables in the spirit of the ba
|
||||
| `HASURA_EVENT_CLEANER` | Enable the hasura event cleaner | `false` |
|
||||
| `HASURA_EVENT_CLEANER_OLDER_THAN` | The interval for the hasura event cleaner (in days) | `1` |
|
||||
| `HASURA_EVENT_METADATA_DB` | URL to the hasura metadata database | `postgresql://localhost:5432/hasura` |
|
||||
| `ENABLE_TRACE` | Enables tracing | `false` |
|
||||
| `TRACER_ENDPOINT` | Tracing endpoint | `localhost:4317` |
|
||||
|
||||
### Speed
|
||||
|
||||
@@ -296,18 +302,6 @@ With the `PURGE_METRICS_ON_CRAWL` enabled, the `graphql_proxy_requests_failed`,
|
||||
|
||||
If you prefer more control over the metrics purging - you can enable `PURGE_METRICS_ON_TIMER` environment variable and set the interval in seconds. This will allow you to purge the metrics on a regular basis, for example every 90 seconds. It could be better solution if you have multiple crawlers checking the metrics endpoints and you want to avoid the situation when metrics are purged by for example healthcheck.
|
||||
|
||||
#### Tracing
|
||||
|
||||
Tracing can be enabled by setting `ENABLE_TRACE` to `true` and providing compatible with OTEL `TRACER_ENDPOINT` value ( default is `localhost:4317` ). From that moment you can include `X-Trace-Span` with content being json encoded in your requests to the proxy endpoint.
|
||||
|
||||
The value of X-Trace-Span should be in following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
||||
}
|
||||
```
|
||||
|
||||
#### Healthcheck
|
||||
|
||||
If you'd like the `/healthz` endpoint to perform actual check for the connectivity to the graphql endpoint - set the `HEALTHCHECK_GRAPHQL_URL` environment variable to the exact URL of the graphql endpoint. The query executed will be `query { __typename }` and if the response is not `200 OK` - the healthcheck will fail. Remember that the endpoint is a full URL which you'd like to check, so it should include the protocol, host and path - for example `http://localhost:8080/v1/graphql` and it's NOT the same as value of `HOST_GRAPHQL` environment variable which should provide only the host, without path, ending with slash.
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
@@ -13,55 +14,66 @@ import (
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
)
|
||||
|
||||
var bannedUsersIDs map[string]string = make(map[string]string)
|
||||
var (
|
||||
bannedUsersIDs = make(map[string]string)
|
||||
bannedUsersIDsMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func enableApi() {
|
||||
if cfg.Server.EnableApi {
|
||||
apiserver := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
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},
|
||||
})
|
||||
|
||||
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()
|
||||
err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort))
|
||||
if err != nil {
|
||||
cfg.Logger.Critical(&libpack_logger.LogMessage{
|
||||
Message: "Can't start the service",
|
||||
Pairs: map[string]interface{}{"port": cfg.Server.ApiPort},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func periodicallyReloadBannedUsers() {
|
||||
for {
|
||||
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},
|
||||
})
|
||||
<-time.After(10 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
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, "found": found},
|
||||
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(403).SendString("User is banned")
|
||||
c.Status(fiber.StatusForbidden).SendString("User is banned")
|
||||
}
|
||||
return found
|
||||
}
|
||||
@@ -69,28 +81,16 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
|
||||
func apiClearCache(c *fiber.Ctx) error {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Clearing cache via API",
|
||||
Pairs: nil,
|
||||
})
|
||||
libpack_cache.CacheClear()
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Cache cleared via API",
|
||||
Pairs: nil,
|
||||
})
|
||||
c.Status(200).SendString("OK: cache cleared")
|
||||
return nil
|
||||
return c.SendString("OK: cache cleared")
|
||||
}
|
||||
|
||||
func apiCacheStats(c *fiber.Ctx) error {
|
||||
stats := libpack_cache.GetCacheStats()
|
||||
err := c.JSON(stats)
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't marshal cache stats",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return c.JSON(libpack_cache.GetCacheStats())
|
||||
}
|
||||
|
||||
type apiBanUserRequest struct {
|
||||
@@ -100,71 +100,92 @@ type apiBanUserRequest struct {
|
||||
|
||||
func apiBanUser(c *fiber.Ctx) error {
|
||||
var req apiBanUserRequest
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
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 err
|
||||
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},
|
||||
})
|
||||
storeBannedUsers()
|
||||
c.Status(200).SendString("OK: user banned")
|
||||
return nil
|
||||
|
||||
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
|
||||
err := c.BodyParser(&req)
|
||||
if err != nil {
|
||||
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 err
|
||||
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},
|
||||
})
|
||||
storeBannedUsers()
|
||||
c.Status(200).SendString("OK: user unbanned")
|
||||
return nil
|
||||
|
||||
if err := storeBannedUsers(); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Failed to store banned users")
|
||||
}
|
||||
|
||||
return c.SendString("OK: user unbanned")
|
||||
}
|
||||
|
||||
func storeBannedUsers() {
|
||||
func storeBannedUsers() error {
|
||||
fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
|
||||
err := fileLock.Lock()
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't lock the file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
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
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(cfg.Api.BannedUsersFile, data, 0644)
|
||||
if err != nil {
|
||||
|
||||
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
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadBannedUsers() {
|
||||
@@ -173,19 +194,9 @@ func loadBannedUsers() {
|
||||
Message: "Banned users file doesn't exist - creating it",
|
||||
Pairs: map[string]interface{}{"file": cfg.Api.BannedUsersFile},
|
||||
})
|
||||
_, err := os.Create(cfg.Api.BannedUsersFile)
|
||||
if err != nil {
|
||||
if err := os.WriteFile(cfg.Api.BannedUsersFile, []byte("{}"), 0644); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't create the file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
// write empty json to the file
|
||||
err = os.WriteFile(cfg.Api.BannedUsersFile, []byte("{}"), 0644)
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't write to the file",
|
||||
Message: "Can't create and write to the file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
@@ -193,8 +204,7 @@ func loadBannedUsers() {
|
||||
}
|
||||
|
||||
fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
|
||||
err := fileLock.RLock() // Use RLock for read lock
|
||||
if err != nil {
|
||||
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()},
|
||||
@@ -211,12 +221,39 @@ func loadBannedUsers() {
|
||||
})
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &bannedUsersIDs)
|
||||
if err != nil {
|
||||
|
||||
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
+53
-2
@@ -1,6 +1,9 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -55,7 +58,7 @@ func EnableCache(cfg *CacheConfig) {
|
||||
}
|
||||
cacheStats = &CacheStats{}
|
||||
if ShouldUseRedisCache(cfg) {
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Using Redis cache",
|
||||
})
|
||||
cfg.Client = libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
|
||||
@@ -64,7 +67,7 @@ func EnableCache(cfg *CacheConfig) {
|
||||
RedisPassword: cfg.Redis.Password,
|
||||
})
|
||||
} else {
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Using in-memory cache",
|
||||
})
|
||||
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
|
||||
@@ -73,9 +76,35 @@ func EnableCache(cfg *CacheConfig) {
|
||||
}
|
||||
|
||||
func CacheLookup(hash string) []byte {
|
||||
if !IsCacheInitialized() {
|
||||
return nil
|
||||
}
|
||||
|
||||
obj, found := config.Client.Get(hash)
|
||||
if found {
|
||||
atomic.AddInt64(&cacheStats.CacheHits, 1)
|
||||
// If the cached data is compressed, decompress it
|
||||
if len(obj) > 2 && obj[0] == 0x1f && obj[1] == 0x8b {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
config.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to create gzip reader for cached data",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "hash": hash},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
config.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to decompress cached data",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "hash": hash},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return decompressed
|
||||
}
|
||||
return obj
|
||||
}
|
||||
atomic.AddInt64(&cacheStats.CacheMisses, 1)
|
||||
@@ -83,6 +112,9 @@ func CacheLookup(hash string) []byte {
|
||||
}
|
||||
|
||||
func CacheDelete(hash string) {
|
||||
if !IsCacheInitialized() {
|
||||
return
|
||||
}
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Deleting data from cache",
|
||||
Pairs: map[string]interface{}{"hash": hash},
|
||||
@@ -92,6 +124,12 @@ func CacheDelete(hash string) {
|
||||
}
|
||||
|
||||
func CacheStore(hash string, data []byte) {
|
||||
if !IsCacheInitialized() {
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Cache not initialized",
|
||||
})
|
||||
return
|
||||
}
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Storing data in cache",
|
||||
Pairs: map[string]interface{}{"hash": hash},
|
||||
@@ -101,6 +139,9 @@ func CacheStore(hash string, data []byte) {
|
||||
}
|
||||
|
||||
func CacheStoreWithTTL(hash string, data []byte, ttl time.Duration) {
|
||||
if !IsCacheInitialized() {
|
||||
return
|
||||
}
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Storing data in cache with TTL",
|
||||
Pairs: map[string]interface{}{"hash": hash, "ttl": ttl},
|
||||
@@ -110,6 +151,9 @@ func CacheStoreWithTTL(hash string, data []byte, ttl time.Duration) {
|
||||
}
|
||||
|
||||
func CacheGetQueries() int64 {
|
||||
if !IsCacheInitialized() {
|
||||
return 0
|
||||
}
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Counting cache queries",
|
||||
})
|
||||
@@ -122,6 +166,9 @@ func CacheClear() {
|
||||
}
|
||||
|
||||
func GetCacheStats() *CacheStats {
|
||||
if !IsCacheInitialized() {
|
||||
return &CacheStats{}
|
||||
}
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Getting cache stats",
|
||||
})
|
||||
@@ -132,3 +179,7 @@ func GetCacheStats() *CacheStats {
|
||||
func ShouldUseRedisCache(cfg *CacheConfig) bool {
|
||||
return cfg.Redis.Enable
|
||||
}
|
||||
|
||||
func IsCacheInitialized() bool {
|
||||
return config != nil && config.Client != nil
|
||||
}
|
||||
|
||||
Vendored
+19
-12
@@ -4,8 +4,8 @@ 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"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ func BenchmarkCacheLookupInMemory(b *testing.B) {
|
||||
Client: libpack_cache_memory.New(5 * time.Minute),
|
||||
TTL: 5,
|
||||
}
|
||||
EnableCache(config)
|
||||
|
||||
hash := "00000000000000000000000000000000001337"
|
||||
data := []byte("it's fine.")
|
||||
@@ -36,16 +37,18 @@ func BenchmarkCacheLookupInMemory(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkCacheLookupRedis(b *testing.B) {
|
||||
mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
|
||||
RedisServer: redisMockServer.Addr(),
|
||||
RedisDB: 0,
|
||||
})
|
||||
|
||||
redis_server, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
config = &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
Client: mockedCache,
|
||||
TTL: 5,
|
||||
}
|
||||
config.Redis.DB = 0
|
||||
config.Redis.URL = redis_server.Addr()
|
||||
config.Redis.Enable = true
|
||||
EnableCache(config)
|
||||
|
||||
hash := "00000000000000000000000000000000001337"
|
||||
data := []byte("it's fine.")
|
||||
@@ -67,6 +70,7 @@ func BenchmarkCacheStoreInMemory(b *testing.B) {
|
||||
Client: libpack_cache_memory.New(5 * time.Minute),
|
||||
TTL: 5,
|
||||
}
|
||||
EnableCache(config)
|
||||
|
||||
hash := "00000000000000000000000000000000001337"
|
||||
data := []byte("it's fine.")
|
||||
@@ -82,16 +86,19 @@ func BenchmarkCacheStoreInMemory(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkCacheStoreRedis(b *testing.B) {
|
||||
mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
|
||||
RedisServer: redisMockServer.Addr(),
|
||||
RedisDB: 0,
|
||||
})
|
||||
redis_server, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
config = &CacheConfig{
|
||||
Logger: libpack_logger.New(),
|
||||
Client: mockedCache,
|
||||
TTL: 5,
|
||||
}
|
||||
config.Redis.DB = 0
|
||||
config.Redis.URL = redis_server.Addr()
|
||||
config.Redis.Enable = true
|
||||
EnableCache(config)
|
||||
|
||||
hash := "00000000000000000000000000000000001337"
|
||||
data := []byte("it's fine.")
|
||||
|
||||
Vendored
+100
-13
@@ -1,10 +1,12 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -58,23 +60,15 @@ func (suite *Tests) Test_cacheLookupInmemory() {
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
config.Redis.DB = 0
|
||||
config.Redis.URL = redisMockServer.Addr()
|
||||
config.Redis.Enable = true
|
||||
EnableCache(config)
|
||||
|
||||
type args struct {
|
||||
hash string
|
||||
@@ -117,3 +111,96 @@ func (suite *Tests) Test_cacheLookupRedis() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
TTL: 5,
|
||||
}
|
||||
config.Redis.DB = 0
|
||||
config.Redis.URL = mr.Addr()
|
||||
config.Redis.Enable = true
|
||||
EnableCache(config)
|
||||
|
||||
// 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
+9
-3
@@ -69,11 +69,17 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
entry, ok := c.entries.Load(key)
|
||||
if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
compressedValue := entry.(CacheEntry).Value
|
||||
value, err := c.decompress(compressedValue)
|
||||
|
||||
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
|
||||
|
||||
Vendored
+28
@@ -1,6 +1,7 @@
|
||||
package libpack_cache_memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -52,3 +53,30 @@ func BenchmarkMemCacheStats(b *testing.B) {
|
||||
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
+56
@@ -1,6 +1,8 @@
|
||||
package libpack_cache_memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -110,3 +112,57 @@ func (suite *MemoryTestSuite) Test_CacheExpire() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
+30
-22
@@ -11,18 +11,14 @@ import (
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
func extractClaimsFromJWTHeader(authorization string) (usr string, role string) {
|
||||
usr, role = "-", "-"
|
||||
const defaultValue = "-"
|
||||
|
||||
handleError := func(msg string, details map[string]interface{}) {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: msg,
|
||||
Pairs: details,
|
||||
})
|
||||
}
|
||||
var emptyMetrics = map[string]string{}
|
||||
|
||||
tokenParts := strings.Split(authorization, ".")
|
||||
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
|
||||
@@ -40,18 +36,30 @@ func extractClaimsFromJWTHeader(authorization string) (usr string, role string)
|
||||
return
|
||||
}
|
||||
|
||||
extractClaim := func(claimPath string, target *string, name string) {
|
||||
if len(claimPath) > 0 {
|
||||
var ok bool
|
||||
*target, ok = ask.For(claimMap, claimPath).String("-")
|
||||
if !ok {
|
||||
handleError(fmt.Sprintf("Can't find the %s", name), map[string]interface{}{"claim_map": claimMap, "path": claimPath})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractClaim(cfg.Client.JWTUserClaimPath, &usr, "user id")
|
||||
extractClaim(cfg.Client.JWTRoleClaimPath, &role, "role")
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,77 +5,84 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
)
|
||||
|
||||
func enableHasuraEventCleaner() {
|
||||
if cfg.HasuraEventCleaner.Enable {
|
||||
if cfg.HasuraEventCleaner.EventMetadataDb == "" {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Event metadata db URL not specified, event cleaner not active",
|
||||
Pairs: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
const (
|
||||
initialDelay = 60 * time.Second
|
||||
cleanupInterval = 1 * time.Hour
|
||||
)
|
||||
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Event cleaner enabled",
|
||||
Pairs: map[string]interface{}{"interval_in_days": cfg.HasuraEventCleaner.ClearOlderThan},
|
||||
})
|
||||
|
||||
time.Sleep(60 * time.Second) // wait for everything to start and settle down
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Initial cleanup of old events",
|
||||
Pairs: nil,
|
||||
})
|
||||
cleanEvents()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Cleaning up old events",
|
||||
Pairs: nil,
|
||||
})
|
||||
cleanEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
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 cleanEvents() {
|
||||
conn, err := pgx.Connect(context.Background(), cfg.HasuraEventCleaner.EventMetadataDb)
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to connect to event metadata db",
|
||||
Pairs: map[string]interface{}{"error": err},
|
||||
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
|
||||
}
|
||||
defer conn.Close(context.Background())
|
||||
|
||||
delQueries := []string{
|
||||
fmt.Sprintf("DELETE FROM hdb_catalog.event_invocation_logs WHERE created_at < now() - interval '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
|
||||
fmt.Sprintf("DELETE FROM hdb_catalog.event_log WHERE created_at < now() - interval '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
|
||||
fmt.Sprintf("DELETE FROM hdb_catalog.hdb_action_log WHERE created_at < NOW() - INTERVAL '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
|
||||
fmt.Sprintf("DELETE FROM hdb_catalog.hdb_cron_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
|
||||
fmt.Sprintf("DELETE FROM hdb_catalog.hdb_scheduled_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
|
||||
}
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Event cleaner enabled",
|
||||
Pairs: map[string]interface{}{"interval_in_days": cfg.HasuraEventCleaner.ClearOlderThan},
|
||||
})
|
||||
|
||||
for _, query := range delQueries {
|
||||
_, err := conn.Exec(context.Background(), query)
|
||||
go func() {
|
||||
pool, err := pgxpool.New(context.Background(), cfg.HasuraEventCleaner.EventMetadataDb)
|
||||
if err != nil {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
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},
|
||||
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},
|
||||
})
|
||||
}
|
||||
}
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Old events cleaned up",
|
||||
Pairs: nil,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,69 +1,55 @@
|
||||
module github.com/lukaszraczylo/graphql-monitoring-proxy
|
||||
|
||||
go 1.21
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/metrics v1.33.1
|
||||
github.com/VictoriaMetrics/metrics v1.35.1
|
||||
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.8.1
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/gofrs/flock v0.12.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/goutil v0.6.15
|
||||
github.com/gookit/goutil v0.6.17
|
||||
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.8
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.14
|
||||
github.com/redis/go-redis/v9 v9.5.3
|
||||
github.com/jackc/pgx/v5 v5.7.1
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.12
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.29
|
||||
github.com/redis/go-redis/v9 v9.6.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/valyala/fasthttp v1.55.0
|
||||
go.opentelemetry.io/otel v1.27.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
|
||||
go.opentelemetry.io/otel/sdk v1.27.0
|
||||
go.opentelemetry.io/otel/trace v1.27.0
|
||||
github.com/valyala/fasthttp v1.56.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.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/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/kr/text v0.2.0 // 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/mattn/go-runewidth v0.0.16 // 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/rogpeppe/go-internal v1.12.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
|
||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // 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
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/term v0.25.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/VictoriaMetrics/metrics v1.33.1 h1:CNV3tfm2Kpv7Y9W3ohmvqgFWPR55tV2c7M2U6OIo+UM=
|
||||
github.com/VictoriaMetrics/metrics v1.33.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
|
||||
github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU=
|
||||
github.com/VictoriaMetrics/metrics v1.35.1/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=
|
||||
@@ -12,81 +12,66 @@ 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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
|
||||
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
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/gookit/goutil v0.6.17 h1:SxmbDz2sn2V+O+xJjJhJT/sq1/kQh6rCJ7vLBiRPZjI=
|
||||
github.com/gookit/goutil v0.6.17/go.mod h1:rSw1LchE1I3TDWITZvefoAC9tS09SFu3lHXLCV7EaEY=
|
||||
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/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
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/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
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.8 h1:ZYm6Wkn58ZAlFWRmC7PaD4oAYHWcu8/0MUDWGe3PnJQ=
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.8/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.14 h1:Dth+yZ+1ialCpnslSb6UgHbXszExjDUu/I95QZbnWVU=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.14/go.mod h1:pSKmm9OLGoS9pjmIvhBB/fo0+LganRrL29CN3fdkRPw=
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9 h1:pL8B9mjv6RPUfKYYGm/uJ7QL6Ndf+z+OEl0qJE6KmEc=
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9/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.29 h1:Fo/3SN4vrST1pyX1UJ5Nd+pQCkurZNJSck4pyx5B/Fk=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.29/go.mod h1:kCvRu01tLxj0iKash5qwL7Em+SltQmZ82bs0yu2aOrk=
|
||||
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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
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=
|
||||
@@ -94,8 +79,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
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/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
|
||||
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
|
||||
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=
|
||||
@@ -106,47 +91,22 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
||||
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=
|
||||
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
|
||||
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
|
||||
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
|
||||
go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
|
||||
go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
|
||||
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
|
||||
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
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/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.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=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
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=
|
||||
|
||||
+107
-102
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
@@ -12,48 +13,26 @@ import (
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
var introspection_queries = []string{
|
||||
"__schema",
|
||||
"__type",
|
||||
"__typename",
|
||||
"__directive",
|
||||
"__directivelocation",
|
||||
"__field",
|
||||
"__inputvalue",
|
||||
"__enumvalue",
|
||||
"__typekind",
|
||||
"__fieldtype",
|
||||
"__inputobjecttype",
|
||||
"__enumtype",
|
||||
"__uniontype",
|
||||
"__scalars",
|
||||
"__objects",
|
||||
"__interfaces",
|
||||
"__unions",
|
||||
"__enums",
|
||||
"__inputobjects",
|
||||
"__directives",
|
||||
}
|
||||
|
||||
// Saving the introspection queries as a map O(1) operation instead of O(n) for a slice.
|
||||
|
||||
var introspectionQuerySet = map[string]struct{}{}
|
||||
var introspectionAllowedQueries = map[string]struct{}{}
|
||||
var allowedUrls = map[string]struct{}{}
|
||||
|
||||
// Utility function to convert a slice of strings to a map for O(1) lookups.
|
||||
func sliceToMap(slice []string) map[string]struct{} {
|
||||
resultMap := make(map[string]struct{}, len(slice))
|
||||
for _, item := range slice {
|
||||
resultMap[strings.ToLower(item)] = struct{}{}
|
||||
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": {},
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
introspectionAllowedQueries = make(map[string]struct{})
|
||||
allowedUrls = make(map[string]struct{})
|
||||
)
|
||||
|
||||
func prepareQueriesAndExemptions() {
|
||||
introspectionQuerySet = sliceToMap(introspection_queries)
|
||||
introspectionAllowedQueries = sliceToMap(cfg.Security.IntrospectionAllowed)
|
||||
allowedUrls = sliceToMap(cfg.Server.AllowURLs)
|
||||
for _, q := range cfg.Security.IntrospectionAllowed {
|
||||
introspectionAllowedQueries[strings.ToLower(q)] = struct{}{}
|
||||
}
|
||||
for _, u := range cfg.Server.AllowURLs {
|
||||
allowedUrls[u] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
type parseGraphQLQueryResult struct {
|
||||
@@ -67,11 +46,32 @@ type parseGraphQLQueryResult struct {
|
||||
shouldIgnore bool
|
||||
}
|
||||
|
||||
func parseGraphQLQuery(c *fiber.Ctx) (res *parseGraphQLQueryResult) {
|
||||
res = &parseGraphQLQueryResult{shouldIgnore: true}
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal(c.Body(), &m)
|
||||
if err != nil {
|
||||
var (
|
||||
queryPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make(map[string]interface{}, 48)
|
||||
},
|
||||
}
|
||||
resultPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &parseGraphQLQueryResult{}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
res := resultPool.Get().(*parseGraphQLQueryResult)
|
||||
*res = parseGraphQLQueryResult{shouldIgnore: true, activeEndpoint: cfg.Server.HostGraphQL}
|
||||
|
||||
m := queryPool.Get().(map[string]interface{})
|
||||
defer func() {
|
||||
for k := range m {
|
||||
delete(m, k)
|
||||
}
|
||||
queryPool.Put(m)
|
||||
}()
|
||||
|
||||
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": string(c.Body())},
|
||||
@@ -79,9 +79,13 @@ func parseGraphQLQuery(c *fiber.Ctx) (res *parseGraphQLQueryResult) {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
return
|
||||
if res.shouldBlock {
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
// get the query
|
||||
|
||||
query, ok := m["query"].(string)
|
||||
if !ok {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
@@ -91,7 +95,8 @@ func parseGraphQLQuery(c *fiber.Ctx) (res *parseGraphQLQueryResult) {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
return
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
|
||||
p, err := parser.Parse(parser.ParseParams{Source: query})
|
||||
@@ -103,59 +108,65 @@ func parseGraphQLQuery(c *fiber.Ctx) (res *parseGraphQLQueryResult) {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
}
|
||||
return
|
||||
resultPool.Put(res)
|
||||
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 {
|
||||
res.operationType = strings.ToLower(oper.Operation)
|
||||
|
||||
if oper.Name != nil {
|
||||
res.operationName = oper.Name.Value
|
||||
if res.operationType == "" {
|
||||
res.operationType = strings.ToLower(oper.Operation)
|
||||
if oper.Name != nil {
|
||||
res.operationName = oper.Name.Value
|
||||
}
|
||||
}
|
||||
|
||||
// If the query is a mutation then direct it to the RW endpoint,
|
||||
// otherwise direct it to the RO endpoint if it's set.
|
||||
if cfg.Server.HostGraphQLReadOnly != "" && res.operationType != "mutation" {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
|
||||
if cfg.Server.HostGraphQLReadOnly != "" {
|
||||
if res.operationType == "" {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
|
||||
} else if res.operationType != "mutation" {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Endpoint selection",
|
||||
Pairs: map[string]interface{}{
|
||||
"operationType": res.operationType,
|
||||
"selectedEndpoint": res.activeEndpoint,
|
||||
},
|
||||
})
|
||||
|
||||
if res.operationType == "mutation" && cfg.Server.ReadOnlyMode {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Mutation blocked",
|
||||
Message: "Mutation blocked - server in read-only mode",
|
||||
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")
|
||||
_ = c.Status(403).SendString("The server is in read-only mode")
|
||||
res.shouldBlock = true
|
||||
return
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
|
||||
for _, dir := range oper.Directives {
|
||||
if dir.Name.Value == "cached" {
|
||||
res.cacheRequest = true
|
||||
for _, arg := range dir.Arguments {
|
||||
if arg.Name.Value == "ttl" {
|
||||
res.cacheTime, err = strconv.Atoi(arg.Value.GetValue().(string))
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't parse the ttl, using global",
|
||||
Pairs: map[string]interface{}{"bad_ttl": arg.Value.GetValue().(string)},
|
||||
})
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
}
|
||||
return
|
||||
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 arg.Name.Value == "refresh" {
|
||||
res.cacheRefresh = arg.Value.GetValue().(bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,59 +175,53 @@ func parseGraphQLQuery(c *fiber.Ctx) (res *parseGraphQLQueryResult) {
|
||||
if cfg.Security.BlockIntrospection {
|
||||
res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections)
|
||||
if res.shouldBlock {
|
||||
return
|
||||
resultPool.Put(res)
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
|
||||
for _, s := range selections {
|
||||
field, ok := s.(*ast.Field)
|
||||
if !ok {
|
||||
continue // or handle the case where the type assertion fails
|
||||
}
|
||||
shouldBlock := checkIfContainsIntrospection(c, field.Name.Value)
|
||||
if shouldBlock {
|
||||
return true
|
||||
}
|
||||
if field.SelectionSet != nil {
|
||||
if checkSelections(c, field.GetSelectionSet().Selections) {
|
||||
stack := make([]ast.Selection, len(selections))
|
||||
copy(stack, selections)
|
||||
|
||||
for len(stack) > 0 {
|
||||
var s ast.Selection
|
||||
s, stack = stack[len(stack)-1], stack[:len(stack)-1]
|
||||
|
||||
if field, ok := s.(*ast.Field); ok {
|
||||
if checkIfContainsIntrospection(c, field.Name.Value) {
|
||||
return true
|
||||
}
|
||||
if field.SelectionSet != nil {
|
||||
stack = append(stack, field.GetSelectionSet().Selections...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) (shouldBlock bool) {
|
||||
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) bool {
|
||||
whateverLower := strings.ToLower(whatever)
|
||||
got_exemption := false
|
||||
|
||||
// If the query is an introspection query, we need to check if it's allowed.
|
||||
if _, exists := introspectionQuerySet[whateverLower]; exists {
|
||||
if _, exists := introspectionQueries[whateverLower]; exists {
|
||||
if len(cfg.Security.IntrospectionAllowed) > 0 {
|
||||
|
||||
if _, allowed_exists := introspectionAllowedQueries[whateverLower]; allowed_exists {
|
||||
if _, allowed := introspectionAllowedQueries[whateverLower]; allowed {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Introspection query allowed, passing through",
|
||||
Pairs: map[string]interface{}{"query": whatever},
|
||||
})
|
||||
got_exemption = true
|
||||
shouldBlock = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !got_exemption {
|
||||
shouldBlock = true
|
||||
}
|
||||
}
|
||||
if shouldBlock {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
c.Status(403).SendString("Introspection queries are not allowed")
|
||||
_ = c.Status(403).SendString("Introspection queries are not allowed")
|
||||
return true
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
+112
@@ -1,6 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
@@ -318,3 +322,111 @@ func (suite *Tests) Test_parseGraphQLQuery() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2/middleware/proxy"
|
||||
"github.com/gookit/goutil/envutil"
|
||||
@@ -12,11 +13,12 @@ import (
|
||||
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"
|
||||
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
|
||||
)
|
||||
|
||||
var cfg *config
|
||||
var once sync.Once
|
||||
var (
|
||||
cfg *config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// function get value from the env where the value can be anything
|
||||
func getDetailsFromEnv[T any](key string, defaultValue T) T {
|
||||
@@ -56,6 +58,7 @@ func parseConfig() {
|
||||
c.Cache.CacheRedisURL = getDetailsFromEnv("CACHE_REDIS_URL", "localhost:6379")
|
||||
c.Cache.CacheRedisPassword = getDetailsFromEnv("CACHE_REDIS_PASSWORD", "")
|
||||
c.Cache.CacheRedisDB = getDetailsFromEnv("CACHE_REDIS_DB", 0)
|
||||
/* security */
|
||||
c.Security.BlockIntrospection = getDetailsFromEnv("BLOCK_SCHEMA_INTROSPECTION", false)
|
||||
c.Security.IntrospectionAllowed = func() []string {
|
||||
urls := getDetailsFromEnv("ALLOWED_INTROSPECTION", "")
|
||||
@@ -89,8 +92,6 @@ func parseConfig() {
|
||||
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", "")
|
||||
c.Trace.Enable = getDetailsFromEnv("ENABLE_TRACE", false)
|
||||
c.Trace.TraceEndpoint = getDetailsFromEnv("TRACER_ENDPOINT", "localhost:4317")
|
||||
cfg = &c
|
||||
|
||||
if cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable {
|
||||
@@ -109,18 +110,6 @@ func parseConfig() {
|
||||
|
||||
loadRatelimitConfig()
|
||||
once.Do(func() {
|
||||
if cfg.Trace.Enable {
|
||||
var err error
|
||||
cfg.Trace.Client, err = libpack_trace.NewClient(cfg.Logger, cfg.Trace.TraceEndpoint)
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Failed to start tracer",
|
||||
Pairs: map[string]interface{}{
|
||||
"error": err,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
go enableApi()
|
||||
go enableHasuraEventCleaner()
|
||||
})
|
||||
@@ -130,6 +119,7 @@ func parseConfig() {
|
||||
func main() {
|
||||
parseConfig()
|
||||
StartMonitoringServer()
|
||||
time.Sleep(5 * time.Second)
|
||||
StartHTTPProxy()
|
||||
}
|
||||
|
||||
|
||||
@@ -112,3 +112,29 @@ func (suite *Tests) Test_envVariableSetting() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+38
-49
@@ -12,17 +12,14 @@ import (
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
)
|
||||
|
||||
// Cache for sorted label keys to avoid repeated sorting
|
||||
var sortedLabelKeysCache = struct {
|
||||
m map[string][]string
|
||||
sync.RWMutex
|
||||
}{m: make(map[string][]string)}
|
||||
m sync.Map
|
||||
}{}
|
||||
|
||||
func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) string {
|
||||
const unknownPodName = "unknown"
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Prepare default labels without initializing a new map
|
||||
podName := getPodName()
|
||||
if labels == nil {
|
||||
labels = defaultLabels(podName)
|
||||
@@ -30,18 +27,16 @@ func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string)
|
||||
ensureDefaultLabels(&labels, podName)
|
||||
}
|
||||
|
||||
// Prefix handling
|
||||
if ms.metrics_prefix != "" {
|
||||
buf.WriteString(ms.metrics_prefix)
|
||||
buf.WriteString("_")
|
||||
buf.WriteByte('_')
|
||||
}
|
||||
buf.WriteString(name)
|
||||
|
||||
// Append labels if any
|
||||
if len(labels) > 0 {
|
||||
buf.WriteString("{")
|
||||
buf.WriteByte('{')
|
||||
appendSortedLabels(&buf, labels)
|
||||
buf.WriteString("}")
|
||||
buf.WriteByte('}')
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
@@ -78,64 +73,64 @@ func appendSortedLabels(buf *bytes.Buffer, labels map[string]string) {
|
||||
keys := getSortedKeys(labels)
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteString(",")
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteString("=\"")
|
||||
buf.WriteString(`="`)
|
||||
buf.WriteString(labels[k])
|
||||
buf.WriteString("\"")
|
||||
buf.WriteByte('"')
|
||||
}
|
||||
}
|
||||
|
||||
func getSortedKeys(labels map[string]string) []string {
|
||||
labelsKey := labelsToString(labels)
|
||||
|
||||
sortedLabelKeysCache.RLock()
|
||||
keys, exists := sortedLabelKeysCache.m[labelsKey]
|
||||
sortedLabelKeysCache.RUnlock()
|
||||
|
||||
if !exists {
|
||||
keys = make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
sortedLabelKeysCache.Lock()
|
||||
sortedLabelKeysCache.m[labelsKey] = keys
|
||||
sortedLabelKeysCache.Unlock()
|
||||
// Check if the sorted keys are already cached
|
||||
if keys, ok := sortedLabelKeysCache.m.Load(labelsKey); ok {
|
||||
return keys.([]string)
|
||||
}
|
||||
|
||||
// Compute the sorted keys
|
||||
keys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// Store the sorted keys in the cache
|
||||
sortedLabelKeysCache.m.Store(labelsKey, keys)
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func labelsToString(labels map[string]string) string {
|
||||
keys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var sb strings.Builder
|
||||
for k, v := range labels {
|
||||
for _, k := range keys {
|
||||
sb.WriteString(k)
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(v)
|
||||
sb.WriteString(";")
|
||||
sb.WriteByte('=')
|
||||
sb.WriteString(labels[k])
|
||||
sb.WriteByte(';')
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// validate_metrics_name validates the name of the metric to adhere to the Prometheus naming conventions
|
||||
// https://prometheus.io/docs/practices/naming/
|
||||
func validate_metrics_name(name string) error {
|
||||
cleanedName := clean_metric_name(name)
|
||||
|
||||
// Trim leading and trailing underscores
|
||||
finalName := strings.Trim(cleanedName, "_")
|
||||
|
||||
// Check if the processed name matches the original input
|
||||
if finalName != name {
|
||||
return fmt.Errorf("Invalid metric name: %s, expected %s", name, finalName)
|
||||
return fmt.Errorf("invalid metric name: %s, expected %s", name, finalName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clean_metric_name processes the metric name according to Prometheus naming conventions
|
||||
func clean_metric_name(name string) string {
|
||||
var buf bytes.Buffer
|
||||
lastWasUnderscore := false
|
||||
@@ -144,31 +139,27 @@ func clean_metric_name(name string) string {
|
||||
if is_allowed_rune(r) {
|
||||
if is_special_rune(r) {
|
||||
if lastWasUnderscore {
|
||||
continue // Skip if the previous character was also an underscore
|
||||
continue
|
||||
}
|
||||
r = '_' // Convert spaces and special characters to underscores
|
||||
r = '_'
|
||||
lastWasUnderscore = true
|
||||
} else {
|
||||
lastWasUnderscore = false
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
} else if !lastWasUnderscore {
|
||||
buf.WriteRune('_')
|
||||
buf.WriteByte('_')
|
||||
lastWasUnderscore = true
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing underscore
|
||||
result := buf.String()
|
||||
return strings.Trim(result, "_")
|
||||
return strings.Trim(buf.String(), "_")
|
||||
}
|
||||
|
||||
// is_allowed_rune checks if the rune is allowed in the metric name
|
||||
func is_allowed_rune(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == ' ' || r == '_'
|
||||
}
|
||||
|
||||
// is_special_rune checks if the rune is a space or an underscore
|
||||
func is_special_rune(r rune) bool {
|
||||
return r == ' ' || r == '_'
|
||||
}
|
||||
@@ -178,14 +169,12 @@ func compile_metrics_with_labels(name string, labels map[string]string) string {
|
||||
|
||||
buf.WriteString(name)
|
||||
|
||||
// Collect keys and sort them
|
||||
keys := getSortedKeys(labels)
|
||||
|
||||
// Append sorted key-value pairs to the buffer
|
||||
for _, k := range keys {
|
||||
buf.WriteString("_")
|
||||
buf.WriteByte('_')
|
||||
buf.WriteString(k)
|
||||
buf.WriteString("_")
|
||||
buf.WriteByte('_')
|
||||
buf.WriteString(labels[k])
|
||||
}
|
||||
|
||||
|
||||
+16
-20
@@ -1,6 +1,3 @@
|
||||
// Package `libpack_monitoring` provides and easy way to add prometheus metrics to your application.
|
||||
// It also provides a way to add custom metrics to the already started prometheus registry.
|
||||
|
||||
package libpack_monitoring
|
||||
|
||||
import (
|
||||
@@ -22,9 +19,7 @@ type MetricsSetup struct {
|
||||
metrics_prefix string
|
||||
}
|
||||
|
||||
var (
|
||||
log *libpack_logger.Logger
|
||||
)
|
||||
var log = libpack_logger.New().SetMinLogLevel(libpack_logger.LEVEL_INFO)
|
||||
|
||||
type InitConfig struct {
|
||||
PurgeOnCrawl bool
|
||||
@@ -32,11 +27,11 @@ type InitConfig struct {
|
||||
}
|
||||
|
||||
func NewMonitoring(ic *InitConfig) *MetricsSetup {
|
||||
log = libpack_logger.New().SetMinLogLevel(libpack_logger.LEVEL_INFO)
|
||||
ms := &MetricsSetup{ic: ic}
|
||||
ms.metrics_set = metrics.NewSet()
|
||||
ms.metrics_set_custom = metrics.NewSet()
|
||||
// if not testing, start the prometheus endpoint
|
||||
ms := &MetricsSetup{
|
||||
ic: ic,
|
||||
metrics_set: metrics.NewSet(),
|
||||
metrics_set_custom: metrics.NewSet(),
|
||||
}
|
||||
|
||||
if flag.Lookup("test.v") == nil {
|
||||
go ms.startPrometheusEndpoint()
|
||||
@@ -60,9 +55,11 @@ func (ms *MetricsSetup) startPrometheusEndpoint() {
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
})
|
||||
app.Get("/metrics", ms.metricsEndpoint)
|
||||
err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393)))
|
||||
if err != nil {
|
||||
fmt.Println("Can't start the service: ", err)
|
||||
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},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +82,7 @@ func (ms *MetricsSetup) ListActiveMetrics() []string {
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
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},
|
||||
@@ -93,13 +90,12 @@ func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[stri
|
||||
return nil
|
||||
}
|
||||
return ms.metrics_set_custom.GetOrCreateGauge(ms.get_metrics_name(metric_name, labels), func() float64 {
|
||||
// get current value of the gauge and add val to it
|
||||
return val
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
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},
|
||||
@@ -113,7 +109,7 @@ func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[st
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
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},
|
||||
@@ -124,7 +120,7 @@ func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[stri
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
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},
|
||||
@@ -135,7 +131,7 @@ func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[st
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
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},
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/goccy/go-json"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"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"
|
||||
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
@@ -24,15 +25,15 @@ func createFasthttpClient(timeout int) *fasthttp.Client {
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
MaxConnsPerHost: 2048,
|
||||
ReadTimeout: time.Second * time.Duration(timeout),
|
||||
WriteTimeout: time.Second * time.Duration(timeout),
|
||||
MaxIdleConnDuration: time.Second * time.Duration(timeout),
|
||||
MaxConnDuration: time.Second * time.Duration(timeout),
|
||||
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, ctx context.Context) error {
|
||||
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
|
||||
if !checkAllowedURLs(c) {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Request blocked",
|
||||
@@ -41,116 +42,118 @@ func proxyTheRequest(c *fiber.Ctx, currentEndpoint string, ctx context.Context)
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
}
|
||||
c.Status(403).SendString("Request blocked - not allowed URL")
|
||||
return 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)
|
||||
}
|
||||
c.Request().Header.DisableNormalizing()
|
||||
c.Request().Header.Add("X-Real-IP", c.IP())
|
||||
c.Request().Header.Add(fiber.HeaderXForwardedFor, string(c.Request().Header.Peek("X-Forwarded-For")))
|
||||
c.Request().Header.Del(fiber.HeaderAcceptEncoding)
|
||||
|
||||
// added dummy check for the log level because it executes additional functions which could
|
||||
// potentially slow down the execution.
|
||||
if cfg.LogLevel == "debug" {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Proxying the request",
|
||||
Pairs: map[string]interface{}{
|
||||
"path": c.Path(),
|
||||
"body": string(c.Request().Body()),
|
||||
"headers": c.GetReqHeaders(),
|
||||
"request_uuid": c.Locals("request_uuid"),
|
||||
},
|
||||
})
|
||||
logDebugRequest(c)
|
||||
}
|
||||
|
||||
err := retry.Do(
|
||||
err = retry.Do(
|
||||
func() error {
|
||||
errInt := proxy.DoRedirects(c, currentEndpoint+c.Path(), 3, cfg.Client.FastProxyClient)
|
||||
if errInt != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't proxy the request",
|
||||
Pairs: map[string]interface{}{
|
||||
"error": errInt.Error(),
|
||||
},
|
||||
})
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
}
|
||||
return errInt
|
||||
proxyErr := proxy.DoRedirects(c, proxyURL, 3, cfg.Client.FastProxyClient)
|
||||
if proxyErr != nil {
|
||||
return proxyErr
|
||||
}
|
||||
if c.Response().StatusCode() != 200 {
|
||||
return fmt.Errorf("received non-200 response from the GraphQL server: %d", c.Response().StatusCode())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
retry.Attempts(5),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.Delay(250*time.Millisecond),
|
||||
retry.MaxDelay(5*time.Second),
|
||||
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(),
|
||||
"path": c.Path(),
|
||||
"attempt": n + 1,
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
}),
|
||||
retry.Attempts(uint(3)),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.Delay(time.Duration(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(),
|
||||
},
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
}
|
||||
return fmt.Errorf("failed to proxy request: %v", err)
|
||||
}
|
||||
|
||||
if cfg.LogLevel == "debug" {
|
||||
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"),
|
||||
},
|
||||
})
|
||||
logDebugResponse(c)
|
||||
}
|
||||
|
||||
if c.Response().Header.Peek("Content-Encoding") != nil && string(c.Response().Header.Peek("Content-Encoding")) == "gzip" {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(c.Response().Body()))
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to create gzip reader",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to decompress response",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
c.Response().SetBody(decompressed)
|
||||
c.Response().Header.Del("Content-Encoding")
|
||||
}
|
||||
|
||||
if c.Response().StatusCode() != 200 {
|
||||
if ifNotInTest() {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
}
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Received non-200 response from the GraphQL server",
|
||||
Pairs: map[string]interface{}{
|
||||
"status_code": c.Response().StatusCode(),
|
||||
},
|
||||
})
|
||||
return fmt.Errorf("Received non-200 response from the GraphQL server: %d", c.Response().StatusCode())
|
||||
return fmt.Errorf("received non-200 response from the GraphQL server: %d", c.Response().StatusCode())
|
||||
}
|
||||
|
||||
c.Response().Header.Del(fiber.HeaderServer)
|
||||
if cfg.Trace.Enable {
|
||||
tracingContext := libpack_trace.TraceContextInject(ctx)
|
||||
if tracingContext == nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't inject empty tracing context",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
traceJsonEncoded, err := json.Marshal(tracingContext)
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't convert tracing context to JSON",
|
||||
Pairs: map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
c.Response().Header.Set("X-Trace-Span", string(traceJsonEncoded))
|
||||
}
|
||||
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"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
+173
-33
@@ -1,6 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
@@ -12,47 +16,63 @@ func (suite *Tests) Test_proxyTheRequest() {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
headers map[string]string
|
||||
name string
|
||||
body string
|
||||
host string
|
||||
hostRO string
|
||||
path string
|
||||
wantErr bool
|
||||
headers map[string]string
|
||||
name string
|
||||
body string
|
||||
host string
|
||||
hostRO string
|
||||
path string
|
||||
wantErr bool
|
||||
wantEndpoint string
|
||||
}{
|
||||
{
|
||||
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_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,
|
||||
wantEndpoint: "https://telegram-bot.app/",
|
||||
},
|
||||
{
|
||||
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_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,
|
||||
wantEndpoint: "https://google.com/",
|
||||
},
|
||||
{
|
||||
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",
|
||||
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,
|
||||
wantEndpoint: "https://telegram-bot.app/",
|
||||
},
|
||||
{
|
||||
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,
|
||||
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,
|
||||
wantEndpoint: "https://google.com/",
|
||||
},
|
||||
{
|
||||
name: "Test mutation with endpoint flip",
|
||||
body: `{"query":"mutation {\n __type(name: \"Query\") {\n name\n }\n }"}`,
|
||||
host: "https://telegram-bot.app/",
|
||||
hostRO: "https://google.com/",
|
||||
path: "/v1/graphql",
|
||||
headers: supplied_headers,
|
||||
wantErr: false,
|
||||
wantEndpoint: "https://telegram-bot.app/",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -92,6 +112,126 @@ func (suite *Tests) Test_proxyTheRequest() {
|
||||
} else {
|
||||
assert.Nil(err, "Error is not nil", tt.name)
|
||||
}
|
||||
assert.Equal(tt.wantEndpoint, res.activeEndpoint, "Unexpected endpoint", tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) Test_proxyTheRequestWithPayloads() {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
url string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test with invalid URL",
|
||||
payload: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
|
||||
url: "://invalid-url",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Test with network error",
|
||||
payload: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
|
||||
url: "http://non-existent-host.invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
// {
|
||||
// name: "Test with large payload",
|
||||
// payload: strings.Repeat("a", 10*1024*1024), // 10MB payload
|
||||
// url: "https://google.com/",
|
||||
// wantErr: false,
|
||||
// },
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.Run(tt.name, func() {
|
||||
cfg.Server.HostGraphQL = tt.url
|
||||
ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
err := proxyTheRequest(ctx, cfg.Server.HostGraphQL)
|
||||
if tt.wantErr {
|
||||
assert.NotNil(err)
|
||||
} else {
|
||||
assert.Nil(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) Test_proxyTheRequestWithTimeouts() {
|
||||
originalTimeout := cfg.Client.ClientTimeout
|
||||
defer func() {
|
||||
cfg.Client.ClientTimeout = originalTimeout
|
||||
cfg.Client.FastProxyClient = createFasthttpClient(cfg.Client.ClientTimeout)
|
||||
}()
|
||||
|
||||
// Create a mock server
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sleepDuration, _ := time.ParseDuration(r.Header.Get("X-Sleep-Duration"))
|
||||
time.Sleep(sleepDuration)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"data":{"test":"response"}}`))
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
clientTimeout int
|
||||
sleepDuration string
|
||||
body string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Short timeout, long wait for response",
|
||||
clientTimeout: 1,
|
||||
sleepDuration: "2s",
|
||||
body: `{"query":"query { test }"}`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Short timeout, short wait for response",
|
||||
clientTimeout: 2,
|
||||
sleepDuration: "500ms",
|
||||
body: `{"query":"query { test }"}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Long timeout, short wait for response",
|
||||
clientTimeout: 10,
|
||||
sleepDuration: "1s",
|
||||
body: `{"query":"query { test }"}`,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.Run(tt.name, func() {
|
||||
cfg.Client.ClientTimeout = tt.clientTimeout
|
||||
cfg.Client.FastProxyClient = createFasthttpClient(cfg.Client.ClientTimeout)
|
||||
cfg.Server.HostGraphQL = mockServer.URL
|
||||
|
||||
req := &fasthttp.Request{}
|
||||
req.SetBody([]byte(tt.body))
|
||||
req.SetRequestURI("/v1/graphql")
|
||||
req.Header.SetMethod("POST")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Sleep-Duration", tt.sleepDuration)
|
||||
|
||||
ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
ctx.Request().Header.SetMethod("POST")
|
||||
ctx.Request().SetBody(req.Body())
|
||||
ctx.Request().SetRequestURI(string(req.RequestURI())) // Convert []byte to string
|
||||
ctx.Request().Header.SetContentType("application/json")
|
||||
ctx.Request().Header.Set("X-Sleep-Duration", tt.sleepDuration)
|
||||
|
||||
err := proxyTheRequest(ctx, cfg.Server.HostGraphQL)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.NotNil(err, "Expected an error for test: %s", tt.name)
|
||||
} else {
|
||||
assert.Nil(err, "Expected no error for test: %s", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+31
-54
@@ -2,89 +2,80 @@ 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 string `json:"interval"`
|
||||
Req int `json:"req"`
|
||||
Interval time.Duration `json:"interval"`
|
||||
Req int `json:"req"`
|
||||
}
|
||||
|
||||
var rateLimits map[string]RateLimitConfig
|
||||
var ratelimit_intervals = map[string]time.Duration{
|
||||
"milli": time.Millisecond,
|
||||
"micro": time.Microsecond,
|
||||
"nano": time.Nanosecond,
|
||||
"second": time.Second,
|
||||
"minute": time.Minute,
|
||||
"hour": time.Hour,
|
||||
"day": time.Hour * 24,
|
||||
}
|
||||
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 {
|
||||
err := loadConfigFromPath(path)
|
||||
if err == nil {
|
||||
if err := loadConfigFromPath(path); err == nil {
|
||||
return nil
|
||||
}
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Failed to load config",
|
||||
Pairs: map[string]interface{}{"path": path, "error": err},
|
||||
})
|
||||
}
|
||||
|
||||
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.Open(path)
|
||||
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
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
config := struct {
|
||||
var config struct {
|
||||
RateLimit map[string]RateLimitConfig `json:"ratelimit"`
|
||||
}{}
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
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: time.Duration(value.Req) * ratelimit_intervals[value.Interval],
|
||||
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_provided": value.Interval,
|
||||
"interval_used": ratelimit_intervals[value.Interval],
|
||||
"ratelimit": value.Req,
|
||||
"role": key,
|
||||
"interval_used": value.Interval,
|
||||
"ratelimit": value.Req,
|
||||
},
|
||||
})
|
||||
}
|
||||
config.RateLimit[key] = value
|
||||
newRateLimits[key] = value
|
||||
}
|
||||
|
||||
rateLimits = config.RateLimit
|
||||
rateLimitMu.Lock()
|
||||
rateLimits = newRateLimits
|
||||
rateLimitMu.Unlock()
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit config loaded",
|
||||
Pairs: map[string]interface{}{"ratelimit": rateLimits},
|
||||
@@ -92,28 +83,14 @@ func loadConfigFromPath(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func rateLimitedRequest(userID string, userRole string) (shouldAllow bool) {
|
||||
if rateLimits == nil {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit config not found",
|
||||
Pairs: map[string]interface{}{"user_role": userRole},
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// Fetch role config once to avoid multiple map lookups
|
||||
func rateLimitedRequest(userID, userRole string) bool {
|
||||
rateLimitMu.RLock()
|
||||
roleConfig, ok := rateLimits[userRole]
|
||||
if !ok {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit role not found",
|
||||
Pairs: map[string]interface{}{"user_role": userRole},
|
||||
})
|
||||
return true
|
||||
}
|
||||
rateLimitMu.RUnlock()
|
||||
|
||||
if roleConfig.RateCounterTicker == nil {
|
||||
if !ok || roleConfig.RateCounterTicker == nil {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit ticker not found",
|
||||
Message: "Rate limit role not found or ticker not initialized",
|
||||
Pairs: map[string]interface{}{"user_role": userRole},
|
||||
})
|
||||
return true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
@@ -15,30 +15,41 @@ import (
|
||||
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"
|
||||
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
|
||||
)
|
||||
|
||||
// StartHTTPProxy starts the HTTP and points it to the GraphQL server.
|
||||
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",
|
||||
Pairs: nil,
|
||||
})
|
||||
server := fiber.New(fiber.Config{
|
||||
|
||||
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,
|
||||
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
|
||||
JSONEncoder: json.Marshal,
|
||||
JSONDecoder: json.Unmarshal,
|
||||
})
|
||||
}
|
||||
|
||||
server := fiber.New(serverConfig)
|
||||
|
||||
server.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "*",
|
||||
}))
|
||||
|
||||
// add middleware to check if the request is a GraphQL query
|
||||
server.Use(AddRequestUUID)
|
||||
|
||||
server.Get("/healthz", healthCheck)
|
||||
@@ -51,11 +62,11 @@ func StartHTTPProxy() {
|
||||
Message: "GraphQL proxy started",
|
||||
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL},
|
||||
})
|
||||
err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL))
|
||||
if err != nil {
|
||||
|
||||
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},
|
||||
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL, "error": err.Error()},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,168 +84,125 @@ func checkAllowedURLs(c *fiber.Ctx) bool {
|
||||
if len(allowedUrls) == 0 {
|
||||
return true
|
||||
}
|
||||
_, ok := allowedUrls[c.Path()]
|
||||
path := c.OriginalURL()
|
||||
_, ok := allowedUrls[path]
|
||||
return ok
|
||||
}
|
||||
|
||||
func extractTraceHeaders(c *fiber.Ctx) (found bool, traceHeaders map[string]string) {
|
||||
if !cfg.Trace.Enable {
|
||||
return
|
||||
}
|
||||
headers := c.Request().Header
|
||||
traceHeader := headers.Peek("X-Trace-Span")
|
||||
if traceHeader != nil {
|
||||
traceHeaders = make(map[string]string)
|
||||
if err := json.Unmarshal(traceHeader, &traceHeaders); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Error unmarshalling tracer header",
|
||||
Pairs: map[string]interface{}{"error": err},
|
||||
})
|
||||
return
|
||||
}
|
||||
found = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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},
|
||||
})
|
||||
query := `{ __typename }`
|
||||
_, err := cfg.Client.GQLClient.Query(query, nil, nil)
|
||||
|
||||
_, 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)
|
||||
c.Status(500).SendString("Can't reach the GraphQL server with {__typename} query")
|
||||
return err
|
||||
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",
|
||||
Pairs: nil,
|
||||
})
|
||||
c.Status(200).SendString("Health check OK")
|
||||
return nil
|
||||
return c.Status(200).SendString("Health check OK")
|
||||
}
|
||||
|
||||
func processGraphQLRequest(c *fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// Initialize variables with default values
|
||||
extractedUserID, extractedRoleName := "-", "-"
|
||||
extractedUserID := "-"
|
||||
extractedRoleName := "-"
|
||||
|
||||
// Pre-fetch headers and trace header processing
|
||||
headers := c.Request().Header
|
||||
authorization := headers.Peek("Authorization")
|
||||
ctx := context.Background()
|
||||
traceHeaderFound, traceHeader := extractTraceHeaders(c)
|
||||
|
||||
if traceHeaderFound {
|
||||
ctx = libpack_trace.TraceContextExtract(ctx, traceHeader)
|
||||
_, span := libpack_trace.ContinueSpanFromContext(ctx, "GraphQLRequest")
|
||||
defer span.End()
|
||||
if authorization := c.Get("Authorization"); authorization != "" && (len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0) {
|
||||
extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(authorization)
|
||||
}
|
||||
|
||||
// JWT and role extraction with pre-check
|
||||
if authorization != nil && (len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0) {
|
||||
extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(string(authorization))
|
||||
}
|
||||
|
||||
// Check for banned users early
|
||||
if checkIfUserIsBanned(c, extractedUserID) {
|
||||
return c.Status(403).SendString("User is banned")
|
||||
}
|
||||
|
||||
// Role extraction from header
|
||||
if len(cfg.Client.RoleFromHeader) > 0 {
|
||||
extractedRoleName = string(headers.Peek(cfg.Client.RoleFromHeader))
|
||||
if extractedRoleName == "" {
|
||||
extractedRoleName = "-"
|
||||
if cfg.Client.RoleFromHeader != "" {
|
||||
if role := c.Get(cfg.Client.RoleFromHeader); role != "" {
|
||||
extractedRoleName = role
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting check
|
||||
if cfg.Client.RoleRateLimit && !rateLimitedRequest(extractedUserID, extractedRoleName) {
|
||||
if cfg.Client.RoleRateLimit {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limiting enabled",
|
||||
Pairs: map[string]interface{}{"user_id": extractedUserID, "role_name": extractedRoleName},
|
||||
})
|
||||
return c.Status(429).SendString("Rate limit exceeded, try again later")
|
||||
if !rateLimitedRequest(extractedUserID, extractedRoleName) {
|
||||
return c.Status(429).SendString("Rate limit exceeded, try again later")
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing GraphQL query
|
||||
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",
|
||||
Pairs: nil,
|
||||
})
|
||||
return proxyTheRequest(c, parsedResult.activeEndpoint, ctx)
|
||||
return proxyTheRequest(c, parsedResult.activeEndpoint)
|
||||
}
|
||||
// Cache handling logic
|
||||
queryCacheHash := libpack_cache.CalculateHash(c)
|
||||
|
||||
calculatedQueryHash := libpack_cache.CalculateHash(c)
|
||||
|
||||
if parsedResult.cacheTime == 0 {
|
||||
cacheQuery := headers.Peek("X-Cache-Graphql-Query")
|
||||
if cacheQuery != nil {
|
||||
parsedResult.cacheTime, _ = strconv.Atoi(string(cacheQuery))
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Cache time set via header",
|
||||
Pairs: map[string]interface{}{"cacheTime": parsedResult.cacheTime},
|
||||
})
|
||||
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(queryCacheHash)
|
||||
libpack_cache.CacheDelete(calculatedQueryHash)
|
||||
}
|
||||
|
||||
wasCached := false
|
||||
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(queryCacheHash); cachedResponse != nil {
|
||||
|
||||
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": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
|
||||
Pairs: map[string]interface{}{"hash": calculatedQueryHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
|
||||
})
|
||||
headers.Add("X-Cache-Hit", "true")
|
||||
if err := c.Send(cachedResponse); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't send the cached response",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
return c.Status(500).SendString("Can't send the cached response - try again later")
|
||||
}
|
||||
c.Set("X-Cache-Hit", "true")
|
||||
wasCached = true
|
||||
} else {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheMiss, nil)
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Cache miss",
|
||||
Pairs: map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
|
||||
})
|
||||
proxyAndCacheTheRequest(c, queryCacheHash, parsedResult.cacheTime, parsedResult.activeEndpoint, ctx)
|
||||
c.Set("Content-Type", "application/json")
|
||||
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, ctx); err != nil {
|
||||
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()},
|
||||
@@ -244,33 +212,31 @@ func processGraphQLRequest(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
timeTaken := time.Since(startTime)
|
||||
logAndMonitorRequest(c, extractedUserID, parsedResult.operationType, parsedResult.operationName, wasCached, timeTaken, startTime)
|
||||
logAndMonitorRequest(c, extractedUserID, parsedResult.operationType, parsedResult.operationName, wasCached, time.Since(startTime), startTime)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Additional helper function to avoid code repetition
|
||||
func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string, ctx context.Context) {
|
||||
err := proxyTheRequest(c, currentEndpoint, ctx)
|
||||
if err != 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)
|
||||
c.Status(500).SendString("Can't proxy the request - try again later")
|
||||
return
|
||||
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)
|
||||
c.Send(c.Response().Body())
|
||||
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": fmt.Sprintf("%t", wasCached),
|
||||
"cached": strconv.FormatBool(wasCached),
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
@@ -279,7 +245,7 @@ func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached
|
||||
Message: "Request processed",
|
||||
Pairs: map[string]interface{}{
|
||||
"ip": c.IP(),
|
||||
"fwd-ip": string(c.Request().Header.Peek("X-Forwarded-For")),
|
||||
"fwd-ip": c.Get("X-Forwarded-For"),
|
||||
"user_id": userID,
|
||||
"op_type": opType,
|
||||
"op_name": opName,
|
||||
|
||||
@@ -9,11 +9,6 @@ import (
|
||||
|
||||
// config is a struct that holds the configuration of the application.
|
||||
type config struct {
|
||||
Trace struct {
|
||||
Client func()
|
||||
TraceEndpoint string
|
||||
Enable bool
|
||||
}
|
||||
Logger *libpack_logging.Logger
|
||||
LogLevel string
|
||||
Monitoring *libpack_monitoring.MetricsSetup
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package libpack_trace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
||||
)
|
||||
|
||||
func NewClient(log *libpack_logging.Logger, otelGRPCCollector string, attr ...attribute.KeyValue) (func(), error) {
|
||||
attr = append(attr, semconv.ServiceNameKey.String(libpack_config.PKG_NAME))
|
||||
fmt.Printf("Starting OpenTelemetry tracer: otlp, configured with endpoint: %s\n", otelGRPCCollector)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client := otlptracegrpc.NewClient(
|
||||
otlptracegrpc.WithInsecure(),
|
||||
otlptracegrpc.WithEndpoint(otelGRPCCollector),
|
||||
)
|
||||
|
||||
exporter, err := otlptrace.New(ctx, client)
|
||||
if err != nil {
|
||||
log.Error(&libpack_logging.LogMessage{
|
||||
Message: "Failed to create exporter",
|
||||
Pairs: map[string]interface{}{"error": err},
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tp := trace.NewTracerProvider(
|
||||
trace.WithSampler(trace.AlwaysSample()),
|
||||
trace.WithBatcher(exporter, trace.WithMaxExportBatchSize(1), trace.WithBatchTimeout(30*time.Second)),
|
||||
trace.WithResource(resource.NewWithAttributes(semconv.SchemaURL, attr...)),
|
||||
)
|
||||
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
|
||||
|
||||
shutdownFunc := func() {
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCancel()
|
||||
log.Info(&libpack_logging.LogMessage{
|
||||
Message: "Shutting down tracer",
|
||||
Pairs: nil,
|
||||
})
|
||||
if err := tp.Shutdown(shutdownCtx); err != nil {
|
||||
log.Warning(&libpack_logging.LogMessage{
|
||||
Message: "Failed to shutdown tracer provider",
|
||||
Pairs: map[string]interface{}{"error": err},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return shutdownFunc, nil
|
||||
}
|
||||
|
||||
func TraceContextInject(ctx context.Context) map[string]string {
|
||||
carrier := propagation.MapCarrier{}
|
||||
propagator := otel.GetTextMapPropagator()
|
||||
propagator.Inject(ctx, carrier)
|
||||
return map[string]string(carrier)
|
||||
}
|
||||
|
||||
func TraceContextExtract(ctx context.Context, traceContext map[string]string) context.Context {
|
||||
carrier := propagation.MapCarrier(traceContext)
|
||||
propagator := otel.GetTextMapPropagator()
|
||||
return propagator.Extract(ctx, carrier)
|
||||
}
|
||||
|
||||
func StartSpanFromContext(ctx context.Context, operationName string) (context.Context, oteltrace.Span) {
|
||||
tr := otel.GetTracerProvider().Tracer("")
|
||||
return tr.Start(ctx, operationName, oteltrace.WithSpanKind(oteltrace.SpanKindServer))
|
||||
}
|
||||
|
||||
func ContinueSpanFromContext(ctx context.Context, operationName string) (context.Context, oteltrace.Span) {
|
||||
tr := otel.GetTracerProvider().Tracer("")
|
||||
options := []oteltrace.SpanStartOption{
|
||||
oteltrace.WithSpanKind(oteltrace.SpanKindInternal),
|
||||
oteltrace.WithAttributes(attribute.String("cont", "true")),
|
||||
}
|
||||
return tr.Start(ctx, operationName, options...)
|
||||
}
|
||||
|
||||
func AddAttributesToSpan(span oteltrace.Span, attributes ...attribute.KeyValue) {
|
||||
span.SetAttributes(attributes...)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package libpack_trace
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type TraceTestSuite struct {
|
||||
suite.Suite
|
||||
logger *libpack_logging.Logger
|
||||
}
|
||||
|
||||
func (suite *TraceTestSuite) SetupTest() {
|
||||
suite.logger = libpack_logging.New()
|
||||
}
|
||||
|
||||
func (suite *TraceTestSuite) TearDownTest() {
|
||||
// Any cleanup logic can be added here
|
||||
}
|
||||
|
||||
func TestTraceTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(TraceTestSuite))
|
||||
}
|
||||
|
||||
func (suite *TraceTestSuite) Test_NewClient() {
|
||||
shutdownFunc, err := NewClient(suite.logger, "localhost:4317")
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotNil(suite.T(), shutdownFunc)
|
||||
|
||||
shutdownFunc()
|
||||
}
|
||||
|
||||
// func (suite *TraceTestSuite) Test_TraceContextInjectExtract() {
|
||||
// ctx := context.Background()
|
||||
// traceContext := TraceContextInject(ctx)
|
||||
// assert.NotEmpty(suite.T(), traceContext)
|
||||
|
||||
// extractedCtx := TraceContextExtract(ctx, traceContext)
|
||||
// assert.NotNil(suite.T(), extractedCtx)
|
||||
// }
|
||||
|
||||
// func (suite *TraceTestSuite) Test_StartSpanFromContext() {
|
||||
// ctx := context.Background()
|
||||
// ctx, span := StartSpanFromContext(ctx, "operation")
|
||||
// assert.NotNil(suite.T(), ctx)
|
||||
// assert.NotNil(suite.T(), span)
|
||||
// span.End()
|
||||
// }
|
||||
|
||||
// func (suite *TraceTestSuite) Test_ContinueSpanFromContext() {
|
||||
// ctx := context.Background()
|
||||
// ctx, span := ContinueSpanFromContext(ctx, "operation")
|
||||
// assert.NotNil(suite.T(), ctx)
|
||||
// assert.NotNil(suite.T(), span)
|
||||
// span.End()
|
||||
// }
|
||||
|
||||
// func (suite *TraceTestSuite) Test_AddAttributesToSpan() {
|
||||
// ctx := context.Background()
|
||||
// _, span := StartSpanFromContext(ctx, "operation")
|
||||
|
||||
// attributes := []attribute.KeyValue{
|
||||
// attribute.String("key1", "value1"),
|
||||
// attribute.Int("key2", 2),
|
||||
// }
|
||||
// AddAttributesToSpan(span, attributes...)
|
||||
// span.End()
|
||||
|
||||
// // Create an in-memory span exporter
|
||||
// exporter := tracetest.NewSpanRecorder()
|
||||
// tracerProvider := trace.NewTracerProvider(trace.WithSpanProcessor(exporter))
|
||||
// otel.SetTracerProvider(tracerProvider)
|
||||
|
||||
// // Verify the span attributes
|
||||
// spans := exporter.Ended()
|
||||
// assert.Len(suite.T(), spans, 1)
|
||||
// exportedSpan := spans[0]
|
||||
|
||||
// for _, attr := range attributes {
|
||||
// assert.Contains(suite.T(), exportedSpan.Attributes(), attr)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (suite *TraceTestSuite) Test_Shutdown() {
|
||||
// shutdownFunc, err := NewClient(suite.logger, "localhost:4317")
|
||||
// assert.NoError(suite.T(), err)
|
||||
// assert.NotNil(suite.T(), shutdownFunc)
|
||||
|
||||
// shutdownFunc()
|
||||
// logOutput := captureStdOut(func() { suite.logger.Info(&libpack_logging.LogMessage{Message: "Shutting down tracer"}) })
|
||||
// assert.Contains(suite.T(), logOutput, "Shutting down tracer")
|
||||
// }
|
||||
|
||||
// // Helper function to capture standard output for testing logs
|
||||
// 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()
|
||||
// }
|
||||
Reference in New Issue
Block a user