mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-11 00:09:37 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
dd25e4a4a5
|
|||
|
8a2b90ef8b
|
|||
|
e358e2a720
|
|||
|
1a3628837f
|
|||
|
0758cd5b52
|
|||
|
51dfc8d9be
|
|||
|
2f87f40822
|
|||
|
377a1a4a26
|
|||
|
7de1cf7cc7
|
|||
| 917ee1a431 | |||
| bc128493b0 | |||
|
c213a49c32
|
|||
|
ac44056a00
|
|||
|
743eed7f71
|
|||
|
b89053c015
|
@@ -4,14 +4,17 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**/**.md'
|
||||
- '**/**.yaml'
|
||||
- 'static/**'
|
||||
branches:
|
||||
- "*"
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
shared:
|
||||
uses: telegram-bot-app/ci-scripts/.github/workflows/build-test-publish-inject.yaml@main
|
||||
with:
|
||||
enable-code-scans: false
|
||||
should-deploy: false
|
||||
secrets:
|
||||
ghcr-token: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
@@ -4,5 +4,6 @@ WORKDIR /go/src/app
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
ADD dist/bot-$TARGETOS-$TARGETARCH /go/src/app/graphql-proxy
|
||||
ADD static/default-ratelimit.json /app/ratelimit.json
|
||||
RUN chmod +x /go/src/app/graphql-proxy
|
||||
ENTRYPOINT ["/go/src/app/graphql-proxy"]
|
||||
|
||||
@@ -10,8 +10,8 @@ help: ## display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: run
|
||||
run: ## run application
|
||||
@LOG_LEVEL=debug BLOCK_SCHEMA_INTROSPECTION=true JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/v1/graphql go run *.go
|
||||
run: build ## run application
|
||||
@LOG_LEVEL=debug BLOCK_SCHEMA_INTROSPECTION=false JWT_ROLE_RATE_LIMIT=false JWT_ROLE_CLAIM_PATH="Hasura.x-hasura-default-role" JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/v1/graphql ./graphql-proxy
|
||||
|
||||
.PHONY: build
|
||||
build: ## build the binary
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
## graphql monitoring proxy
|
||||
|
||||
Creates a passthrough proxy to a graphql endpoint(s), allowing you for analysis of 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 10mb of RAM and 0.1% CPU.
|
||||
This project is in active use by [telegram-bot.app](https://telegram-bot.app), and was tested with 30k queries per second on a single instance, consuming 10 MB of RAM and 0.1% CPU.
|
||||
|
||||

|
||||
|
||||
You can find the example of the kubernetes manifest in the [example deployment](static/kubernetes-deployment.yaml) file.
|
||||
You can find the example of the Kubernetes manifest in the [example deployment](static/kubernetes-deployment.yaml) file.
|
||||
|
||||
### Why this project exists
|
||||
|
||||
I wanted to monitor the queries and responses of our graphql endpoint, but we didn't want to pay the price of the graphql server itself ( and I will not point fingers and certain well-known project), as monitoring and basic security features should be a common, free functionality.
|
||||
I wanted to monitor the queries and responses of our graphql endpoint. Still, we didn't want to pay the price of the graphql server itself ( and I will not point fingers at a particular well-known project), as monitoring and basic security features should be a standard, free functionality.
|
||||
|
||||
### Endpoints
|
||||
|
||||
@@ -20,23 +20,86 @@ I wanted to monitor the queries and responses of our graphql endpoint, but we di
|
||||
|
||||
### Features
|
||||
|
||||
* MONITORING: Prometheus / VictoriaMetrics metrics
|
||||
* MONITORING: Extracting user id from JWT token and adding it as a label to the metrics
|
||||
* MONITORING: Extracting the query name and type and adding it as a label to the metrics
|
||||
* MONITORING: Calculating the query duration and adding it to the metrics
|
||||
* SPEED: Caching the queries
|
||||
* SECURITY: Blocking schema introspection
|
||||
| Category | Detail |
|
||||
|------------|-----------------------------------------------------------------------|
|
||||
| monitor | Prometheus / VictoriaMetrics metrics |
|
||||
| monitor | Extracting user id from JWT token and adding it as a label to metrics |
|
||||
| monitor | Extracting the query name and type and adding it as a label to metrics|
|
||||
| monitor | Calculating the query duration and adding it to the metrics |
|
||||
| speed | Caching the queries, together with per-query cache and TTL |
|
||||
| security | Blocking schema introspection |
|
||||
| security | Rate limiting queries based on user role |
|
||||
| security | Blocking mutations in read-only mode |
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
* `MONITORING_PORT` - the port to expose the metrics endpoint on (default: 9393)
|
||||
* `PORT_GRAPHQL` - the port to expose the graphql endpoint on (default: 8080)
|
||||
* `HOST_GRAPHQL` - the host to proxy the graphql endpoint to (default: `http://localhost/v1/graphql`)
|
||||
* `JWT_USER_CLAIM_PATH` - the path to the user claim in the JWT token (default: ``)
|
||||
* `ENABLE_CACHE` - enable the cache (default: `false`)
|
||||
* `CACHE_TTL` - the cache TTL (default: `60s`)
|
||||
* `LOG_LEVEL` - the log level (default: `info`)
|
||||
* `BLOCK_SCHEMA_INTROSPECTION` - blocks the schema introspection (default: `false`)
|
||||
| Parameter | Description | Default Value |
|
||||
|---------------------------|------------------------------------------|----------------------------|
|
||||
| `MONITORING_PORT` | The port to expose the metrics endpoint | `9393` |
|
||||
| `PORT_GRAPHQL` | The port to expose the graphql endpoint | `8080` |
|
||||
| `HOST_GRAPHQL` | The host to proxy the graphql endpoint | `http://localhost/v1/graphql` |
|
||||
| `JWT_USER_CLAIM_PATH` | Path to the user claim in the JWT token | `` |
|
||||
| `JWT_ROLE_CLAIM_PATH` | Path to the role claim in the JWT token | `` |
|
||||
| `JWT_ROLE_FROM_HEADER` | Header name to extract the role from | `` |
|
||||
| `ROLE_RATE_LIMIT` | Enable request rate limiting based on role| `false` |
|
||||
| `ENABLE_GLOBAL_CACHE` | Enable the cache | `false` |
|
||||
| `CACHE_TTL` | The cache TTL | `60` |
|
||||
| `LOG_LEVEL` | The log level | `info` |
|
||||
| `BLOCK_SCHEMA_INTROSPECTION`| Blocks the schema introspection | `false` |
|
||||
| `ENABLE_ACCESS_LOG` | Enable the access log | `false` |
|
||||
| `READ_ONLY_MODE` | Enable the read only mode | `false` |
|
||||
|
||||
|
||||
### Caching
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Role-based rate limiting
|
||||
|
||||
You can rate limit requests using the `ROLE_RATE_LIMIT` environment variable. If enabled, the proxy will rate limit the requests based on the role claim in the JWT token. You can then provide the JSON file in the following format to specify the limits.
|
||||
The default interval is `second`, but you can use other values as well. If you want to disable the rate limiting for a specific role, you can set the `req` to `0`.
|
||||
|
||||
Available values:
|
||||
`nano`, `micro`, `milli`, `second`, `minute`, `hour`, `day`
|
||||
|
||||
To define path in JWT token where the current user role is present, use the `JWT_ROLE_CLAIM_PATH` environment variable.
|
||||
|
||||
You can also set up the `ROLE_FROM_HEADER` environment variable to extract the role from the header instead of the JWT token. This is useful if you want to rate limit the requests for unauthenticated users. It's worth mentioning that `ROLE_FROM_HEADER` takes a priority over the `JWT_ROLE_CLAIM_PATH` environment variable and if its set, the proxy will not try to extract the role from the JWT token.
|
||||
|
||||
*Default/sample configuration:*
|
||||
|
||||
```json
|
||||
{
|
||||
"ratelimit": {
|
||||
"admin": {
|
||||
"req": 100,
|
||||
"interval": "second"
|
||||
},
|
||||
"guest": {
|
||||
"req": 50,
|
||||
"interval": "minute"
|
||||
},
|
||||
"-": {
|
||||
"req": 100,
|
||||
"interval": "day"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you'd like to change it - mount your configmap as `/app/ratelimit.json` file.
|
||||
Remember to include the `-` role, which is used for unauthenticated users or when claim can't be found for any reason.
|
||||
If rate limit has been reached - the proxy will return `429 Too Many Requests` error.
|
||||
|
||||
|
||||
### Read-only mode
|
||||
|
||||
You can enable the read-only mode by setting the `READ_ONLY_MODE` environment variable to `true` - which will block all the `mutation` queries.
|
||||
|
||||
### Monitoring endpoint
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (suite *Tests) Test_cacheLookup() {
|
||||
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: "00000000000000000000000000000000000001",
|
||||
},
|
||||
want: []byte("it's fine."),
|
||||
addCache: struct {
|
||||
data []byte
|
||||
}{
|
||||
data: []byte("it's fine."),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.addCache.data != nil {
|
||||
cfg.Cache.CacheClient.Set(tt.args.hash, tt.addCache.data, time.Duration(1)*time.Second)
|
||||
}
|
||||
got := cacheLookup(tt.args.hash)
|
||||
assert.Equal(tt.want, got, "Unexpected cache lookup result")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package libpack_config
|
||||
|
||||
var (
|
||||
PKG_NAME string = "not-specified"
|
||||
PKG_VERSION string = "0.0.0-dev"
|
||||
)
|
||||
+30
-16
@@ -2,37 +2,51 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/ask"
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
func extractClaimsFromJWTHeader(authorization string) (usr string) {
|
||||
func extractClaimsFromJWTHeader(authorization string) (usr string, role string) {
|
||||
usr, role = "-", "-"
|
||||
|
||||
handleError := func(msg string, details map[string]interface{}) {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
cfg.Logger.Error(msg, details)
|
||||
}
|
||||
|
||||
tokenParts := strings.Split(authorization, ".")
|
||||
if len(tokenParts) != 3 {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
cfg.Logger.Error("Can't split the token", map[string]interface{}{"token": authorization})
|
||||
handleError("Can't split the token", map[string]interface{}{"token": authorization})
|
||||
return
|
||||
}
|
||||
|
||||
claim, err := base64.RawURLEncoding.DecodeString(tokenParts[1])
|
||||
if err != nil {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
cfg.Logger.Error("Can't decode the token", map[string]interface{}{"token": authorization})
|
||||
handleError("Can't decode the token", map[string]interface{}{"token": authorization})
|
||||
return
|
||||
}
|
||||
|
||||
var claimMap map[string]interface{}
|
||||
err = json.Unmarshal(claim, &claimMap)
|
||||
if err != nil {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
cfg.Logger.Error("Can't unmarshal the claim", map[string]interface{}{"token": authorization})
|
||||
if err = json.Unmarshal(claim, &claimMap); err != nil {
|
||||
handleError("Can't unmarshal the claim", map[string]interface{}{"token": authorization})
|
||||
return
|
||||
}
|
||||
usr, ok := ask.For(claimMap, cfg.Client.JWTUserClaimPath).String("-")
|
||||
if !ok {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
cfg.Logger.Error("Can't find the user id", map[string]interface{}{"claim_map": claimMap, "path": cfg.Client.JWTUserClaimPath})
|
||||
return
|
||||
|
||||
extractClaim := func(claimPath string, target *string, name string) {
|
||||
if len(claimPath) > 0 {
|
||||
var ok bool
|
||||
*target, ok = ask.For(claimMap, claimPath).String("-")
|
||||
if !ok {
|
||||
handleError(fmt.Sprintf("Can't find the %s", name), map[string]interface{}{"claim_map": claimMap, "path": claimPath})
|
||||
}
|
||||
}
|
||||
}
|
||||
return usr
|
||||
|
||||
extractClaim(cfg.Client.JWTUserClaimPath, &usr, "user id")
|
||||
extractClaim(cfg.Client.JWTRoleClaimPath, &role, "role")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func (suite *Tests) Test_extractClaimsFromJWTHeader() {
|
||||
jwt_token_for_tests := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiSGFzdXJhIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsiZ3Vlc3QiLCJ1c2VyIiwiZ3JvdXBhZG1pbiIsInBheWFkbWluIl0sIngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6Imd1ZXN0IiwieC1oYXN1cmEtdXNlci1pZCI6IjE2NyIsIngtaGFzdXJhLXVzZXItdXVpZCI6ImRkM2U2ZTM1LTA0MDktNDNiMC1iZmYxLWNlZjNjNmVkNWYxMCJ9LCJpc3MiOiJBdXRoU2VydmljZSIsImV4cCI6MTY5NjgwMTcyNiwibmJmIjoxNjk2NTg1NzI2LCJpYXQiOjE2OTY1ODU3MjZ9.dsJ5JKzG5tXOlqeZ_Gfe2XC-vyrcwtYwOGfhvt8q9UY"
|
||||
|
||||
type args struct {
|
||||
authorization string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantUsr string
|
||||
wantRole string
|
||||
jwt_token_path string
|
||||
jwt_role_path string
|
||||
}{
|
||||
{
|
||||
name: "test_empty",
|
||||
wantUsr: "-",
|
||||
wantRole: "-",
|
||||
},
|
||||
{
|
||||
name: "test_invalid_path",
|
||||
args: args{
|
||||
authorization: jwt_token_for_tests,
|
||||
},
|
||||
wantUsr: "-",
|
||||
wantRole: "-",
|
||||
jwt_token_path: "invalid",
|
||||
},
|
||||
{
|
||||
name: "test_invalid_role_path",
|
||||
args: args{
|
||||
authorization: jwt_token_for_tests,
|
||||
},
|
||||
wantUsr: "-",
|
||||
wantRole: "-",
|
||||
jwt_role_path: "invalid",
|
||||
},
|
||||
{
|
||||
name: "test_valid",
|
||||
args: args{
|
||||
authorization: jwt_token_for_tests,
|
||||
},
|
||||
wantUsr: "167",
|
||||
wantRole: "guest",
|
||||
jwt_token_path: "Hasura.x-hasura-user-id",
|
||||
jwt_role_path: "Hasura.x-hasura-default-role",
|
||||
},
|
||||
{
|
||||
name: "test_invalid_token",
|
||||
args: args{
|
||||
authorization: "invalid",
|
||||
},
|
||||
wantUsr: "-",
|
||||
wantRole: "-",
|
||||
},
|
||||
{
|
||||
name: "test_invalid_three_part_token",
|
||||
args: args{
|
||||
authorization: "invalid.threepart.token",
|
||||
},
|
||||
wantUsr: "-",
|
||||
wantRole: "-",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if len(tt.jwt_token_path) > 0 {
|
||||
cfg.Client.JWTUserClaimPath = tt.jwt_token_path
|
||||
}
|
||||
if len(tt.jwt_role_path) > 0 {
|
||||
cfg.Client.JWTRoleClaimPath = tt.jwt_role_path
|
||||
}
|
||||
gotUsr, gotRole := extractClaimsFromJWTHeader(tt.args.authorization)
|
||||
assert.Equal(tt.wantUsr, gotUsr, "Unexpected user ID")
|
||||
assert.Equal(tt.wantRole, gotRole, "Unexpected role")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,45 +3,48 @@ module github.com/lukaszraczylo/graphql-monitoring-proxy
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/metrics v1.24.0
|
||||
github.com/akyoto/cache v1.0.6
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/gofiber/fiber/v2 v2.49.2
|
||||
github.com/gookit/goutil v0.6.12
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/k0kubun/pp v3.0.1+incompatible
|
||||
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.8
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.1.31
|
||||
github.com/telegram-bot-app/libpack v0.0.0-20231008100411-9f7f8bf94315
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/VictoriaMetrics/metrics v1.24.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/avast/retry-go/v4 v4.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/lukaszraczylo/pandati v0.0.29 // 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.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/rs/zerolog v1.31.0 // indirect
|
||||
github.com/telegram-bot-app/lib-logging v0.0.19 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.50.0 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/wI2L/jsondiff v0.4.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/net v0.16.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sync v0.4.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw=
|
||||
github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys=
|
||||
github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k=
|
||||
@@ -11,6 +9,7 @@ github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5A
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
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=
|
||||
@@ -28,18 +27,21 @@ github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuM
|
||||
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40=
|
||||
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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=
|
||||
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c=
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.8 h1:ZYm6Wkn58ZAlFWRmC7PaD4oAYHWcu8/0MUDWGe3PnJQ=
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.8/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.1.31 h1:UA3f8M1cV+XnO8UZlAqveW0qF/2NN512eB/gRqe+BHs=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.1.31/go.mod h1:MyftQ8jTdtkYImPXJpHoxz6+E53Ydv+7q9+Jr+eT8WU=
|
||||
github.com/lukaszraczylo/pandati v0.0.29 h1:WUEWm1+hWjE5KJbIL8OctG00x2dk4XKGJSlrjhxZ55k=
|
||||
github.com/lukaszraczylo/pandati v0.0.29/go.mod h1:+DyTWKFaXd+jIfe7GW5w2S5PyTko/RXxMyOa+Vl713A=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -52,12 +54,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
@@ -65,10 +71,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/telegram-bot-app/lib-logging v0.0.19 h1:zbyFr2ygeBY+yuaB9moXyOGk8dIBCn0jPJQjvx7YvLE=
|
||||
github.com/telegram-bot-app/lib-logging v0.0.19/go.mod h1:n8d29fRUTdgJhC4RZ8s4lP2RHiGCCRYEj2ENEClUGc8=
|
||||
github.com/telegram-bot-app/libpack v0.0.0-20231008100411-9f7f8bf94315 h1:gf+3gFgtdh48RQNmLNdK1IcGqpuTuj6RAdHxDMd/YPY=
|
||||
github.com/telegram-bot-app/libpack v0.0.0-20231008100411-9f7f8bf94315/go.mod h1:W2kWHcfNNS0r++dJ1T2XX/C4cTSxI3MsoiMbOtyqu+I=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
|
||||
@@ -79,14 +81,12 @@ github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OL
|
||||
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/wI2L/jsondiff v0.4.0 h1:iP56F9tK83eiLttg3YdmEENtZnwlYd3ezEpNNnfZVyM=
|
||||
github.com/wI2L/jsondiff v0.4.0/go.mod h1:nR/vyy1efuDeAtMwc3AF6nZf/2LD1ID8GTyyJ+K8YB0=
|
||||
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/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.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -99,5 +99,7 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+23
-2
@@ -1,10 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/graphql-go/graphql/language/parser"
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
var retrospection_queries = []string{
|
||||
@@ -33,7 +36,7 @@ var retrospection_queries = []string{
|
||||
// Saving the introspection queries as a map O(1) operation instead of O(n) for a slice.
|
||||
var retrospectionQuerySet = make(map[string]struct{}, len(retrospection_queries))
|
||||
|
||||
func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cacheRequest bool, should_block bool) {
|
||||
func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cacheRequest bool, cache_time int, should_block bool) {
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal(c.Body(), &m)
|
||||
if err != nil {
|
||||
@@ -60,6 +63,14 @@ func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cache
|
||||
for _, d := range p.Definitions {
|
||||
if oper, ok := d.(*ast.OperationDefinition); ok {
|
||||
operationType = oper.Operation
|
||||
if strings.ToLower(operationType) == "mutation" && cfg.Server.ReadOnlyMode {
|
||||
cfg.Logger.Warning("Mutation blocked", m)
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
c.Status(403).SendString("The server is in read-only mode")
|
||||
should_block = true
|
||||
return
|
||||
}
|
||||
|
||||
if oper.Name != nil {
|
||||
operationName = oper.Name.Value
|
||||
} else {
|
||||
@@ -68,6 +79,16 @@ func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cache
|
||||
for _, dir := range oper.Directives {
|
||||
if dir.Name.Value == "cached" {
|
||||
cacheRequest = true
|
||||
for _, arg := range dir.Arguments {
|
||||
if arg.Name.Value == "ttl" {
|
||||
cache_time, err = strconv.Atoi(arg.Value.GetValue().(string))
|
||||
if err != nil {
|
||||
cfg.Logger.Error("Can't parse the ttl", map[string]interface{}{"ttl": arg.Value.GetValue().(string)})
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cfg.Security.BlockIntrospection {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package libpack_logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/goutil/envutil"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
var baseLogger zerolog.Logger
|
||||
|
||||
func init() {
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
zerolog.MessageFieldName = "short_message"
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFieldName = "level"
|
||||
zerolog.LevelFatalValue = "critical"
|
||||
baseLogger = zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
}
|
||||
|
||||
func NewLogger() *LogConfig {
|
||||
switch logLevel := envutil.Getenv("LOG_LEVEL", "info"); logLevel {
|
||||
case "debug":
|
||||
baseLogger = baseLogger.Level(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
baseLogger = baseLogger.Level(zerolog.WarnLevel)
|
||||
case "error":
|
||||
baseLogger = baseLogger.Level(zerolog.ErrorLevel)
|
||||
default:
|
||||
baseLogger = baseLogger.Level(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
return &LogConfig{logger: baseLogger}
|
||||
}
|
||||
|
||||
func (lw *LogConfig) log(w io.Writer, level zerolog.Level, message string, v map[string]interface{}) {
|
||||
e := lw.logger.With().Logger()
|
||||
e = e.Output(w)
|
||||
event := e.WithLevel(level).CallerSkipFrame(3)
|
||||
for k, val := range v {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
event.Str(k, v)
|
||||
case int:
|
||||
event.Int(k, v)
|
||||
case float64:
|
||||
event.Float64(k, v)
|
||||
default:
|
||||
event.Interface(k, val)
|
||||
}
|
||||
}
|
||||
event.Msg(message)
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Debug(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stdout, zerolog.DebugLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Info(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stdout, zerolog.InfoLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Warning(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stdout, zerolog.WarnLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Error(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stderr, zerolog.ErrorLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Critical(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stderr, zerolog.FatalLevel, message, mergeMaps(v))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func mergeMaps(maps []map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package libpack_logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkNewLogger(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewLogger()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInfoLog(b *testing.B) {
|
||||
oldEnv := os.Getenv("LOG_LEVEL")
|
||||
os.Setenv("LOG_LEVEL", "info")
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
os.Stdout, _ = os.Open(os.DevNull)
|
||||
os.Stderr, _ = os.Open(os.DevNull)
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
os.Setenv("LOG_LEVEL", oldEnv)
|
||||
}()
|
||||
|
||||
testsLogger := NewLogger()
|
||||
for i := 0; i < b.N; i++ {
|
||||
testsLogger.Info("test", map[string]interface{}{"test": "test"})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package libpack_logging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LoggingTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
var (
|
||||
testsLogger *LogConfig
|
||||
)
|
||||
|
||||
type stdoutCapture struct {
|
||||
oldStdout *os.File
|
||||
readPipe *os.File
|
||||
}
|
||||
|
||||
func (sc *stdoutCapture) StartCapture() {
|
||||
sc.oldStdout = os.Stdout
|
||||
sc.readPipe, os.Stdout, _ = os.Pipe()
|
||||
}
|
||||
|
||||
func (sc *stdoutCapture) StopCapture() (string, error) {
|
||||
if sc.oldStdout == nil || sc.readPipe == nil {
|
||||
return "", errors.New("StartCapture not called before StopCapture on Stdout")
|
||||
}
|
||||
os.Stdout.Close()
|
||||
os.Stdout = sc.oldStdout
|
||||
bytes, err := io.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
type stderrCapture struct {
|
||||
oldStderr *os.File
|
||||
readPipe *os.File
|
||||
}
|
||||
|
||||
func (sc *stderrCapture) StartCapture() {
|
||||
sc.oldStderr = os.Stderr
|
||||
sc.readPipe, os.Stderr, _ = os.Pipe()
|
||||
}
|
||||
|
||||
func (sc *stderrCapture) StopCapture() (string, error) {
|
||||
if sc.oldStderr == nil || sc.readPipe == nil {
|
||||
return "", errors.New("StartCapture not called before StopCapture on Stderr")
|
||||
}
|
||||
os.Stderr.Close()
|
||||
os.Stderr = sc.oldStderr
|
||||
bytes, err := io.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func TestLoggingTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(LoggingTestSuite))
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
|
||||
type args struct {
|
||||
message string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
loggerType string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
loggerType: "Error",
|
||||
args: args{
|
||||
message: "This is a error message",
|
||||
},
|
||||
wantLevel: "error",
|
||||
wantMessage: "This is a error message",
|
||||
stdErrExpect: true,
|
||||
stdOutExpect: false,
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "info",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Debug",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "debug",
|
||||
},
|
||||
{
|
||||
name: "Test log: Info",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
},
|
||||
wantLevel: "info",
|
||||
wantMessage: "This is a info message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
},
|
||||
{
|
||||
name: "Test log: Info | Min level: Warn",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
},
|
||||
wantLevel: "",
|
||||
wantMessage: "",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: false,
|
||||
envMinLogLevel: "warn",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Warn",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "warn",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Error",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is an error message",
|
||||
},
|
||||
wantLevel: "",
|
||||
wantMessage: "",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: false,
|
||||
envMinLogLevel: "error",
|
||||
},
|
||||
{
|
||||
name: "Test log: Debug | Min level: Debug",
|
||||
loggerType: "Debug",
|
||||
args: args{
|
||||
message: "This is a debug message",
|
||||
},
|
||||
wantLevel: "debug",
|
||||
wantMessage: "This is a debug message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "debug",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
}
|
||||
testsLogger = NewLogger()
|
||||
|
||||
captureStdout := stdoutCapture{}
|
||||
captureStdout.StartCapture()
|
||||
captureStderr := stderrCapture{}
|
||||
captureStderr.StartCapture()
|
||||
|
||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{reflect.ValueOf(tt.args.message)})
|
||||
|
||||
stdoutOut, err := captureStdout.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
stderrOut, err := captureStderr.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
||||
gotLvl, gotMsg, err := getResponseValues(stderrOut, "short_message")
|
||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal("", stdoutOut, "Failed in [STDERR]: "+tt.name)
|
||||
}
|
||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
||||
gotLvl, gotMsg, err := getResponseValues(stdoutOut, "short_message")
|
||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal("", stderrOut, "Failed in [STDOUT]: "+tt.name)
|
||||
}
|
||||
if !tt.stdErrExpect && !tt.stdOutExpect {
|
||||
suite.Equal("", stderrOut, "Failed in [NEITHER]: "+tt.name)
|
||||
suite.Equal("", stdoutOut, "Failed in [NEITHER]: "+tt.name)
|
||||
}
|
||||
os.Unsetenv("LOG_LEVEL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) TestFullMessage() {
|
||||
type args struct {
|
||||
extraFields map[string]interface{}
|
||||
message string
|
||||
}
|
||||
extraFields := make(map[string]interface{})
|
||||
extraFields["_full_message"] = "full message"
|
||||
|
||||
tests := []struct {
|
||||
args args
|
||||
name string
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
loggerType string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
loggerType: "Error",
|
||||
args: args{
|
||||
message: "This is a error message",
|
||||
extraFields: extraFields,
|
||||
},
|
||||
wantLevel: "error",
|
||||
wantMessage: extraFields["_full_message"].(string),
|
||||
stdErrExpect: true,
|
||||
stdOutExpect: false,
|
||||
},
|
||||
{
|
||||
name: "Test log: Info",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
extraFields: extraFields,
|
||||
},
|
||||
wantMessage: extraFields["_full_message"].(string),
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
}
|
||||
testsLogger = NewLogger()
|
||||
|
||||
captureStdout := stdoutCapture{}
|
||||
captureStdout.StartCapture()
|
||||
captureStderr := stderrCapture{}
|
||||
captureStderr.StartCapture()
|
||||
|
||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{
|
||||
reflect.ValueOf(tt.args.message),
|
||||
reflect.ValueOf(tt.args.extraFields),
|
||||
})
|
||||
|
||||
stdoutOut, err := captureStdout.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
stderrOut, err := captureStderr.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
||||
_, gotMsg, err := getResponseValues(stderrOut, "_full_message")
|
||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
||||
}
|
||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
||||
_, gotMsg, err := getResponseValues(stdoutOut, "_full_message")
|
||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
||||
}
|
||||
os.Unsetenv("LOG_LEVEL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getResponseValues(t *testing.T) {
|
||||
type args struct {
|
||||
sourceJson string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantGotLvl string
|
||||
wantGotMsg string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test with json",
|
||||
args: args{
|
||||
sourceJson: `{"level": "debug", "short_message": "hello world"`,
|
||||
},
|
||||
wantGotLvl: "debug",
|
||||
wantGotMsg: "hello world",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test with json, wrong message field",
|
||||
args: args{
|
||||
sourceJson: `{"level": "debug", "message": "hello world"`,
|
||||
},
|
||||
wantGotLvl: "debug",
|
||||
wantGotMsg: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotGotLvl, gotGotMsg, err := getResponseValues(tt.args.sourceJson, "short_message")
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getResponseValues() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotGotLvl != tt.wantGotLvl {
|
||||
t.Errorf("getResponseValues() gotGotLvl = %v, want %v", gotGotLvl, tt.wantGotLvl)
|
||||
}
|
||||
if gotGotMsg != tt.wantGotMsg {
|
||||
t.Errorf("getResponseValues() gotGotMsg = %v, want %v", gotGotMsg, tt.wantGotMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getResponseValues(sourceJson string, key string) (gotLvl, gotMsg string, err error) {
|
||||
gotLvl, err = jsonparser.GetString([]byte(sourceJson), "level")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gotMsg, err = jsonparser.GetString([]byte(sourceJson), key)
|
||||
return
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package main
|
||||
import (
|
||||
"github.com/gookit/goutil/envutil"
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
libpack_config "github.com/telegram-bot-app/libpack/config"
|
||||
libpack_logging "github.com/telegram-bot-app/libpack/logging"
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
)
|
||||
|
||||
var cfg *config
|
||||
@@ -22,14 +22,20 @@ func parseConfig() {
|
||||
c.Server.PortMonitoring = envutil.GetInt("MONITORING_PORT", 9393)
|
||||
c.Server.HostGraphQL = envutil.Getenv("HOST_GRAPHQL", "http://localhost/v1/graphql")
|
||||
c.Client.JWTUserClaimPath = envutil.Getenv("JWT_USER_CLAIM_PATH", "")
|
||||
c.Cache.CacheEnable = envutil.GetBool("CACHE_ENABLE", false)
|
||||
c.Client.JWTRoleClaimPath = envutil.Getenv("JWT_ROLE_CLAIM_PATH", "")
|
||||
c.Client.RoleFromHeader = envutil.Getenv("ROLE_FROM_HEADER", "")
|
||||
c.Client.RoleRateLimit = envutil.GetBool("ROLE_RATE_LIMIT", false)
|
||||
c.Cache.CacheEnable = envutil.GetBool("ENABLE_GLOBAL_CACHE", false)
|
||||
c.Cache.CacheTTL = envutil.GetInt("CACHE_TTL", 60)
|
||||
c.Security.BlockIntrospection = envutil.GetBool("BLOCK_SCHEMA_INTROSPECTION", false)
|
||||
c.Logger = libpack_logging.NewLogger()
|
||||
c.Client.GQLClient = graphql.NewConnection()
|
||||
c.Client.GQLClient.SetEndpoint(c.Server.HostGraphQL)
|
||||
c.Server.AccessLog = envutil.GetBool("ENABLE_ACCESS_LOG", false)
|
||||
c.Server.ReadOnlyMode = envutil.GetBool("READ_ONLY_MODE", false)
|
||||
cfg = &c
|
||||
enableCache() // takes close to no resources, but can be used with dynamic query cache
|
||||
loadRatelimitConfig()
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
assertions "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type Tests struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
var (
|
||||
assert *assertions.Assertions
|
||||
)
|
||||
|
||||
func (suite *Tests) SetupTest() {
|
||||
assert = assertions.New(suite.T())
|
||||
}
|
||||
|
||||
func (suite *Tests) BeforeTest(suiteName, testName string) {
|
||||
fmt.Println("BeforeTest")
|
||||
cfg = &config{}
|
||||
parseConfig()
|
||||
StartMonitoringServer()
|
||||
}
|
||||
|
||||
// func (suite *Tests) AfterTest(suiteName, testName string) {)
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
suite.Run(t, new(Tests))
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
func StartMonitoringServer() {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package libpack_monitoring
|
||||
|
||||
func (ms *MetricsSetup) RegisterDefaultMetrics() {
|
||||
ms.RegisterMetricsCounter(MetricsSucceeded, nil)
|
||||
ms.RegisterMetricsCounter(MetricsFailed, nil)
|
||||
ms.RegisterMetricsCounter(MetricsSkipped, nil)
|
||||
ms.RegisterMetricsHistogram(MetricsDuration, nil)
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterGoMetrics() {
|
||||
// TODO: metrics.WriteProcessMetrics(ms.metrics_set)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package libpack_monitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
||||
)
|
||||
|
||||
func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) (complete_name string) {
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
labels["microservice"] = libpack_config.PKG_NAME
|
||||
|
||||
if ms.metrics_prefix != "" {
|
||||
complete_name = ms.metrics_prefix + "_" + name
|
||||
} else {
|
||||
complete_name = name
|
||||
}
|
||||
if labels != nil {
|
||||
complete_name += "{"
|
||||
for k, v := range labels {
|
||||
complete_name += k + "=\"" + v + "\","
|
||||
}
|
||||
complete_name = strings.TrimSuffix(complete_name, ",")
|
||||
complete_name += "}"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// replace all spaces with underscores and remove all other non-alphanumeric characters
|
||||
name_new := strings.ReplaceAll(name, " ", "_")
|
||||
name_new = strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, name_new)
|
||||
name_new = strings.ReplaceAll(name_new, "__", "_")
|
||||
name_new = strings.Trim(name_new, "_")
|
||||
if name_new != name {
|
||||
return fmt.Errorf("Invalid metric name: %s, expected %s", name, name_new)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compile_metrics_with_labels(name string, labels map[string]string) string {
|
||||
metric_name := name
|
||||
for k, v := range labels {
|
||||
metric_name += "_" + k + "_" + v
|
||||
}
|
||||
return metric_name
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Package `libpack_monitoring` provides and easy way to add prometheus metrics to your application.
|
||||
// It also provides a way to add custom metrics to the already started prometheus registry.
|
||||
|
||||
package libpack_monitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gookit/goutil/envutil"
|
||||
logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
)
|
||||
|
||||
type MetricsSetup struct {
|
||||
metrics_prefix string
|
||||
metrics_set *metrics.Set
|
||||
}
|
||||
|
||||
var (
|
||||
log *logging.LogConfig
|
||||
)
|
||||
|
||||
func NewMonitoring() *MetricsSetup {
|
||||
log = logging.NewLogger()
|
||||
ms := &MetricsSetup{}
|
||||
ms.metrics_set = metrics.NewSet()
|
||||
go ms.startPrometheusEndpoint()
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) startPrometheusEndpoint() {
|
||||
app := fiber.New()
|
||||
app.Get("/metrics", ms.metricsEndpoint)
|
||||
err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393)))
|
||||
if err != nil {
|
||||
fmt.Println("Can't start the service: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) metricsEndpoint(c *fiber.Ctx) error {
|
||||
ms.metrics_set.WritePrometheus(c.Response().BodyWriter())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) AddMetricsPrefix(prefix string) {
|
||||
ms.metrics_prefix = prefix
|
||||
return
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) ListActiveMetrics() []string {
|
||||
return ms.metrics_set.ListMetricNames()
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
log.Critical("RegisterMetricsGauge() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name})
|
||||
return nil
|
||||
}
|
||||
return ms.metrics_set.GetOrCreateGauge(ms.get_metrics_name(metric_name, labels), func() float64 {
|
||||
// get current value of the gauge and add val to it
|
||||
return val
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
log.Critical("RegisterMetricsCounter() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name})
|
||||
return nil
|
||||
}
|
||||
return ms.metrics_set.GetOrCreateCounter(ms.get_metrics_name(metric_name, labels))
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
log.Critical("RegisterFloatCounter() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name})
|
||||
return nil
|
||||
}
|
||||
return ms.metrics_set.GetOrCreateFloatCounter(ms.get_metrics_name(metric_name, labels))
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
log.Critical("RegisterMetricsSummary() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name})
|
||||
return nil
|
||||
}
|
||||
return ms.metrics_set.GetOrCreateSummary(ms.get_metrics_name(metric_name, labels))
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram {
|
||||
if validate_metrics_name(metric_name) != nil {
|
||||
log.Critical("RegisterMetricsHistogram() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name})
|
||||
return nil
|
||||
}
|
||||
return ms.metrics_set.GetOrCreateHistogram(ms.get_metrics_name(metric_name, labels))
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) Increment(metric_name string, labels map[string]string) {
|
||||
ms.RegisterMetricsCounter(metric_name, labels).Inc()
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) IncrementFloat(metric_name string, labels map[string]string, value float64) {
|
||||
ms.RegisterFloatCounter(metric_name, labels).Add(value)
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) Set(metric_name string, labels map[string]string, value uint64) {
|
||||
ms.RegisterMetricsCounter(metric_name, labels).Set(value)
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) Update(metric_name string, labels map[string]string, value float64) {
|
||||
ms.RegisterMetricsHistogram(metric_name, labels).Update(value)
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) UpdateDuration(metric_name string, labels map[string]string, value time.Time) {
|
||||
ms.RegisterMetricsHistogram(metric_name, labels).UpdateDuration(value)
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) UpdateSummary(metric_name string, labels map[string]string, value float64) {
|
||||
ms.RegisterMetricsSummary(metric_name, labels).Update(value)
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RemoveMetrics(metric_name string, labels map[string]string) {
|
||||
ms.metrics_set.UnregisterMetric(ms.get_metrics_name(metric_name, labels))
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) PurgeMetrics() {
|
||||
ms.metrics_set.UnregisterAllMetrics()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package libpack_monitoring
|
||||
|
||||
const (
|
||||
MetricsSucceeded = "requests_succesful"
|
||||
MetricsFailed = "requests_failed"
|
||||
MetricsDuration = "requests_duration"
|
||||
MetricsSkipped = "requests_skipped"
|
||||
)
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/proxy"
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
func proxyTheRequest(c *fiber.Ctx) error {
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
goratecounter "github.com/lukaszraczylo/go-ratecounter"
|
||||
)
|
||||
|
||||
type RateLimitConfig struct {
|
||||
Req int `json:"req"`
|
||||
Interval string `json:"interval"`
|
||||
RateCounterTicker *goratecounter.RateCounter
|
||||
}
|
||||
|
||||
var rateLimits map[string]RateLimitConfig
|
||||
var ratelimit_intervals = map[string]time.Duration{
|
||||
"milli": time.Millisecond,
|
||||
"micro": time.Microsecond,
|
||||
"nano": time.Nanosecond,
|
||||
"second": time.Second,
|
||||
"minute": time.Minute,
|
||||
"hour": time.Hour,
|
||||
"day": time.Hour * 24,
|
||||
}
|
||||
|
||||
func loadRatelimitConfig() error {
|
||||
paths := []string{"/app/ratelimit.json", "./ratelimit.json", "./static/default-ratelimit.json"}
|
||||
|
||||
for _, path := range paths {
|
||||
err := loadConfigFromPath(path)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
cfg.Logger.Debug("Failed to load config", map[string]interface{}{"path": path, "error": err})
|
||||
}
|
||||
|
||||
cfg.Logger.Error("Rate limit config not found")
|
||||
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"`
|
||||
}{}
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, value := range config.RateLimit {
|
||||
value.RateCounterTicker = goratecounter.NewRateCounter().WithConfig(goratecounter.RateCounterConfig{
|
||||
Interval: time.Duration(value.Req) * ratelimit_intervals[value.Interval],
|
||||
})
|
||||
cfg.Logger.Debug("Setting ratelimit config for role", map[string]interface{}{
|
||||
"role": key,
|
||||
"interval_provided": value.Interval,
|
||||
"interval_used": ratelimit_intervals[value.Interval],
|
||||
"ratelimit": value.Req,
|
||||
})
|
||||
config.RateLimit[key] = value
|
||||
}
|
||||
|
||||
rateLimits = config.RateLimit
|
||||
cfg.Logger.Debug("Rate limit config loaded", map[string]interface{}{"ratelimit": rateLimits})
|
||||
return nil
|
||||
}
|
||||
|
||||
func rateLimitedRequest(userID string, userRole string) (shouldAllow bool) {
|
||||
if rateLimits == nil {
|
||||
cfg.Logger.Debug("Rate limit config not found", 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.Warning("Rate limit role not found", map[string]interface{}{"user_role": userRole})
|
||||
return true
|
||||
}
|
||||
|
||||
if roleConfig.RateCounterTicker == nil {
|
||||
cfg.Logger.Warning("Rate limit ticker not found", map[string]interface{}{"user_role": userRole})
|
||||
return true
|
||||
}
|
||||
|
||||
roleConfig.RateCounterTicker.Incr(1)
|
||||
tickerRate := roleConfig.RateCounterTicker.GetRate()
|
||||
|
||||
logDetails := map[string]interface{}{
|
||||
"user_role": userRole,
|
||||
"user_id": userID,
|
||||
"rate": tickerRate,
|
||||
"config_rate": roleConfig.Req,
|
||||
"interval": roleConfig.Interval,
|
||||
}
|
||||
|
||||
cfg.Logger.Debug("Rate limit ticker", logDetails)
|
||||
|
||||
if tickerRate > float64(roleConfig.Req) {
|
||||
cfg.Logger.Debug("Rate limit exceeded", logDetails)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -11,5 +11,6 @@ wording:
|
||||
minor:
|
||||
- change
|
||||
- improve
|
||||
- release
|
||||
major:
|
||||
- breaking
|
||||
@@ -6,8 +6,9 @@ import (
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
@@ -41,58 +42,102 @@ func healthCheck(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func processGraphQLRequest(c *fiber.Ctx) error {
|
||||
t := time.Now()
|
||||
startTime := time.Now()
|
||||
|
||||
var extracted_user_id string = "-"
|
||||
var query_cache_hash string = ""
|
||||
// Initialize variables with default values
|
||||
extractedUserID := "-"
|
||||
extractedRoleName := "-"
|
||||
var queryCacheHash string
|
||||
|
||||
authorization := c.Request().Header.Peek("Authorization")
|
||||
if authorization != nil && len(cfg.Client.JWTUserClaimPath) > 0 {
|
||||
extracted_user_id = extractClaimsFromJWTHeader(string(authorization))
|
||||
if authorization != nil && (len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0) {
|
||||
extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(string(authorization))
|
||||
}
|
||||
opType, opName, cache_from_query, should_block := parseGraphQLQuery(c)
|
||||
|
||||
if should_block {
|
||||
if len(cfg.Client.RoleFromHeader) > 0 {
|
||||
extractedRoleName = string(c.Request().Header.Peek(cfg.Client.RoleFromHeader))
|
||||
if extractedRoleName == "" {
|
||||
extractedRoleName = "-"
|
||||
}
|
||||
}
|
||||
|
||||
// Implementing rate limiting if enabled
|
||||
if cfg.Client.RoleRateLimit {
|
||||
cfg.Logger.Debug("Rate limiting enabled", map[string]interface{}{"user_id": extractedUserID, "role_name": extractedRoleName})
|
||||
if !rateLimitedRequest(extractedUserID, extractedRoleName) {
|
||||
c.Status(429).SendString("Rate limit exceeded, try again later")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
opType, opName, cacheFromQuery, cache_time, shouldBlock := parseGraphQLQuery(c)
|
||||
if shouldBlock {
|
||||
return nil
|
||||
}
|
||||
|
||||
was_cached := false
|
||||
if cache_time > 0 {
|
||||
cfg.Logger.Debug("Cache time set via query", map[string]interface{}{"cache_time": cache_time})
|
||||
cache_time = cfg.Cache.CacheTTL
|
||||
}
|
||||
|
||||
if cache_from_query || cfg.Cache.CacheEnable {
|
||||
cfg.Logger.Debug("Cache enabled", map[string]interface{}{"via_query": cache_from_query, "via_env": cfg.Cache.CacheEnable})
|
||||
query_cache_hash = calculateHash(c)
|
||||
cachedResponse := cacheLookup(query_cache_hash)
|
||||
if cachedResponse != nil {
|
||||
cfg.Logger.Debug("Cache hit", map[string]interface{}{"hash": query_cache_hash, "user_id": extracted_user_id})
|
||||
wasCached := false
|
||||
|
||||
// Handling Cache Logic
|
||||
if cacheFromQuery || cfg.Cache.CacheEnable {
|
||||
cfg.Logger.Debug("Cache enabled", map[string]interface{}{"via_query": cacheFromQuery, "via_env": cfg.Cache.CacheEnable})
|
||||
queryCacheHash = calculateHash(c)
|
||||
|
||||
if cachedResponse := cacheLookup(queryCacheHash); cachedResponse != nil {
|
||||
cfg.Logger.Debug("Cache hit", map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID})
|
||||
c.Send(cachedResponse)
|
||||
was_cached = true
|
||||
wasCached = true
|
||||
} else {
|
||||
cfg.Logger.Debug("Cache miss", map[string]interface{}{"hash": query_cache_hash, "user_id": extracted_user_id})
|
||||
proxyTheRequest(c)
|
||||
cfg.Cache.CacheClient.Set(query_cache_hash, c.Response().Body(), time.Duration(cfg.Cache.CacheTTL)*time.Second)
|
||||
c.Send(c.Response().Body())
|
||||
cfg.Logger.Debug("Cache miss", map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID})
|
||||
proxyAndCacheTheRequest(c, queryCacheHash, cache_time)
|
||||
}
|
||||
} else {
|
||||
proxyTheRequest(c)
|
||||
}
|
||||
time_taken := time.Since(t)
|
||||
|
||||
cfg.Logger.Info("Request processed", map[string]interface{}{"ip": c.IP(), "user_id": extracted_user_id, "op_type": opType, "op_name": opName, "time": time_taken, "cache": was_cached})
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSucceeded, nil)
|
||||
timeTaken := time.Since(startTime)
|
||||
|
||||
// Logging & Monitoring
|
||||
logAndMonitorRequest(c, extractedUserID, opType, opName, wasCached, timeTaken, startTime)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Additional helper function to avoid code repetition
|
||||
func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cache_time int) {
|
||||
proxyTheRequest(c)
|
||||
cfg.Cache.CacheClient.Set(queryCacheHash, c.Response().Body(), time.Duration(cache_time)*time.Second)
|
||||
c.Send(c.Response().Body())
|
||||
}
|
||||
|
||||
func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) {
|
||||
labels := map[string]string{
|
||||
"op_type": opType,
|
||||
"op_name": opName,
|
||||
"cached": fmt.Sprintf("%t", was_cached),
|
||||
"user_id": extracted_user_id,
|
||||
"cached": fmt.Sprintf("%t", wasCached),
|
||||
"user_id": userID,
|
||||
}
|
||||
|
||||
if cfg.Server.AccessLog {
|
||||
cfg.Logger.Info("Request processed", map[string]interface{}{
|
||||
"ip": c.IP(),
|
||||
"user_id": userID,
|
||||
"op_type": opType,
|
||||
"op_name": opName,
|
||||
"time": duration,
|
||||
"cache": wasCached,
|
||||
})
|
||||
}
|
||||
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSucceeded, nil)
|
||||
cfg.Monitoring.Increment("executed_query", labels)
|
||||
|
||||
if !was_cached {
|
||||
cfg.Monitoring.UpdateDuration("timed_query", labels, t)
|
||||
cfg.Monitoring.Update("timed_query", labels, float64(time_taken.Milliseconds()))
|
||||
if !wasCached {
|
||||
cfg.Monitoring.UpdateDuration("timed_query", labels, startTime)
|
||||
cfg.Monitoring.Update("timed_query", labels, float64(duration.Milliseconds()))
|
||||
}
|
||||
// // cfg.Monitoring.Set("timed_query", time_taken.Milliseconds())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"ratelimit": {
|
||||
"admin": {
|
||||
"req": 100,
|
||||
"interval": "second"
|
||||
},
|
||||
"guest": {
|
||||
"req": 3,
|
||||
"interval": "second"
|
||||
},
|
||||
"-": {
|
||||
"req": 100,
|
||||
"interval": "hour"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ spec:
|
||||
value: "9393"
|
||||
- name: HOST_GRAPHQL
|
||||
value: http://hasura-internal:8080/v1/graphql
|
||||
- name: ENABLE_CACHE
|
||||
- name: ENABLE_GLOBAL_CACHE
|
||||
value: "true"
|
||||
- name: CACHE_TTL
|
||||
value: "10"
|
||||
|
||||
+7
-2
@@ -3,8 +3,8 @@ package main
|
||||
import (
|
||||
"github.com/akyoto/cache"
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
libpack_logging "github.com/telegram-bot-app/libpack/logging"
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring"
|
||||
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
|
||||
)
|
||||
|
||||
// config is a struct that holds the configuration of the application.
|
||||
@@ -17,10 +17,15 @@ type config struct {
|
||||
PortGraphQL int
|
||||
PortMonitoring int
|
||||
HostGraphQL string
|
||||
AccessLog bool
|
||||
ReadOnlyMode bool
|
||||
}
|
||||
|
||||
Client struct {
|
||||
JWTUserClaimPath string
|
||||
JWTRoleClaimPath string
|
||||
RoleRateLimit bool
|
||||
RoleFromHeader string
|
||||
GQLClient *graphql.BaseClient
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user