Compare commits

..

5 Commits

5 changed files with 70 additions and 34 deletions
+47 -31
View File
@@ -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.
![Example of monitoring dashboard](static/monitoring-at-glance.png?raw=true)
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,47 +20,58 @@ 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
* SECURITY: Rate limiting queries based on user role
| 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: ``)
* `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`)
| 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
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 `@cached` directive to the query.
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 case of the `@cached` you can add additional parameters to the directive which will set the cache for specific query to provided time.
For example `query MyCachedQuery @cached(ttl: 90) ....` will set the cache for the query to 90 seconds.
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
### 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`.
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 current user role is present use the `JWT_ROLE_CLAIM_PATH` environment variable.
To define path in JWT token where the current user role is present, use the `JWT_ROLE_CLAIM_PATH` environment variable.
*Default / sample configuration:*
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
{
@@ -85,6 +96,11 @@ 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
Example metrics produced by the proxy:
+9
View File
@@ -2,6 +2,7 @@ package main
import (
"strconv"
"strings"
fiber "github.com/gofiber/fiber/v2"
"github.com/graphql-go/graphql/language/ast"
@@ -62,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 {
+3 -1
View File
@@ -23,7 +23,8 @@ func parseConfig() {
c.Server.HostGraphQL = envutil.Getenv("HOST_GRAPHQL", "http://localhost/v1/graphql")
c.Client.JWTUserClaimPath = envutil.Getenv("JWT_USER_CLAIM_PATH", "")
c.Client.JWTRoleClaimPath = envutil.Getenv("JWT_ROLE_CLAIM_PATH", "")
c.Client.JWTRoleRateLimit = envutil.GetBool("JWT_ROLE_RATE_LIMIT", false)
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)
@@ -31,6 +32,7 @@ func parseConfig() {
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()
+8 -1
View File
@@ -54,8 +54,15 @@ func processGraphQLRequest(c *fiber.Ctx) error {
extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(string(authorization))
}
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.JWTRoleRateLimit {
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")
+3 -1
View File
@@ -18,12 +18,14 @@ type config struct {
PortMonitoring int
HostGraphQL string
AccessLog bool
ReadOnlyMode bool
}
Client struct {
JWTUserClaimPath string
JWTRoleClaimPath string
JWTRoleRateLimit bool
RoleRateLimit bool
RoleFromHeader string
GQLClient *graphql.BaseClient
}