mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-15 02:47:52 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de31912d2f | |||
| e0e9b4278f | |||
| 9a7635bd35 | |||
| e8b07d2e01 | |||
| efdd2de035 | |||
| 57d2fd8e80 | |||
| e5b3eff1cd | |||
| a23f9de262 | |||
| d98f87f609 | |||
| ceed490680 | |||
| b2380c689b | |||
| 2e40ee0c62 | |||
| df9f43718a |
@@ -43,7 +43,7 @@ jobs:
|
||||
name: "Unit testing"
|
||||
# needs: [prepare]
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu
|
||||
container: golang:1
|
||||
# container: github/super-linter:v4
|
||||
needs: [prepare]
|
||||
|
||||
@@ -74,7 +74,9 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo update-ca-certificates
|
||||
apt-get update
|
||||
apt-get install ca-certificates make -y
|
||||
update-ca-certificates
|
||||
go mod tidy
|
||||
|
||||
- name: Run unit tests
|
||||
|
||||
@@ -16,6 +16,8 @@ This project is in active use by [telegram-bot.app](https://telegram-bot.app), a
|
||||
- [Speed](#speed)
|
||||
- [Caching](#caching)
|
||||
- [Read-only endpoint](#read-only-endpoint)
|
||||
- [Maintenance](#maintenance)
|
||||
- [Hasura event cleaner](#hasura-event-cleaner)
|
||||
- [Security](#security)
|
||||
- [Role-based rate limiting](#role-based-rate-limiting)
|
||||
- [Read-only mode](#read-only-mode)
|
||||
@@ -101,6 +103,7 @@ In this case, both proxy and websockets will be available under the `/v1/graphql
|
||||
| security | Blocking mutations in read-only mode |
|
||||
| security | Allow access only to listed URLs |
|
||||
| security | Ban / unban specific user from accessing the application |
|
||||
| maintenance | Hasura event cleaner |
|
||||
|
||||
|
||||
### Configuration
|
||||
@@ -138,6 +141,9 @@ You can still use the non-prefixed environment variables in the spirit of the ba
|
||||
| `PROXIED_CLIENT_TIMEOUT` | The timeout for the proxied client in seconds | `120` |
|
||||
| `PURGE_METRICS_ON_CRAWL` | Purge metrics on each /metrics crawl | `false` |
|
||||
| `PURGE_METRICS_ON_TIMER` | Purge metrics every x seconds. `0` - disabled | `0` |
|
||||
| `HASURA_EVENT_CLEANER` | Enable the hasura event cleaner | `false` |
|
||||
| `HASURA_EVENT_CLEANER_OLDER_THAN` | The interval for the hasura event cleaner (in days) | `1` |
|
||||
| `HASURA_EVENT_METADATA_DB` | URL to the hasura metadata database | `postgresql://localhost:5432/hasura` |
|
||||
|
||||
### Speed
|
||||
|
||||
@@ -171,6 +177,20 @@ You can now specify the read-only GraphQL endpoint by setting the `HOST_GRAPHQL_
|
||||
|
||||
You can check out the [example of combined deployment with RW and read-only hasura](static/kubernetes-single-deployment-with-ro.yaml).
|
||||
|
||||
### Maintenance
|
||||
|
||||
#### Hasura event cleaner
|
||||
|
||||
When enabled via `HASURA_EVENT_CLEANER=true` - proxy needs to have a direct access to the database to execute simple delete queries on schedule. You can specify number of days the logs should be kept for using `HASURA_EVENT_CLEANER_OLDER_THAN`, for example `HASURA_EVENT_CLEANER_OLDER_THAN=14` will keep 14 days of event execution logs. Ticker managing the cleaner routine will be executed every hour.
|
||||
|
||||
Following tables are being cleaned:
|
||||
- `hdb_catalog.event_invocation_logs`
|
||||
- `hdb_catalog.event_log`
|
||||
- `hdb_catalog.hdb_action_log`
|
||||
- `hdb_catalog.hdb_cron_event_invocation_logs`
|
||||
- `hdb_catalog.hdb_scheduled_event_invocation_logs`
|
||||
|
||||
|
||||
### Security
|
||||
|
||||
#### Role-based rate limiting
|
||||
|
||||
Vendored
+48
-30
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -14,11 +15,11 @@ type CacheEntry struct {
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
entries sync.Map
|
||||
globalTTL time.Duration
|
||||
compressPool sync.Pool
|
||||
decompressPool sync.Pool
|
||||
sync.RWMutex // Reintroduced to provide lock methods
|
||||
entries sync.Map
|
||||
globalTTL time.Duration
|
||||
mu sync.RWMutex // Added sync.RWMutex field for locking
|
||||
}
|
||||
|
||||
func New(globalTTL time.Duration) *Cache {
|
||||
@@ -26,13 +27,11 @@ func New(globalTTL time.Duration) *Cache {
|
||||
globalTTL: globalTTL,
|
||||
compressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
w := gzip.NewWriter(nil)
|
||||
return w
|
||||
return gzip.NewWriter(nil)
|
||||
},
|
||||
},
|
||||
decompressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Ensure that new is returning a new reader initialized with an empty byte buffer
|
||||
r, _ := gzip.NewReader(bytes.NewReader([]byte{}))
|
||||
return r
|
||||
},
|
||||
@@ -53,13 +52,14 @@ func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
|
||||
}
|
||||
|
||||
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||
c.Lock() // use the lock
|
||||
defer c.Unlock()
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
|
||||
expiresAt := time.Now().Add(ttl)
|
||||
|
||||
compressedValue, err := c.compress(value)
|
||||
if err != nil {
|
||||
log.Printf("Error compressing value for key %s: %v", key, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
c.RLock() // use the read lock
|
||||
defer c.RUnlock()
|
||||
c.rlock()
|
||||
defer c.runlock()
|
||||
|
||||
entry, ok := c.entries.Load(key)
|
||||
if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
|
||||
@@ -81,30 +81,33 @@ func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
compressedValue := entry.(CacheEntry).Value
|
||||
value, err := c.decompress(compressedValue)
|
||||
if err != nil {
|
||||
log.Printf("Error decompressing value for key %s: %v", key, err)
|
||||
return nil, false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (c *Cache) Delete(key string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
_, ok := c.entries.Load(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
|
||||
c.entries.Delete(key)
|
||||
}
|
||||
|
||||
func (c *Cache) Clear() {
|
||||
c.entries = sync.Map{}
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
|
||||
c.entries.Range(func(key, value interface{}) bool {
|
||||
c.entries.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) CountQueries() int {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
c.rlock()
|
||||
defer c.runlock()
|
||||
|
||||
var count int
|
||||
c.entries.Range(func(_, _ interface{}) bool {
|
||||
count++
|
||||
@@ -131,27 +134,24 @@ func (c *Cache) compress(data []byte) ([]byte, error) {
|
||||
func (c *Cache) decompress(data []byte) ([]byte, error) {
|
||||
r, ok := c.decompressPool.Get().(*gzip.Reader)
|
||||
if !ok || r == nil {
|
||||
// If r is nil or type assertion fails, create a new gzip.Reader
|
||||
var err error
|
||||
r, err = gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err // Handle the error if gzip.NewReader fails
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Reset the existing reader with new data
|
||||
if err := r.Reset(bytes.NewReader(data)); err != nil {
|
||||
return nil, err // Handle the error if Reset fails
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
defer r.Close()
|
||||
defer func() {
|
||||
r.Close()
|
||||
c.decompressPool.Put(r)
|
||||
}()
|
||||
|
||||
// Ensure the reader is returned to the pool
|
||||
defer c.decompressPool.Put(r)
|
||||
|
||||
// Read all the data from the reader
|
||||
decompressedData, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err // Handle the error if reading fails
|
||||
return nil, err
|
||||
}
|
||||
return decompressedData, nil
|
||||
}
|
||||
@@ -166,3 +166,21 @@ func (c *Cache) CleanExpiredEntries() {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Private methods to handle locking
|
||||
|
||||
func (c *Cache) lock() {
|
||||
c.mu.Lock()
|
||||
}
|
||||
|
||||
func (c *Cache) unlock() {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Cache) rlock() {
|
||||
c.mu.RLock()
|
||||
}
|
||||
|
||||
func (c *Cache) runlock() {
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func enableHasuraEventCleaner() {
|
||||
if cfg.HasuraEventCleaner.Enable {
|
||||
if cfg.HasuraEventCleaner.EventMetadataDb == "" {
|
||||
cfg.Logger.Warning("Event metadata db URL not specified, event cleaner not active", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
cfg.Logger.Info("Event cleaner enabled", 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("Initial cleanup of old events", nil)
|
||||
cleanEvents()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
cfg.Logger.Info("Cleaning up old events", nil)
|
||||
cleanEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanEvents() {
|
||||
conn, err := pgx.Connect(context.Background(), cfg.HasuraEventCleaner.EventMetadataDb)
|
||||
if err != nil {
|
||||
cfg.Logger.Error("Failed to connect to event metadata db", 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("Failed to execute query", map[string]interface{}{"query": query, "error": err})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/goutil v0.6.15
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.8
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.14
|
||||
@@ -22,11 +23,13 @@ require (
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -39,12 +42,12 @@ require (
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -8,10 +8,11 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
@@ -31,13 +32,18 @@ github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
|
||||
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
|
||||
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
|
||||
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 h1:lvI8Wlbg4PxkRcg2f10wgoaRpfN19v+YdRek3+dLtlM=
|
||||
@@ -69,6 +75,9 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
@@ -83,6 +92,8 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
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-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
@@ -101,5 +112,6 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -76,6 +76,9 @@ func (lw *LogConfig) log(w io.Writer, level zerolog.Level, message string, field
|
||||
}
|
||||
|
||||
func (lw *LogConfig) logWithLevel(level zerolog.Level, message string, fields map[string]interface{}) {
|
||||
if lw.logger.GetLevel() > level {
|
||||
return
|
||||
}
|
||||
if lw.logger.GetLevel() <= level {
|
||||
w := os.Stdout
|
||||
if level >= zerolog.ErrorLevel {
|
||||
|
||||
@@ -83,12 +83,16 @@ func parseConfig() {
|
||||
c.Api.BannedUsersFile = getDetailsFromEnv("BANNED_USERS_FILE", "/go/src/app/banned_users.json")
|
||||
c.Server.PurgeOnCrawl = getDetailsFromEnv("PURGE_METRICS_ON_CRAWL", false)
|
||||
c.Server.PurgeEvery = getDetailsFromEnv("PURGE_METRICS_ON_TIMER", 0)
|
||||
c.HasuraEventCleaner.Enable = getDetailsFromEnv("HASURA_EVENT_CLEANER", false)
|
||||
c.HasuraEventCleaner.ClearOlderThan = getDetailsFromEnv("HASURA_EVENT_CLEANER_OLDER_THAN", 1)
|
||||
c.HasuraEventCleaner.EventMetadataDb = getDetailsFromEnv("HASURA_EVENT_METADATA_DB", "")
|
||||
cfg = &c
|
||||
|
||||
enableCache() // takes close to no resources, but can be used with dynamic query cache
|
||||
loadRatelimitConfig()
|
||||
once.Do(func() {
|
||||
go enableApi()
|
||||
go enableHasuraEventCleaner()
|
||||
})
|
||||
prepareQueriesAndExemptions()
|
||||
}
|
||||
|
||||
+59
-20
@@ -3,6 +3,7 @@ package libpack_monitoring
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -10,37 +11,53 @@ import (
|
||||
)
|
||||
|
||||
func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) (complete_name string) {
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
|
||||
// Adding default labels
|
||||
labels["microservice"] = libpack_config.PKG_NAME
|
||||
if podName, err := os.Hostname(); err == nil {
|
||||
labels["pod"] = podName
|
||||
} else {
|
||||
labels["pod"] = "unknown"
|
||||
}
|
||||
|
||||
const unknownPodName = "unknown"
|
||||
var sb strings.Builder
|
||||
|
||||
// Prepare default labels without initializing a new map
|
||||
podName := unknownPodName
|
||||
if hn, err := os.Hostname(); err == nil {
|
||||
podName = hn
|
||||
}
|
||||
if labels == nil {
|
||||
labels = map[string]string{
|
||||
"microservice": libpack_config.PKG_NAME,
|
||||
"pod": podName,
|
||||
}
|
||||
} else {
|
||||
if _, exists := labels["microservice"]; !exists {
|
||||
labels["microservice"] = libpack_config.PKG_NAME
|
||||
}
|
||||
if _, exists := labels["pod"]; !exists {
|
||||
labels["pod"] = podName
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix handling
|
||||
if ms.metrics_prefix != "" {
|
||||
sb.WriteString(ms.metrics_prefix)
|
||||
sb.WriteString("_")
|
||||
}
|
||||
sb.WriteString(name)
|
||||
|
||||
// Append labels if any
|
||||
if len(labels) > 0 {
|
||||
sb.WriteString("{")
|
||||
first := true
|
||||
for k, v := range labels {
|
||||
if !first {
|
||||
|
||||
keys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(k)
|
||||
sb.WriteString("=\"")
|
||||
sb.WriteString(v)
|
||||
sb.WriteString(labels[k])
|
||||
sb.WriteString("\"")
|
||||
first = false
|
||||
}
|
||||
sb.WriteString("}")
|
||||
}
|
||||
@@ -82,9 +99,31 @@ func validate_metrics_name(name string) error {
|
||||
}
|
||||
|
||||
func compile_metrics_with_labels(name string, labels map[string]string) string {
|
||||
metric_name := name
|
||||
var totalLength int
|
||||
totalLength += len(name)
|
||||
for k, v := range labels {
|
||||
metric_name += "_" + k + "_" + v
|
||||
totalLength += len(k) + len(v) + 2
|
||||
}
|
||||
return metric_name
|
||||
|
||||
var sb strings.Builder
|
||||
sb.Grow(totalLength + 1)
|
||||
|
||||
sb.WriteString(name)
|
||||
|
||||
// Collect keys and sort them
|
||||
keys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// Append sorted key-value pairs to the builder
|
||||
for _, k := range keys {
|
||||
sb.WriteString("_")
|
||||
sb.WriteString(k)
|
||||
sb.WriteString("_")
|
||||
sb.WriteString(labels[k])
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package libpack_monitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
)
|
||||
|
||||
func BenchmarkGetMetricsName(b *testing.B) {
|
||||
// Setup environment
|
||||
libpack_config.PKG_NAME = "test_service"
|
||||
|
||||
ms := &MetricsSetup{metrics_prefix: "test_prefix"}
|
||||
|
||||
labels := map[string]string{
|
||||
"env": "production",
|
||||
"region": "us-west-2",
|
||||
}
|
||||
|
||||
// Run the benchmark
|
||||
for n := 0; n < b.N; n++ {
|
||||
ms.get_metrics_name("request_count", labels)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompileMetricsWithLabels(b *testing.B) {
|
||||
labels := map[string]string{
|
||||
"env": "production",
|
||||
"region": "us-west-2",
|
||||
"app": "api-server",
|
||||
}
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
compile_metrics_with_labels("request_count", labels)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidateMetricsName(b *testing.B) {
|
||||
input := "valid metric name with special chars @#! and underscores__"
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
validate_metrics_name(input)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package libpack_monitoring
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetMetricsName(t *testing.T) {
|
||||
ms := &MetricsSetup{metrics_prefix: "prefix"}
|
||||
libpack_config.PKG_NAME = "example_microservice"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
metricName string
|
||||
labels map[string]string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "No labels",
|
||||
metricName: "test_metric",
|
||||
labels: nil,
|
||||
expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
{
|
||||
name: "With labels",
|
||||
metricName: "test_metric",
|
||||
labels: map[string]string{
|
||||
"label1": "value1",
|
||||
"label2": "value2",
|
||||
},
|
||||
expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
{
|
||||
name: "Alphabetical order labels",
|
||||
metricName: "test_metric",
|
||||
labels: map[string]string{
|
||||
"label2": "value2",
|
||||
"label1": "value1",
|
||||
},
|
||||
expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
{
|
||||
name: "Empty metric name",
|
||||
metricName: "",
|
||||
labels: nil,
|
||||
expectedOutput: "prefix_{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
{
|
||||
name: "Empty labels map",
|
||||
metricName: "test_metric",
|
||||
labels: map[string]string{},
|
||||
expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
{
|
||||
name: "Single label",
|
||||
metricName: "test_metric",
|
||||
labels: map[string]string{
|
||||
"label1": "value1",
|
||||
},
|
||||
expectedOutput: "prefix_test_metric{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
{
|
||||
name: "Multiple labels with special characters",
|
||||
metricName: "test_metric",
|
||||
labels: map[string]string{
|
||||
"label-2": "value-2",
|
||||
"label_1": "value_1",
|
||||
},
|
||||
expectedOutput: "prefix_test_metric{label-2=\"value-2\",label_1=\"value_1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
{
|
||||
name: "Prefix only",
|
||||
metricName: "",
|
||||
labels: map[string]string{
|
||||
"label1": "value1",
|
||||
},
|
||||
expectedOutput: "prefix_{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ms.get_metrics_name(tt.metricName, tt.labels)
|
||||
assert.Equal(t, tt.expectedOutput, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileMetricsWithLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels map[string]string
|
||||
want string
|
||||
}{
|
||||
{"request_count", map[string]string{"env": "production", "region": "us-west-2"}, "request_count_env_production_region_us-west-2"},
|
||||
{"metric_name", map[string]string{}, "metric_name"},
|
||||
{"metric_name", nil, "metric_name"},
|
||||
{"metric_name", map[string]string{"key1": "value1"}, "metric_name_key1_value1"},
|
||||
{"metric_name", map[string]string{"k": "v", "key2": "value2"}, "metric_name_k_v_key2_value2"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := compile_metrics_with_labels(tt.name, tt.labels); got != tt.want {
|
||||
t.Errorf("compile_metrics_with_labels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMetricsName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{"Valid name", "valid_metric_name", false},
|
||||
{"Name with spaces", "valid metric name", true},
|
||||
{"Name with special chars", "valid@metric#name!", true},
|
||||
{"Name with leading underscore", "_valid_metric_name", true},
|
||||
{"Name with trailing underscore", "valid_metric_name_", true},
|
||||
{"Name with consecutive underscores", "valid__metric__name", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := validate_metrics_name(tt.input); (err != nil) != tt.wantErr {
|
||||
t.Errorf("validate_metrics_name() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getPodName() string {
|
||||
podName, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return podName
|
||||
}
|
||||
+14
-9
@@ -9,15 +9,6 @@ import (
|
||||
|
||||
// config is a struct that holds the configuration of the application.
|
||||
type config struct {
|
||||
Cache struct {
|
||||
Client CacheClient
|
||||
CacheTTL int
|
||||
CacheEnable bool
|
||||
CacheRedisEnable bool
|
||||
CacheRedisURL string
|
||||
CacheRedisPassword string
|
||||
CacheRedisDB int
|
||||
}
|
||||
Logger *libpack_logging.LogConfig
|
||||
Monitoring *libpack_monitoring.MetricsSetup
|
||||
Api struct{ BannedUsersFile string }
|
||||
@@ -35,6 +26,20 @@ type config struct {
|
||||
IntrospectionAllowed []string
|
||||
BlockIntrospection bool
|
||||
}
|
||||
HasuraEventCleaner struct {
|
||||
EventMetadataDb string
|
||||
ClearOlderThan int
|
||||
Enable bool
|
||||
}
|
||||
Cache struct {
|
||||
Client CacheClient
|
||||
CacheRedisURL string
|
||||
CacheRedisPassword string
|
||||
CacheTTL int
|
||||
CacheRedisDB int
|
||||
CacheEnable bool
|
||||
CacheRedisEnable bool
|
||||
}
|
||||
Server struct {
|
||||
HostGraphQL string
|
||||
HostGraphQLReadOnly string
|
||||
|
||||
Reference in New Issue
Block a user