Compare commits

..
8 Commits
28 changed files with 1009 additions and 1182 deletions
+10 -10
View File
@@ -1,4 +1,4 @@
name: Autoupdate go.mod and go.sum name: Test and release
on: on:
workflow_dispatch: workflow_dispatch:
@@ -34,16 +34,15 @@ jobs:
# This job is responsible for running tests and linting the codebase # This job is responsible for running tests and linting the codebase
test: test:
name: "Unit testing" name: "Unit testing"
# needs: [prepare]
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: golang:1 container: golang:1
# container: github/super-linter:v4
needs: [prepare] needs: [prepare]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0 # Ensure full history is checked out
token: ${{ secrets.GHCR_TOKEN }}
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
@@ -63,11 +62,12 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: | run: |
CI_RUN=${CI} make test 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 - name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5 run: |
with: git config --global user.email "github-actions[bot]@users.noreply.github.com"
commit_message: "Update go.mod and go.sum" git config --global user.name "github-actions[bot]"
commit_options: "--no-verify --signoff" git add go.mod go.sum
file_pattern: "go.mod go.sum" git commit -m "Update go.mod and go.sum"
git push
-25
View File
@@ -15,12 +15,6 @@ on:
env: env:
GO_VERSION: ">=1.21" 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: jobs:
# This job is responsible for preparation of the build # This job is responsible for preparation of the build
# environment variables. # environment variables.
@@ -84,26 +78,7 @@ jobs:
apt-get install ca-certificates make -y apt-get install ca-certificates make -y
update-ca-certificates update-ca-certificates
go mod tidy go mod tidy
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Run unit tests - name: Run unit tests
run: | run: |
CI_RUN=${CI} make test 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"
-52
View File
@@ -10,15 +10,6 @@ on:
branches: branches:
- "main" - "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: jobs:
shared: shared:
uses: telegram-bot-app/ci-scripts/.github/workflows/build-test-publish-inject.yaml@main uses: telegram-bot-app/ci-scripts/.github/workflows/build-test-publish-inject.yaml@main
@@ -27,46 +18,3 @@ jobs:
should-deploy: false should-deploy: false
secrets: secrets:
ghcr-token: ${{ secrets.GHCR_TOKEN }} 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"
+17 -2
View File
@@ -2,7 +2,7 @@
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. 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. [Benchmarks](https://lukaszraczylo.github.io/graphql-monitoring-proxy/dev/bench/) are available. 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.
![Example of monitoring dashboard](static/monitoring-at-glance.png?raw=true) ![Example of monitoring dashboard](static/monitoring-at-glance.png?raw=true)
@@ -28,6 +28,7 @@ This project is in active use by [telegram-bot.app](https://telegram-bot.app), a
- [Cache operations](#cache-operations) - [Cache operations](#cache-operations)
- [General](#general) - [General](#general)
- [Metrics which matter](#metrics-which-matter) - [Metrics which matter](#metrics-which-matter)
- [Tracing](#tracing)
- [Healthcheck](#healthcheck) - [Healthcheck](#healthcheck)
- [Monitoring endpoint](#monitoring-endpoint) - [Monitoring endpoint](#monitoring-endpoint)
@@ -103,7 +104,7 @@ In this case, both proxy and websockets will be available under the `/v1/graphql
| security | Blocking mutations in read-only mode | | security | Blocking mutations in read-only mode |
| security | Allow access only to listed URLs | | security | Allow access only to listed URLs |
| security | Ban / unban specific user from accessing the application | | security | Ban / unban specific user from accessing the application |
| maintenance | Hasura events cleaner | | maintenance | Hasura event cleaner |
### Configuration ### Configuration
@@ -144,6 +145,8 @@ 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` | Enable the hasura event cleaner | `false` |
| `HASURA_EVENT_CLEANER_OLDER_THAN` | The interval for the hasura event cleaner (in days) | `1` | | `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` | | `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 ### Speed
@@ -293,6 +296,18 @@ 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. 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 #### 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. 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.
+78 -115
View File
@@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"sync"
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
@@ -14,66 +13,55 @@ import (
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
) )
var ( var bannedUsersIDs map[string]string = make(map[string]string)
bannedUsersIDs = make(map[string]string)
bannedUsersIDsMutex sync.RWMutex
)
func enableApi() { func enableApi() {
if !cfg.Server.EnableApi { if cfg.Server.EnableApi {
return apiserver := fiber.New(fiber.Config{
} DisableStartupMessage: true,
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
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() { func periodicallyReloadBannedUsers() {
ticker := time.NewTicker(10 * time.Second) for {
defer ticker.Stop()
for range ticker.C {
loadBannedUsers() loadBannedUsers()
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Banned users reloaded", Message: "Banned users reloaded",
Pairs: map[string]interface{}{"users": bannedUsersIDs}, Pairs: map[string]interface{}{"users": bannedUsersIDs},
}) })
<-time.After(10 * time.Second)
} }
} }
func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool { func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
bannedUsersIDsMutex.RLock()
_, found := bannedUsersIDs[userID] _, found := bannedUsersIDs[userID]
bannedUsersIDsMutex.RUnlock()
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Checking if user is banned", Message: "Checking if user is banned",
Pairs: map[string]interface{}{"user_id": userID, "banned": found}, Pairs: map[string]interface{}{"user_id": userID, "found": found},
}) })
if found { if found {
cfg.Logger.Info(&libpack_logger.LogMessage{ cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "User is banned", Message: "User is banned",
Pairs: map[string]interface{}{"user_id": userID}, Pairs: map[string]interface{}{"user_id": userID},
}) })
c.Status(fiber.StatusForbidden).SendString("User is banned") c.Status(403).SendString("User is banned")
} }
return found return found
} }
@@ -81,16 +69,28 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
func apiClearCache(c *fiber.Ctx) error { func apiClearCache(c *fiber.Ctx) error {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Clearing cache via API", Message: "Clearing cache via API",
Pairs: nil,
}) })
libpack_cache.CacheClear() libpack_cache.CacheClear()
cfg.Logger.Info(&libpack_logger.LogMessage{ cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Cache cleared via API", Message: "Cache cleared via API",
Pairs: nil,
}) })
return c.SendString("OK: cache cleared") c.Status(200).SendString("OK: cache cleared")
return nil
} }
func apiCacheStats(c *fiber.Ctx) error { func apiCacheStats(c *fiber.Ctx) error {
return c.JSON(libpack_cache.GetCacheStats()) 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
} }
type apiBanUserRequest struct { type apiBanUserRequest struct {
@@ -100,92 +100,71 @@ type apiBanUserRequest struct {
func apiBanUser(c *fiber.Ctx) error { func apiBanUser(c *fiber.Ctx) error {
var req apiBanUserRequest var req apiBanUserRequest
if err := c.BodyParser(&req); err != nil { err := c.BodyParser(&req)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the ban user request", Message: "Can't parse the ban user request",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
return c.Status(fiber.StatusBadRequest).SendString("Invalid request payload") return err
} }
if req.UserID == "" || req.Reason == "" {
return c.Status(fiber.StatusBadRequest).SendString("user_id and reason are required")
}
bannedUsersIDsMutex.Lock()
bannedUsersIDs[req.UserID] = req.Reason bannedUsersIDs[req.UserID] = req.Reason
bannedUsersIDsMutex.Unlock()
cfg.Logger.Info(&libpack_logger.LogMessage{ cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Banned user", Message: "Banned user",
Pairs: map[string]interface{}{"user_id": req.UserID, "reason": req.Reason}, Pairs: map[string]interface{}{"user_id": req.UserID, "reason": req.Reason},
}) })
storeBannedUsers()
if err := storeBannedUsers(); err != nil { c.Status(200).SendString("OK: user banned")
return c.Status(fiber.StatusInternalServerError).SendString("Failed to store banned users") return nil
}
return c.SendString("OK: user banned")
} }
func apiUnbanUser(c *fiber.Ctx) error { func apiUnbanUser(c *fiber.Ctx) error {
var req apiBanUserRequest var req apiBanUserRequest
if err := c.BodyParser(&req); err != nil { err := c.BodyParser(&req)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the unban user request", Message: "Can't parse the unban user request",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
return c.Status(fiber.StatusBadRequest).SendString("Invalid request payload") return err
} }
if req.UserID == "" {
return c.Status(fiber.StatusBadRequest).SendString("user_id is required")
}
bannedUsersIDsMutex.Lock()
delete(bannedUsersIDs, req.UserID) delete(bannedUsersIDs, req.UserID)
bannedUsersIDsMutex.Unlock()
cfg.Logger.Info(&libpack_logger.LogMessage{ cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Unbanned user", Message: "Unbanned user",
Pairs: map[string]interface{}{"user_id": req.UserID}, Pairs: map[string]interface{}{"user_id": req.UserID},
}) })
storeBannedUsers()
if err := storeBannedUsers(); err != nil { c.Status(200).SendString("OK: user unbanned")
return c.Status(fiber.StatusInternalServerError).SendString("Failed to store banned users") return nil
}
return c.SendString("OK: user unbanned")
} }
func storeBannedUsers() error { func storeBannedUsers() {
fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile)) fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
if err := lockFile(fileLock); err != nil { err := fileLock.Lock()
return err if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't lock the file",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
} }
defer fileLock.Unlock() defer fileLock.Unlock()
bannedUsersIDsMutex.RLock()
data, err := json.Marshal(bannedUsersIDs) data, err := json.Marshal(bannedUsersIDs)
bannedUsersIDsMutex.RUnlock()
if err != nil { if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't marshal banned users", Message: "Can't marshal banned users",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
return err return
} }
err = os.WriteFile(cfg.Api.BannedUsersFile, data, 0644)
if err := os.WriteFile(cfg.Api.BannedUsersFile, data, 0644); err != nil { if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't write banned users to file", Message: "Can't write banned users to file",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
return err return
} }
return nil
} }
func loadBannedUsers() { func loadBannedUsers() {
@@ -194,9 +173,19 @@ func loadBannedUsers() {
Message: "Banned users file doesn't exist - creating it", Message: "Banned users file doesn't exist - creating it",
Pairs: map[string]interface{}{"file": cfg.Api.BannedUsersFile}, Pairs: map[string]interface{}{"file": cfg.Api.BannedUsersFile},
}) })
if err := os.WriteFile(cfg.Api.BannedUsersFile, []byte("{}"), 0644); err != nil { _, err := os.Create(cfg.Api.BannedUsersFile)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't create and write to the file", 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",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
return return
@@ -204,7 +193,8 @@ func loadBannedUsers() {
} }
fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile)) fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
if err := lockFileRead(fileLock); err != nil { err := fileLock.RLock() // Use RLock for read lock
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't lock the file [load]", Message: "Can't lock the file [load]",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
@@ -221,39 +211,12 @@ func loadBannedUsers() {
}) })
return return
} }
err = json.Unmarshal(data, &bannedUsersIDs)
var newBannedUsers map[string]string if err != nil {
if err := json.Unmarshal(data, &newBannedUsers); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't unmarshal banned users", Message: "Can't unmarshal banned users",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
return 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
} }
+2 -27
View File
@@ -1,9 +1,6 @@
package libpack_cache package libpack_cache
import ( import (
"bytes"
"compress/gzip"
"io"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -58,7 +55,7 @@ func EnableCache(cfg *CacheConfig) {
} }
cacheStats = &CacheStats{} cacheStats = &CacheStats{}
if ShouldUseRedisCache(cfg) { if ShouldUseRedisCache(cfg) {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Using Redis cache", Message: "Using Redis cache",
}) })
cfg.Client = libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{ cfg.Client = libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
@@ -67,7 +64,7 @@ func EnableCache(cfg *CacheConfig) {
RedisPassword: cfg.Redis.Password, RedisPassword: cfg.Redis.Password,
}) })
} else { } else {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Using in-memory cache", Message: "Using in-memory cache",
}) })
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second) cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
@@ -79,28 +76,6 @@ func CacheLookup(hash string) []byte {
obj, found := config.Client.Get(hash) obj, found := config.Client.Get(hash)
if found { if found {
atomic.AddInt64(&cacheStats.CacheHits, 1) 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 return obj
} }
atomic.AddInt64(&cacheStats.CacheMisses, 1) atomic.AddInt64(&cacheStats.CacheMisses, 1)
+12 -19
View File
@@ -4,8 +4,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/alicebob/miniredis/v2"
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" 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" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
) )
@@ -20,7 +20,6 @@ func BenchmarkCacheLookupInMemory(b *testing.B) {
Client: libpack_cache_memory.New(5 * time.Minute), Client: libpack_cache_memory.New(5 * time.Minute),
TTL: 5, TTL: 5,
} }
EnableCache(config)
hash := "00000000000000000000000000000000001337" hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.") data := []byte("it's fine.")
@@ -37,18 +36,16 @@ func BenchmarkCacheLookupInMemory(b *testing.B) {
} }
func BenchmarkCacheLookupRedis(b *testing.B) { func BenchmarkCacheLookupRedis(b *testing.B) {
redis_server, err := miniredis.Run() mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
if err != nil { RedisServer: redisMockServer.Addr(),
panic(err) RedisDB: 0,
} })
config = &CacheConfig{ config = &CacheConfig{
Logger: libpack_logger.New(), Logger: libpack_logger.New(),
Client: mockedCache,
TTL: 5, TTL: 5,
} }
config.Redis.DB = 0
config.Redis.URL = redis_server.Addr()
config.Redis.Enable = true
EnableCache(config)
hash := "00000000000000000000000000000000001337" hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.") data := []byte("it's fine.")
@@ -70,7 +67,6 @@ func BenchmarkCacheStoreInMemory(b *testing.B) {
Client: libpack_cache_memory.New(5 * time.Minute), Client: libpack_cache_memory.New(5 * time.Minute),
TTL: 5, TTL: 5,
} }
EnableCache(config)
hash := "00000000000000000000000000000000001337" hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.") data := []byte("it's fine.")
@@ -86,19 +82,16 @@ func BenchmarkCacheStoreInMemory(b *testing.B) {
} }
func BenchmarkCacheStoreRedis(b *testing.B) { func BenchmarkCacheStoreRedis(b *testing.B) {
redis_server, err := miniredis.Run() mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
if err != nil { RedisServer: redisMockServer.Addr(),
panic(err) RedisDB: 0,
} })
config = &CacheConfig{ config = &CacheConfig{
Logger: libpack_logger.New(), Logger: libpack_logger.New(),
Client: mockedCache,
TTL: 5, TTL: 5,
} }
config.Redis.DB = 0
config.Redis.URL = redis_server.Addr()
config.Redis.Enable = true
EnableCache(config)
hash := "00000000000000000000000000000000001337" hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.") data := []byte("it's fine.")
+13 -100
View File
@@ -1,12 +1,10 @@
package libpack_cache package libpack_cache
import ( import (
"fmt"
"sync"
"time" "time"
"github.com/alicebob/miniredis/v2"
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" 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" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
) )
@@ -60,15 +58,23 @@ func (suite *Tests) Test_cacheLookupInmemory() {
} }
func (suite *Tests) Test_cacheLookupRedis() { 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{ config = &CacheConfig{
Logger: libpack_logger.New(), Logger: libpack_logger.New(),
Client: mockedCache,
TTL: 5, TTL: 5,
} }
config.Redis.DB = 0
config.Redis.URL = redisMockServer.Addr()
config.Redis.Enable = true
EnableCache(config)
type args struct { type args struct {
hash string hash string
@@ -111,96 +117,3 @@ 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")
}
+3 -9
View File
@@ -69,17 +69,11 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
func (c *Cache) Get(key string) ([]byte, bool) { func (c *Cache) Get(key string) ([]byte, bool) {
entry, ok := c.entries.Load(key) entry, ok := c.entries.Load(key)
if !ok { if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
return nil, false return nil, false
} }
compressedValue := entry.(CacheEntry).Value
cacheEntry := entry.(CacheEntry) value, err := c.decompress(compressedValue)
if cacheEntry.ExpiresAt.Before(time.Now()) {
c.entries.Delete(key)
return nil, false
}
value, err := c.decompress(cacheEntry.Value)
if err != nil { if err != nil {
log.Printf("Error decompressing value for key %s: %v", key, err) log.Printf("Error decompressing value for key %s: %v", key, err)
return nil, false return nil, false
-28
View File
@@ -1,7 +1,6 @@
package libpack_cache_memory package libpack_cache_memory
import ( import (
"fmt"
"testing" "testing"
"time" "time"
) )
@@ -53,30 +52,3 @@ func BenchmarkMemCacheStats(b *testing.B) {
cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve
cache.Get(key) 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)
}
}
-56
View File
@@ -1,8 +1,6 @@
package libpack_cache_memory package libpack_cache_memory
import ( import (
"fmt"
"sync"
"testing" "testing"
"time" "time"
@@ -112,57 +110,3 @@ 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)
}
+22 -30
View File
@@ -11,14 +11,18 @@ import (
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
) )
const defaultValue = "-" func extractClaimsFromJWTHeader(authorization string) (usr string, role string) {
usr, role = "-", "-"
var emptyMetrics = map[string]string{} 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,
})
}
func extractClaimsFromJWTHeader(authorization string) (usr, role string) { tokenParts := strings.Split(authorization, ".")
usr, role = defaultValue, defaultValue
tokenParts := strings.SplitN(authorization, ".", 3)
if len(tokenParts) != 3 { if len(tokenParts) != 3 {
handleError("Can't split the token", map[string]interface{}{"token": authorization}) handleError("Can't split the token", map[string]interface{}{"token": authorization})
return return
@@ -36,30 +40,18 @@ func extractClaimsFromJWTHeader(authorization string) (usr, role string) {
return return
} }
usr = extractClaim(claimMap, cfg.Client.JWTUserClaimPath, "user id") extractClaim := func(claimPath string, target *string, name string) {
role = extractClaim(claimMap, cfg.Client.JWTRoleClaimPath, "role") 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")
return 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,
})
}
+58 -65
View File
@@ -5,84 +5,77 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
) )
const (
initialDelay = 60 * time.Second
cleanupInterval = 1 * time.Hour
)
var delQueries = [...]string{
"DELETE FROM hdb_catalog.event_invocation_logs WHERE created_at < NOW() - interval '%d days';",
"DELETE FROM hdb_catalog.event_log WHERE created_at < NOW() - interval '%d days';",
"DELETE FROM hdb_catalog.hdb_action_log WHERE created_at < NOW() - INTERVAL '%d days';",
"DELETE FROM hdb_catalog.hdb_cron_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';",
"DELETE FROM hdb_catalog.hdb_scheduled_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';",
}
func enableHasuraEventCleaner() { func enableHasuraEventCleaner() {
if !cfg.HasuraEventCleaner.Enable { 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",
if cfg.HasuraEventCleaner.EventMetadataDb == "" { Pairs: nil,
cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Event metadata db URL not specified, event cleaner not active",
})
return
}
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Event cleaner enabled",
Pairs: map[string]interface{}{"interval_in_days": cfg.HasuraEventCleaner.ClearOlderThan},
})
go func() {
pool, err := pgxpool.New(context.Background(), cfg.HasuraEventCleaner.EventMetadataDb)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Failed to create connection pool",
Pairs: map[string]interface{}{"error": err.Error()},
}) })
return return
} }
defer pool.Close()
time.Sleep(initialDelay) 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{ cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Initial cleanup of old events", Message: "Initial cleanup of old events",
Pairs: nil,
}) })
cleanEvents(pool) cleanEvents()
ticker := time.NewTicker(cleanupInterval) for {
defer ticker.Stop() select {
case <-ticker.C:
for range ticker.C { cfg.Logger.Info(&libpack_logger.LogMessage{
cfg.Logger.Info(&libpack_logger.LogMessage{ Message: "Cleaning up old events",
Message: "Cleaning up old events", Pairs: nil,
}) })
cleanEvents(pool) cleanEvents()
} }
}()
}
func cleanEvents(pool *pgxpool.Pool) {
ctx := context.Background()
for _, query := range delQueries {
_, err := pool.Exec(ctx, fmt.Sprintf(query, cfg.HasuraEventCleaner.ClearOlderThan))
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Failed to execute query",
Pairs: map[string]interface{}{"query": query, "error": err.Error()},
})
} else {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Successfully executed query",
Pairs: map[string]interface{}{"query": query},
})
} }
} }
} }
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},
})
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),
}
for _, query := range delQueries {
_, err := conn.Exec(context.Background(), query)
if err != nil {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Failed to execute query",
Pairs: map[string]interface{}{"query": query, "error": err},
})
}
}
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Old events cleaned up",
Pairs: nil,
})
}
+32 -18
View File
@@ -1,55 +1,69 @@
module github.com/lukaszraczylo/graphql-monitoring-proxy module github.com/lukaszraczylo/graphql-monitoring-proxy
go 1.22.4 go 1.21
require ( require (
github.com/VictoriaMetrics/metrics v1.35.1 github.com/VictoriaMetrics/metrics v1.33.1
github.com/alicebob/miniredis/v2 v2.33.0 github.com/alicebob/miniredis/v2 v2.33.0
github.com/avast/retry-go/v4 v4.6.0 github.com/avast/retry-go/v4 v4.6.0
github.com/goccy/go-json v0.10.3 github.com/goccy/go-json v0.10.3
github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/fiber/v2 v2.52.4
github.com/gofrs/flock v0.12.1 github.com/gofrs/flock v0.8.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gookit/goutil v0.6.16 github.com/gookit/goutil v0.6.15
github.com/graphql-go/graphql v0.8.1 github.com/graphql-go/graphql v0.8.1
github.com/jackc/pgx/v5 v5.6.0 github.com/jackc/pgx/v5 v5.6.0
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415
github.com/lukaszraczylo/go-ratecounter v0.1.12 github.com/lukaszraczylo/go-ratecounter v0.1.8
github.com/lukaszraczylo/go-simple-graphql v1.2.19 github.com/lukaszraczylo/go-simple-graphql v1.2.14
github.com/redis/go-redis/v9 v9.6.1 github.com/redis/go-redis/v9 v9.5.3
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/valyala/fasthttp v1.55.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
) )
require ( require (
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/andybalholm/brotli v1.1.0 // 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/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/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/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/zerolog v1.33.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect github.com/valyala/histogram v1.2.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect
golang.org/x/crypto v0.26.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect
golang.org/x/net v0.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/crypto v0.24.0 // indirect
golang.org/x/sys v0.23.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/term v0.23.0 // indirect golang.org/x/net v0.26.0 // indirect
golang.org/x/text v0.17.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
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+73 -31
View File
@@ -1,5 +1,5 @@
github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU= github.com/VictoriaMetrics/metrics v1.33.1 h1:CNV3tfm2Kpv7Y9W3ohmvqgFWPR55tV2c7M2U6OIo+UM=
github.com/VictoriaMetrics/metrics v1.35.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= github.com/VictoriaMetrics/metrics v1.33.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 h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
@@ -12,28 +12,40 @@ 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/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 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/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 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.16 h1:9fRMCF4X9abdRD5+2HhBS/GwafjBlTUBjRtA5dgkvuw= github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
github.com/gookit/goutil v0.6.16/go.mod h1:op2q8AoPDFSiY2+qkHxcBWQMYxOLQ1GbLXqe7vrwscI= github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= 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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -50,26 +62,31 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:lvI8Wlbg4PxkRcg2f10wgoaRpfN19v+YdRek3+dLtlM=
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c= github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c=
github.com/lukaszraczylo/go-ratecounter v0.1.12 h1:VO6hHYGw/Jy9JUizXf/bS0AI2QX1ueWWAWckMFVJ/w4= github.com/lukaszraczylo/go-ratecounter v0.1.8 h1:ZYm6Wkn58ZAlFWRmC7PaD4oAYHWcu8/0MUDWGe3PnJQ=
github.com/lukaszraczylo/go-ratecounter v0.1.12/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM= github.com/lukaszraczylo/go-ratecounter v0.1.8/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
github.com/lukaszraczylo/go-simple-graphql v1.2.19 h1:7MebwQP1epVos+D5nDOAZDVtiPbZSy+JfJzixgoUMoA= github.com/lukaszraczylo/go-simple-graphql v1.2.14 h1:Dth+yZ+1ialCpnslSb6UgHbXszExjDUu/I95QZbnWVU=
github.com/lukaszraczylo/go-simple-graphql v1.2.19/go.mod h1:0PKR4rmAYrtZ93yMMKvLqL70GNvce3xascdYtCV7REo= github.com/lukaszraczylo/go-simple-graphql v1.2.14/go.mod h1:pSKmm9OLGoS9pjmIvhBB/fo0+LganRrL29CN3fdkRPw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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-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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -89,22 +106,47 @@ 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/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 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+102 -108
View File
@@ -3,8 +3,6 @@ package main
import ( import (
"strconv" "strconv"
"strings" "strings"
"sync"
"unsafe"
"github.com/goccy/go-json" "github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v2"
@@ -14,29 +12,48 @@ import (
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
) )
var ( var introspection_queries = []string{
introspectionQueries = map[string]struct{}{ "__schema",
"__schema": {}, "__type": {}, "__typename": {}, "__directive": {}, "__type",
"__directivelocation": {}, "__field": {}, "__inputvalue": {}, "__typename",
"__enumvalue": {}, "__typekind": {}, "__fieldtype": {}, "__directive",
"__inputobjecttype": {}, "__enumtype": {}, "__uniontype": {}, "__directivelocation",
"__scalars": {}, "__objects": {}, "__interfaces": {}, "__field",
"__unions": {}, "__enums": {}, "__inputobjects": {}, "__directives": {}, "__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{}{}
} }
introspectionAllowedQueries = make(map[string]struct{}) return resultMap
allowedUrls = make(map[string]struct{}) }
mu sync.RWMutex
)
func prepareQueriesAndExemptions() { func prepareQueriesAndExemptions() {
mu.Lock() introspectionQuerySet = sliceToMap(introspection_queries)
defer mu.Unlock() introspectionAllowedQueries = sliceToMap(cfg.Security.IntrospectionAllowed)
for _, q := range cfg.Security.IntrospectionAllowed { allowedUrls = sliceToMap(cfg.Server.AllowURLs)
introspectionAllowedQueries[strings.ToLower(q)] = struct{}{}
}
for _, u := range cfg.Server.AllowURLs {
allowedUrls[u] = struct{}{}
}
} }
type parseGraphQLQueryResult struct { type parseGraphQLQueryResult struct {
@@ -50,43 +67,21 @@ type parseGraphQLQueryResult struct {
shouldIgnore bool shouldIgnore bool
} }
var ( func parseGraphQLQuery(c *fiber.Ctx) (res *parseGraphQLQueryResult) {
queryPool = sync.Pool{ res = &parseGraphQLQueryResult{shouldIgnore: true}
New: func() interface{} { m := make(map[string]interface{})
return make(map[string]interface{}, 48) err := json.Unmarshal(c.Body(), &m)
}, if err != nil {
}
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{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't unmarshal the request", Message: "Can't unmarshal the request",
Pairs: map[string]interface{}{"error": err.Error(), "body": unsafeString(c.Body())}, Pairs: map[string]interface{}{"error": err.Error(), "body": string(c.Body())},
}) })
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
} }
resultPool.Put(res) return
return res
} }
// get the query
query, ok := m["query"].(string) query, ok := m["query"].(string)
if !ok { if !ok {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
@@ -96,8 +91,7 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
} }
resultPool.Put(res) return
return res
} }
p, err := parser.Parse(parser.ParseParams{Source: query}) p, err := parser.Parse(parser.ParseParams{Source: query})
@@ -109,66 +103,60 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
} }
resultPool.Put(res) return
return res
} }
res.shouldIgnore = false res.shouldIgnore = false
res.operationName = "undefined" res.operationName = "undefined"
res.activeEndpoint = cfg.Server.HostGraphQL
for _, d := range p.Definitions { for _, d := range p.Definitions {
if oper, ok := d.(*ast.OperationDefinition); ok { if oper, ok := d.(*ast.OperationDefinition); ok {
if res.operationType == "" { res.operationType = strings.ToLower(oper.Operation)
res.operationType = strings.ToLower(oper.Operation)
if oper.Name != nil { if oper.Name != nil {
res.operationName = oper.Name.Value res.operationName = oper.Name.Value
}
} }
if cfg.Server.HostGraphQLReadOnly != "" { // If the query is a mutation then direct it to the RW endpoint,
if res.operationType == "" { // otherwise direct it to the RO endpoint if it's set.
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly if cfg.Server.HostGraphQLReadOnly != "" && res.operationType != "mutation" {
} else if res.operationType != "mutation" { res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
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 { if res.operationType == "mutation" && cfg.Server.ReadOnlyMode {
cfg.Logger.Warning(&libpack_logger.LogMessage{ cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Mutation blocked - server in read-only mode", Message: "Mutation blocked",
Pairs: map[string]interface{}{"query": query}, Pairs: map[string]interface{}{"query": query},
}) })
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) 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 res.shouldBlock = true
resultPool.Put(res) return
return res
} }
for _, dir := range oper.Directives { for _, dir := range oper.Directives {
if dir.Name.Value == "cached" { if dir.Name.Value == "cached" {
res.cacheRequest = true res.cacheRequest = true
for _, arg := range dir.Arguments { for _, arg := range dir.Arguments {
switch arg.Name.Value { if arg.Name.Value == "ttl" {
case "ttl": res.cacheTime, err = strconv.Atoi(arg.Value.GetValue().(string))
if v, ok := arg.Value.GetValue().(string); ok { if err != nil {
res.cacheTime, _ = strconv.Atoi(v) cfg.Logger.Error(&libpack_logger.LogMessage{
} Message: "Can't parse the ttl, using global",
case "refresh": Pairs: map[string]interface{}{"bad_ttl": arg.Value.GetValue().(string)},
if v, ok := arg.Value.GetValue().(bool); ok { })
res.cacheRefresh = v if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
return
} }
} }
if arg.Name.Value == "refresh" {
res.cacheRefresh = arg.Value.GetValue().(bool)
}
} }
} }
} }
@@ -176,26 +164,26 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
if cfg.Security.BlockIntrospection { if cfg.Security.BlockIntrospection {
res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections) res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections)
if res.shouldBlock { if res.shouldBlock {
resultPool.Put(res) return
return res
} }
} }
} }
} }
return res return
}
func unsafeString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
} }
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool { func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
for _, s := range selections { for _, s := range selections {
if field, ok := s.(*ast.Field); ok { field, ok := s.(*ast.Field)
if checkIfContainsIntrospection(c, field.Name.Value) { if !ok {
return true continue // or handle the case where the type assertion fails
} }
if field.SelectionSet != nil && checkSelections(c, field.GetSelectionSet().Selections) { shouldBlock := checkIfContainsIntrospection(c, field.Name.Value)
if shouldBlock {
return true
}
if field.SelectionSet != nil {
if checkSelections(c, field.GetSelectionSet().Selections) {
return true return true
} }
} }
@@ -203,26 +191,32 @@ func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
return false return false
} }
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) bool { func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) (shouldBlock bool) {
whateverLower := strings.ToLower(whatever) whateverLower := strings.ToLower(whatever)
mu.RLock() got_exemption := false
defer mu.RUnlock()
if _, exists := introspectionQueries[whateverLower]; exists { // If the query is an introspection query, we need to check if it's allowed.
if _, exists := introspectionQuerySet[whateverLower]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 { if len(cfg.Security.IntrospectionAllowed) > 0 {
if _, allowed := introspectionAllowedQueries[whateverLower]; allowed {
if _, allowed_exists := introspectionAllowedQueries[whateverLower]; allowed_exists {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Introspection query allowed, passing through", Message: "Introspection query allowed, passing through",
Pairs: map[string]interface{}{"query": whatever}, Pairs: map[string]interface{}{"query": whatever},
}) })
return false got_exemption = true
shouldBlock = false
} }
} }
if !got_exemption {
shouldBlock = true
}
}
if shouldBlock {
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) 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 false return
} }
-112
View File
@@ -1,10 +1,6 @@
package main package main
import ( import (
"fmt"
"strings"
fiber "github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@@ -322,111 +318,3 @@ 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
}
+18 -4
View File
@@ -12,12 +12,11 @@ import (
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
) )
var ( var cfg *config
cfg *config var once sync.Once
once sync.Once
)
// function get value from the env where the value can be anything // function get value from the env where the value can be anything
func getDetailsFromEnv[T any](key string, defaultValue T) T { func getDetailsFromEnv[T any](key string, defaultValue T) T {
@@ -90,6 +89,8 @@ func parseConfig() {
c.HasuraEventCleaner.Enable = getDetailsFromEnv("HASURA_EVENT_CLEANER", false) c.HasuraEventCleaner.Enable = getDetailsFromEnv("HASURA_EVENT_CLEANER", false)
c.HasuraEventCleaner.ClearOlderThan = getDetailsFromEnv("HASURA_EVENT_CLEANER_OLDER_THAN", 1) c.HasuraEventCleaner.ClearOlderThan = getDetailsFromEnv("HASURA_EVENT_CLEANER_OLDER_THAN", 1)
c.HasuraEventCleaner.EventMetadataDb = getDetailsFromEnv("HASURA_EVENT_METADATA_DB", "") 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 cfg = &c
if cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable { if cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable {
@@ -105,8 +106,21 @@ func parseConfig() {
} }
libpack_cache.EnableCache(cacheConfig) libpack_cache.EnableCache(cacheConfig)
} }
loadRatelimitConfig() loadRatelimitConfig()
once.Do(func() { 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 enableApi()
go enableHasuraEventCleaner() go enableHasuraEventCleaner()
}) })
-26
View File
@@ -112,29 +112,3 @@ 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)
})
}
}
+48 -37
View File
@@ -12,14 +12,17 @@ import (
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
) )
// Cache for sorted label keys to avoid repeated sorting
var sortedLabelKeysCache = struct { var sortedLabelKeysCache = struct {
m sync.Map m map[string][]string
}{} sync.RWMutex
}{m: make(map[string][]string)}
func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) string { func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) string {
const unknownPodName = "unknown" const unknownPodName = "unknown"
var buf bytes.Buffer var buf bytes.Buffer
// Prepare default labels without initializing a new map
podName := getPodName() podName := getPodName()
if labels == nil { if labels == nil {
labels = defaultLabels(podName) labels = defaultLabels(podName)
@@ -27,16 +30,18 @@ func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string)
ensureDefaultLabels(&labels, podName) ensureDefaultLabels(&labels, podName)
} }
// Prefix handling
if ms.metrics_prefix != "" { if ms.metrics_prefix != "" {
buf.WriteString(ms.metrics_prefix) buf.WriteString(ms.metrics_prefix)
buf.WriteByte('_') buf.WriteString("_")
} }
buf.WriteString(name) buf.WriteString(name)
// Append labels if any
if len(labels) > 0 { if len(labels) > 0 {
buf.WriteByte('{') buf.WriteString("{")
appendSortedLabels(&buf, labels) appendSortedLabels(&buf, labels)
buf.WriteByte('}') buf.WriteString("}")
} }
return buf.String() return buf.String()
@@ -73,64 +78,64 @@ func appendSortedLabels(buf *bytes.Buffer, labels map[string]string) {
keys := getSortedKeys(labels) keys := getSortedKeys(labels)
for i, k := range keys { for i, k := range keys {
if i > 0 { if i > 0 {
buf.WriteByte(',') buf.WriteString(",")
} }
buf.WriteString(k) buf.WriteString(k)
buf.WriteString(`="`) buf.WriteString("=\"")
buf.WriteString(labels[k]) buf.WriteString(labels[k])
buf.WriteByte('"') buf.WriteString("\"")
} }
} }
func getSortedKeys(labels map[string]string) []string { func getSortedKeys(labels map[string]string) []string {
labelsKey := labelsToString(labels) labelsKey := labelsToString(labels)
// Check if the sorted keys are already cached sortedLabelKeysCache.RLock()
if keys, ok := sortedLabelKeysCache.m.Load(labelsKey); ok { keys, exists := sortedLabelKeysCache.m[labelsKey]
return keys.([]string) sortedLabelKeysCache.RUnlock()
}
// Compute the sorted keys if !exists {
keys := make([]string, 0, len(labels)) keys = make([]string, 0, len(labels))
for k := range labels { for k := range labels {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Strings(keys) sort.Strings(keys)
// Store the sorted keys in the cache sortedLabelKeysCache.Lock()
sortedLabelKeysCache.m.Store(labelsKey, keys) sortedLabelKeysCache.m[labelsKey] = keys
sortedLabelKeysCache.Unlock()
}
return keys return keys
} }
func labelsToString(labels map[string]string) string { 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 var sb strings.Builder
for _, k := range keys { for k, v := range labels {
sb.WriteString(k) sb.WriteString(k)
sb.WriteByte('=') sb.WriteString("=")
sb.WriteString(labels[k]) sb.WriteString(v)
sb.WriteByte(';') sb.WriteString(";")
} }
return sb.String() 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 { func validate_metrics_name(name string) error {
cleanedName := clean_metric_name(name) cleanedName := clean_metric_name(name)
// Trim leading and trailing underscores
finalName := strings.Trim(cleanedName, "_") finalName := strings.Trim(cleanedName, "_")
// Check if the processed name matches the original input
if finalName != name { 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 return nil
} }
// clean_metric_name processes the metric name according to Prometheus naming conventions
func clean_metric_name(name string) string { func clean_metric_name(name string) string {
var buf bytes.Buffer var buf bytes.Buffer
lastWasUnderscore := false lastWasUnderscore := false
@@ -139,27 +144,31 @@ func clean_metric_name(name string) string {
if is_allowed_rune(r) { if is_allowed_rune(r) {
if is_special_rune(r) { if is_special_rune(r) {
if lastWasUnderscore { if lastWasUnderscore {
continue continue // Skip if the previous character was also an underscore
} }
r = '_' r = '_' // Convert spaces and special characters to underscores
lastWasUnderscore = true lastWasUnderscore = true
} else { } else {
lastWasUnderscore = false lastWasUnderscore = false
} }
buf.WriteRune(r) buf.WriteRune(r)
} else if !lastWasUnderscore { } else if !lastWasUnderscore {
buf.WriteByte('_') buf.WriteRune('_')
lastWasUnderscore = true lastWasUnderscore = true
} }
} }
return strings.Trim(buf.String(), "_") // Remove trailing underscore
result := buf.String()
return strings.Trim(result, "_")
} }
// is_allowed_rune checks if the rune is allowed in the metric name
func is_allowed_rune(r rune) bool { func is_allowed_rune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == ' ' || r == '_' 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 { func is_special_rune(r rune) bool {
return r == ' ' || r == '_' return r == ' ' || r == '_'
} }
@@ -169,12 +178,14 @@ func compile_metrics_with_labels(name string, labels map[string]string) string {
buf.WriteString(name) buf.WriteString(name)
// Collect keys and sort them
keys := getSortedKeys(labels) keys := getSortedKeys(labels)
// Append sorted key-value pairs to the buffer
for _, k := range keys { for _, k := range keys {
buf.WriteByte('_') buf.WriteString("_")
buf.WriteString(k) buf.WriteString(k)
buf.WriteByte('_') buf.WriteString("_")
buf.WriteString(labels[k]) buf.WriteString(labels[k])
} }
+20 -16
View File
@@ -1,3 +1,6 @@
// 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 package libpack_monitoring
import ( import (
@@ -19,7 +22,9 @@ type MetricsSetup struct {
metrics_prefix string metrics_prefix string
} }
var log = libpack_logger.New().SetMinLogLevel(libpack_logger.LEVEL_INFO) var (
log *libpack_logger.Logger
)
type InitConfig struct { type InitConfig struct {
PurgeOnCrawl bool PurgeOnCrawl bool
@@ -27,11 +32,11 @@ type InitConfig struct {
} }
func NewMonitoring(ic *InitConfig) *MetricsSetup { func NewMonitoring(ic *InitConfig) *MetricsSetup {
ms := &MetricsSetup{ log = libpack_logger.New().SetMinLogLevel(libpack_logger.LEVEL_INFO)
ic: ic, ms := &MetricsSetup{ic: ic}
metrics_set: metrics.NewSet(), ms.metrics_set = metrics.NewSet()
metrics_set_custom: metrics.NewSet(), ms.metrics_set_custom = metrics.NewSet()
} // if not testing, start the prometheus endpoint
if flag.Lookup("test.v") == nil { if flag.Lookup("test.v") == nil {
go ms.startPrometheusEndpoint() go ms.startPrometheusEndpoint()
@@ -55,11 +60,9 @@ func (ms *MetricsSetup) startPrometheusEndpoint() {
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION), AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
}) })
app.Get("/metrics", ms.metricsEndpoint) app.Get("/metrics", ms.metricsEndpoint)
if err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393))); err != nil { err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393)))
log.Critical(&libpack_logger.LogMessage{ if err != nil {
Message: "Can't start the service", fmt.Println("Can't start the service: ", err)
Pairs: map[string]interface{}{"error": err},
})
} }
} }
@@ -82,7 +85,7 @@ func (ms *MetricsSetup) ListActiveMetrics() []string {
} }
func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge { func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge {
if err := validate_metrics_name(metric_name); err != nil { if validate_metrics_name(metric_name) != nil {
log.Critical(&libpack_logger.LogMessage{ log.Critical(&libpack_logger.LogMessage{
Message: "RegisterMetricsGauge() error", Message: "RegisterMetricsGauge() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
@@ -90,12 +93,13 @@ func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[stri
return nil return nil
} }
return ms.metrics_set_custom.GetOrCreateGauge(ms.get_metrics_name(metric_name, labels), func() float64 { 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 return val
}) })
} }
func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter { func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter {
if err := validate_metrics_name(metric_name); err != nil { if validate_metrics_name(metric_name) != nil {
log.Critical(&libpack_logger.LogMessage{ log.Critical(&libpack_logger.LogMessage{
Message: "RegisterMetricsCounter() error", Message: "RegisterMetricsCounter() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
@@ -109,7 +113,7 @@ func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[st
} }
func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter { func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter {
if err := validate_metrics_name(metric_name); err != nil { if validate_metrics_name(metric_name) != nil {
log.Critical(&libpack_logger.LogMessage{ log.Critical(&libpack_logger.LogMessage{
Message: "RegisterFloatCounter() error", Message: "RegisterFloatCounter() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
@@ -120,7 +124,7 @@ func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[stri
} }
func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary { func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary {
if err := validate_metrics_name(metric_name); err != nil { if validate_metrics_name(metric_name) != nil {
log.Critical(&libpack_logger.LogMessage{ log.Critical(&libpack_logger.LogMessage{
Message: "RegisterMetricsSummary() error", Message: "RegisterMetricsSummary() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
@@ -131,7 +135,7 @@ func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[st
} }
func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram { func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram {
if err := validate_metrics_name(metric_name); err != nil { if validate_metrics_name(metric_name) != nil {
log.Critical(&libpack_logger.LogMessage{ log.Critical(&libpack_logger.LogMessage{
Message: "RegisterMetricsHistogram() error", Message: "RegisterMetricsHistogram() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
+85 -86
View File
@@ -1,30 +1,21 @@
package main package main
import ( import (
"bytes" "context"
"compress/gzip"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
"net/url"
"time" "time"
"github.com/avast/retry-go/v4" "github.com/avast/retry-go/v4"
"github.com/gofiber/fiber/v2" "github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy" "github.com/gofiber/fiber/v2/middleware/proxy"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
var (
httpClient *fasthttp.Client
)
func init() {
httpClient = createFasthttpClient(30) // Assuming a default timeout of 30 seconds
}
func createFasthttpClient(timeout int) *fasthttp.Client { func createFasthttpClient(timeout int) *fasthttp.Client {
return &fasthttp.Client{ return &fasthttp.Client{
Name: "graphql_proxy", Name: "graphql_proxy",
@@ -33,14 +24,15 @@ func createFasthttpClient(timeout int) *fasthttp.Client {
InsecureSkipVerify: true, InsecureSkipVerify: true,
}, },
MaxConnsPerHost: 2048, MaxConnsPerHost: 2048,
ReadTimeout: time.Duration(timeout) * time.Second, ReadTimeout: time.Second * time.Duration(timeout),
WriteTimeout: time.Duration(timeout) * time.Second, WriteTimeout: time.Second * time.Duration(timeout),
MaxIdleConnDuration: time.Duration(timeout) * time.Second, MaxIdleConnDuration: time.Second * time.Duration(timeout),
MaxConnDuration: time.Duration(timeout) * time.Second, MaxConnDuration: time.Second * time.Duration(timeout),
DisableHeaderNamesNormalizing: true, DisableHeaderNamesNormalizing: true,
} }
} }
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string, ctx context.Context) error {
if !checkAllowedURLs(c) { if !checkAllowedURLs(c) {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Request blocked", Message: "Request blocked",
@@ -49,22 +41,44 @@ func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
} }
return fmt.Errorf("request blocked - not allowed URL: %s", c.Path()) c.Status(403).SendString("Request blocked - not allowed URL")
} return nil
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" { if cfg.LogLevel == "debug" {
logDebugRequest(c) 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"),
},
})
} }
err = retry.Do( err := retry.Do(
func() error { func() error {
return proxy.DoRedirects(c, proxyURL, 3, httpClient) 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
}
return nil
}, },
retry.OnRetry(func(n uint, err error) { retry.OnRetry(func(n uint, err error) {
cfg.Logger.Warning(&libpack_logger.LogMessage{ cfg.Logger.Warning(&libpack_logger.LogMessage{
@@ -75,83 +89,68 @@ func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
}, },
}) })
}), }),
retry.Attempts(3), retry.Attempts(uint(3)),
retry.DelayType(retry.BackOffDelay), retry.DelayType(retry.BackOffDelay),
retry.Delay(250*time.Millisecond), retry.Delay(time.Duration(250*time.Millisecond)),
retry.LastErrorOnly(true), retry.LastErrorOnly(true),
) )
if err != nil { if err != nil {
cfg.Logger.Warning(&libpack_logger.LogMessage{ cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Can't proxy the request", Message: "Can't proxy the request",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{
"error": err.Error(),
},
}) })
if ifNotInTest() { return err
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
return fmt.Errorf("failed to proxy request: %v", err)
} }
if cfg.LogLevel == "debug" { if cfg.LogLevel == "debug" {
logDebugResponse(c) cfg.Logger.Debug(&libpack_logger.LogMessage{
} Message: "Received proxied response",
Pairs: map[string]interface{}{
if c.Response().Header.Peek("Content-Encoding") != nil && string(c.Response().Header.Peek("Content-Encoding")) == "gzip" { "path": c.Path(),
reader, err := gzip.NewReader(bytes.NewReader(c.Response().Body())) "response_body": string(c.Response().Body()),
if err != nil { "response_code": c.Response().StatusCode(),
cfg.Logger.Error(&libpack_logger.LogMessage{ "headers": c.GetRespHeaders(),
Message: "Failed to create gzip reader", "request_uuid": c.Locals("request_uuid"),
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 c.Response().StatusCode() != 200 {
if ifNotInTest() { if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
} }
return fmt.Errorf("received non-200 response from the GraphQL server: %d", c.Response().StatusCode()) 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())
} }
c.Response().Header.Del(fiber.HeaderServer) 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 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"),
},
})
}
+33 -92
View File
@@ -12,63 +12,47 @@ func (suite *Tests) Test_proxyTheRequest() {
} }
tests := []struct { tests := []struct {
headers map[string]string headers map[string]string
name string name string
body string body string
host string host string
hostRO string hostRO string
path string path string
wantErr bool wantErr bool
wantEndpoint string
}{ }{
{ {
name: "test_empty", name: "test_empty",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://telegram-bot.app/", host: "https://telegram-bot.app/",
path: "/v1/graphql", path: "/v1/graphql",
headers: supplied_headers, headers: supplied_headers,
wantErr: false, wantErr: false,
wantEndpoint: "https://telegram-bot.app/",
}, },
{ {
name: "test_wrong_url", name: "test_wrong_url",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://google.com/", host: "https://google.com/",
path: "/v1/wrongURL", path: "/v1/wrongURL",
headers: supplied_headers, headers: supplied_headers,
wantErr: true, wantErr: true,
wantEndpoint: "https://google.com/",
}, },
{ {
name: "Test read only mode", name: "Test read only mode",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://google.com/", host: "https://google.com/",
hostRO: "https://telegram-bot.app/", hostRO: "https://telegram-bot.app/",
path: "/v1/graphql", path: "/v1/graphql",
headers: supplied_headers, headers: supplied_headers,
wantErr: false, wantErr: false,
wantEndpoint: "https://telegram-bot.app/",
}, },
{ {
name: "Test read only mode wrong host", name: "Test read only mode wrong host",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://telegram-bot.app/", host: "https://telegram-bot.app/",
hostRO: "https://google.com/", hostRO: "https://google.com/",
path: "/v1/graphql",
path: "/v1/graphql", headers: supplied_headers,
headers: supplied_headers, wantErr: true,
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/",
}, },
} }
@@ -108,49 +92,6 @@ func (suite *Tests) Test_proxyTheRequest() {
} else { } else {
assert.Nil(err, "Error is not nil", tt.name) 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)
}
}) })
} }
} }
+63 -40
View File
@@ -2,80 +2,89 @@ package main
import ( import (
"os" "os"
"sync"
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
goratecounter "github.com/lukaszraczylo/go-ratecounter" goratecounter "github.com/lukaszraczylo/go-ratecounter"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
) )
type RateLimitConfig struct { type RateLimitConfig struct {
RateCounterTicker *goratecounter.RateCounter RateCounterTicker *goratecounter.RateCounter
Interval time.Duration `json:"interval"` Interval string `json:"interval"`
Req int `json:"req"` Req int `json:"req"`
} }
var ( var rateLimits map[string]RateLimitConfig
rateLimits = make(map[string]RateLimitConfig) var ratelimit_intervals = map[string]time.Duration{
rateLimitMu sync.RWMutex "milli": time.Millisecond,
) "micro": time.Microsecond,
"nano": time.Nanosecond,
"second": time.Second,
"minute": time.Minute,
"hour": time.Hour,
"day": time.Hour * 24,
}
func loadRatelimitConfig() error { func loadRatelimitConfig() error {
paths := []string{"/go/src/app/ratelimit.json", "./ratelimit.json", "./static/app/default-ratelimit.json"} paths := []string{"/go/src/app/ratelimit.json", "./ratelimit.json", "./static/app/default-ratelimit.json"}
for _, path := range paths { for _, path := range paths {
if err := loadConfigFromPath(path); err == nil { err := loadConfigFromPath(path)
if err == nil {
return nil return nil
} }
}
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Rate limit config not found",
Pairs: map[string]interface{}{"paths": paths},
})
return os.ErrNotExist
}
func loadConfigFromPath(path string) error {
file, err := os.ReadFile(path)
if err != nil {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Failed to load config", Message: "Failed to load config",
Pairs: map[string]interface{}{"path": path, "error": err}, Pairs: map[string]interface{}{"path": path, "error": err},
}) })
return err
} }
var config struct { 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)
if err != nil {
return err
}
defer file.Close()
config := struct {
RateLimit map[string]RateLimitConfig `json:"ratelimit"` RateLimit map[string]RateLimitConfig `json:"ratelimit"`
} }{}
if err := json.Unmarshal(file, &config); err != nil { decoder := json.NewDecoder(file)
if err := decoder.Decode(&config); err != nil {
return err return err
} }
newRateLimits := make(map[string]RateLimitConfig, len(config.RateLimit))
for key, value := range config.RateLimit { for key, value := range config.RateLimit {
value.RateCounterTicker = goratecounter.NewRateCounter().WithConfig(goratecounter.RateCounterConfig{ value.RateCounterTicker = goratecounter.NewRateCounter().WithConfig(goratecounter.RateCounterConfig{
Interval: value.Interval, Interval: time.Duration(value.Req) * ratelimit_intervals[value.Interval],
}) })
if cfg.LogLevel == "debug" { if cfg.LogLevel == "debug" {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Setting ratelimit config for role", Message: "Setting ratelimit config for role",
Pairs: map[string]interface{}{ Pairs: map[string]interface{}{
"role": key, "role": key,
"interval_used": value.Interval, "interval_provided": value.Interval,
"ratelimit": value.Req, "interval_used": ratelimit_intervals[value.Interval],
"ratelimit": value.Req,
}, },
}) })
} }
newRateLimits[key] = value config.RateLimit[key] = value
} }
rateLimitMu.Lock() rateLimits = config.RateLimit
rateLimits = newRateLimits
rateLimitMu.Unlock()
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limit config loaded", Message: "Rate limit config loaded",
Pairs: map[string]interface{}{"ratelimit": rateLimits}, Pairs: map[string]interface{}{"ratelimit": rateLimits},
@@ -83,14 +92,28 @@ func loadConfigFromPath(path string) error {
return nil return nil
} }
func rateLimitedRequest(userID, userRole string) bool { func rateLimitedRequest(userID string, userRole string) (shouldAllow bool) {
rateLimitMu.RLock() if rateLimits == nil {
roleConfig, ok := rateLimits[userRole]
rateLimitMu.RUnlock()
if !ok || roleConfig.RateCounterTicker == nil {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limit role not found or ticker not initialized", Message: "Rate limit config not found",
Pairs: map[string]interface{}{"user_role": userRole},
})
return true
}
// Fetch role config once to avoid multiple map lookups
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
}
if roleConfig.RateCounterTicker == nil {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limit ticker not found",
Pairs: map[string]interface{}{"user_role": userRole}, Pairs: map[string]interface{}{"user_role": userRole},
}) })
return true return true
+108 -74
View File
@@ -1,9 +1,9 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
@@ -15,26 +15,16 @@ import (
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
) )
const ( // StartHTTPProxy starts the HTTP and points it to the GraphQL server.
healthCheckQueryStr = `{ __typename }`
)
var (
ctxPool = sync.Pool{
New: func() interface{} {
return new(fiber.Ctx)
},
}
)
func StartHTTPProxy() { func StartHTTPProxy() {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Starting the HTTP proxy", Message: "Starting the HTTP proxy",
Pairs: nil,
}) })
server := fiber.New(fiber.Config{
serverConfig := fiber.Config{
DisableStartupMessage: true, DisableStartupMessage: true,
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION), 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, IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
@@ -42,14 +32,13 @@ func StartHTTPProxy() {
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2, WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
JSONEncoder: json.Marshal, JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal, JSONDecoder: json.Unmarshal,
} })
server := fiber.New(serverConfig)
server.Use(cors.New(cors.Config{ server.Use(cors.New(cors.Config{
AllowOrigins: "*", AllowOrigins: "*",
})) }))
// add middleware to check if the request is a GraphQL query
server.Use(AddRequestUUID) server.Use(AddRequestUUID)
server.Get("/healthz", healthCheck) server.Get("/healthz", healthCheck)
@@ -62,11 +51,11 @@ func StartHTTPProxy() {
Message: "GraphQL proxy started", Message: "GraphQL proxy started",
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL}, Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL},
}) })
err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL))
if err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL)); err != nil { if err != nil {
cfg.Logger.Critical(&libpack_logger.LogMessage{ cfg.Logger.Critical(&libpack_logger.LogMessage{
Message: "Can't start the service", Message: "Can't start the service",
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL, "error": err.Error()}, Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL},
}) })
} }
} }
@@ -84,125 +73,168 @@ func checkAllowedURLs(c *fiber.Ctx) bool {
if len(allowedUrls) == 0 { if len(allowedUrls) == 0 {
return true return true
} }
path := c.OriginalURL() _, ok := allowedUrls[c.Path()]
_, ok := allowedUrls[path]
return ok 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 { func healthCheck(c *fiber.Ctx) error {
if len(cfg.Server.HealthcheckGraphQL) > 0 { if len(cfg.Server.HealthcheckGraphQL) > 0 {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Health check enabled", Message: "Health check enabled",
Pairs: map[string]interface{}{"url": cfg.Server.HealthcheckGraphQL}, Pairs: map[string]interface{}{"url": cfg.Server.HealthcheckGraphQL},
}) })
query := `{ __typename }`
_, err := cfg.Client.GQLClient.Query(healthCheckQueryStr, nil, nil) _, err := cfg.Client.GQLClient.Query(query, nil, nil)
if err != nil { if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't reach the GraphQL server", Message: "Can't reach the GraphQL server",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
return c.Status(500).SendString("Can't reach the GraphQL server with {__typename} query") c.Status(500).SendString("Can't reach the GraphQL server with {__typename} query")
return err
} }
} }
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Health check returning OK", Message: "Health check returning OK",
Pairs: nil,
}) })
return c.Status(200).SendString("Health check OK") c.Status(200).SendString("Health check OK")
return nil
} }
func processGraphQLRequest(c *fiber.Ctx) error { func processGraphQLRequest(c *fiber.Ctx) error {
startTime := time.Now() startTime := time.Now()
extractedUserID := "-" // Initialize variables with default values
extractedRoleName := "-" extractedUserID, extractedRoleName := "-", "-"
if authorization := c.Get("Authorization"); authorization != "" && (len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0) { // Pre-fetch headers and trace header processing
extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(authorization) 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()
} }
// 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) { if checkIfUserIsBanned(c, extractedUserID) {
return c.Status(403).SendString("User is banned") return c.Status(403).SendString("User is banned")
} }
if cfg.Client.RoleFromHeader != "" { // Role extraction from header
if role := c.Get(cfg.Client.RoleFromHeader); role != "" { if len(cfg.Client.RoleFromHeader) > 0 {
extractedRoleName = role extractedRoleName = string(headers.Peek(cfg.Client.RoleFromHeader))
if extractedRoleName == "" {
extractedRoleName = "-"
} }
} }
if cfg.Client.RoleRateLimit { // Rate limiting check
if cfg.Client.RoleRateLimit && !rateLimitedRequest(extractedUserID, extractedRoleName) {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limiting enabled", Message: "Rate limiting enabled",
Pairs: map[string]interface{}{"user_id": extractedUserID, "role_name": extractedRoleName}, Pairs: map[string]interface{}{"user_id": extractedUserID, "role_name": extractedRoleName},
}) })
if !rateLimitedRequest(extractedUserID, extractedRoleName) { return c.Status(429).SendString("Rate limit exceeded, try again later")
return c.Status(429).SendString("Rate limit exceeded, try again later")
}
} }
// Parsing GraphQL query
parsedResult := parseGraphQLQuery(c) parsedResult := parseGraphQLQuery(c)
if parsedResult.shouldBlock { if parsedResult.shouldBlock {
return c.Status(403).SendString("Request blocked") return c.Status(403).SendString("Request blocked")
} }
if parsedResult.shouldIgnore { if parsedResult.shouldIgnore {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Request passed as-is - probably not a GraphQL", Message: "Request passed as-is - probably not a GraphQL",
Pairs: nil,
}) })
return proxyTheRequest(c, parsedResult.activeEndpoint) return proxyTheRequest(c, parsedResult.activeEndpoint, ctx)
} }
// Cache handling logic
calculatedQueryHash := libpack_cache.CalculateHash(c) queryCacheHash := libpack_cache.CalculateHash(c)
if parsedResult.cacheTime == 0 { if parsedResult.cacheTime == 0 {
if cacheQuery := c.Get("X-Cache-Graphql-Query"); cacheQuery != "" { cacheQuery := headers.Peek("X-Cache-Graphql-Query")
parsedResult.cacheTime, _ = strconv.Atoi(cacheQuery) 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},
})
} else { } else {
parsedResult.cacheTime = cfg.Cache.CacheTTL parsedResult.cacheTime = cfg.Cache.CacheTTL
} }
} }
wasCached := false
if parsedResult.cacheRefresh { if parsedResult.cacheRefresh {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache refresh requested via query", Message: "Cache refresh requested via query",
Pairs: map[string]interface{}{"user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")}, Pairs: map[string]interface{}{"user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
}) })
libpack_cache.CacheDelete(calculatedQueryHash) libpack_cache.CacheDelete(queryCacheHash)
} }
wasCached := false
if parsedResult.cacheRequest || cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable { if parsedResult.cacheRequest || cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache enabled", Message: "Cache enabled",
Pairs: map[string]interface{}{"via_query": parsedResult.cacheRequest, "via_env": cfg.Cache.CacheEnable}, 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.Monitoring.Increment(libpack_monitoring.MetricsCacheHit, nil)
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache hit", Message: "Cache hit",
Pairs: map[string]interface{}{"hash": calculatedQueryHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")}, Pairs: map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
}) })
c.Set("X-Cache-Hit", "true") 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")
}
wasCached = true wasCached = true
c.Set("Content-Type", "application/json") } else {
return c.Send(cachedResponse) cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheMiss, nil)
} cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache miss",
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheMiss, nil) Pairs: map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
cfg.Logger.Debug(&libpack_logger.LogMessage{ })
Message: "Cache miss", proxyAndCacheTheRequest(c, queryCacheHash, parsedResult.cacheTime, parsedResult.activeEndpoint, ctx)
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 { } else {
if err := proxyTheRequest(c, parsedResult.activeEndpoint); err != nil { if err := proxyTheRequest(c, parsedResult.activeEndpoint, ctx); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't proxy the request", Message: "Can't proxy the request",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
@@ -212,31 +244,33 @@ func processGraphQLRequest(c *fiber.Ctx) error {
} }
} }
logAndMonitorRequest(c, extractedUserID, parsedResult.operationType, parsedResult.operationName, wasCached, time.Since(startTime), startTime) timeTaken := time.Since(startTime)
logAndMonitorRequest(c, extractedUserID, parsedResult.operationType, parsedResult.operationName, wasCached, timeTaken, startTime)
return nil return nil
} }
func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string) error { // Additional helper function to avoid code repetition
if err := proxyTheRequest(c, currentEndpoint); err != nil { func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string, ctx context.Context) {
err := proxyTheRequest(c, currentEndpoint, ctx)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't proxy the request", Message: "Can't proxy the request",
Pairs: map[string]interface{}{"error": err.Error()}, Pairs: map[string]interface{}{"error": err.Error()},
}) })
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
return c.Status(500).SendString("Can't proxy the request - try again later") c.Status(500).SendString("Can't proxy the request - try again later")
return
} }
libpack_cache.CacheStoreWithTTL(queryCacheHash, c.Response().Body(), time.Duration(cacheTime)*time.Second) libpack_cache.CacheStoreWithTTL(queryCacheHash, c.Response().Body(), time.Duration(cacheTime)*time.Second)
cfg.Monitoring.Increment(libpack_monitoring.MetricsQueriesCached, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsQueriesCached, nil)
return c.Send(c.Response().Body()) c.Send(c.Response().Body())
} }
func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) { func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) {
labels := map[string]string{ labels := map[string]string{
"op_type": opType, "op_type": opType,
"op_name": opName, "op_name": opName,
"cached": strconv.FormatBool(wasCached), "cached": fmt.Sprintf("%t", wasCached),
"user_id": userID, "user_id": userID,
} }
@@ -245,7 +279,7 @@ func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached
Message: "Request processed", Message: "Request processed",
Pairs: map[string]interface{}{ Pairs: map[string]interface{}{
"ip": c.IP(), "ip": c.IP(),
"fwd-ip": c.Get("X-Forwarded-For"), "fwd-ip": string(c.Request().Header.Peek("X-Forwarded-For")),
"user_id": userID, "user_id": userID,
"op_type": opType, "op_type": opType,
"op_name": opName, "op_name": opName,
+5
View File
@@ -9,6 +9,11 @@ import (
// config is a struct that holds the configuration of the application. // config is a struct that holds the configuration of the application.
type config struct { type config struct {
Trace struct {
Client func()
TraceEndpoint string
Enable bool
}
Logger *libpack_logging.Logger Logger *libpack_logging.Logger
LogLevel string LogLevel string
Monitoring *libpack_monitoring.MetricsSetup Monitoring *libpack_monitoring.MetricsSetup
+98
View File
@@ -0,0 +1,98 @@
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...)
}
+109
View File
@@ -0,0 +1,109 @@
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()
// }