mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-11 00:09:37 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc128493b0 | |||
|
c213a49c32
|
|||
|
ac44056a00
|
|||
|
743eed7f71
|
|||
|
b89053c015
|
|||
|
16f29488c5
|
|||
|
5ca37fc9fb
|
|||
|
ed1de61e2e
|
|||
|
e7b2cc1deb
|
|||
|
3ac7c115aa
|
|||
|
eee6016b5a
|
|||
|
3b8df8ee76
|
|||
|
f9e917f2ea
|
@@ -0,0 +1,2 @@
|
||||
github: [ lukaszraczylo ]
|
||||
custom: [ monzo.me/lukaszraczylo ]
|
||||
@@ -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"]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Lukasz Raczylo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -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 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=warn 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
|
||||
|
||||
@@ -2,21 +2,101 @@
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
### Endpoints
|
||||
|
||||
/v1/graphql - the graphql endpoint
|
||||
/metrics - the prometheus metrics endpoint
|
||||
/healthz - the healthcheck endpoint
|
||||
* `:8080/v1/graphql` - the graphql endpoint
|
||||
* `:9393/metrics` - the prometheus metrics endpoint
|
||||
* `:8080/healthz` - the healthcheck endpoint
|
||||
|
||||
### 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
|
||||
* SECURITY: Rate limiting queries based on user role
|
||||
|
||||
### 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: `localhost/v1/graphql`)
|
||||
* `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: ``)
|
||||
* `JWT_ROLE_CLAIM_PATH` - the path to the role claim in the JWT token (default: ``)
|
||||
* `JWT_ROLE_RATE_LIMITING` - enable request rate limiting based on the role (default: `false`)
|
||||
* `ENABLE_GLOBAL_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`)
|
||||
* `ENABLE_ACCESS_LOG` - enable the access log (default: `false`)
|
||||
|
||||
`JWT_USER_CLAIM_PATH` - the path to the user claim in the JWT token (default: ``)
|
||||
### Caching
|
||||
|
||||
`ENABLE_CACHE` - enable the cache (default: `false`)
|
||||
`CACHE_TTL` - the cache TTL (default: `60s`)
|
||||
Cache engine is enabled in background as it does not use any 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 of the query. You can leave the global cache disabled and enable the cache for specific queries by adding the `@cache` directive to the query.
|
||||
|
||||
`LOG_LEVEL` - the log level (default: `info`)
|
||||
### Role based rate limiting
|
||||
|
||||
You are able to rate limit requests using the `JWT_ROLE_RATE_LIMITING` 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 following format to specify the limits.
|
||||
Default interval is `second`, but you can use other values as well. If you want to disable the rate limiting for 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 current user role is present use the `JWT_ROLE_CLAIM_PATH` environment variable.
|
||||
|
||||
*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.
|
||||
|
||||
### Monitoring endpoint
|
||||
|
||||
Example metrics produced by the proxy:
|
||||
|
||||
```
|
||||
graphql_proxy_timed_query_bucket{cached="false",user_id="-",op_type="mutation",op_name="updateUserDetails",vmrange="1.000e-02...1.136e-02"} 6
|
||||
graphql_proxy_timed_query_count{op_name="",cached="false",user_id="-",op_type=""} 78
|
||||
graphql_proxy_timed_query_bucket{op_name="MyQuery",cached="false",user_id="-",op_type="query",vmrange="5.995e+00...6.813e+00"} 1
|
||||
graphql_proxy_timed_query_sum{op_name="MyQuery",cached="false",user_id="-",op_type="query"} 6
|
||||
graphql_proxy_timed_query_count{op_name="MyQuery",cached="false",user_id="-",op_type="query"} 1
|
||||
graphql_proxy_executed_query{user_id="-",op_type="mutation",op_name="updateKnownSpammer",cached="false"} 1486
|
||||
graphql_proxy_executed_query{user_id="-",op_type="query",op_name="checkIfAdminsNeedRefreshing",cached="false"} 13167
|
||||
graphql_proxy_executed_query{user_id="1337",op_type="query",op_name="checkIfKnownMedia",cached="false"} 429
|
||||
graphql_proxy_executed_query{user_id="-",op_type="query",op_name="checkIfSpamAIRequiresUpdate",cached="false"} 8891
|
||||
graphql_proxy_requests_failed 324
|
||||
graphql_proxy_requests_skipped 0
|
||||
graphql_proxy_requests_succesful 454823
|
||||
```
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
+29
-15
@@ -2,37 +2,51 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/ask"
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@ require (
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
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-20231007021518-909ce2741a36
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/telegram-bot-app/libpack v0.0.0-20231008100411-9f7f8bf94315
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -18,15 +20,18 @@ require (
|
||||
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/klauspost/compress v1.17.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lukaszraczylo/pandati v0.0.29 // 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
|
||||
@@ -42,4 +47,5 @@ require (
|
||||
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/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -11,6 +11,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=
|
||||
@@ -30,8 +31,14 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
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=
|
||||
@@ -54,6 +61,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
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.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=
|
||||
@@ -63,8 +72,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
||||
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-20231007021518-909ce2741a36 h1:DqXg0y57Q7BziHDu85OXgo/b8OlP7/+gDZvASQCkaW0=
|
||||
github.com/telegram-bot-app/libpack v0.0.0-20231007021518-909ce2741a36/go.mod h1:W2kWHcfNNS0r++dJ1T2XX/C4cTSxI3MsoiMbOtyqu+I=
|
||||
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=
|
||||
@@ -95,5 +104,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=
|
||||
|
||||
+46
-2
@@ -7,7 +7,33 @@ import (
|
||||
libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring"
|
||||
)
|
||||
|
||||
func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cacheRequest bool) {
|
||||
var retrospection_queries = []string{
|
||||
"__schema",
|
||||
"__type",
|
||||
"__typename",
|
||||
"__directive",
|
||||
"__directivelocation",
|
||||
"__field",
|
||||
"__inputvalue",
|
||||
"__enumvalue",
|
||||
"__typekind",
|
||||
"__fieldtype",
|
||||
"__inputobjecttype",
|
||||
"__enumtype",
|
||||
"__uniontype",
|
||||
"__scalars",
|
||||
"__objects",
|
||||
"__interfaces",
|
||||
"__unions",
|
||||
"__enums",
|
||||
"__inputobjects",
|
||||
"__directives",
|
||||
}
|
||||
|
||||
// Saving the introspection queries as a map O(1) operation instead of O(n) for a slice.
|
||||
var retrospectionQuerySet = make(map[string]struct{}, len(retrospection_queries))
|
||||
|
||||
func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cacheRequest bool, should_block bool) {
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal(c.Body(), &m)
|
||||
if err != nil {
|
||||
@@ -34,13 +60,31 @@ func parseGraphQLQuery(c *fiber.Ctx) (operationType, operationName string, cache
|
||||
for _, d := range p.Definitions {
|
||||
if oper, ok := d.(*ast.OperationDefinition); ok {
|
||||
operationType = oper.Operation
|
||||
operationName = oper.Name.Value
|
||||
if oper.Name != nil {
|
||||
operationName = oper.Name.Value
|
||||
} else {
|
||||
operationName = "undefined"
|
||||
}
|
||||
for _, dir := range oper.Directives {
|
||||
if dir.Name.Value == "cached" {
|
||||
cacheRequest = true
|
||||
}
|
||||
}
|
||||
if cfg.Security.BlockIntrospection {
|
||||
for _, s := range oper.SelectionSet.Selections {
|
||||
for _, s2 := range s.GetSelectionSet().Selections {
|
||||
if _, exists := retrospectionQuerySet[s2.(*ast.Field).Name.Value]; exists {
|
||||
cfg.Logger.Warning("Introspection query blocked", m)
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
|
||||
c.Status(403).SendString("Introspection queries are not allowed")
|
||||
should_block = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,20 +9,31 @@ import (
|
||||
|
||||
var cfg *config
|
||||
|
||||
func init() {
|
||||
for _, query := range retrospection_queries {
|
||||
retrospectionQuerySet[query] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig() {
|
||||
libpack_config.PKG_NAME = "graphql_proxy"
|
||||
var c config
|
||||
c.Server.PortGraphQL = envutil.GetInt("PORT_GRAPHQL", 8080)
|
||||
c.Server.PortMonitoring = envutil.GetInt("MONITORING_PORT", 9393)
|
||||
c.Server.HostGraphQL = envutil.Getenv("HOST_GRAPHQL", "localhost/v1/graphql")
|
||||
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.JWTRoleRateLimit = envutil.GetBool("JWT_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)
|
||||
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))
|
||||
}
|
||||
+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.Error("Failed to load config", map[string]interface{}{"path": path, "error": err})
|
||||
}
|
||||
|
||||
cfg.Logger.Debug("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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ version: 1
|
||||
force:
|
||||
existing: true
|
||||
strict: false
|
||||
minor: 1
|
||||
wording:
|
||||
patch:
|
||||
- update
|
||||
@@ -10,5 +11,6 @@ wording:
|
||||
minor:
|
||||
- change
|
||||
- improve
|
||||
- release
|
||||
major:
|
||||
- breaking
|
||||
@@ -6,6 +6,7 @@ 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"
|
||||
)
|
||||
@@ -30,65 +31,101 @@ func StartHTTPProxy() {
|
||||
}
|
||||
|
||||
func healthCheck(c *fiber.Ctx) error {
|
||||
query := `{ __typename }`
|
||||
_, err := cfg.Client.GQLClient.Query(query, nil, nil)
|
||||
if err != nil {
|
||||
cfg.Logger.Error("Can't reach the GraphQL server", map[string]interface{}{"error": err.Error()})
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
return c.SendStatus(500)
|
||||
}
|
||||
// query := `{ __typename }`
|
||||
// _, err := cfg.Client.GQLClient.Query(query, nil, nil)
|
||||
// if err != nil {
|
||||
// cfg.Logger.Error("Can't reach the GraphQL server", map[string]interface{}{"error": err.Error()})
|
||||
// cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
// return c.SendStatus(500)
|
||||
// }
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
|
||||
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 := parseGraphQLQuery(c)
|
||||
|
||||
was_cached := false
|
||||
// Implementing rate limiting if enabled
|
||||
if cfg.Client.JWTRoleRateLimit {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
opType, opName, cacheFromQuery, shouldBlock := parseGraphQLQuery(c)
|
||||
if shouldBlock {
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} 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) {
|
||||
proxyTheRequest(c)
|
||||
cfg.Cache.CacheClient.Set(queryCacheHash, c.Response().Body(), time.Duration(cfg.Cache.CacheTTL)*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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hasura-proxy-internal
|
||||
labels:
|
||||
app: hasura-proxy-internal
|
||||
type: support
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hasura-proxy-internal
|
||||
type: support
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hasura-proxy-internal
|
||||
type: support
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "9393"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 65534 # nobody
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: node-role.kubernetes.io/worker
|
||||
operator: Exists
|
||||
containers:
|
||||
- name: graphql-proxy
|
||||
image: ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest
|
||||
imagePullPolicy: Always
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: "640Mi"
|
||||
requests:
|
||||
cpu: "0.75"
|
||||
memory: "512Mi"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
ports:
|
||||
- name: web
|
||||
containerPort: 8080
|
||||
- name: monitoring
|
||||
containerPort: 9393
|
||||
env:
|
||||
- name: PORT_GRAPHQL
|
||||
value: "8080"
|
||||
- name: MONITORING_PORT
|
||||
value: "9393"
|
||||
- name: HOST_GRAPHQL
|
||||
value: http://hasura-internal:8080/v1/graphql
|
||||
- name: ENABLE_GLOBAL_CACHE
|
||||
value: "true"
|
||||
- name: CACHE_TTL
|
||||
value: "10"
|
||||
- name: BLOCK_SCHEMA_INTROSPECTION
|
||||
value: "true"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: hasura-proxy-internal
|
||||
labels:
|
||||
app: hasura-proxy-internal
|
||||
type: support
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "9393"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
ports:
|
||||
- name: web
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
- name: monitoring
|
||||
port: 9393
|
||||
targetPort: 9393
|
||||
selector:
|
||||
app: hasura-proxy-internal
|
||||
type: support
|
||||
type: ClusterIP
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 336 KiB |
@@ -17,10 +17,13 @@ type config struct {
|
||||
PortGraphQL int
|
||||
PortMonitoring int
|
||||
HostGraphQL string
|
||||
AccessLog bool
|
||||
}
|
||||
|
||||
Client struct {
|
||||
JWTUserClaimPath string
|
||||
JWTRoleClaimPath string
|
||||
JWTRoleRateLimit bool
|
||||
GQLClient *graphql.BaseClient
|
||||
}
|
||||
|
||||
@@ -29,4 +32,8 @@ type config struct {
|
||||
CacheTTL int
|
||||
CacheClient *cache.Cache
|
||||
}
|
||||
|
||||
Security struct {
|
||||
BlockIntrospection bool
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user