Add distibuted cache with Redis

This commit is contained in:
2024-06-11 09:46:38 +01:00
parent a01a4da9b5
commit 12e0294945
15 changed files with 407 additions and 89 deletions
+9 -1
View File
@@ -122,6 +122,10 @@ You can still use the non-prefixed environment variables in the spirit of the ba
| `ROLE_RATE_LIMIT` | Enable request rate limiting based on role| `false` | | `ROLE_RATE_LIMIT` | Enable request rate limiting based on role| `false` |
| `ENABLE_GLOBAL_CACHE` | Enable the cache | `false` | | `ENABLE_GLOBAL_CACHE` | Enable the cache | `false` |
| `CACHE_TTL` | The cache TTL | `60` | | `CACHE_TTL` | The cache TTL | `60` |
| `ENABLE_REDIS_CACHE` | Enable distributed Redis cache | `false` |
| `CACHE_REDIS_URL` | URL to redis server / cluster endpoint | `localhost:6379` |
| `CACHE_REDIS_PASSWORD` | Redis connection password | `` |
| `CACHE_REDIS_DB` | Redis DB id | `0` |
| `LOG_LEVEL` | The log level | `info` | | `LOG_LEVEL` | The log level | `info` |
| `BLOCK_SCHEMA_INTROSPECTION`| Blocks the schema introspection | `false` | | `BLOCK_SCHEMA_INTROSPECTION`| Blocks the schema introspection | `false` |
| `ALLOWED_INTROSPECTION` | Allow only certain queries in introspection | `` | | `ALLOWED_INTROSPECTION` | Allow only certain queries in introspection | `` |
@@ -140,7 +144,7 @@ You can still use the non-prefixed environment variables in the spirit of the ba
#### Caching #### Caching
The cache engine is enabled in the background by default, using no additional resources. The cache engine is enabled in the background by default, using no additional resources.
You can then start using the cache by setting the `ENABLE_GLOBAL_CACHE` environment variable to `true` - which will enable the cache for all queries without introspection. You can leave the global cache disabled and enable the cache for specific queries by adding the `@cached` directive to the query. You can then start using the cache by setting the `ENABLE_GLOBAL_CACHE` or `ENABLE_REDIS_CACHE` environment variable to `true` - which will enable the cache for all queries without introspection. You can leave the global cache disabled and enable the cache for specific queries by adding the `@cached` directive to the query.
In the case of the `@cached` you can add additional parameters to the directive which will set the cache for specific queries to the provided time. In the case of the `@cached` you can add additional parameters to the directive which will set the cache for specific queries to the provided time.
For example, `query MyCachedQuery @cached(ttl: 90) ....` will set the cache for the query to 90 seconds. For example, `query MyCachedQuery @cached(ttl: 90) ....` will set the cache for the query to 90 seconds.
@@ -159,6 +163,7 @@ query MyProducts @cached(refresh: true) {
``` ```
Since version `0.5.30` the cache is gzipped in the memory, which should optimise the memory usage quite significantly. Since version `0.5.30` the cache is gzipped in the memory, which should optimise the memory usage quite significantly.
Since version `0.15.48` the you can also use the distributed Redis cache.
#### Read-only endpoint #### Read-only endpoint
@@ -289,4 +294,7 @@ graphql_proxy_executed_query{user_id="-",op_type="query",op_name="checkIfSpamAIR
graphql_proxy_requests_failed 324 graphql_proxy_requests_failed 324
graphql_proxy_requests_skipped 0 graphql_proxy_requests_skipped 0
graphql_proxy_requests_succesful 454823 graphql_proxy_requests_succesful 454823
graphql_proxy_cache_hit{microservice="graphql_proxy",pod="hasura-w-proxy-internal-6b5f4b4bbb-9xwfc"} 7
graphql_proxy_cache_hit{pod="hasura-w-proxy-internal-6b5f4b4bbb-9xwfc",microservice="graphql_proxy"} 1
graphql_proxy_cache_miss{microservice="graphql_proxy",pod="hasura-w-proxy-internal-6b5f4b4bbb-9xwfc"} 23
``` ```
+13 -3
View File
@@ -54,16 +54,20 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
func apiClearCache(c *fiber.Ctx) error { func apiClearCache(c *fiber.Ctx) error {
cfg.Logger.Debug("Clearing cache via API", nil) cfg.Logger.Debug("Clearing cache via API", nil)
cfg.Cache.CacheClient.ClearCache() cacheClear()
cfg.Logger.Info("Cache cleared via API", nil) cfg.Logger.Info("Cache cleared via API", nil)
c.Status(200).SendString("OK: cache cleared") c.Status(200).SendString("OK: cache cleared")
return nil return nil
} }
func apiCacheStats(c *fiber.Ctx) error { func apiCacheStats(c *fiber.Ctx) error {
stats := cfg.Cache.CacheClient.ShowStats() stats := getCacheStats()
cfg.Logger.Debug("Getting cache stats via API", map[string]interface{}{"stats": stats}) cfg.Logger.Debug("Getting cache stats via API", map[string]interface{}{"stats": stats})
c.JSON(stats) err := c.JSON(stats)
if err != nil {
cfg.Logger.Error("Can't marshal cache stats", map[string]interface{}{"error": err.Error()})
return err
}
return nil return nil
} }
@@ -128,6 +132,12 @@ func loadBannedUsers() {
cfg.Logger.Error("Can't create the file", map[string]interface{}{"error": err.Error()}) cfg.Logger.Error("Can't create the file", map[string]interface{}{"error": err.Error()})
return return
} }
// write empty json to the file
err = os.WriteFile(cfg.Api.BannedUsersFile, []byte("{}"), 0644)
if err != nil {
cfg.Logger.Error("Can't write to the file", map[string]interface{}{"error": err.Error()})
return
}
} }
fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile)) fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
+70 -4
View File
@@ -5,7 +5,26 @@ import (
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v2"
"github.com/gookit/goutil/strutil" "github.com/gookit/goutil/strutil"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
)
type CacheStats struct {
CachedQueries int `json:"cached_queries"`
CacheHits int `json:"cache_hits"`
CacheMisses int `json:"cache_misses"`
}
type CacheClient interface {
Set(key string, value []byte, ttl time.Duration)
Get(key string) ([]byte, bool)
Delete(key string)
Clear()
CountQueries() int
}
var (
cacheStats *CacheStats
) )
func calculateHash(c *fiber.Ctx) string { func calculateHash(c *fiber.Ctx) string {
@@ -13,17 +32,64 @@ func calculateHash(c *fiber.Ctx) string {
} }
func enableCache() { func enableCache() {
cfg.Cache.CacheClient = libpack_cache.New(time.Duration(cfg.Cache.CacheTTL) * time.Second) cacheStats = &CacheStats{}
if shouldUseRedisCache() {
cfg.Logger.Info("Using Redis cache", nil)
cfg.Cache.Client = libpack_redis.NewClient(&libpack_redis.RedisClientConfig{
RedisDB: cfg.Cache.CacheRedisDB,
RedisServer: cfg.Cache.CacheRedisURL,
RedisPassword: cfg.Cache.CacheRedisPassword,
})
} else {
cfg.Logger.Info("Using in-memory cache", nil)
cfg.Cache.Client = libpack_cache.New(time.Duration(cfg.Cache.CacheTTL) * time.Second)
}
} }
func cacheLookup(hash string) []byte { func cacheLookup(hash string) []byte {
obj, found := cfg.Cache.CacheClient.Get(hash) obj, found := cfg.Cache.Client.Get(hash)
if found { if found {
cacheStats.CacheHits++
return obj return obj
} }
cacheStats.CacheMisses++
return nil return nil
} }
func cacheDelete(hash string) { func cacheDelete(hash string) {
cfg.Cache.CacheClient.Delete(hash) cfg.Logger.Debug("Deleting data from cache", map[string]interface{}{"hash": hash})
cacheStats.CachedQueries--
cfg.Cache.Client.Delete(hash)
}
func cacheStore(hash string, data []byte) {
cfg.Logger.Debug("Storing data in cache", map[string]interface{}{"hash": hash})
cacheStats.CachedQueries++
cfg.Cache.Client.Set(hash, data, time.Duration(cfg.Cache.CacheTTL)*time.Second)
}
func cacheStoreWithTTL(hash string, data []byte, ttl time.Duration) {
cfg.Logger.Debug("Storing data in cache with TTL", map[string]interface{}{"hash": hash, "ttl": ttl})
cacheStats.CachedQueries++
cfg.Cache.Client.Set(hash, data, ttl)
}
func cacheGetQueries() int {
cfg.Logger.Debug("Counting cache queries", nil)
return cfg.Cache.Client.CountQueries()
}
func cacheClear() {
cfg.Cache.Client.Clear()
cacheStats = &CacheStats{}
}
func getCacheStats() *CacheStats {
cfg.Logger.Debug("Getting cache stats", nil)
cacheStats.CachedQueries = cacheGetQueries()
return cacheStats
}
func shouldUseRedisCache() bool {
return cfg.Cache.CacheRedisEnable
} }
+16 -33
View File
@@ -18,8 +18,6 @@ type Cache struct {
globalTTL time.Duration globalTTL time.Duration
compressPool sync.Pool compressPool sync.Pool
decompressPool sync.Pool decompressPool sync.Pool
cacheHits int
cacheMisses int
sync.RWMutex // Reintroduced to provide lock methods sync.RWMutex // Reintroduced to provide lock methods
} }
@@ -53,6 +51,7 @@ func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
c.CleanExpiredEntries() c.CleanExpiredEntries()
} }
} }
func (c *Cache) Set(key string, value []byte, ttl time.Duration) { func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
c.Lock() // use the lock c.Lock() // use the lock
defer c.Unlock() defer c.Unlock()
@@ -77,16 +76,13 @@ func (c *Cache) Get(key string) ([]byte, bool) {
entry, ok := c.entries.Load(key) entry, ok := c.entries.Load(key)
if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) { if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
c.cacheMisses++
return nil, false return nil, false
} }
compressedValue := entry.(CacheEntry).Value compressedValue := entry.(CacheEntry).Value
value, err := c.decompress(compressedValue) value, err := c.decompress(compressedValue)
if err != nil { if err != nil {
c.cacheMisses++
return nil, false return nil, false
} }
c.cacheHits++
return value, true return value, true
} }
@@ -102,24 +98,11 @@ func (c *Cache) Delete(key string) {
c.entries.Delete(key) c.entries.Delete(key)
} }
func (c *Cache) CleanExpiredEntries() { func (c *Cache) Clear() {
now := time.Now() c.entries = sync.Map{}
c.entries.Range(func(key, value interface{}) bool {
entry := value.(CacheEntry)
if entry.ExpiresAt.Before(now) {
c.entries.Delete(key)
}
return true
})
} }
type CacheStats struct { func (c *Cache) CountQueries() int {
CachedQueries int `json:"cached_queries"`
CacheHits int `json:"cache_hits"`
CacheMisses int `json:"cache_misses"`
}
func (c *Cache) ShowStats() CacheStats {
c.RLock() c.RLock()
defer c.RUnlock() defer c.RUnlock()
var count int var count int
@@ -127,18 +110,7 @@ func (c *Cache) ShowStats() CacheStats {
count++ count++
return true return true
}) })
cs := CacheStats{ return count
CachedQueries: count,
CacheHits: c.cacheHits,
CacheMisses: c.cacheMisses,
}
return cs
}
func (c *Cache) ClearCache() {
c.cacheHits = 0
c.cacheMisses = 0
c.entries = sync.Map{}
} }
func (c *Cache) compress(data []byte) ([]byte, error) { func (c *Cache) compress(data []byte) ([]byte, error) {
@@ -183,3 +155,14 @@ func (c *Cache) decompress(data []byte) ([]byte, error) {
} }
return decompressedData, nil return decompressedData, nil
} }
func (c *Cache) CleanExpiredEntries() {
now := time.Now()
c.entries.Range(func(key, value interface{}) bool {
entry := value.(CacheEntry)
if entry.ExpiresAt.Before(now) {
c.entries.Delete(key)
}
return true
})
}
-33
View File
@@ -110,36 +110,3 @@ func (suite *CacheTestSuite) Test_CacheExpire() {
}) })
} }
} }
func (suite *CacheTestSuite) Test_CacheStats() {
cache := New(30 * time.Second)
tests := []struct {
name string
cache_value string
ttl time.Duration
}{
{
name: "test1",
cache_value: "test1-123",
ttl: 2 * time.Second,
},
{
name: "test2",
cache_value: "test2-123",
ttl: 5 * time.Second,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
cache.Set(tt.name, []byte(tt.name), tt.ttl)
c, ok := cache.Get(tt.name)
suite.Equal(true, ok)
suite.Equal(tt.name, string(c))
})
}
cache.Get("non-existent-non-cached-key")
stats := cache.ShowStats()
suite.Equal(2, stats.CacheHits, "CacheHits")
suite.Equal(1, stats.CacheMisses, "CacheMisses")
suite.Equal(2, stats.CachedQueries, "CachedQueries")
}
+73
View File
@@ -0,0 +1,73 @@
package libpack_redis
import (
"context"
"time"
redis "github.com/redis/go-redis/v9"
)
var ()
type RedisConfig struct {
client *redis.Client
ctx context.Context
}
func prependKeyName(key string) string {
return "gmp_cache:" + key
}
type RedisClientConfig struct {
RedisServer string
RedisPassword string
RedisDB int
}
func NewClient(redisClientConfig *RedisClientConfig) *RedisConfig {
c := &RedisConfig{
client: redis.NewClient(&redis.Options{
Addr: redisClientConfig.RedisServer,
Password: redisClientConfig.RedisPassword,
DB: redisClientConfig.RedisDB,
}),
ctx: context.Background(),
}
return c
}
func (c *RedisConfig) Set(key string, value []byte, ttl time.Duration) {
c.client.Set(c.ctx, prependKeyName(key), value, ttl)
}
func (c *RedisConfig) Get(key string) ([]byte, bool) {
val, err := c.client.Get(c.ctx, prependKeyName(key)).Result()
if err == redis.Nil || err != nil {
return nil, false
}
return []byte(val), true
}
func (c *RedisConfig) Delete(key string) {
c.client.Del(c.ctx, prependKeyName(key))
}
func (c *RedisConfig) Clear() {
c.client.FlushDB(c.ctx)
}
func (c *RedisConfig) CountQueries() int {
keys, err := c.client.Keys(c.ctx, prependKeyName("*")).Result()
if err != nil {
return 0
}
return len(keys)
}
func (c *RedisConfig) CountQueriesWithPattern(pattern string) int {
keys, err := c.client.Keys(c.ctx, prependKeyName(pattern)).Result()
if err != nil {
return 0
}
return len(keys)
}
+123
View File
@@ -0,0 +1,123 @@
package libpack_redis
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type RedisConfigSuite struct {
suite.Suite
redisConfig *RedisConfig
}
func (suite *RedisConfigSuite) SetupTest() {
suite.redisConfig = NewClient(&RedisClientConfig{
RedisServer: "localhost:6379",
RedisPassword: "",
RedisDB: 0,
})
suite.redisConfig.Delete("testkey")
}
func TestRedisConfigSuite(t *testing.T) {
suite.Run(t, new(RedisConfigSuite))
}
func (suite *RedisConfigSuite) TestSet() {
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, 0)
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
// Test overwriting an existing key-value pair
newValue := []byte("newvalue")
suite.redisConfig.Set(key, newValue, 0)
storedValue, found = suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), newValue, storedValue)
suite.redisConfig.Delete(key) // Clean up after the test
}
func (suite *RedisConfigSuite) TestSetWithExpiry() {
key := "testkey"
value := []byte("testvalue")
expiry := 1 * time.Second
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, expiry)
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
// Test that key expires after the specified time
time.Sleep(2 * time.Second)
_, found = suite.redisConfig.Get(key)
assert.False(suite.T(), found)
suite.redisConfig.Delete(key) // Clean up after the test
}
func (suite *RedisConfigSuite) TestGet() {
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
}
func (suite *RedisConfigSuite) TestDeleteKey() {
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
suite.redisConfig.Delete(key)
_, found := suite.redisConfig.Get(key)
assert.False(suite.T(), found)
}
func (suite *RedisConfigSuite) TestCheckIfKeyExists() {
ttl := time.Duration(10) * time.Second
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, ttl) // Set the key-value pair
_, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
suite.redisConfig.Delete(key)
_, found = suite.redisConfig.Get(key)
assert.False(suite.T(), found)
}
func (suite *RedisConfigSuite) TestGetKeys() {
ttl := time.Duration(10) * time.Second
suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
keys, _ := suite.redisConfig.client.Keys(suite.redisConfig.ctx, prependKeyName("testkey*")).Result()
expectedKeys := []string{prependKeyName("testkey1"), prependKeyName("testkey2")}
assert.ElementsMatch(suite.T(), expectedKeys, keys)
suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey")
}
func (suite *RedisConfigSuite) TestGetKeysCount() {
ttl := time.Duration(10) * time.Second
suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
assert.Equal(suite.T(), 2, suite.redisConfig.CountQueriesWithPattern("testkey*"))
suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey")
}
+52 -5
View File
@@ -1,10 +1,8 @@
package main package main
import ( import libpack_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
"time"
)
func (suite *Tests) Test_cacheLookup() { func (suite *Tests) Test_cacheLookupInmemory() {
type args struct { type args struct {
hash string hash string
} }
@@ -39,7 +37,56 @@ func (suite *Tests) Test_cacheLookup() {
for _, tt := range tests { for _, tt := range tests {
suite.Run(tt.name, func() { suite.Run(tt.name, func() {
if tt.addCache.data != nil { if tt.addCache.data != nil {
cfg.Cache.CacheClient.Set(tt.args.hash, tt.addCache.data, time.Duration(90*time.Second)) cacheStore(tt.args.hash, tt.addCache.data)
}
got := cacheLookup(tt.args.hash)
assert.Equal(tt.want, got, "Unexpected cache lookup result")
})
}
}
func (suite *Tests) Test_cacheLookupRedis() {
cfg.Cache.Client = libpack_redis.NewClient(&libpack_redis.RedisClientConfig{
RedisDB: 0,
RedisServer: "localhost:6379",
RedisPassword: "",
})
type args struct {
hash string
}
tests := []struct {
name string
args args
want []byte
addCache struct {
data []byte
}
}{
{
name: "test_non_existent",
args: args{
hash: "00000000000000000000000000000000000000",
},
want: nil,
},
{
name: "test_existent",
args: args{
hash: "00000000000000000000000000000000001337",
},
want: []byte("it's fine."),
addCache: struct {
data []byte
}{
data: []byte("it's fine."),
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
if tt.addCache.data != nil {
cacheStore(tt.args.hash, tt.addCache.data)
} }
got := cacheLookup(tt.args.hash) got := cacheLookup(tt.args.hash)
assert.Equal(tt.want, got, "Unexpected cache lookup result") assert.Equal(tt.want, got, "Unexpected cache lookup result")
+3
View File
@@ -14,6 +14,7 @@ require (
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.8 github.com/lukaszraczylo/go-ratecounter v0.1.8
github.com/lukaszraczylo/go-simple-graphql v1.2.14 github.com/lukaszraczylo/go-simple-graphql v1.2.14
github.com/redis/go-redis/v9 v9.5.3
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/valyala/fasthttp v1.54.0 github.com/valyala/fasthttp v1.54.0
@@ -21,7 +22,9 @@ require (
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.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/gookit/color v1.5.4 // indirect github.com/gookit/color v1.5.4 // indirect
github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/compress v1.17.8 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
+10
View File
@@ -4,10 +4,18 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -50,6 +58,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.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=
+6
View File
@@ -46,8 +46,14 @@ func parseConfig() {
c.Client.JWTRoleClaimPath = getDetailsFromEnv("JWT_ROLE_CLAIM_PATH", "") c.Client.JWTRoleClaimPath = getDetailsFromEnv("JWT_ROLE_CLAIM_PATH", "")
c.Client.RoleFromHeader = getDetailsFromEnv("ROLE_FROM_HEADER", "") c.Client.RoleFromHeader = getDetailsFromEnv("ROLE_FROM_HEADER", "")
c.Client.RoleRateLimit = getDetailsFromEnv("ROLE_RATE_LIMIT", false) c.Client.RoleRateLimit = getDetailsFromEnv("ROLE_RATE_LIMIT", false)
/* in-memory cache */
c.Cache.CacheEnable = getDetailsFromEnv("ENABLE_GLOBAL_CACHE", false) c.Cache.CacheEnable = getDetailsFromEnv("ENABLE_GLOBAL_CACHE", false)
c.Cache.CacheTTL = getDetailsFromEnv("CACHE_TTL", 60) c.Cache.CacheTTL = getDetailsFromEnv("CACHE_TTL", 60)
/* redis cache */
c.Cache.CacheRedisEnable = getDetailsFromEnv("ENABLE_REDIS_CACHE", false)
c.Cache.CacheRedisURL = getDetailsFromEnv("CACHE_REDIS_URL", "localhost:6379")
c.Cache.CacheRedisPassword = getDetailsFromEnv("CACHE_REDIS_PASSWORD", "")
c.Cache.CacheRedisDB = getDetailsFromEnv("CACHE_REDIS_DB", 0)
c.Security.BlockIntrospection = getDetailsFromEnv("BLOCK_SCHEMA_INTROSPECTION", false) c.Security.BlockIntrospection = getDetailsFromEnv("BLOCK_SCHEMA_INTROSPECTION", false)
c.Security.IntrospectionAllowed = func() []string { c.Security.IntrospectionAllowed = func() []string {
urls := getDetailsFromEnv("ALLOWED_INTROSPECTION", "") urls := getDetailsFromEnv("ALLOWED_INTROSPECTION", "")
+6
View File
@@ -3,9 +3,11 @@ package main
import ( import (
"os" "os"
"testing" "testing"
"time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
assertions "github.com/stretchr/testify/assert" assertions "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -32,6 +34,10 @@ func (suite *Tests) SetupTest() {
JSONDecoder: json.Unmarshal, JSONDecoder: json.Unmarshal,
}, },
) )
cacheStats = &CacheStats{}
// Initialize a simple in-memory cache client for testing purposes
cfg.Cache.Client = libpack_cache.New(5 * time.Minute)
parseConfig() parseConfig()
enableApi() enableApi()
StartMonitoringServer() StartMonitoringServer()
+19 -6
View File
@@ -126,6 +126,8 @@ func processGraphQLRequest(c *fiber.Ctx) error {
return proxyTheRequest(c, parsedResult.activeEndpoint) return proxyTheRequest(c, parsedResult.activeEndpoint)
} }
calculatedQueryHash := calculateHash(c)
if parsedResult.cacheTime > 0 { if parsedResult.cacheTime > 0 {
cfg.Logger.Debug("Cache time set via query", map[string]interface{}{"cacheTime": parsedResult.cacheTime}) cfg.Logger.Debug("Cache time set via query", map[string]interface{}{"cacheTime": parsedResult.cacheTime})
} else { } else {
@@ -143,19 +145,24 @@ func processGraphQLRequest(c *fiber.Ctx) error {
if parsedResult.cacheRefresh { if parsedResult.cacheRefresh {
cfg.Logger.Debug("Cache refresh requested via query", map[string]interface{}{"user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")}) cfg.Logger.Debug("Cache refresh requested via query", map[string]interface{}{"user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")})
cacheDelete(calculateHash(c)) cacheDelete(calculatedQueryHash)
} }
// Handling Cache Logic // Handling Cache Logic
if parsedResult.cacheRequest || cfg.Cache.CacheEnable { if parsedResult.cacheRequest || cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable {
cfg.Logger.Debug("Cache enabled", map[string]interface{}{"via_query": parsedResult.cacheRequest, "via_env": cfg.Cache.CacheEnable}) cfg.Logger.Debug("Cache enabled", map[string]interface{}{"via_query": parsedResult.cacheRequest, "via_env": cfg.Cache.CacheEnable})
queryCacheHash = calculateHash(c) queryCacheHash = calculatedQueryHash
if cachedResponse := cacheLookup(queryCacheHash); cachedResponse != nil { if cachedResponse := cacheLookup(queryCacheHash); cachedResponse != nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheHit, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheHit, nil)
cfg.Logger.Debug("Cache hit", map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")}) cfg.Logger.Debug("Cache hit", map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")})
c.Request().Header.Add("X-Cache-Hit", "true") c.Request().Header.Add("X-Cache-Hit", "true")
c.Send(cachedResponse) err := c.Send(cachedResponse)
if err != nil {
cfg.Logger.Error("Can't send the cached response", map[string]interface{}{"error": err.Error()})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
c.Status(500).SendString("Can't send the cached response - try again later")
}
wasCached = true wasCached = true
} else { } else {
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheMiss, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheMiss, nil)
@@ -163,7 +170,13 @@ func processGraphQLRequest(c *fiber.Ctx) error {
proxyAndCacheTheRequest(c, queryCacheHash, parsedResult.cacheTime, parsedResult.activeEndpoint) proxyAndCacheTheRequest(c, queryCacheHash, parsedResult.cacheTime, parsedResult.activeEndpoint)
} }
} else { } else {
proxyTheRequest(c, parsedResult.activeEndpoint) err := proxyTheRequest(c, parsedResult.activeEndpoint)
if err != nil {
cfg.Logger.Error("Can't proxy the request", map[string]interface{}{"error": err.Error()})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
c.Status(500).SendString("Can't proxy the request - try again later")
return nil
}
} }
timeTaken := time.Since(startTime) timeTaken := time.Since(startTime)
@@ -183,7 +196,7 @@ func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int,
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 return
} }
cfg.Cache.CacheClient.Set(queryCacheHash, c.Response().Body(), time.Duration(cacheTime)*time.Second) cacheStoreWithTTL(queryCacheHash, c.Response().Body(), time.Duration(cacheTime)*time.Second)
cfg.Monitoring.Increment(libpack_monitoring.MetricsQueriesCached, nil) cfg.Monitoring.Increment(libpack_monitoring.MetricsQueriesCached, nil)
c.Send(c.Response().Body()) c.Send(c.Response().Body())
} }
+5 -2
View File
@@ -2,7 +2,6 @@ package main
import ( import (
graphql "github.com/lukaszraczylo/go-simple-graphql" graphql "github.com/lukaszraczylo/go-simple-graphql"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logging "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"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@@ -11,9 +10,13 @@ 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 {
Cache struct { Cache struct {
CacheClient *libpack_cache.Cache Client CacheClient
CacheTTL int CacheTTL int
CacheEnable bool CacheEnable bool
CacheRedisEnable bool
CacheRedisURL string
CacheRedisPassword string
CacheRedisDB int
} }
Logger *libpack_logging.LogConfig Logger *libpack_logging.LogConfig
Monitoring *libpack_monitoring.MetricsSetup Monitoring *libpack_monitoring.MetricsSetup