Compare commits

...

13 Commits

Author SHA1 Message Date
lukaszraczylo de31912d2f increase error handling and mutex encapsulation (#12)
* increase error handling and mutex encapsulation

* undo method rename for now

* set cant return error

---------

Co-authored-by: Chris Clayton <chris.clayton@contino.io>
2024-06-15 10:21:49 +01:00
lukaszraczylo e0e9b4278f Release: Improve documentation and number of logs cleaned. 2024-06-12 12:59:54 +01:00
lukaszraczylo 9a7635bd35 fixup! fixup! Add cleaning up action logs as well. 2024-06-12 12:46:13 +01:00
lukaszraczylo e8b07d2e01 fixup! Add cleaning up action logs as well. 2024-06-12 12:27:13 +01:00
lukaszraczylo efdd2de035 Add cleaning up action logs as well. 2024-06-12 12:23:14 +01:00
lukaszraczylo 57d2fd8e80 Update documentation. 2024-06-12 12:12:25 +01:00
lukaszraczylo e5b3eff1cd Adjust field alignment. 2024-06-12 12:07:22 +01:00
lukaszraczylo a23f9de262 fixup! Update dependencies. 2024-06-12 12:05:50 +01:00
lukaszraczylo d98f87f609 Update dependencies. 2024-06-12 11:57:10 +01:00
lukaszraczylo ceed490680 Additional updates. 2024-06-12 11:54:03 +01:00
lukaszraczylo b2380c689b Add cleanup of the event and invocation logs on timer. 2024-06-12 11:47:21 +01:00
lukaszraczylo 2e40ee0c62 Update the helpers to sort labels alpabetically.
It will help to avoid the flaky tests and duplicated metrics.

As a bonus - added tests and benchmarks for monitoring package.
2024-06-11 19:57:18 +01:00
lukaszraczylo df9f43718a fixup! fixup! fixup! fixup! fixup! fixup! Fix: Redis connection for tests. 2024-06-11 12:53:29 +01:00
12 changed files with 422 additions and 71 deletions
+4 -2
View File
@@ -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
+20
View File
@@ -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
+48 -30
View File
@@ -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()
}
+58
View File
@@ -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})
}
}
}
+6 -3
View File
@@ -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
)
+19 -7
View File
@@ -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=
+3
View File
@@ -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 {
+4
View File
@@ -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
View File
@@ -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()
}
+44
View File
@@ -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)
}
}
+143
View File
@@ -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
View File
@@ -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