Compare commits

..
131 Commits
Author SHA1 Message Date
lukaszraczylo adaa79dd6a fixup! fixup! fixup! fixup! fixup! Aligning struct fields for better memory management. 2024-06-27 08:41:08 +01:00
lukaszraczylo b6a3638490 fixup! fixup! fixup! fixup! Aligning struct fields for better memory management. 2024-06-20 16:31:13 +01:00
lukaszraczylo a56b298882 fixup! fixup! fixup! Aligning struct fields for better memory management. 2024-06-20 16:20:48 +01:00
lukaszraczylo 6b4b761176 fixup! fixup! Aligning struct fields for better memory management. 2024-06-20 16:18:42 +01:00
lukaszraczylo 3fb7c7e0b1 fixup! Aligning struct fields for better memory management. 2024-06-20 16:00:51 +01:00
lukaszraczylo a4f7e1690d Aligning struct fields for better memory management. 2024-06-20 08:40:55 +01:00
lukaszraczylo fe34a03eb8 fixup! Add draft of tracing package. 2024-06-20 00:27:18 +01:00
lukaszraczylo 934bddae1b Add draft of tracing package. 2024-06-20 00:23:00 +01:00
lukaszraczylo 16844e325e Disable caller as it's not necessary and generates slight delay. 2024-06-19 23:40:44 +01:00
lukaszraczyloandGitHub 61d7a45d00 Update cache library, use miniredis for testing, add additional benchmarks. (#14)
Update cache library,
Update logging library,
use miniredis for testing, add additional benchmarks.
2024-06-19 23:10:36 +01:00
12e4237997 divide long functions, replace strings.builder with bytes.buffer. (#13)
Co-authored-by: Chris Clayton <[email protected]>
2024-06-17 10:23:41 +01:00
de31912d2f increase error handling and mutex encapsulation (#12)
* increase error handling and mutex encapsulation

* undo method rename for now

* set cant return error

---------

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

As a bonus - added tests and benchmarks for monitoring package.
2024-06-11 19:57:18 +01:00
lukaszraczylo df9f43718a fixup! fixup! fixup! fixup! fixup! fixup! Fix: Redis connection for tests. 2024-06-11 12:53:29 +01:00
lukaszraczylo 91d824636d fixup! fixup! fixup! fixup! fixup! Fix: Redis connection for tests. 2024-06-11 12:12:17 +01:00
lukaszraczylo cecccc1441 fixup! fixup! fixup! fixup! Fix: Redis connection for tests. 2024-06-11 12:08:44 +01:00
lukaszraczylo 32eef4af37 fixup! fixup! fixup! Fix: Redis connection for tests. 2024-06-11 12:07:30 +01:00
lukaszraczylo d05172294c fixup! fixup! Fix: Redis connection for tests. 2024-06-11 11:49:50 +01:00
lukaszraczylo 44cd694086 fixup! Fix: Redis connection for tests. 2024-06-11 11:44:17 +01:00
lukaszraczylo fe7af0b8ca Fix: Redis connection for tests. 2024-06-11 11:43:07 +01:00
lukaszraczylo 12e0294945 Add distibuted cache with Redis 2024-06-11 11:35:50 +01:00
lukaszraczylo a01a4da9b5 Add metrics for cached queries + cache hit/miss 2024-06-11 11:35:49 +01:00
lukaszraczylo 371d51f96f Update dependencies. 2024-06-11 11:35:49 +01:00
lukaszraczylo a9fd6b3d0a Release: Add cache operations via API + support distributed redis cache. 2024-06-11 11:35:46 +01:00
lukaszraczylo 9291ac03db Improved graphql library. 2024-05-14 11:33:35 +01:00
lukaszraczylo 75944a3a52 Fixup: Update graphql client library. 2024-05-14 09:40:21 +01:00
lukaszraczylo 5a01ec3876 Improve logging and cache sub-packages to decrease the number of allocations
and improve performance.
2024-05-14 09:21:16 +01:00
lukaszraczylo c3e5b85f57 Update dependencies. 2024-05-04 21:22:19 +01:00
lukaszraczylo bc2dff0185 Update dependencies. 2024-04-12 23:30:18 +01:00
lukaszraczyloandGitHub ce344d17eb Add read only replica (#11)
* Improve stats gathering.
2024-03-13 23:09:38 +00:00
lukaszraczylo dc916d36cd Fix documentation after testing. 2024-03-12 23:12:50 +00:00
lukaszraczyloandGitHub e495cf23d9 Read only endpoint support (#10)
* This change introduces ability to set additional endpoint leading to the
instance of the graphql server connected to the read only database.
If regular query is detected and endpoint for `HOST_GRAPHQL_READONLY` value is set,
the query will be proxied to it. Mutations and non-graphql will be sent
to the `HOST_GRAPHQL` endpoint.
2024-03-12 11:16:35 +00:00
lukaszraczyloandGitHub ba1fef9b57 Improve stats gathering (#9) 2024-03-05 22:42:30 +00:00
lukaszraczyloandGitHub 3a18e0e935 Improve stats gathering and tests improvements. (#8) 2024-03-05 22:40:06 +00:00
lukaszraczylo b6c284b66d Update dependencies. 2024-03-05 14:40:45 +00:00
lukaszraczylo 88ef1aac7f Reuse http client and add retry to the proxied requests. 2024-03-05 14:38:03 +00:00
lukaszraczylo 6d32278851 Reuse http client. 2024-03-05 14:24:49 +00:00
lukaszraczylo f2085c8491 Update dependencies. 2024-02-15 10:22:31 +00:00
lukaszraczylo ebbb1c53f5 Micro fixes. 2024-02-15 10:21:51 +00:00
lukaszraczylo 0bdea741bf Move results to the struct for ease of management. 2024-02-15 09:50:51 +00:00
lukaszraczylo 4cb0d22874 Return 403 on blocked queries. 2024-02-15 09:34:57 +00:00
lukaszraczylo 9910bb1d45 Update documentation. 2024-02-15 09:31:49 +00:00
lukaszraczylo 756c63c0d1 Add support for 'refresh' in @cached section of the query.
Example:

```
query MyProducts @cached(refresh: true) {
  products {
    id
    name
  }
}
```
2024-02-15 09:29:27 +00:00
lukaszraczylo 029e0166c0 Docker to use distroless image. 2024-02-08 20:50:50 +00:00
lukaszraczylo 4cf27e0e3b Missed on files requiring json encoding. 2024-02-05 15:37:24 +00:00
lukaszraczylo 3149a27466 Update semver config. 2024-02-05 15:31:45 +00:00
lukaszraczylo bb28f2fcd8 Change the json library to the fully compatible ( and fast ) one. 2024-02-05 15:30:59 +00:00
lukaszraczylo d3a8da1dcf Move location of the global proxy client from the per-req to main.
There's no need to re-create it every single time.
2024-02-05 14:35:33 +00:00
lukaszraczylo 794cb1ddf4 Add the prefixed environment variables to avoid potential conflicts. 2024-02-05 14:24:17 +00:00
lukaszraczylo 95f2236c96 Dependencies refresh. 2024-02-05 13:31:33 +00:00
lukaszraczylo 1ff568a271 Update dependencies. 2024-01-11 10:39:15 +00:00
lukaszraczylo b19b17b7c4 Realign the structs to decrease memory footprint.
Add the timeout settings to address the connection drops.
2023-12-14 17:16:38 +00:00
lukaszraczylo cd9c650226 Remove compression from proxied request. 2023-12-13 23:13:34 +00:00
lukaszraczylo d09940ebc4 Update connection settings. 2023-12-13 22:50:41 +00:00
lukaszraczylo 3596b03953 Update dependencies. 2023-12-13 09:34:37 +00:00
lukaszraczylo 760a168365 Update dependencies. 2023-12-12 21:43:45 +00:00
lukaszraczylo bc305dd8e9 Improve tests and speed things up a little. 2023-11-20 11:38:02 +00:00
lukaszraczylo b4c047819f Update dependencies. 2023-11-18 02:12:59 +00:00
lukaszraczylo 1390e7cdd1 Fix blocking the introspection + add unit tests. 2023-11-18 02:11:38 +00:00
lukaszraczylo a71b3950db Load retrospection query set once. 2023-11-17 22:32:58 +00:00
lukaszraczylo 827c26e88d Fix retrospection query blocking. 2023-11-17 22:29:42 +00:00
lukaszraczylo 30528e4a9a Sort labels by keys before pushing them to metrics registry.
This is needed to ensure that labels are always in the same order and
metrics won't produce duplicates.
2023-11-17 14:36:23 +00:00
lukaszraczylo 94657ddff4 fixup! fixup! Add purging metrics on timer. 2023-11-17 14:11:55 +00:00
lukaszraczylo a29733a52a fixup! Add purging metrics on timer. 2023-11-17 14:09:26 +00:00
lukaszraczylo 105c624426 Add purging metrics on timer. 2023-11-17 13:47:54 +00:00
lukaszraczylo 1a790ffb52 Make sure that pod name is included in metrics. 2023-11-16 17:18:27 +00:00
lukaszraczyloandGitHub 0b642f8be1 Add ability to reset metrics between crawl to limit payload absorbed (#5)
by the prometheus/victoria metric crawlers.
2023-11-16 16:45:48 +00:00
lukaszraczylo 9c9fa94140 Add ability to set cache via query header. 2023-11-14 09:52:51 +00:00
lukaszraczylo 93318df9fe Update dependencies. 2023-11-14 09:43:33 +00:00
lukaszraczylo b497ad1d1c Add generation of query uuid logs and easier debugging. 2023-10-25 14:11:44 +01:00
lukaszraczylo 3e6fa2036e Remove idle connections setup and allow the client to handle them 2023-10-24 16:56:01 +01:00
lukaszraczylo 4640eb2596 Adjust the timeout settings to prevent forever-hanging connections. 2023-10-24 15:52:40 +01:00
lukaszraczylo 3d70018179 Add configurable timeout for queries. 2023-10-24 10:40:17 +01:00
lukaszraczylo 8fc5782d29 Update documentation with websockets. 2023-10-24 00:22:28 +01:00
lukaszraczylo 4255f87efd Add cache compression. 2023-10-20 11:21:01 +01:00
lukaszraczylo 1e299c0dc4 Update documentation on healthcheck. 2023-10-19 15:55:08 +01:00
lukaszraczylo 35e6069f5e Add the healtcheck checks on the end server. 2023-10-19 15:43:49 +01:00
lukaszraczylo ef8731300c Fix the tests. 2023-10-19 14:53:49 +01:00
lukaszraczyloandGitHub 92359c1114 Cleanup pt 1 (#4)
* Disable startup headers.

* Add banning / unbanning of specific user.
2023-10-19 14:36:16 +01:00
lukaszraczylo 2be4f17ea3 Disable header normalisation.
It looks like a bug in hasura which makes headers case sensitive.
2023-10-16 15:47:48 +01:00
lukaszraczylo 3cb9088b73 Revert disable headers normalising. 2023-10-16 15:31:45 +01:00
lukaszraczylo f50f98b3d6 Add printing out the request headers in debug mode. 2023-10-16 15:31:16 +01:00
lukaszraczylo 29f7fec5a3 Update dependencies. 2023-10-16 15:20:28 +01:00
lukaszraczylo 57cf36ba02 Add /livez endpoint. 2023-10-16 09:09:46 +01:00
lukaszraczylo 2a0302ab75 Create allow list for event when intospection is blocked but developers
really want to use certain subqueries.
2023-10-15 10:01:23 +01:00
lukaszraczyloandGitHub 29ffb8a817 Update README.md 2023-10-14 08:58:55 +01:00
lukaszraczylo 6ac3937066 Fix leaky bytes allocation for cache. 2023-10-13 16:29:52 +01:00
lukaszraczylo 089d05b7c3 Improve cache mechanism using sync map 2023-10-13 15:37:57 +01:00
lukaszraczylo 7293583a99 Resources allocation improvement. 2023-10-13 15:26:24 +01:00
lukaszraczylo dbd005bdcf Remove external library dependency, use homebrewed cache instead. 2023-10-13 15:22:47 +01:00
lukaszraczylo bf18f36e45 If proxying of the query fails - return 500. 2023-10-13 14:48:53 +01:00
lukaszraczylo 3c0f9f49fd Add debugging option for the request / response cycle. 2023-10-13 14:23:05 +01:00
lukaszraczylo bf9ec2c877 Reuse fasthttp client 2023-10-12 21:16:57 +01:00
lukaszraczylo 815a6841ed Add ability to set up allowed paths for proxying. 2023-10-12 14:12:03 +01:00
lukaszraczylo f41b2ae46f New: Proxy all the requests to the graphql server 2023-10-11 11:26:55 +01:00
lukaszraczylo dd25e4a4a5 fixup! Remove last leftover of internal libraries. 2023-10-10 22:31:11 +01:00
lukaszraczylo 8a2b90ef8b Remove last leftover of internal libraries. 2023-10-10 22:29:56 +01:00
lukaszraczylo e358e2a720 Remove redundant tests. 2023-10-10 22:28:38 +01:00
lukaszraczylo 1a3628837f Extract helper libraries from private repo of telegram-bot.app 2023-10-10 22:16:50 +01:00
lukaszraczylo 0758cd5b52 fixup! Add ability to look for the role in header. 2023-10-10 19:49:43 +01:00
lukaszraczylo 51dfc8d9be Add ability to look for the role in header. 2023-10-10 19:48:56 +01:00
lukaszraczylo 2f87f40822 Update README to something more readable. 2023-10-10 19:31:07 +01:00
lukaszraczylo 377a1a4a26 Update documentation. 2023-10-10 19:28:34 +01:00
lukaszraczylo 7de1cf7cc7 Add read only mode to block all the queries with mutations. 2023-10-10 19:26:36 +01:00
lukaszraczyloandGitHub 917ee1a431 Add cache ttl support (#3)
* Add ability to use `@cached(ttl: 120)`

* Update documentation.
2023-10-10 19:21:25 +01:00
lukaszraczyloandGitHub bc128493b0 Add tests (#2)
* Initial tests draft.

* Add tests for parsing jwt token

* Further code optimisations
2023-10-10 13:54:03 +01:00
lukaszraczylo c213a49c32 New: release rate limit per role. 2023-10-09 17:47:10 +01:00
lukaszraczylo ac44056a00 Add role ratelimit (#1)
* Add ratelimit configuration.
* Add rate limiting :party:
2023-10-09 17:46:50 +01:00
lukaszraczylo 743eed7f71 Add ability to enable / disable access log.
In high frequency environments it can be a little bit noisy.
2023-10-09 17:46:50 +01:00
lukaszraczylo b89053c015 Update README. 2023-10-09 17:46:50 +01:00
lukaszraczylo 16f29488c5 Add license. 2023-10-08 18:45:53 +01:00
lukaszraczylo 5ca37fc9fb Fix README formatting. 2023-10-08 18:44:13 +01:00
lukaszraczylo ed1de61e2e Force minor version for public release. 2023-10-08 18:42:21 +01:00
lukaszraczylo e7b2cc1deb Update readme and make it release ready. 2023-10-08 18:38:55 +01:00
lukaszraczylo 3ac7c115aa Blocking introspection queries. 2023-10-08 18:07:24 +01:00
lukaszraczylo eee6016b5a Update dependencies. 2023-10-08 17:32:45 +01:00
lukaszraczylo 3b8df8ee76 Temporary fix to disable healthcheck. 2023-10-07 14:44:49 +01:00
lukaszraczylo f9e917f2ea Add 'unnamed' for the operations without the name. 2023-10-07 14:35:31 +01:00
lukaszraczylo 8673f1caf8 Remove println and replace it with our logging 2023-10-07 14:13:29 +01:00
lukaszraczylo 39d3afdd05 Initial commit. 2023-10-07 11:14:20 +01:00
lukaszraczylo d3fc632470 first commit 2023-10-07 10:52:22 +01:00
57 changed files with 5279 additions and 135908 deletions
+2
View File
@@ -0,0 +1,2 @@
github: [ lukaszraczylo ]
custom: [ monzo.me/lukaszraczylo ]
+73
View File
@@ -0,0 +1,73 @@
name: Test and release
on:
workflow_dispatch:
schedule:
- cron: "0 3 * * *"
env:
GO_VERSION: ">=1.21"
jobs:
# This job is responsible for preparation of the build
# environment variables.
prepare:
name: Preparing build context
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
id: cache
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Go get dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
go get ./...
# This job is responsible for running tests and linting the codebase
test:
name: "Unit testing"
# needs: [prepare]
runs-on: ubuntu-latest
container: golang:1
# container: github/super-linter:v4
needs: [prepare]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Install dependencies
run: |
apt-get update
apt-get install ca-certificates make -y
update-ca-certificates
go mod tidy
go get -u -v ./...
go mod tidy -v
- name: Run unit tests
run: |
CI_RUN=${CI} make test
# if go.mod or go.sum have changed then commit the changes to the repository
- name: Commit changes
run: |
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git add go.mod go.sum
git commit -m "Update go.mod and go.sum"
git push
+84
View File
@@ -0,0 +1,84 @@
name: Run tests on PR
on:
pull_request:
branches:
- "main"
push:
paths-ignore:
- "**/**.md"
- "**/**.yaml"
- "static/**"
branches:
- "!main"
env:
GO_VERSION: ">=1.21"
jobs:
# This job is responsible for preparation of the build
# environment variables.
prepare:
name: Preparing build context
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
id: cache
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Go get dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
go get ./...
# This job is responsible for running tests and linting the codebase
test:
name: "Unit testing"
# needs: [prepare]
runs-on: ubuntu-latest
container: golang:1
# container: github/super-linter:v4
needs: [prepare]
# services:
# # Label used to access the service container
# redis:
# # Docker Hub image
# image: redis
# # Set health checks to wait until redis has started
# options: >-
# --health-cmd "redis-cli ping"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# # Maps the container port to the host machine
# - 6379:6379
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Install dependencies
run: |
apt-get update
apt-get install ca-certificates make -y
update-ca-certificates
go mod tidy
- name: Run unit tests
run: |
CI_RUN=${CI} make test
+20
View File
@@ -0,0 +1,20 @@
name: Test and release
on:
workflow_dispatch:
push:
paths-ignore:
- "**/**.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 }}
+8
View File
@@ -0,0 +1,8 @@
FROM gcr.io/distroless/base-debian12:nonroot
WORKDIR /go/src/app
ARG TARGETARCH
ARG TARGETOS
# silly workaround for distroless image as no chmod is available
COPY --chmod=777 --chown=nonroot:nonroot static/app /go/src/app
ADD dist/bot-$TARGETOS-$TARGETARCH /go/src/app/graphql-proxy
ENTRYPOINT ["/go/src/app/graphql-proxy"]
+21
View File
@@ -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.
+34
View File
@@ -0,0 +1,34 @@
CI_RUN?=false
# ADDITIONAL_BUILD_FLAGS=""
# ifeq ($(CI_RUN), true)
# ADDITIONAL_BUILD_FLAGS="-test.short"
# endif
.PHONY: help
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: build ## run application
@LOG_LEVEL=debug PURGE_METRICS_ON_CRAWL=true BLOCK_SCHEMA_INTROSPECTION=false CACHE_TTL=10 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/ HEALTHCHECK_GRAPHQL_URL=https://hasura8.lan/v1/graphql ./graphql-proxy
.PHONY: build
build: ## build the binary
go build -o graphql-proxy *.go
.PHONY: test
test: ## run tests on library
@LOG_LEVEL=info go test -v -cover -race ./...
.PHONY: test-packages
test-packages: ## run tests on packages
@go test -v -cover ./pkg/...
.PHONY: all
all: test-packages test
.PHONY: update
update: ## update dependencies
@go get -u -v ./...
@go mod tidy -v
+16 -1
View File
@@ -2,7 +2,7 @@
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 10 MB of RAM and 0.1% CPU. [Benchmarks](https://lukaszraczylo.github.io/graphql-monitoring-proxy/dev/bench/) are available.
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)
@@ -28,6 +28,7 @@ This project is in active use by [telegram-bot.app](https://telegram-bot.app), a
- [Cache operations](#cache-operations)
- [General](#general)
- [Metrics which matter](#metrics-which-matter)
- [Tracing](#tracing)
- [Healthcheck](#healthcheck)
- [Monitoring endpoint](#monitoring-endpoint)
@@ -144,6 +145,8 @@ You can still use the non-prefixed environment variables in the spirit of the ba
| `HASURA_EVENT_CLEANER` | Enable the hasura event cleaner | `false` |
| `HASURA_EVENT_CLEANER_OLDER_THAN` | The interval for the hasura event cleaner (in days) | `1` |
| `HASURA_EVENT_METADATA_DB` | URL to the hasura metadata database | `postgresql://localhost:5432/hasura` |
| `ENABLE_TRACE` | Enables tracing | `false` |
| `TRACER_ENDPOINT` | Tracing endpoint | `localhost:4317` |
### Speed
@@ -293,6 +296,18 @@ With the `PURGE_METRICS_ON_CRAWL` enabled, the `graphql_proxy_requests_failed`,
If you prefer more control over the metrics purging - you can enable `PURGE_METRICS_ON_TIMER` environment variable and set the interval in seconds. This will allow you to purge the metrics on a regular basis, for example every 90 seconds. It could be better solution if you have multiple crawlers checking the metrics endpoints and you want to avoid the situation when metrics are purged by for example healthcheck.
#### Tracing
Tracing can be enabled by setting `ENABLE_TRACE` to `true` and providing compatible with OTEL `TRACER_ENDPOINT` value ( default is `localhost:4317` ). From that moment you can include `X-Trace-Span` with content being json encoded in your requests to the proxy endpoint.
The value of X-Trace-Span should be in following format:
```json
{
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
}
```
#### Healthcheck
If you'd like the `/healthz` endpoint to perform actual check for the connectivity to the graphql endpoint - set the `HEALTHCHECK_GRAPHQL_URL` environment variable to the exact URL of the graphql endpoint. The query executed will be `query { __typename }` and if the response is not `200 OK` - the healthcheck will fail. Remember that the endpoint is a full URL which you'd like to check, so it should include the protocol, host and path - for example `http://localhost:8080/v1/graphql` and it's NOT the same as value of `HOST_GRAPHQL` environment variable which should provide only the host, without path, ending with slash.
+222
View File
@@ -0,0 +1,222 @@
package main
import (
"fmt"
"os"
"time"
"github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2"
"github.com/gofrs/flock"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
var bannedUsersIDs map[string]string = make(map[string]string)
func enableApi() {
if cfg.Server.EnableApi {
apiserver := fiber.New(fiber.Config{
DisableStartupMessage: true,
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
})
api := apiserver.Group("/api")
api.Post("/user-ban", apiBanUser)
api.Post("/user-unban", apiUnbanUser)
api.Post("/cache-clear", apiClearCache)
api.Get("/cache-stats", apiCacheStats)
go periodicallyReloadBannedUsers()
err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort))
if err != nil {
cfg.Logger.Critical(&libpack_logger.LogMessage{
Message: "Can't start the service",
Pairs: map[string]interface{}{"port": cfg.Server.ApiPort},
})
}
}
}
func periodicallyReloadBannedUsers() {
for {
loadBannedUsers()
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Banned users reloaded",
Pairs: map[string]interface{}{"users": bannedUsersIDs},
})
<-time.After(10 * time.Second)
}
}
func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
_, found := bannedUsersIDs[userID]
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Checking if user is banned",
Pairs: map[string]interface{}{"user_id": userID, "found": found},
})
if found {
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "User is banned",
Pairs: map[string]interface{}{"user_id": userID},
})
c.Status(403).SendString("User is banned")
}
return found
}
func apiClearCache(c *fiber.Ctx) error {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Clearing cache via API",
Pairs: nil,
})
libpack_cache.CacheClear()
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Cache cleared via API",
Pairs: nil,
})
c.Status(200).SendString("OK: cache cleared")
return nil
}
func apiCacheStats(c *fiber.Ctx) error {
stats := libpack_cache.GetCacheStats()
err := c.JSON(stats)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't marshal cache stats",
Pairs: map[string]interface{}{"error": err.Error()},
})
return err
}
return nil
}
type apiBanUserRequest struct {
UserID string `json:"user_id"`
Reason string `json:"reason"`
}
func apiBanUser(c *fiber.Ctx) error {
var req apiBanUserRequest
err := c.BodyParser(&req)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the ban user request",
Pairs: map[string]interface{}{"error": err.Error()},
})
return err
}
bannedUsersIDs[req.UserID] = req.Reason
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Banned user",
Pairs: map[string]interface{}{"user_id": req.UserID, "reason": req.Reason},
})
storeBannedUsers()
c.Status(200).SendString("OK: user banned")
return nil
}
func apiUnbanUser(c *fiber.Ctx) error {
var req apiBanUserRequest
err := c.BodyParser(&req)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the unban user request",
Pairs: map[string]interface{}{"error": err.Error()},
})
return err
}
delete(bannedUsersIDs, req.UserID)
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Unbanned user",
Pairs: map[string]interface{}{"user_id": req.UserID},
})
storeBannedUsers()
c.Status(200).SendString("OK: user unbanned")
return nil
}
func storeBannedUsers() {
fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
err := fileLock.Lock()
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't lock the file",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
defer fileLock.Unlock()
data, err := json.Marshal(bannedUsersIDs)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't marshal banned users",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
err = os.WriteFile(cfg.Api.BannedUsersFile, data, 0644)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't write banned users to file",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
}
func loadBannedUsers() {
if _, err := os.Stat(cfg.Api.BannedUsersFile); os.IsNotExist(err) {
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Banned users file doesn't exist - creating it",
Pairs: map[string]interface{}{"file": cfg.Api.BannedUsersFile},
})
_, err := os.Create(cfg.Api.BannedUsersFile)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't create the file",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
// write empty json to the file
err = os.WriteFile(cfg.Api.BannedUsersFile, []byte("{}"), 0644)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't write to the file",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
}
fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile))
err := fileLock.RLock() // Use RLock for read lock
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't lock the file [load]",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
defer fileLock.Unlock()
data, err := os.ReadFile(cfg.Api.BannedUsersFile)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't read banned users from file",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
err = json.Unmarshal(data, &bannedUsersIDs)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't unmarshal banned users",
Pairs: map[string]interface{}{"error": err.Error()},
})
return
}
}
+134
View File
@@ -0,0 +1,134 @@
package libpack_cache
import (
"sync/atomic"
"time"
fiber "github.com/gofiber/fiber/v2"
"github.com/gookit/goutil/strutil"
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
type CacheConfig struct {
Logger *libpack_logger.Logger
Client CacheClient
Redis struct {
URL string `json:"url"`
Password string `json:"password"`
DB int `json:"db"`
Enable bool `json:"enable"`
}
TTL int `json:"ttl"`
}
type CacheStats struct {
CachedQueries int64 `json:"cached_queries"`
CacheHits int64 `json:"cache_hits"`
CacheMisses int64 `json:"cache_misses"`
}
type CacheClient interface {
Set(key string, value []byte, ttl time.Duration)
Get(key string) ([]byte, bool)
Delete(key string)
Clear()
CountQueries() int64
}
var (
cacheStats *CacheStats
config *CacheConfig
)
func CalculateHash(c *fiber.Ctx) string {
return strutil.Md5(c.Body())
}
func EnableCache(cfg *CacheConfig) {
if cfg.Logger == nil {
cfg.Logger = libpack_logger.New()
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Initializing in-module logger",
})
}
cacheStats = &CacheStats{}
if ShouldUseRedisCache(cfg) {
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Using Redis cache",
})
cfg.Client = libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
RedisDB: cfg.Redis.DB,
RedisServer: cfg.Redis.URL,
RedisPassword: cfg.Redis.Password,
})
} else {
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Using in-memory cache",
})
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
}
config = cfg
}
func CacheLookup(hash string) []byte {
obj, found := config.Client.Get(hash)
if found {
atomic.AddInt64(&cacheStats.CacheHits, 1)
return obj
}
atomic.AddInt64(&cacheStats.CacheMisses, 1)
return nil
}
func CacheDelete(hash string) {
config.Logger.Debug(&libpack_logger.LogMessage{
Message: "Deleting data from cache",
Pairs: map[string]interface{}{"hash": hash},
})
atomic.AddInt64(&cacheStats.CachedQueries, -1)
config.Client.Delete(hash)
}
func CacheStore(hash string, data []byte) {
config.Logger.Debug(&libpack_logger.LogMessage{
Message: "Storing data in cache",
Pairs: map[string]interface{}{"hash": hash},
})
atomic.AddInt64(&cacheStats.CachedQueries, 1)
config.Client.Set(hash, data, time.Duration(config.TTL)*time.Second)
}
func CacheStoreWithTTL(hash string, data []byte, ttl time.Duration) {
config.Logger.Debug(&libpack_logger.LogMessage{
Message: "Storing data in cache with TTL",
Pairs: map[string]interface{}{"hash": hash, "ttl": ttl},
})
atomic.AddInt64(&cacheStats.CachedQueries, 1)
config.Client.Set(hash, data, ttl)
}
func CacheGetQueries() int64 {
config.Logger.Debug(&libpack_logger.LogMessage{
Message: "Counting cache queries",
})
return config.Client.CountQueries()
}
func CacheClear() {
config.Client.Clear()
cacheStats = &CacheStats{}
}
func GetCacheStats() *CacheStats {
config.Logger.Debug(&libpack_logger.LogMessage{
Message: "Getting cache stats",
})
cacheStats.CachedQueries = CacheGetQueries()
return cacheStats
}
func ShouldUseRedisCache(cfg *CacheConfig) bool {
return cfg.Redis.Enable
}
+107
View File
@@ -0,0 +1,107 @@
package libpack_cache
import (
"testing"
"time"
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
const (
Parallelism = 4
RequestPerSec = 10000
)
func BenchmarkCacheLookupInMemory(b *testing.B) {
config = &CacheConfig{
Logger: libpack_logger.New(),
Client: libpack_cache_memory.New(5 * time.Minute),
TTL: 5,
}
hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.")
CacheStore(hash, data)
b.SetParallelism(Parallelism)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
CacheLookup(hash)
}
})
}
func BenchmarkCacheLookupRedis(b *testing.B) {
mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
RedisServer: redisMockServer.Addr(),
RedisDB: 0,
})
config = &CacheConfig{
Logger: libpack_logger.New(),
Client: mockedCache,
TTL: 5,
}
hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.")
CacheStore(hash, data)
b.SetParallelism(Parallelism)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
CacheLookup(hash)
}
})
}
func BenchmarkCacheStoreInMemory(b *testing.B) {
config = &CacheConfig{
Logger: libpack_logger.New(),
Client: libpack_cache_memory.New(5 * time.Minute),
TTL: 5,
}
hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.")
b.SetParallelism(Parallelism)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
CacheStore(hash, data)
}
})
}
func BenchmarkCacheStoreRedis(b *testing.B) {
mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
RedisServer: redisMockServer.Addr(),
RedisDB: 0,
})
config = &CacheConfig{
Logger: libpack_logger.New(),
Client: mockedCache,
TTL: 5,
}
hash := "00000000000000000000000000000000001337"
data := []byte("it's fine.")
b.SetParallelism(Parallelism)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
CacheStore(hash, data)
}
})
}
+34
View File
@@ -0,0 +1,34 @@
package libpack_cache
import (
"testing"
"github.com/alicebob/miniredis/v2"
assertions "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type Tests struct {
suite.Suite
}
var (
assert *assertions.Assertions
redisMockServer, _ = miniredis.Run()
)
func (suite *Tests) BeforeTest(suiteName, testName string) {
}
func (suite *Tests) SetupTest() {
cacheStats = &CacheStats{}
assert = assertions.New(suite.T())
}
// TearDownTest is run after each test to clean up
func (suite *Tests) TearDownTest() {
}
func TestSuite(t *testing.T) {
suite.Run(t, new(Tests))
}
+119
View File
@@ -0,0 +1,119 @@
package libpack_cache
import (
"time"
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
func (suite *Tests) Test_cacheLookupInmemory() {
config = &CacheConfig{
Logger: libpack_logger.New(),
Client: libpack_cache_memory.New(5 * time.Minute),
TTL: 5,
}
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: "00000000000000000000000000000000001337",
},
want: []byte("it's fine."),
addCache: struct {
data []byte
}{
data: []byte("it's fine."),
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
if tt.addCache.data != nil {
CacheStore(tt.args.hash, tt.addCache.data)
}
got := CacheLookup(tt.args.hash)
assert.Equal(tt.want, got, "Unexpected cache lookup result")
})
}
}
func (suite *Tests) Test_cacheLookupRedis() {
// redis_server := envutil.Getenv("REDIS_SERVER", "localhost:6379")
// config.Client = libpack_cache_redis.NewClient(&libpack_cache_redis.RedisClientConfig{
// RedisServer: redis_server,
// RedisPassword: "",
// RedisDB: 0,
// })
mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{
RedisServer: redisMockServer.Addr(),
RedisDB: 0,
})
config = &CacheConfig{
Logger: libpack_logger.New(),
Client: mockedCache,
TTL: 5,
}
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: "00000000000000000000000000000000001337",
},
want: []byte("it's fine."),
addCache: struct {
data []byte
}{
data: []byte("it's fine."),
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
if tt.addCache.data != nil {
CacheStore(tt.args.hash, tt.addCache.data)
}
got := CacheLookup(tt.args.hash)
assert.Equal(tt.want, got, "Unexpected cache lookup result")
})
}
}
+151
View File
@@ -0,0 +1,151 @@
package libpack_cache_memory
import (
"bytes"
"compress/gzip"
"io"
"log"
"sync"
"time"
)
type CacheEntry struct {
ExpiresAt time.Time
Value []byte
}
type Cache struct {
compressPool sync.Pool
decompressPool sync.Pool
entries sync.Map
globalTTL time.Duration
sync.RWMutex
}
func New(globalTTL time.Duration) *Cache {
cache := &Cache{
globalTTL: globalTTL,
compressPool: sync.Pool{
New: func() interface{} {
return gzip.NewWriter(nil)
},
},
decompressPool: sync.Pool{
New: func() interface{} {
r, _ := gzip.NewReader(bytes.NewReader([]byte{}))
return r
},
},
}
go cache.cleanupRoutine(globalTTL)
return cache
}
func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
ticker := time.NewTicker(globalTTL / 2)
defer ticker.Stop()
for range ticker.C {
c.CleanExpiredEntries()
}
}
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
expiresAt := time.Now().Add(ttl)
compressedValue, err := c.compress(value)
if err != nil {
log.Printf("Error compressing value for key %s: %v", key, err)
return
}
entry := CacheEntry{
Value: compressedValue,
ExpiresAt: expiresAt,
}
c.entries.Store(key, entry)
}
func (c *Cache) Get(key string) ([]byte, bool) {
entry, ok := c.entries.Load(key)
if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
return nil, false
}
compressedValue := entry.(CacheEntry).Value
value, err := c.decompress(compressedValue)
if err != nil {
log.Printf("Error decompressing value for key %s: %v", key, err)
return nil, false
}
return value, true
}
func (c *Cache) Delete(key string) {
c.entries.Delete(key)
}
func (c *Cache) Clear() {
c.entries.Range(func(key, value interface{}) bool {
c.entries.Delete(key)
return true
})
}
func (c *Cache) CountQueries() int64 {
var count int
c.entries.Range(func(_, _ interface{}) bool {
count++
return true
})
return int64(count)
}
func (c *Cache) compress(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := c.compressPool.Get().(*gzip.Writer)
defer func() {
w.Close()
c.compressPool.Put(w)
}()
w.Reset(&buf)
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (c *Cache) decompress(data []byte) ([]byte, error) {
r, ok := c.decompressPool.Get().(*gzip.Reader)
if !ok || r == nil {
var err error
r, err = gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
} else {
if err := r.Reset(bytes.NewReader(data)); err != nil {
return nil, err
}
}
defer func() {
r.Close()
c.decompressPool.Put(r)
}()
return io.ReadAll(r)
}
func (c *Cache) CleanExpiredEntries() {
now := time.Now()
c.entries.Range(func(key, value interface{}) bool {
entry := value.(CacheEntry)
if entry.ExpiresAt.Before(now) {
c.entries.Delete(key)
}
return true
})
}
+54
View File
@@ -0,0 +1,54 @@
package libpack_cache_memory
import (
"testing"
"time"
)
// Assume that New function initializes the cache and it is defined somewhere in the libpack_cache package.
func BenchmarkMemCacheSet(b *testing.B) {
cache := New(30 * time.Second) // Initializing the cache with a TTL of 30 seconds
key := "benchmark-key"
value := []byte("benchmark-value")
b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark
for i := 0; i < b.N; i++ {
cache.Set(key, value, 5*time.Second)
}
}
func BenchmarkMemCacheGet(b *testing.B) {
cache := New(30 * time.Second) // Initializing the cache
key := "benchmark-key"
value := []byte("benchmark-value")
cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve
b.ResetTimer() // Start timing
for i := 0; i < b.N; i++ {
_, _ = cache.Get(key)
}
}
func BenchmarkMemCacheExpire(b *testing.B) {
key := "benchmark-expire-key"
value := []byte("benchmark-value")
ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration
for i := 0; i < b.N; i++ {
cache := New(30 * time.Second)
cache.Set(key, value, ttl)
time.Sleep(ttl) // Wait for the key to expire
_, _ = cache.Get(key)
}
}
func BenchmarkMemCacheStats(b *testing.B) {
cache := New(30 * time.Second) // Initializing the cache
key := "benchmark-key"
value := []byte("benchmark-value")
cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve
cache.Get(key)
}
+112
View File
@@ -0,0 +1,112 @@
package libpack_cache_memory
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type MemoryTestSuite struct {
suite.Suite
}
func (suite *MemoryTestSuite) SetupTest() {
}
func TestCachingTestSuite(t *testing.T) {
suite.Run(t, new(MemoryTestSuite))
}
func (suite *MemoryTestSuite) Test_New() {
suite.T().Run("should return a new cache", func(t *testing.T) {
cache := New(2 * time.Second)
suite.NotNil(cache)
})
}
func (suite *MemoryTestSuite) Test_CacheUse() {
cache := New(30 * time.Second)
tests := []struct {
name string
cache_value string
}{
{
name: "test1",
cache_value: "test1-123",
},
{
name: "test2",
cache_value: "test2-123",
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
c, ok := cache.Get(tt.name)
suite.Equal(true, ok)
suite.Equal(tt.name, string(c))
})
}
}
func (suite *MemoryTestSuite) Test_CacheDelete() {
cache := New(30 * time.Second)
tests := []struct {
name string
cache_value string
}{
{
name: "test1",
cache_value: "test1-123",
},
{
name: "test2",
cache_value: "test2-123",
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
c, ok := cache.Get(tt.name)
suite.Equal(true, ok)
suite.Equal(tt.name, string(c))
cache.Delete(tt.name)
c, ok = cache.Get(tt.name)
suite.Equal(false, ok)
suite.Equal("", string(c))
})
}
}
func (suite *MemoryTestSuite) Test_CacheExpire() {
cache := New(30 * time.Second)
tests := []struct {
name string
cache_value string
ttl time.Duration
}{
{
name: "test1",
cache_value: "test1-123",
ttl: 2 * time.Second,
},
{
name: "test2",
cache_value: "test2-123",
ttl: 5 * time.Second,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
cache.Set(tt.name, []byte(tt.name), tt.ttl)
c, ok := cache.Get(tt.name)
suite.Equal(true, ok)
suite.Equal(tt.name, string(c))
time.Sleep(tt.ttl)
c, ok = cache.Get(tt.name)
suite.Equal(false, ok)
suite.Equal("", string(c))
})
}
}
+96
View File
@@ -0,0 +1,96 @@
package libpack_cache_redis
import (
"context"
"strings"
"time"
"sync"
redis "github.com/redis/go-redis/v9"
)
type RedisConfig struct {
ctx context.Context
client *redis.Client
builderPool *sync.Pool
prefix string
}
func (c *RedisConfig) prependKeyName(key string) string {
builder := c.builderPool.Get().(*strings.Builder)
defer c.builderPool.Put(builder)
builder.Reset()
builder.WriteString(c.prefix)
builder.WriteString(key)
return builder.String()
}
type RedisClientConfig struct {
RedisServer string
RedisPassword string
Prefix string
RedisDB int
}
func New(redisClientConfig *RedisClientConfig) *RedisConfig {
c := &RedisConfig{
client: redis.NewClient(&redis.Options{
Addr: redisClientConfig.RedisServer,
Password: redisClientConfig.RedisPassword,
DB: redisClientConfig.RedisDB,
}),
ctx: context.Background(),
prefix: redisClientConfig.Prefix,
builderPool: &sync.Pool{
New: func() interface{} {
return &strings.Builder{}
},
},
}
_, err := c.client.Ping(c.ctx).Result()
if err != nil {
panic(err)
}
return c
}
func (c *RedisConfig) Set(key string, value []byte, ttl time.Duration) {
c.client.Set(c.ctx, c.prependKeyName(key), value, ttl)
}
func (c *RedisConfig) Get(key string) ([]byte, bool) {
val, err := c.client.Get(c.ctx, c.prependKeyName(key)).Result()
if err == redis.Nil {
return nil, false
}
if err != nil {
return nil, false
}
return []byte(val), true
}
func (c *RedisConfig) Delete(key string) {
c.client.Del(c.ctx, c.prependKeyName(key))
}
func (c *RedisConfig) Clear() {
c.client.FlushDB(c.ctx)
}
func (c *RedisConfig) CountQueries() int64 {
keys, err := c.client.Keys(c.ctx, c.prependKeyName("*")).Result()
if err != nil {
return 0
}
return int64(len(keys))
}
func (c *RedisConfig) CountQueriesWithPattern(pattern string) int {
keys, err := c.client.Keys(c.ctx, c.prependKeyName(pattern)).Result()
if err != nil {
return 0
}
return len(keys)
}
+130
View File
@@ -0,0 +1,130 @@
package libpack_cache_redis
import (
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type RedisConfigSuite struct {
suite.Suite
redisConfig *RedisConfig
redis_server *miniredis.Miniredis
}
func (suite *RedisConfigSuite) SetupTest() {
suite.redis_server, _ = miniredis.Run()
suite.redisConfig = New(&RedisClientConfig{
RedisServer: suite.redis_server.Addr(),
RedisPassword: "",
RedisDB: 0,
})
suite.redisConfig.Delete("testkey")
}
func TestRedisConfigSuite(t *testing.T) {
suite.Run(t, new(RedisConfigSuite))
}
func (suite *RedisConfigSuite) TestSet() {
key := "testkeyset"
value := []byte("testvalue")
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, 0)
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
// Test overwriting an existing key-value pair
newValue := []byte("newvalue")
suite.redisConfig.Set(key, newValue, 0)
storedValue, found = suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), newValue, storedValue)
suite.redisConfig.Delete(key) // Clean up after the test
}
func (suite *RedisConfigSuite) TestSetWithExpiry() {
key := "testkey_with_expiry"
value := []byte("testvaluewithexpiry")
expiry := 2 * time.Second
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, expiry)
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
_, found = suite.redisConfig.Get(key)
assert.True(suite.T(), found, "Key should exist")
// Test that key expires after the specified time
suite.redis_server.FastForward(3 * time.Second)
_, found = suite.redisConfig.Get(key)
assert.False(suite.T(), found, "Key should have expired after 2 seconds")
suite.redisConfig.Delete(key) // Clean up after the test
}
func (suite *RedisConfigSuite) TestGet() {
key := "testkeyget"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
}
func (suite *RedisConfigSuite) TestDeleteKey() {
key := "testkeydelete"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
suite.redisConfig.Delete(key)
_, found := suite.redisConfig.Get(key)
assert.False(suite.T(), found)
}
func (suite *RedisConfigSuite) TestCheckIfKeyExists() {
ttl := time.Duration(10) * time.Second
key := "testkeyifexists"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, ttl) // Set the key-value pair
_, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
suite.redisConfig.Delete(key)
_, found = suite.redisConfig.Get(key)
assert.False(suite.T(), found)
}
func (suite *RedisConfigSuite) TestGetKeys() {
ttl := time.Duration(10) * time.Second
suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
keys, _ := suite.redisConfig.client.Keys(suite.redisConfig.ctx, "testkey*").Result()
expectedKeys := []string{"testkey1", "testkey2"}
assert.ElementsMatch(suite.T(), expectedKeys, keys)
suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey")
}
func (suite *RedisConfigSuite) TestGetKeysCount() {
ttl := time.Duration(10) * time.Second
suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
assert.Equal(suite.T(), 2, suite.redisConfig.CountQueriesWithPattern("testkey*"))
assert.Equal(suite.T(), 1, suite.redisConfig.CountQueriesWithPattern("otherkey*"))
assert.Equal(suite.T(), int64(3), suite.redisConfig.CountQueries())
suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey")
}
+6
View File
@@ -0,0 +1,6 @@
package libpack_config
var (
PKG_NAME string = "not-specified"
PKG_VERSION string = "0.0.0-dev"
)
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"encoding/base64"
"fmt"
"strings"
"github.com/goccy/go-json"
"github.com/lukaszraczylo/ask"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
)
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(&libpack_logger.LogMessage{
Message: msg,
Pairs: details,
})
}
tokenParts := strings.Split(authorization, ".")
if len(tokenParts) != 3 {
handleError("Can't split the token", map[string]interface{}{"token": authorization})
return
}
claim, err := base64.RawURLEncoding.DecodeString(tokenParts[1])
if err != nil {
handleError("Can't decode the token", map[string]interface{}{"token": authorization})
return
}
var claimMap map[string]interface{}
if err = json.Unmarshal(claim, &claimMap); err != nil {
handleError("Can't unmarshal the claim", map[string]interface{}{"token": authorization})
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})
}
}
}
extractClaim(cfg.Client.JWTUserClaimPath, &usr, "user id")
extractClaim(cfg.Client.JWTRoleClaimPath, &role, "role")
return
}
+81
View File
@@ -0,0 +1,81 @@
package main
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.Run(tt.name, func() {
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")
})
}
}
-135626
View File
File diff suppressed because one or more lines are too long
-281
View File
@@ -1,281 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" />
<style>
html {
font-family: BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
-webkit-font-smoothing: antialiased;
background-color: #fff;
font-size: 16px;
}
body {
color: #4a4a4a;
margin: 8px;
font-size: 1em;
font-weight: 400;
}
header {
margin-bottom: 8px;
display: flex;
flex-direction: column;
}
main {
width: 100%;
display: flex;
flex-direction: column;
}
a {
color: #3273dc;
cursor: pointer;
text-decoration: none;
}
a:hover {
color: #000;
}
button {
color: #fff;
background-color: #3298dc;
border-color: transparent;
cursor: pointer;
text-align: center;
}
button:hover {
background-color: #2793da;
flex: none;
}
.spacer {
flex: auto;
}
.small {
font-size: 0.75rem;
}
footer {
margin-top: 16px;
display: flex;
align-items: center;
}
.header-label {
margin-right: 4px;
}
.benchmark-set {
margin: 8px 0;
width: 100%;
display: flex;
flex-direction: column;
}
.benchmark-title {
font-size: 3rem;
font-weight: 600;
word-break: break-word;
text-align: center;
}
.benchmark-graphs {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
width: 100%;
}
.benchmark-chart {
max-width: 1000px;
}
</style>
<title>Benchmarks</title>
</head>
<body>
<header id="header">
<div class="header-item">
<strong class="header-label">Last Update:</strong>
<span id="last-update"></span>
</div>
<div class="header-item">
<strong class="header-label">Repository:</strong>
<a id="repository-link" rel="noopener"></a>
</div>
</header>
<main id="main"></main>
<footer>
<button id="dl-button">Download data as JSON</button>
<div class="spacer"></div>
<div class="small">Powered by <a rel="noopener" href="https://github.com/marketplace/actions/continuous-benchmark">github-action-benchmark</a></div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Chart.min.js"></script>
<script src="data.js"></script>
<script id="main-script">
'use strict';
(function() {
// Colors from https://github.com/github/linguist/blob/master/lib/linguist/languages.yml
const toolColors = {
cargo: '#dea584',
go: '#00add8',
benchmarkjs: '#f1e05a',
benchmarkluau: '#000080',
pytest: '#3572a5',
googlecpp: '#f34b7d',
catch2: '#f34b7d',
julia: '#a270ba',
jmh: '#b07219',
benchmarkdotnet: '#178600',
customBiggerIsBetter: '#38ff38',
customSmallerIsBetter: '#ff3838',
_: '#333333'
};
function init() {
function collectBenchesPerTestCase(entries) {
const map = new Map();
for (const entry of entries) {
const {commit, date, tool, benches} = entry;
for (const bench of benches) {
const result = { commit, date, tool, bench };
const arr = map.get(bench.name);
if (arr === undefined) {
map.set(bench.name, [result]);
} else {
arr.push(result);
}
}
}
return map;
}
const data = window.BENCHMARK_DATA;
// Render header
document.getElementById('last-update').textContent = new Date(data.lastUpdate).toString();
const repoLink = document.getElementById('repository-link');
repoLink.href = data.repoUrl;
repoLink.textContent = data.repoUrl;
// Render footer
document.getElementById('dl-button').onclick = () => {
const dataUrl = 'data:,' + JSON.stringify(data, null, 2);
const a = document.createElement('a');
a.href = dataUrl;
a.download = 'benchmark_data.json';
a.click();
};
// Prepare data points for charts
return Object.keys(data.entries).map(name => ({
name,
dataSet: collectBenchesPerTestCase(data.entries[name]),
}));
}
function renderAllChars(dataSets) {
function renderGraph(parent, name, dataset) {
const canvas = document.createElement('canvas');
canvas.className = 'benchmark-chart';
parent.appendChild(canvas);
const color = toolColors[dataset.length > 0 ? dataset[0].tool : '_'];
const data = {
labels: dataset.map(d => d.commit.id.slice(0, 7)),
datasets: [
{
label: name,
data: dataset.map(d => d.bench.value),
borderColor: color,
backgroundColor: color + '60', // Add alpha for #rrggbbaa
}
],
};
const options = {
scales: {
xAxes: [
{
scaleLabel: {
display: true,
labelString: 'commit',
},
}
],
yAxes: [
{
scaleLabel: {
display: true,
labelString: dataset.length > 0 ? dataset[0].bench.unit : '',
},
ticks: {
beginAtZero: true,
}
}
],
},
tooltips: {
callbacks: {
afterTitle: items => {
const {index} = items[0];
const data = dataset[index];
return '\n' + data.commit.message + '\n\n' + data.commit.timestamp + ' committed by @' + data.commit.committer.username + '\n';
},
label: item => {
let label = item.value;
const { range, unit } = dataset[item.index].bench;
label += ' ' + unit;
if (range) {
label += ' (' + range + ')';
}
return label;
},
afterLabel: item => {
const { extra } = dataset[item.index].bench;
return extra ? '\n' + extra : '';
}
}
},
onClick: (_mouseEvent, activeElems) => {
if (activeElems.length === 0) {
return;
}
// XXX: Undocumented. How can we know the index?
const index = activeElems[0]._index;
const url = dataset[index].commit.url;
window.open(url, '_blank');
},
};
new Chart(canvas, {
type: 'line',
data,
options,
});
}
function renderBenchSet(name, benchSet, main) {
const setElem = document.createElement('div');
setElem.className = 'benchmark-set';
main.appendChild(setElem);
const nameElem = document.createElement('h1');
nameElem.className = 'benchmark-title';
nameElem.textContent = name;
setElem.appendChild(nameElem);
const graphsElem = document.createElement('div');
graphsElem.className = 'benchmark-graphs';
setElem.appendChild(graphsElem);
for (const [benchName, benches] of benchSet.entries()) {
renderGraph(graphsElem, benchName, benches)
}
}
const main = document.getElementById('main');
for (const {name, dataSet} of dataSets) {
renderBenchSet(name, dataSet, main);
}
}
renderAllChars(init()); // Start
})();
</script>
</body>
</html>
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
func enableHasuraEventCleaner() {
if cfg.HasuraEventCleaner.Enable {
if cfg.HasuraEventCleaner.EventMetadataDb == "" {
cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Event metadata db URL not specified, event cleaner not active",
Pairs: nil,
})
return
}
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Event cleaner enabled",
Pairs: map[string]interface{}{"interval_in_days": cfg.HasuraEventCleaner.ClearOlderThan},
})
time.Sleep(60 * time.Second) // wait for everything to start and settle down
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Initial cleanup of old events",
Pairs: nil,
})
cleanEvents()
for {
select {
case <-ticker.C:
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Cleaning up old events",
Pairs: nil,
})
cleanEvents()
}
}
}
}
func cleanEvents() {
conn, err := pgx.Connect(context.Background(), cfg.HasuraEventCleaner.EventMetadataDb)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Failed to connect to event metadata db",
Pairs: map[string]interface{}{"error": err},
})
return
}
defer conn.Close(context.Background())
delQueries := []string{
fmt.Sprintf("DELETE FROM hdb_catalog.event_invocation_logs WHERE created_at < now() - interval '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
fmt.Sprintf("DELETE FROM hdb_catalog.event_log WHERE created_at < now() - interval '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
fmt.Sprintf("DELETE FROM hdb_catalog.hdb_action_log WHERE created_at < NOW() - INTERVAL '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
fmt.Sprintf("DELETE FROM hdb_catalog.hdb_cron_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
fmt.Sprintf("DELETE FROM hdb_catalog.hdb_scheduled_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';", cfg.HasuraEventCleaner.ClearOlderThan),
}
for _, query := range delQueries {
_, err := conn.Exec(context.Background(), query)
if err != nil {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Failed to execute query",
Pairs: map[string]interface{}{"query": query, "error": err},
})
}
}
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Old events cleaned up",
Pairs: nil,
})
}
+69
View File
@@ -0,0 +1,69 @@
module github.com/lukaszraczylo/graphql-monitoring-proxy
go 1.21
require (
github.com/VictoriaMetrics/metrics v1.33.1
github.com/alicebob/miniredis/v2 v2.33.0
github.com/avast/retry-go/v4 v4.6.0
github.com/goccy/go-json v0.10.3
github.com/gofiber/fiber/v2 v2.52.4
github.com/gofrs/flock v0.8.1
github.com/google/uuid v1.6.0
github.com/gookit/goutil v0.6.15
github.com/graphql-go/graphql v0.8.1
github.com/jackc/pgx/v5 v5.6.0
github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415
github.com/lukaszraczylo/go-ratecounter v0.1.8
github.com/lukaszraczylo/go-simple-graphql v1.2.14
github.com/redis/go-redis/v9 v9.5.3
github.com/stretchr/testify v1.9.0
github.com/valyala/fasthttp v1.55.0
go.opentelemetry.io/otel v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
go.opentelemetry.io/otel/sdk v1.27.0
go.opentelemetry.io/otel/trace v1.27.0
)
require (
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/valyala/bytebufferpool v1.0.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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+155
View File
@@ -0,0 +1,155 @@
github.com/VictoriaMetrics/metrics v1.33.1 h1:CNV3tfm2Kpv7Y9W3ohmvqgFWPR55tV2c7M2U6OIo+UM=
github.com/VictoriaMetrics/metrics v1.33.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/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.2.14 h1:Dth+yZ+1ialCpnslSb6UgHbXszExjDUu/I95QZbnWVU=
github.com/lukaszraczylo/go-simple-graphql v1.2.14/go.mod h1:pSKmm9OLGoS9pjmIvhBB/fo0+LganRrL29CN3fdkRPw=
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=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
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/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=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc=
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+222
View File
@@ -0,0 +1,222 @@
package main
import (
"strconv"
"strings"
"github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2"
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/parser"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
)
var introspection_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 introspectionQuerySet = map[string]struct{}{}
var introspectionAllowedQueries = map[string]struct{}{}
var allowedUrls = map[string]struct{}{}
// Utility function to convert a slice of strings to a map for O(1) lookups.
func sliceToMap(slice []string) map[string]struct{} {
resultMap := make(map[string]struct{}, len(slice))
for _, item := range slice {
resultMap[strings.ToLower(item)] = struct{}{}
}
return resultMap
}
func prepareQueriesAndExemptions() {
introspectionQuerySet = sliceToMap(introspection_queries)
introspectionAllowedQueries = sliceToMap(cfg.Security.IntrospectionAllowed)
allowedUrls = sliceToMap(cfg.Server.AllowURLs)
}
type parseGraphQLQueryResult struct {
operationType string
operationName string
activeEndpoint string
cacheTime int
cacheRequest bool
cacheRefresh bool
shouldBlock bool
shouldIgnore bool
}
func parseGraphQLQuery(c *fiber.Ctx) (res *parseGraphQLQueryResult) {
res = &parseGraphQLQueryResult{shouldIgnore: true}
m := make(map[string]interface{})
err := json.Unmarshal(c.Body(), &m)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't unmarshal the request",
Pairs: map[string]interface{}{"error": err.Error(), "body": string(c.Body())},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
return
}
// get the query
query, ok := m["query"].(string)
if !ok {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't find the query",
Pairs: map[string]interface{}{"m_val": m},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
return
}
p, err := parser.Parse(parser.ParseParams{Source: query})
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the query",
Pairs: map[string]interface{}{"query": query, "m_val": m},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
return
}
res.shouldIgnore = false
res.operationName = "undefined"
res.activeEndpoint = cfg.Server.HostGraphQL
for _, d := range p.Definitions {
if oper, ok := d.(*ast.OperationDefinition); ok {
res.operationType = strings.ToLower(oper.Operation)
if oper.Name != nil {
res.operationName = oper.Name.Value
}
// If the query is a mutation then direct it to the RW endpoint,
// otherwise direct it to the RO endpoint if it's set.
if cfg.Server.HostGraphQLReadOnly != "" && res.operationType != "mutation" {
res.activeEndpoint = cfg.Server.HostGraphQLReadOnly
}
if res.operationType == "mutation" && cfg.Server.ReadOnlyMode {
cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Mutation blocked",
Pairs: map[string]interface{}{"query": query},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
c.Status(403).SendString("The server is in read-only mode")
res.shouldBlock = true
return
}
for _, dir := range oper.Directives {
if dir.Name.Value == "cached" {
res.cacheRequest = true
for _, arg := range dir.Arguments {
if arg.Name.Value == "ttl" {
res.cacheTime, err = strconv.Atoi(arg.Value.GetValue().(string))
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the ttl, using global",
Pairs: map[string]interface{}{"bad_ttl": arg.Value.GetValue().(string)},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
return
}
}
if arg.Name.Value == "refresh" {
res.cacheRefresh = arg.Value.GetValue().(bool)
}
}
}
}
if cfg.Security.BlockIntrospection {
res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections)
if res.shouldBlock {
return
}
}
}
}
return
}
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
for _, s := range selections {
field, ok := s.(*ast.Field)
if !ok {
continue // or handle the case where the type assertion fails
}
shouldBlock := checkIfContainsIntrospection(c, field.Name.Value)
if shouldBlock {
return true
}
if field.SelectionSet != nil {
if checkSelections(c, field.GetSelectionSet().Selections) {
return true
}
}
}
return false
}
func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) (shouldBlock bool) {
whateverLower := strings.ToLower(whatever)
got_exemption := false
// If the query is an introspection query, we need to check if it's allowed.
if _, exists := introspectionQuerySet[whateverLower]; exists {
if len(cfg.Security.IntrospectionAllowed) > 0 {
if _, allowed_exists := introspectionAllowedQueries[whateverLower]; allowed_exists {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Introspection query allowed, passing through",
Pairs: map[string]interface{}{"query": whatever},
})
got_exemption = true
shouldBlock = false
}
}
if !got_exemption {
shouldBlock = true
}
}
if shouldBlock {
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
c.Status(403).SendString("Introspection queries are not allowed")
}
return
}
+320
View File
@@ -0,0 +1,320 @@
package main
import (
"github.com/valyala/fasthttp"
)
func (suite *Tests) Test_parseGraphQLQuery() {
type results struct {
op_name string
op_type string
cached_ttl int
returnCode int
is_cached bool
shouldBlock bool
shouldIgnore bool
}
type queries struct {
headers map[string]string
body string
}
tests := []struct {
name string
suppliedSettings *config
suppliedQuery queries
wantResults results
}{
{
name: "test empty body",
suppliedQuery: queries{
body: "",
headers: map[string]string{},
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: true,
op_name: "",
op_type: "",
},
},
{
name: "test empty json",
suppliedQuery: queries{
body: "{}",
headers: map[string]string{},
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: true,
op_name: "",
op_type: "",
},
},
{
name: "test empty with some random garbage",
suppliedQuery: queries{
body: "{\"variables\": {\"id\": \"1\"}}",
headers: map[string]string{},
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: true,
op_name: "",
op_type: "",
},
},
{
name: "test valid query with op name",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}",
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test valid query with op name, variables and cache",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery @cached { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}",
},
wantResults: results{
is_cached: true,
shouldBlock: false,
shouldIgnore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test valid query with op name, cache and ttl",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery @cached(ttl: 60) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}",
},
wantResults: results{
is_cached: true,
cached_ttl: 60,
shouldBlock: false,
shouldIgnore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test valid query with op name, force refreshed cache",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery @cached(refresh: true) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}",
},
wantResults: results{
is_cached: true,
cached_ttl: 0,
shouldBlock: false,
shouldIgnore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test valid query with op name, cache and INVALID ttl",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery @cached(ttl: nope) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}",
},
wantResults: results{
is_cached: true,
cached_ttl: 0,
shouldBlock: false,
shouldIgnore: false,
op_name: "MyQuery",
op_type: "query",
},
},
{
name: "test mutation query with op name",
suppliedQuery: queries{
body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}",
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: false,
op_name: "MyMutation",
op_type: "mutation",
},
},
{
name: "test mutation query with config: read only",
suppliedSettings: func() *config {
parseConfig()
cfg.Server.ReadOnlyMode = true
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}",
},
wantResults: results{
is_cached: false,
shouldBlock: true,
shouldIgnore: false,
op_name: "MyMutation",
op_type: "mutation",
returnCode: 403,
},
},
{
name: "test simple query with introspection __schema",
suppliedQuery: queries{
body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __schema } }\"}",
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: false,
op_name: "MyMutation",
op_type: "mutation",
},
},
{
name: "test simple query with introspection __schema config: block introspection",
suppliedSettings: func() *config {
parseConfig()
cfg.Security.BlockIntrospection = true
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"query MyIntroQuery { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __schema } }\"}",
},
wantResults: results{
is_cached: false,
shouldBlock: true,
shouldIgnore: false,
op_name: "MyIntroQuery",
op_type: "query",
returnCode: 403,
},
},
{
name: "test user supplied query with introspection #1 - config: block",
suppliedSettings: func() *config {
parseConfig()
cfg.Security.BlockIntrospection = true
cfg.Security.IntrospectionAllowed = []string{}
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"{__schema {queryType {fields {name description}}}}\"}",
},
wantResults: results{
is_cached: false,
shouldBlock: true,
shouldIgnore: false,
op_name: "undefined",
op_type: "query",
returnCode: 403,
},
},
{
name: "test user supplied query with introspection #1 - config: block & allow __schema",
suppliedSettings: func() *config {
parseConfig()
cfg.Security.BlockIntrospection = true
cfg.Security.IntrospectionAllowed = []string{"__schema"}
return cfg
}(),
suppliedQuery: queries{
body: "{\"query\":\"{__schema {queryType {fields {name description}}}}\"}",
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: false,
op_name: "undefined",
op_type: "query",
returnCode: 200,
},
},
{
name: "test invalid query",
suppliedQuery: queries{
body: "{\"query\":\"query MyQuery tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } \"}",
},
wantResults: results{
is_cached: false,
shouldBlock: false,
shouldIgnore: true,
op_name: "",
op_type: "",
},
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
cfg = &config{}
parseConfig()
ctx_headers := func() *fasthttp.RequestHeader {
h := fasthttp.RequestHeader{}
for k, v := range tt.suppliedQuery.headers {
h.Add(k, v)
}
return &h
}()
ctx_request := fasthttp.Request{
Header: *ctx_headers,
}
ctx_request.AppendBody([]byte(tt.suppliedQuery.body))
ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{
Request: ctx_request,
})
// defer func() {
// cfg = &config{}
// parseConfig()
// suite.app.ReleaseCtx(ctx)
// }()
assert.NotNil(ctx, "Fiber context is nil")
if tt.suppliedSettings != nil {
cfg = tt.suppliedSettings
}
prepareQueriesAndExemptions()
parseResult := parseGraphQLQuery(ctx)
assert.Equal(tt.wantResults.op_type, parseResult.operationType, "Unexpected operation type "+tt.name)
assert.Equal(tt.wantResults.op_name, parseResult.operationName, "Unexpected operation name "+tt.name)
assert.Equal(tt.wantResults.is_cached, parseResult.cacheRequest, "Unexpected cache value "+tt.name)
assert.Equal(tt.wantResults.cached_ttl, parseResult.cacheTime, "Unexpected cache TTL value "+tt.name)
assert.Equal(tt.wantResults.shouldBlock, parseResult.shouldBlock, "Unexpected block value "+tt.name)
assert.Equal(tt.wantResults.shouldIgnore, parseResult.shouldIgnore, "Unexpected ignore value "+tt.name)
if tt.wantResults.returnCode > 0 {
assert.Equal(tt.wantResults.returnCode, ctx.Response().StatusCode(), "Unexpected return code", tt.name)
}
})
}
}
+204
View File
@@ -0,0 +1,204 @@
package libpack_logger
import (
"bytes"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/goccy/go-json"
)
const (
_ = iota
LEVEL_DEBUG
LEVEL_INFO
LEVEL_WARN
LEVEL_ERROR
LEVEL_FATAL
)
var LevelNames = [...]string{
"none",
"debug",
"info",
"warn",
"error",
"fatal",
}
const (
defaultFormat = time.RFC3339
defaultMinLevel = LEVEL_INFO
defaultShowCaller = false
)
var defaultOutput = os.Stdout
type Logger struct {
output io.Writer
format string
minLogLevel int
showCaller bool
}
type LogMessage struct {
output io.Writer
Pairs map[string]any
Message string
}
func (m *LogMessage) String() string {
return m.Message
}
var fieldNames = map[string]string{
"timestamp": "timestamp",
"level": "level",
"message": "message",
}
func New() *Logger {
return &Logger{
format: defaultFormat,
minLogLevel: defaultMinLevel,
output: defaultOutput,
showCaller: defaultShowCaller,
}
}
func (l *Logger) SetOutput(output io.Writer) *Logger {
l.output = output
return l
}
var bufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
var defaultPairs = make(map[string]any)
func GetLogLevel(level string) int {
for i, name := range LevelNames {
if name == strings.ToLower(level) {
return i
}
}
return defaultMinLevel
}
func (l *Logger) log(level int, m *LogMessage) {
if m.Pairs == nil {
m.Pairs = defaultPairs
}
m.Pairs[fieldNames["timestamp"]] = time.Now().Format(l.format)
m.Pairs[fieldNames["level"]] = LevelNames[level]
m.Pairs[fieldNames["message"]] = m.Message
if l.showCaller {
m.Pairs["caller"] = getCaller()
}
buffer := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buffer)
buffer.Reset()
var encoder = json.NewEncoder(buffer)
err := encoder.Encode(m.Pairs)
if err != nil {
fmt.Println("Error marshalling log message:", err)
return
}
// if not running in test - use stderr and stdout, otherwise - use logger's output setting
if flag.Lookup("test.v") != nil {
m.output = os.Stdout
if level >= LEVEL_ERROR {
m.output = os.Stderr
}
}
// Use logger's output setting instead of os.Stdout or os.Stderr
l.output.Write(buffer.Bytes())
}
func (l *Logger) Debug(m *LogMessage) {
if l.shouldLog(LEVEL_DEBUG) {
l.log(LEVEL_DEBUG, m)
}
}
func (l *Logger) Info(m *LogMessage) {
if l.shouldLog(LEVEL_INFO) {
l.log(LEVEL_INFO, m)
}
}
func (l *Logger) Warn(m *LogMessage) {
if l.shouldLog(LEVEL_WARN) {
l.log(LEVEL_WARN, m)
}
}
func (l *Logger) Warning(m *LogMessage) {
l.Warn(m)
}
func (l *Logger) Error(m *LogMessage) {
if l.shouldLog(LEVEL_ERROR) {
l.log(LEVEL_ERROR, m)
}
}
func (l *Logger) Fatal(m *LogMessage) {
if l.shouldLog(LEVEL_FATAL) {
l.log(LEVEL_FATAL, m)
}
}
func (l *Logger) Critical(m *LogMessage) {
l.Fatal(m)
os.Exit(1)
}
func (l *Logger) shouldLog(level int) bool {
return level >= l.minLogLevel
}
func (l *Logger) SetFormat(format string) *Logger {
l.format = format
return l
}
func (l *Logger) SetMinLogLevel(level int) *Logger {
l.minLogLevel = level
return l
}
func (l *Logger) SetFieldName(field, name string) *Logger {
fieldNames[field] = name
return l
}
func (l *Logger) SetShowCaller(show bool) *Logger {
l.showCaller = show
return l
}
func getCaller() string {
_, file, line, ok := runtime.Caller(3)
if !ok {
return "unknown:0"
}
file = filepath.Base(file)
return fmt.Sprintf("%s:%d", file, line)
}
+140
View File
@@ -0,0 +1,140 @@
package libpack_logger
import (
"bytes"
"testing"
"time"
)
func Benchmark_NewLogger(b *testing.B) {
type triggers struct {
ModFormat struct {
Format string
}
ModLevel struct {
Level int
}
}
tests := []struct {
name string
triggers triggers
}{
{
name: "BenchmarkNew",
},
{
name: "BenchmarkNewChangeTimeFormat",
triggers: triggers{
ModFormat: struct{ Format string }{
Format: time.RFC3339Nano,
},
},
},
{
name: "BenchmarkNewChangeLogLevel",
triggers: triggers{
ModLevel: struct{ Level int }{
Level: LEVEL_DEBUG,
},
},
},
{
name: "BenchmarkNewChangeTimeFormatAndLogLevel",
triggers: triggers{
ModFormat: struct{ Format string }{
Format: time.RFC3339Nano,
},
ModLevel: struct{ Level int }{
Level: LEVEL_DEBUG,
},
},
},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
got := New()
if tt.triggers.ModFormat.Format != "" {
got = got.SetFormat(tt.triggers.ModFormat.Format)
}
if tt.triggers.ModLevel.Level != 0 {
got = got.SetMinLogLevel(tt.triggers.ModLevel.Level)
}
}
})
}
}
func Benchmark_Log_Debug(b *testing.B) {
output := &bytes.Buffer{}
logger := New().SetMinLogLevel(LEVEL_DEBUG).SetOutput(output)
msg := &LogMessage{
Message: "debug message",
Pairs: make(map[string]any),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Debug(msg)
}
}
func Benchmark_Log_Info(b *testing.B) {
output := &bytes.Buffer{}
logger := New().SetMinLogLevel(LEVEL_INFO).SetOutput(output)
msg := &LogMessage{
Message: "info message",
Pairs: make(map[string]any),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Info(msg)
}
}
func Benchmark_Log_Warn(b *testing.B) {
output := &bytes.Buffer{}
logger := New().SetMinLogLevel(LEVEL_WARN).SetOutput(output)
msg := &LogMessage{
Message: "warn message",
Pairs: make(map[string]any),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Warn(msg)
}
}
func Benchmark_Log_Error(b *testing.B) {
output := &bytes.Buffer{}
logger := New().SetMinLogLevel(LEVEL_ERROR).SetOutput(output)
msg := &LogMessage{
Message: "error message",
Pairs: map[string]any{"key": "value"},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Error(msg)
}
}
func Benchmark_Log_Fatal(b *testing.B) {
output := &bytes.Buffer{}
logger := New().SetMinLogLevel(LEVEL_FATAL).SetOutput(output)
msg := &LogMessage{
Message: "fatal message",
Pairs: make(map[string]any),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Fatal(msg)
}
}
+31
View File
@@ -0,0 +1,31 @@
package libpack_logger
import (
"testing"
assertions "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type LoggerTestSuite struct {
suite.Suite
}
var (
assert *assertions.Assertions
)
func (suite *LoggerTestSuite) BeforeTest(suiteName, testName string) {
}
func (suite *LoggerTestSuite) SetupTest() {
assert = assertions.New(suite.T())
}
// TearDownTest is run after each test to clean up
func (suite *LoggerTestSuite) TearDownTest() {
}
func TestSuite(t *testing.T) {
suite.Run(t, new(LoggerTestSuite))
}
+182
View File
@@ -0,0 +1,182 @@
package libpack_logger
import (
"bytes"
"fmt"
"os"
"reflect"
"testing"
"time"
"github.com/goccy/go-json"
)
func captureStderr(f func()) string {
originalStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
f()
w.Close()
var buf bytes.Buffer
buf.ReadFrom(r)
os.Stderr = originalStderr
return buf.String()
}
func captureStdOut(f func()) string {
originalStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
w.Close()
var buf bytes.Buffer
buf.ReadFrom(r)
os.Stdout = originalStdout
return buf.String()
}
func (suite *LoggerTestSuite) Test_LogMessageString() {
msg := &LogMessage{
Message: "test message",
}
assert.Equal("test message", msg.String())
}
func callLoggerMethod(logger *Logger, methodName string, message *LogMessage) {
// Get the method by name using reflection
method := reflect.ValueOf(logger).MethodByName(methodName)
if method.IsValid() {
// Call the method with the message as an argument
method.Call([]reflect.Value{reflect.ValueOf(message)})
} else {
fmt.Printf("Method %s does not exist on Logger\n", methodName)
}
}
func (suite *LoggerTestSuite) Test_LogsLevelsPrint() {
output := &bytes.Buffer{}
logger := New().SetOutput(output)
tests := []struct {
pairs map[string]any
name string
method string
message string
loggerMinLevel int
messageLogLevel int
wantOutput bool
}{
{
name: "Log: Debug, Level: Debug - no pairs",
method: "Debug",
loggerMinLevel: LEVEL_DEBUG,
messageLogLevel: LEVEL_DEBUG,
message: "debug message",
wantOutput: true,
},
{
name: "Log: Info, Level: Info - one pair",
method: "Info",
loggerMinLevel: LEVEL_INFO,
messageLogLevel: LEVEL_INFO,
message: "info message",
pairs: map[string]any{
"key": "value",
},
wantOutput: true,
},
{
name: "Log: Info, Level: Warn - with pairs",
method: "Info",
loggerMinLevel: LEVEL_WARN,
messageLogLevel: LEVEL_INFO,
message: "warn message",
pairs: map[string]any{
"key1": "value1",
"key2": "value2",
},
wantOutput: false,
},
{
name: "Log: Warn, Level: Info - with 500 pairs",
method: "Warn",
loggerMinLevel: LEVEL_INFO,
messageLogLevel: LEVEL_WARN,
message: "warn message with 500 pairs",
pairs: func() map[string]any {
pairs := make(map[string]any)
for i := 0; i < 500; i++ {
pairs[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
}
return pairs
}(),
wantOutput: true,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
msg := &LogMessage{
Message: tt.message,
Pairs: tt.pairs,
}
output.Reset()
// Set logger's minimum log level
logger.SetMinLogLevel(tt.loggerMinLevel)
fmt.Println("Logger min log level:", LevelNames[logger.minLogLevel])
// Call the logging method
callLoggerMethod(logger, tt.method, msg)
logOutput := output.String()
fmt.Println("Output:", logOutput)
if tt.wantOutput {
var loggedMessage map[string]any
err := json.Unmarshal([]byte(logOutput), &loggedMessage)
if err != nil {
t.Fatalf("Error unmarshalling log message: %v\nLog output: %s", err, logOutput)
}
if !containsLogMessage(logOutput, tt.message) {
t.Errorf("Expected log message %q, but got %q", tt.message, logOutput)
}
assert.Equal(LevelNames[tt.messageLogLevel], loggedMessage["level"])
if tt.pairs != nil {
for k, v := range tt.pairs {
assert.Equal(v, loggedMessage[k])
}
}
} else {
assert.Equal("", logOutput)
}
})
}
}
func containsLogMessage(logOutput, expectedMessage string) bool {
return bytes.Contains([]byte(logOutput), []byte(expectedMessage))
}
func (suite *LoggerTestSuite) Test_SetFormat() {
logger := New().SetFormat(time.RFC3339Nano)
assert.Equal(time.RFC3339Nano, logger.format)
}
func (suite *LoggerTestSuite) Test_SetMinLogLevel() {
logger := New().SetMinLogLevel(LEVEL_DEBUG)
assert.Equal(LEVEL_DEBUG, logger.minLogLevel)
}
func (suite *LoggerTestSuite) Test_ShouldLog() {
logger := New().SetMinLogLevel(LEVEL_WARN)
assert.True(logger.shouldLog(LEVEL_WARN))
assert.True(logger.shouldLog(LEVEL_ERROR))
assert.False(logger.shouldLog(LEVEL_INFO))
assert.False(logger.shouldLog(LEVEL_DEBUG))
}
+138
View File
@@ -0,0 +1,138 @@
package main
import (
"flag"
"os"
"strings"
"sync"
"github.com/gofiber/fiber/v2/middleware/proxy"
"github.com/gookit/goutil/envutil"
graphql "github.com/lukaszraczylo/go-simple-graphql"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
)
var cfg *config
var once sync.Once
// function get value from the env where the value can be anything
func getDetailsFromEnv[T any](key string, defaultValue T) T {
var result any
if _, ok := os.LookupEnv("GMP_" + key); ok {
key = "GMP_" + key
}
switch v := any(defaultValue).(type) {
case string:
result = envutil.Getenv(key, v)
case int:
result = envutil.GetInt(key, v)
case bool:
result = envutil.GetBool(key, v)
default:
result = defaultValue
}
return result.(T)
}
func parseConfig() {
libpack_config.PKG_NAME = "graphql_proxy"
c := config{}
c.Server.PortGraphQL = getDetailsFromEnv("PORT_GRAPHQL", 8080)
c.Server.PortMonitoring = getDetailsFromEnv("MONITORING_PORT", 9393)
c.Server.HostGraphQL = getDetailsFromEnv("HOST_GRAPHQL", "http://localhost/")
c.Server.HostGraphQLReadOnly = getDetailsFromEnv("HOST_GRAPHQL_READONLY", "")
c.Client.JWTUserClaimPath = getDetailsFromEnv("JWT_USER_CLAIM_PATH", "")
c.Client.JWTRoleClaimPath = getDetailsFromEnv("JWT_ROLE_CLAIM_PATH", "")
c.Client.RoleFromHeader = getDetailsFromEnv("ROLE_FROM_HEADER", "")
c.Client.RoleRateLimit = getDetailsFromEnv("ROLE_RATE_LIMIT", false)
/* in-memory cache */
c.Cache.CacheEnable = getDetailsFromEnv("ENABLE_GLOBAL_CACHE", false)
c.Cache.CacheTTL = getDetailsFromEnv("CACHE_TTL", 60)
/* redis cache */
c.Cache.CacheRedisEnable = getDetailsFromEnv("ENABLE_REDIS_CACHE", false)
c.Cache.CacheRedisURL = getDetailsFromEnv("CACHE_REDIS_URL", "localhost:6379")
c.Cache.CacheRedisPassword = getDetailsFromEnv("CACHE_REDIS_PASSWORD", "")
c.Cache.CacheRedisDB = getDetailsFromEnv("CACHE_REDIS_DB", 0)
c.Security.BlockIntrospection = getDetailsFromEnv("BLOCK_SCHEMA_INTROSPECTION", false)
c.Security.IntrospectionAllowed = func() []string {
urls := getDetailsFromEnv("ALLOWED_INTROSPECTION", "")
if urls == "" {
return nil
}
return strings.Split(urls, ",")
}()
c.LogLevel = strings.ToUpper(getDetailsFromEnv("LOG_LEVEL", "info"))
c.Logger = libpack_logging.New().SetMinLogLevel(libpack_logging.GetLogLevel(c.LogLevel)).SetFieldName("timestamp", "ts").SetFieldName("message", "msg").SetShowCaller(false)
c.Server.HealthcheckGraphQL = getDetailsFromEnv("HEALTHCHECK_GRAPHQL_URL", "")
c.Client.GQLClient = graphql.NewConnection()
c.Client.GQLClient.SetEndpoint(c.Server.HealthcheckGraphQL)
c.Server.AccessLog = getDetailsFromEnv("ENABLE_ACCESS_LOG", false)
c.Server.ReadOnlyMode = getDetailsFromEnv("READ_ONLY_MODE", false)
c.Server.AllowURLs = func() []string {
urls := getDetailsFromEnv("ALLOWED_URLS", "")
if urls == "" {
return nil
}
return strings.Split(urls, ",")
}()
c.Client.ClientTimeout = getDetailsFromEnv("PROXIED_CLIENT_TIMEOUT", 120)
c.Client.FastProxyClient = createFasthttpClient(c.Client.ClientTimeout)
proxy.WithClient(c.Client.FastProxyClient) // setting the global proxy client here instead of per request
c.Server.EnableApi = getDetailsFromEnv("ENABLE_API", false)
c.Server.ApiPort = getDetailsFromEnv("API_PORT", 9090)
c.Api.BannedUsersFile = getDetailsFromEnv("BANNED_USERS_FILE", "/go/src/app/banned_users.json")
c.Server.PurgeOnCrawl = getDetailsFromEnv("PURGE_METRICS_ON_CRAWL", false)
c.Server.PurgeEvery = getDetailsFromEnv("PURGE_METRICS_ON_TIMER", 0)
c.HasuraEventCleaner.Enable = getDetailsFromEnv("HASURA_EVENT_CLEANER", false)
c.HasuraEventCleaner.ClearOlderThan = getDetailsFromEnv("HASURA_EVENT_CLEANER_OLDER_THAN", 1)
c.HasuraEventCleaner.EventMetadataDb = getDetailsFromEnv("HASURA_EVENT_METADATA_DB", "")
c.Trace.Enable = getDetailsFromEnv("ENABLE_TRACE", false)
c.Trace.TraceEndpoint = getDetailsFromEnv("TRACER_ENDPOINT", "localhost:4317")
cfg = &c
if cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable {
cacheConfig := &libpack_cache.CacheConfig{
Logger: cfg.Logger,
TTL: cfg.Cache.CacheTTL,
}
if cfg.Cache.CacheRedisEnable {
cacheConfig.Redis.Enable = true
cacheConfig.Redis.URL = cfg.Cache.CacheRedisURL
cacheConfig.Redis.Password = cfg.Cache.CacheRedisPassword
cacheConfig.Redis.DB = cfg.Cache.CacheRedisDB
}
libpack_cache.EnableCache(cacheConfig)
}
loadRatelimitConfig()
once.Do(func() {
if cfg.Trace.Enable {
var err error
cfg.Trace.Client, err = libpack_trace.NewClient(cfg.Logger, cfg.Trace.TraceEndpoint)
if err != nil {
cfg.Logger.Error(&libpack_logging.LogMessage{
Message: "Failed to start tracer",
Pairs: map[string]interface{}{
"error": err,
},
})
}
}
go enableApi()
go enableHasuraEventCleaner()
})
prepareQueriesAndExemptions()
}
func main() {
parseConfig()
StartMonitoringServer()
StartHTTPProxy()
}
func ifNotInTest() bool {
return flag.Lookup("test.v") == nil
}
+114
View File
@@ -0,0 +1,114 @@
package main
import (
"os"
"testing"
"time"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
assertions "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type Tests struct {
suite.Suite
app *fiber.App
}
var (
assert *assertions.Assertions
)
func (suite *Tests) BeforeTest(suiteName, testName string) {
}
func (suite *Tests) SetupTest() {
assert = assertions.New(suite.T())
suite.app = fiber.New(
fiber.Config{
DisableStartupMessage: true,
JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal,
},
)
// Initialize a simple in-memory cache client for testing purposes
libpack_cache.New(5 * time.Minute)
parseConfig()
enableApi()
StartMonitoringServer()
cfg.Logger = libpack_logging.New().SetMinLogLevel(libpack_logging.GetLogLevel(getDetailsFromEnv("LOG_LEVEL", "info")))
// Setup environment variables here if needed
os.Setenv("GMP_TEST_STRING", "testValue")
os.Setenv("GMP_TEST_INT", "123")
os.Setenv("GMP_TEST_BOOL", "true")
os.Setenv("NON_GMP_TEST_INT", "31337")
}
// TearDownTest is run after each test to clean up
func (suite *Tests) TearDownTest() {
// Clean up environment variables here if needed
os.Unsetenv("GMP_TEST_STRING")
os.Unsetenv("GMP_TEST_INT")
os.Unsetenv("GMP_TEST_BOOL")
os.Unsetenv("NON_GMP_TEST_INT")
}
// func (suite *Tests) AfterTest(suiteName, testName string) {)
func TestSuite(t *testing.T) {
cfg = &config{}
parseConfig()
StartMonitoringServer()
suite.Run(t, new(Tests))
}
func (suite *Tests) Test_envVariableSetting() {
tests := []struct {
defaultValue any
expected any
name string
envKey string
}{
{
name: "test_string",
envKey: "TEST_STRING",
defaultValue: "default",
expected: "testValue",
},
{
name: "test_int",
envKey: "TEST_INT",
defaultValue: 0,
expected: 123,
},
{
name: "test_bool",
envKey: "TEST_BOOL",
defaultValue: false,
expected: true,
},
{
name: "test_non_prefixed",
envKey: "NON_GMP_TEST_INT",
defaultValue: 0,
expected: 31337,
},
{
name: "test_non_existing",
envKey: "NON_EXISTING",
defaultValue: "default_val",
expected: "default_val",
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
result := getDetailsFromEnv(tt.envKey, tt.defaultValue)
assert.Equal(tt.expected, result)
})
}
}
+11
View File
@@ -0,0 +1,11 @@
package main
import (
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
)
func StartMonitoringServer() {
cfg.Monitoring = libpack_monitoring.NewMonitoring(&libpack_monitoring.InitConfig{PurgeOnCrawl: cfg.Server.PurgeOnCrawl, PurgeEvery: cfg.Server.PurgeEvery})
cfg.Monitoring.AddMetricsPrefix("graphql_proxy")
cfg.Monitoring.RegisterDefaultMetrics()
}
+15
View File
@@ -0,0 +1,15 @@
package libpack_monitoring
func (ms *MetricsSetup) RegisterDefaultMetrics() {
ms.RegisterMetricsCounter(MetricsSucceeded, nil)
ms.RegisterMetricsCounter(MetricsFailed, nil)
ms.RegisterMetricsCounter(MetricsSkipped, nil)
ms.RegisterMetricsHistogram(MetricsDuration, nil)
ms.RegisterMetricsCounter(MetricsCacheHit, nil)
ms.RegisterMetricsCounter(MetricsCacheMiss, nil)
ms.RegisterMetricsCounter(MetricsQueriesCached, nil)
}
func (ms *MetricsSetup) RegisterGoMetrics() {
// TODO: metrics.WriteProcessMetrics(ms.metrics_set)
}
+193
View File
@@ -0,0 +1,193 @@
package libpack_monitoring
import (
"bytes"
"fmt"
"os"
"sort"
"strings"
"sync"
"unicode"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
)
// Cache for sorted label keys to avoid repeated sorting
var sortedLabelKeysCache = struct {
m map[string][]string
sync.RWMutex
}{m: make(map[string][]string)}
func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) string {
const unknownPodName = "unknown"
var buf bytes.Buffer
// Prepare default labels without initializing a new map
podName := getPodName()
if labels == nil {
labels = defaultLabels(podName)
} else {
ensureDefaultLabels(&labels, podName)
}
// Prefix handling
if ms.metrics_prefix != "" {
buf.WriteString(ms.metrics_prefix)
buf.WriteString("_")
}
buf.WriteString(name)
// Append labels if any
if len(labels) > 0 {
buf.WriteString("{")
appendSortedLabels(&buf, labels)
buf.WriteString("}")
}
return buf.String()
}
func getPodName() string {
const unknownPodName = "unknown"
if hn, err := os.Hostname(); err == nil {
return hn
}
return unknownPodName
}
func defaultLabels(podName string) map[string]string {
return map[string]string{
"microservice": libpack_config.PKG_NAME,
"pod": podName,
}
}
func ensureDefaultLabels(labels *map[string]string, podName string) {
if *labels == nil {
*labels = make(map[string]string)
}
if _, exists := (*labels)["microservice"]; !exists {
(*labels)["microservice"] = libpack_config.PKG_NAME
}
if _, exists := (*labels)["pod"]; !exists {
(*labels)["pod"] = podName
}
}
func appendSortedLabels(buf *bytes.Buffer, labels map[string]string) {
keys := getSortedKeys(labels)
for i, k := range keys {
if i > 0 {
buf.WriteString(",")
}
buf.WriteString(k)
buf.WriteString("=\"")
buf.WriteString(labels[k])
buf.WriteString("\"")
}
}
func getSortedKeys(labels map[string]string) []string {
labelsKey := labelsToString(labels)
sortedLabelKeysCache.RLock()
keys, exists := sortedLabelKeysCache.m[labelsKey]
sortedLabelKeysCache.RUnlock()
if !exists {
keys = make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
sort.Strings(keys)
sortedLabelKeysCache.Lock()
sortedLabelKeysCache.m[labelsKey] = keys
sortedLabelKeysCache.Unlock()
}
return keys
}
func labelsToString(labels map[string]string) string {
var sb strings.Builder
for k, v := range labels {
sb.WriteString(k)
sb.WriteString("=")
sb.WriteString(v)
sb.WriteString(";")
}
return sb.String()
}
// 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 {
cleanedName := clean_metric_name(name)
// Trim leading and trailing underscores
finalName := strings.Trim(cleanedName, "_")
// Check if the processed name matches the original input
if finalName != name {
return fmt.Errorf("Invalid metric name: %s, expected %s", name, finalName)
}
return nil
}
// clean_metric_name processes the metric name according to Prometheus naming conventions
func clean_metric_name(name string) string {
var buf bytes.Buffer
lastWasUnderscore := false
for _, r := range name {
if is_allowed_rune(r) {
if is_special_rune(r) {
if lastWasUnderscore {
continue // Skip if the previous character was also an underscore
}
r = '_' // Convert spaces and special characters to underscores
lastWasUnderscore = true
} else {
lastWasUnderscore = false
}
buf.WriteRune(r)
} else if !lastWasUnderscore {
buf.WriteRune('_')
lastWasUnderscore = true
}
}
// Remove trailing underscore
result := buf.String()
return strings.Trim(result, "_")
}
// is_allowed_rune checks if the rune is allowed in the metric name
func is_allowed_rune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == ' ' || r == '_'
}
// is_special_rune checks if the rune is a space or an underscore
func is_special_rune(r rune) bool {
return r == ' ' || r == '_'
}
func compile_metrics_with_labels(name string, labels map[string]string) string {
var buf bytes.Buffer
buf.WriteString(name)
// Collect keys and sort them
keys := getSortedKeys(labels)
// Append sorted key-value pairs to the buffer
for _, k := range keys {
buf.WriteString("_")
buf.WriteString(k)
buf.WriteString("_")
buf.WriteString(labels[k])
}
return buf.String()
}
+44
View File
@@ -0,0 +1,44 @@
package libpack_monitoring
import (
"testing"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
)
func BenchmarkGetMetricsName(b *testing.B) {
// Setup environment
libpack_config.PKG_NAME = "test_service"
ms := &MetricsSetup{metrics_prefix: "test_prefix"}
labels := map[string]string{
"env": "production",
"region": "us-west-2",
}
// Run the benchmark
for n := 0; n < b.N; n++ {
ms.get_metrics_name("request_count", labels)
}
}
func BenchmarkCompileMetricsWithLabels(b *testing.B) {
labels := map[string]string{
"env": "production",
"region": "us-west-2",
"app": "api-server",
}
for n := 0; n < b.N; n++ {
compile_metrics_with_labels("request_count", labels)
}
}
func BenchmarkValidateMetricsName(b *testing.B) {
input := "valid metric name with special chars @#! and underscores__"
for n := 0; n < b.N; n++ {
validate_metrics_name(input)
}
}
+228
View File
@@ -0,0 +1,228 @@
package libpack_monitoring
import (
"testing"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
"github.com/stretchr/testify/assert"
)
func TestGetMetricsName(t *testing.T) {
ms := &MetricsSetup{metrics_prefix: "prefix"}
libpack_config.PKG_NAME = "example_microservice"
tests := []struct {
name string
metricName string
labels map[string]string
expectedOutput string
}{
{
name: "No labels",
metricName: "test_metric",
labels: nil,
expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
{
name: "With labels",
metricName: "test_metric",
labels: map[string]string{
"label1": "value1",
"label2": "value2",
},
expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
{
name: "Alphabetical order labels",
metricName: "test_metric",
labels: map[string]string{
"label2": "value2",
"label1": "value1",
},
expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
{
name: "Empty metric name",
metricName: "",
labels: nil,
expectedOutput: "prefix_{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
{
name: "Empty labels map",
metricName: "test_metric",
labels: map[string]string{},
expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
{
name: "Single label",
metricName: "test_metric",
labels: map[string]string{
"label1": "value1",
},
expectedOutput: "prefix_test_metric{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
{
name: "Multiple labels with special characters",
metricName: "test_metric",
labels: map[string]string{
"label-2": "value-2",
"label_1": "value_1",
},
expectedOutput: "prefix_test_metric{label-2=\"value-2\",label_1=\"value_1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
{
name: "Prefix only",
metricName: "",
labels: map[string]string{
"label1": "value1",
},
expectedOutput: "prefix_{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ms.get_metrics_name(tt.metricName, tt.labels)
assert.Equal(t, tt.expectedOutput, result)
})
}
}
func TestCompileMetricsWithLabels(t *testing.T) {
tests := []struct {
name string
labels map[string]string
want string
}{
{"request_count", map[string]string{"env": "production", "region": "us-west-2"}, "request_count_env_production_region_us-west-2"},
{"metric_name", map[string]string{}, "metric_name"},
{"metric_name", nil, "metric_name"},
{"metric_name", map[string]string{"key1": "value1"}, "metric_name_key1_value1"},
{"metric_name", map[string]string{"k": "v", "key2": "value2"}, "metric_name_k_v_key2_value2"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := compile_metrics_with_labels(tt.name, tt.labels); got != tt.want {
t.Errorf("compile_metrics_with_labels() = %v, want %v", got, tt.want)
}
})
}
}
func TestValidateMetricsName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"Valid name", "valid_metric_name", false},
{"Name with spaces", "valid metric name", true},
{"Name with special chars", "valid@metric#name!", true},
{"Name with leading underscore", "_valid_metric_name", true},
{"Name with trailing underscore", "valid_metric_name_", true},
{"Name with consecutive underscores", "valid__metric__name", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validate_metrics_name(tt.input); (err != nil) != tt.wantErr {
t.Errorf("validate_metrics_name() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestCleanMetricName(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"valid metric name", "valid_metric_name"},
{"valid@metric#name!", "valid_metric_name"},
{"__valid__metric__name__", "valid_metric_name"},
{" valid metric name ", "valid_metric_name"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.expected, clean_metric_name(tt.input))
})
}
}
func TestDefaultLabels(t *testing.T) {
podName := "test-pod"
libpack_config.PKG_NAME = "example_microservice"
expected := map[string]string{
"microservice": "example_microservice",
"pod": podName,
}
assert.Equal(t, expected, defaultLabels(podName))
}
func TestEnsureDefaultLabels(t *testing.T) {
podName := "test-pod"
libpack_config.PKG_NAME = "example_microservice"
tests := []struct {
inputLabels map[string]string
expectedLabels map[string]string
name string
}{
{
name: "Nil labels",
inputLabels: nil,
expectedLabels: map[string]string{"microservice": "example_microservice", "pod": podName},
},
{
name: "Empty labels",
inputLabels: map[string]string{},
expectedLabels: map[string]string{"microservice": "example_microservice", "pod": podName},
},
{
name: "Partial labels",
inputLabels: map[string]string{"microservice": "test_service"},
expectedLabels: map[string]string{"microservice": "test_service", "pod": podName},
},
{
name: "Complete labels",
inputLabels: map[string]string{"microservice": "test_service", "pod": "custom_pod"},
expectedLabels: map[string]string{"microservice": "test_service", "pod": "custom_pod"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ensureDefaultLabels(&tt.inputLabels, podName)
assert.Equal(t, tt.expectedLabels, tt.inputLabels)
})
}
}
func TestLabelsToString(t *testing.T) {
tests := []struct {
labels map[string]string
expected string
}{
{
labels: map[string]string{"key1": "value1", "key2": "value2"},
expected: "key1=value1;key2=value2;",
},
{
labels: map[string]string{"a": "1", "b": "2"},
expected: "a=1;b=2;",
},
{
labels: map[string]string{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
assert.Equal(t, tt.expected, labelsToString(tt.labels))
})
}
}
+178
View File
@@ -0,0 +1,178 @@
// 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 (
"flag"
"fmt"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/gofiber/fiber/v2"
"github.com/gookit/goutil/envutil"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
type MetricsSetup struct {
metrics_set *metrics.Set
metrics_set_custom *metrics.Set
ic *InitConfig
metrics_prefix string
}
var (
log *libpack_logger.Logger
)
type InitConfig struct {
PurgeOnCrawl bool
PurgeEvery int
}
func NewMonitoring(ic *InitConfig) *MetricsSetup {
log = libpack_logger.New().SetMinLogLevel(libpack_logger.LEVEL_INFO)
ms := &MetricsSetup{ic: ic}
ms.metrics_set = metrics.NewSet()
ms.metrics_set_custom = metrics.NewSet()
// if not testing, start the prometheus endpoint
if flag.Lookup("test.v") == nil {
go ms.startPrometheusEndpoint()
if ic.PurgeEvery > 0 {
ticker := time.NewTicker(time.Duration(ic.PurgeEvery) * time.Second)
go func() {
for range ticker.C {
ms.PurgeMetrics()
}
}()
}
}
return ms
}
func (ms *MetricsSetup) startPrometheusEndpoint() {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
})
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())
ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter())
if ms.ic.PurgeOnCrawl && ms.ic.PurgeEvery == 0 {
ms.PurgeMetrics()
}
return nil
}
func (ms *MetricsSetup) AddMetricsPrefix(prefix string) {
ms.metrics_prefix = prefix
}
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(&libpack_logger.LogMessage{
Message: "RegisterMetricsGauge() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
})
return nil
}
return ms.metrics_set_custom.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(&libpack_logger.LogMessage{
Message: "RegisterMetricsCounter() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
})
return nil
}
if metric_name == MetricsSucceeded || metric_name == MetricsFailed || metric_name == MetricsSkipped {
return ms.metrics_set.GetOrCreateCounter(ms.get_metrics_name(metric_name, labels))
}
return ms.metrics_set_custom.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(&libpack_logger.LogMessage{
Message: "RegisterFloatCounter() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
})
return nil
}
return ms.metrics_set_custom.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(&libpack_logger.LogMessage{
Message: "RegisterMetricsSummary() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
})
return nil
}
return ms.metrics_set_custom.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(&libpack_logger.LogMessage{
Message: "RegisterMetricsHistogram() error",
Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name},
})
return nil
}
return ms.metrics_set_custom.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_custom.UnregisterMetric(ms.get_metrics_name(metric_name, labels))
}
func (ms *MetricsSetup) PurgeMetrics() {
ms.metrics_set_custom.UnregisterAllMetrics()
}
+14
View File
@@ -0,0 +1,14 @@
package libpack_monitoring
const (
MetricsSucceeded = "requests_succesful"
MetricsFailed = "requests_failed"
MetricsDuration = "requests_duration"
MetricsSkipped = "requests_skipped"
MetricsExecutedQuery = "executed_query"
MetricsTimedQuery = "timed_query"
MetricsCacheHit = "cache_hit"
MetricsCacheMiss = "cache_miss"
MetricsQueriesCached = "cached_queries"
)
+156
View File
@@ -0,0 +1,156 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"time"
"github.com/avast/retry-go/v4"
"github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
"github.com/valyala/fasthttp"
)
func createFasthttpClient(timeout int) *fasthttp.Client {
return &fasthttp.Client{
Name: "graphql_proxy",
NoDefaultUserAgentHeader: true,
TLSConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxConnsPerHost: 2048,
ReadTimeout: time.Second * time.Duration(timeout),
WriteTimeout: time.Second * time.Duration(timeout),
MaxIdleConnDuration: time.Second * time.Duration(timeout),
MaxConnDuration: time.Second * time.Duration(timeout),
DisableHeaderNamesNormalizing: true,
}
}
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string, ctx context.Context) error {
if !checkAllowedURLs(c) {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Request blocked",
Pairs: map[string]interface{}{"path": c.Path()},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil)
}
c.Status(403).SendString("Request blocked - not allowed URL")
return nil
}
c.Request().Header.DisableNormalizing()
c.Request().Header.Add("X-Real-IP", c.IP())
c.Request().Header.Add(fiber.HeaderXForwardedFor, string(c.Request().Header.Peek("X-Forwarded-For")))
c.Request().Header.Del(fiber.HeaderAcceptEncoding)
// added dummy check for the log level because it executes additional functions which could
// potentially slow down the execution.
if cfg.LogLevel == "debug" {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Proxying the request",
Pairs: map[string]interface{}{
"path": c.Path(),
"body": string(c.Request().Body()),
"headers": c.GetReqHeaders(),
"request_uuid": c.Locals("request_uuid"),
},
})
}
err := retry.Do(
func() error {
errInt := proxy.DoRedirects(c, currentEndpoint+c.Path(), 3, cfg.Client.FastProxyClient)
if errInt != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't proxy the request",
Pairs: map[string]interface{}{
"error": errInt.Error(),
},
})
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
return errInt
}
return nil
},
retry.OnRetry(func(n uint, err error) {
cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Retrying the request",
Pairs: map[string]interface{}{
"path": c.Path(),
"error": err.Error(),
},
})
}),
retry.Attempts(uint(3)),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Duration(250*time.Millisecond)),
retry.LastErrorOnly(true),
)
if err != nil {
cfg.Logger.Warning(&libpack_logger.LogMessage{
Message: "Can't proxy the request",
Pairs: map[string]interface{}{
"error": err.Error(),
},
})
return err
}
if cfg.LogLevel == "debug" {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Received proxied response",
Pairs: map[string]interface{}{
"path": c.Path(),
"response_body": string(c.Response().Body()),
"response_code": c.Response().StatusCode(),
"headers": c.GetRespHeaders(),
"request_uuid": c.Locals("request_uuid"),
},
})
}
if c.Response().StatusCode() != 200 {
if ifNotInTest() {
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
}
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Received non-200 response from the GraphQL server",
Pairs: map[string]interface{}{
"status_code": c.Response().StatusCode(),
},
})
return fmt.Errorf("Received non-200 response from the GraphQL server: %d", c.Response().StatusCode())
}
c.Response().Header.Del(fiber.HeaderServer)
if cfg.Trace.Enable {
tracingContext := libpack_trace.TraceContextInject(ctx)
if tracingContext == nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't inject empty tracing context",
})
return nil
}
traceJsonEncoded, err := json.Marshal(tracingContext)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't convert tracing context to JSON",
Pairs: map[string]interface{}{
"error": err.Error(),
},
})
return err
}
c.Response().Header.Set("X-Trace-Span", string(traceJsonEncoded))
}
return nil
}
+97
View File
@@ -0,0 +1,97 @@
package main
import (
"github.com/valyala/fasthttp"
)
func (suite *Tests) Test_proxyTheRequest() {
supplied_headers := map[string]string{
"X-Forwarded-For": "127.0.0.1",
"Content-Type": "application/json",
}
tests := []struct {
headers map[string]string
name string
body string
host string
hostRO string
path string
wantErr bool
}{
{
name: "test_empty",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://telegram-bot.app/",
path: "/v1/graphql",
headers: supplied_headers,
wantErr: false,
},
{
name: "test_wrong_url",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://google.com/",
path: "/v1/wrongURL",
headers: supplied_headers,
wantErr: true,
},
{
name: "Test read only mode",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://google.com/",
hostRO: "https://telegram-bot.app/",
path: "/v1/graphql",
headers: supplied_headers,
wantErr: false,
},
{
name: "Test read only mode wrong host",
body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`,
host: "https://telegram-bot.app/",
hostRO: "https://google.com/",
path: "/v1/graphql",
headers: supplied_headers,
wantErr: true,
},
}
for _, tt := range tests {
suite.Run(tt.name, func() {
cfg = &config{}
parseConfig()
cfg.Server.HostGraphQL = tt.host
if tt.hostRO != "" {
cfg.Server.HostGraphQLReadOnly = tt.hostRO
}
ctx_headers := func() *fasthttp.RequestHeader {
h := fasthttp.RequestHeader{}
for k, v := range tt.headers {
h.Add(k, v)
}
return &h
}()
ctx_request := fasthttp.Request{
Header: *ctx_headers,
}
ctx_request.SetBody([]byte(tt.body))
ctx_request.SetRequestURI(tt.path)
ctx_request.Header.SetMethod("POST")
ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{
Request: ctx_request,
})
res := parseGraphQLQuery(ctx)
assert.NotNil(ctx, "Fiber context is nil", tt.name)
err := proxyTheRequest(ctx, res.activeEndpoint)
if tt.wantErr {
assert.NotNil(err, "Error is nil", tt.name)
} else {
assert.Nil(err, "Error is not nil", tt.name)
}
})
}
}
+147
View File
@@ -0,0 +1,147 @@
package main
import (
"os"
"time"
"github.com/goccy/go-json"
goratecounter "github.com/lukaszraczylo/go-ratecounter"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
)
type RateLimitConfig struct {
RateCounterTicker *goratecounter.RateCounter
Interval string `json:"interval"`
Req int `json:"req"`
}
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{"/go/src/app/ratelimit.json", "./ratelimit.json", "./static/app/default-ratelimit.json"}
for _, path := range paths {
err := loadConfigFromPath(path)
if err == nil {
return nil
}
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Failed to load config",
Pairs: map[string]interface{}{"path": path, "error": err},
})
}
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Rate limit config not found",
Pairs: map[string]interface{}{"paths": paths},
})
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],
})
if cfg.LogLevel == "debug" {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Setting ratelimit config for role",
Pairs: 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(&libpack_logger.LogMessage{
Message: "Rate limit config loaded",
Pairs: map[string]interface{}{"ratelimit": rateLimits},
})
return nil
}
func rateLimitedRequest(userID string, userRole string) (shouldAllow bool) {
if rateLimits == nil {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limit config not found",
Pairs: 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.Debug(&libpack_logger.LogMessage{
Message: "Rate limit role not found",
Pairs: map[string]interface{}{"user_role": userRole},
})
return true
}
if roleConfig.RateCounterTicker == nil {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limit ticker not found",
Pairs: 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(&libpack_logger.LogMessage{
Message: "Rate limit ticker",
Pairs: map[string]interface{}{"log_details": logDetails},
})
if tickerRate > float64(roleConfig.Req) {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limit exceeded",
Pairs: map[string]interface{}{"log_details": logDetails},
})
return false
}
return true
}
+15
View File
@@ -0,0 +1,15 @@
version: 1
force:
existing: true
strict: false
minor: 1
wording:
patch:
- update
- initial
- fix
minor:
- improve
- release
major:
- breaking
+300
View File
@@ -0,0 +1,300 @@
package main
import (
"context"
"fmt"
"strconv"
"time"
"github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/google/uuid"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
libpack_trace "github.com/lukaszraczylo/graphql-monitoring-proxy/tracing"
)
// StartHTTPProxy starts the HTTP and points it to the GraphQL server.
func StartHTTPProxy() {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Starting the HTTP proxy",
Pairs: nil,
})
server := fiber.New(fiber.Config{
DisableStartupMessage: true,
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2,
JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal,
})
server.Use(cors.New(cors.Config{
AllowOrigins: "*",
}))
// add middleware to check if the request is a GraphQL query
server.Use(AddRequestUUID)
server.Get("/healthz", healthCheck)
server.Get("/livez", healthCheck)
server.Post("/*", processGraphQLRequest)
server.Get("/*", proxyTheRequestToDefault)
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "GraphQL proxy started",
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL},
})
err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL))
if err != nil {
cfg.Logger.Critical(&libpack_logger.LogMessage{
Message: "Can't start the service",
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL},
})
}
}
func proxyTheRequestToDefault(c *fiber.Ctx) error {
return proxyTheRequest(c, cfg.Server.HostGraphQL)
}
func AddRequestUUID(c *fiber.Ctx) error {
c.Locals("request_uuid", uuid.NewString())
return c.Next()
}
func checkAllowedURLs(c *fiber.Ctx) bool {
if len(allowedUrls) == 0 {
return true
}
_, ok := allowedUrls[c.Path()]
return ok
}
func extractTraceHeaders(c *fiber.Ctx) (found bool, traceHeaders map[string]string) {
if !cfg.Trace.Enable {
return
}
headers := c.Request().Header
traceHeader := headers.Peek("X-Trace-Span")
if traceHeader != nil {
traceHeaders = make(map[string]string)
if err := json.Unmarshal(traceHeader, &traceHeaders); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Error unmarshalling tracer header",
Pairs: map[string]interface{}{"error": err},
})
return
}
found = true
}
return
}
func healthCheck(c *fiber.Ctx) error {
if len(cfg.Server.HealthcheckGraphQL) > 0 {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Health check enabled",
Pairs: map[string]interface{}{"url": cfg.Server.HealthcheckGraphQL},
})
query := `{ __typename }`
_, err := cfg.Client.GQLClient.Query(query, nil, nil)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't reach the GraphQL server",
Pairs: map[string]interface{}{"error": err.Error()},
})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
c.Status(500).SendString("Can't reach the GraphQL server with {__typename} query")
return err
}
}
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Health check returning OK",
Pairs: nil,
})
c.Status(200).SendString("Health check OK")
return nil
}
func processGraphQLRequest(c *fiber.Ctx) error {
startTime := time.Now()
// Initialize variables with default values
extractedUserID, extractedRoleName := "-", "-"
// Pre-fetch headers and trace header processing
headers := c.Request().Header
authorization := headers.Peek("Authorization")
ctx := context.Background()
traceHeaderFound, traceHeader := extractTraceHeaders(c)
if traceHeaderFound {
ctx = libpack_trace.TraceContextExtract(ctx, traceHeader)
_, span := libpack_trace.ContinueSpanFromContext(ctx, "GraphQLRequest")
defer span.End()
}
// JWT and role extraction with pre-check
if authorization != nil && (len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0) {
extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(string(authorization))
}
// Check for banned users early
if checkIfUserIsBanned(c, extractedUserID) {
return c.Status(403).SendString("User is banned")
}
// Role extraction from header
if len(cfg.Client.RoleFromHeader) > 0 {
extractedRoleName = string(headers.Peek(cfg.Client.RoleFromHeader))
if extractedRoleName == "" {
extractedRoleName = "-"
}
}
// Rate limiting check
if cfg.Client.RoleRateLimit && !rateLimitedRequest(extractedUserID, extractedRoleName) {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Rate limiting enabled",
Pairs: map[string]interface{}{"user_id": extractedUserID, "role_name": extractedRoleName},
})
return c.Status(429).SendString("Rate limit exceeded, try again later")
}
// Parsing GraphQL query
parsedResult := parseGraphQLQuery(c)
if parsedResult.shouldBlock {
return c.Status(403).SendString("Request blocked")
}
if parsedResult.shouldIgnore {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Request passed as-is - probably not a GraphQL",
Pairs: nil,
})
return proxyTheRequest(c, parsedResult.activeEndpoint, ctx)
}
// Cache handling logic
queryCacheHash := libpack_cache.CalculateHash(c)
if parsedResult.cacheTime == 0 {
cacheQuery := headers.Peek("X-Cache-Graphql-Query")
if cacheQuery != nil {
parsedResult.cacheTime, _ = strconv.Atoi(string(cacheQuery))
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache time set via header",
Pairs: map[string]interface{}{"cacheTime": parsedResult.cacheTime},
})
} else {
parsedResult.cacheTime = cfg.Cache.CacheTTL
}
}
if parsedResult.cacheRefresh {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache refresh requested via query",
Pairs: map[string]interface{}{"user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
})
libpack_cache.CacheDelete(queryCacheHash)
}
wasCached := false
if parsedResult.cacheRequest || cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable {
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache enabled",
Pairs: map[string]interface{}{"via_query": parsedResult.cacheRequest, "via_env": cfg.Cache.CacheEnable},
})
if cachedResponse := libpack_cache.CacheLookup(queryCacheHash); cachedResponse != nil {
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheHit, nil)
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache hit",
Pairs: map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
})
headers.Add("X-Cache-Hit", "true")
if err := c.Send(cachedResponse); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't send the cached response",
Pairs: map[string]interface{}{"error": err.Error()},
})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
return c.Status(500).SendString("Can't send the cached response - try again later")
}
wasCached = true
} else {
cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheMiss, nil)
cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Cache miss",
Pairs: map[string]interface{}{"hash": queryCacheHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")},
})
proxyAndCacheTheRequest(c, queryCacheHash, parsedResult.cacheTime, parsedResult.activeEndpoint, ctx)
}
} else {
if err := proxyTheRequest(c, parsedResult.activeEndpoint, ctx); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't proxy the request",
Pairs: map[string]interface{}{"error": err.Error()},
})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
return c.Status(500).SendString("Can't proxy the request - try again later")
}
}
timeTaken := time.Since(startTime)
logAndMonitorRequest(c, extractedUserID, parsedResult.operationType, parsedResult.operationName, wasCached, timeTaken, startTime)
return nil
}
// Additional helper function to avoid code repetition
func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string, ctx context.Context) {
err := proxyTheRequest(c, currentEndpoint, ctx)
if err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't proxy the request",
Pairs: map[string]interface{}{"error": err.Error()},
})
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
c.Status(500).SendString("Can't proxy the request - try again later")
return
}
libpack_cache.CacheStoreWithTTL(queryCacheHash, c.Response().Body(), time.Duration(cacheTime)*time.Second)
cfg.Monitoring.Increment(libpack_monitoring.MetricsQueriesCached, nil)
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", wasCached),
"user_id": userID,
}
if cfg.Server.AccessLog {
cfg.Logger.Info(&libpack_logger.LogMessage{
Message: "Request processed",
Pairs: map[string]interface{}{
"ip": c.IP(),
"fwd-ip": string(c.Request().Header.Peek("X-Forwarded-For")),
"user_id": userID,
"op_type": opType,
"op_name": opName,
"time": duration,
"cache": wasCached,
"request_uuid": c.Locals("request_uuid"),
},
})
}
cfg.Monitoring.Increment(libpack_monitoring.MetricsSucceeded, nil)
cfg.Monitoring.Increment(libpack_monitoring.MetricsExecutedQuery, labels)
if !wasCached {
cfg.Monitoring.UpdateDuration(libpack_monitoring.MetricsTimedQuery, labels, startTime)
cfg.Monitoring.Update(libpack_monitoring.MetricsTimedQuery, labels, float64(duration.Milliseconds()))
}
}
View File
+1
View File
@@ -0,0 +1 @@
{}
View File
+16
View File
@@ -0,0 +1,16 @@
{
"ratelimit": {
"admin": {
"req": 100,
"interval": "second"
},
"guest": {
"req": 3,
"interval": "second"
},
"-": {
"req": 100,
"interval": "hour"
}
}
}
+92
View File
@@ -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
@@ -0,0 +1,165 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: hasura-w-proxy-internal
labels:
app: hasura-w-proxy-internal
type: support
spec:
replicas: 2
selector:
matchLabels:
app: hasura-w-proxy-internal
type: support
template:
metadata:
labels:
app: hasura-w-proxy-internal
type: support
spec:
securityContext:
runAsUser: 65534 # nobody
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/worker
operator: Exists
containers:
- name: hasura
image: hasura/graphql-engine:v2.33.1-ce
ports:
- name: hasura-internal
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
resources:
limits:
cpu: "1"
memory: "640Mi"
requests:
cpu: "0.75"
memory: "512Mi"
env:
- name: HASURA_GRAPHQL_DATABASE_URL
value: postgres://postgres:xxx@yyy:5432/postgres
- name: HASURA_GRAPHQL_ENABLE_CONSOLE
value: "true"
- name: HASURA_GRAPHQL_DEV_MODE
value: "true"
- name: HASURA_GRAPHQL_ENABLE_TELEMETRY
value: "false"
- name: HASURA_GRAPHQL_EXPERIMENTAL_FEATURES
value: "inherited_roles"
- name: HASURA_GRAPHQL_PG_CONNECTIONS
value: "20"
- name: HASURA_GRAPHQL_LOG_LEVEL
value: "error"
- name: hasura-ro
image: hasura/graphql-engine:v2.33.1-ce
ports:
- name: hasura-internal-ro
containerPort: 8088
livenessProbe:
httpGet:
path: /healthz
port: 8088
initialDelaySeconds: 30
resources:
limits:
cpu: "1"
memory: "640Mi"
requests:
cpu: "0.75"
memory: "512Mi"
env:
- name: HASURA_GRAPHQL_DATABASE_URL
value: postgres://postgres:[email protected]:5432/postgres
# POINT METADATA TO THE RW database (!!!)
- name: HASURA_GRAPHQL_METADATA_DATABASE_URL
value: postgres://postgres:xxx@yyy:5432/postgres
- name: HASURA_GRAPHQL_ENABLE_CONSOLE
value: "true"
- name: HASURA_GRAPHQL_DEV_MODE
value: "true"
- name: HASURA_GRAPHQL_ENABLE_TELEMETRY
value: "false"
- name: HASURA_GRAPHQL_EXPERIMENTAL_FEATURES
value: "inherited_roles"
- name: HASURA_GRAPHQL_PG_CONNECTIONS
value: "20"
- name: HASURA_GRAPHQL_LOG_LEVEL
value: "error"
- name: HASURA_GRAPHQL_SERVER_PORT
value: "8088"
- name: graphql-proxy
image: ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest
imagePullPolicy: Always
resources:
limits:
cpu: "1"
memory: "640Mi"
requests:
cpu: "0.75"
memory: "128Mi"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 5
ports:
- name: web
containerPort: 8181
- name: monitoring
containerPort: 9393
env:
- name: PORT_GRAPHQL
value: "8181"
- name: MONITORING_PORT
value: "9393"
- name: HOST_GRAPHQL
value: http://localhost:8080/
- name: HOST_GRAPHQL_READONLY
value: http://localhost:8088/
- name: ENABLE_GLOBAL_CACHE
value: "true"
- name: CACHE_TTL
value: "10"
---
apiVersion: v1
kind: Service
metadata:
name: hasura-w-proxy-internal
labels:
app: hasura-w-proxy-internal
type: support
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9393"
prometheus.io/path: "/metrics"
spec:
ports:
- name: hasura
port: 8080
targetPort: 8080
- name: hasura-ro
port: 8088
targetPort: 8088
- name: proxy
port: 8181
targetPort: 8181
- name: monitoring
port: 9393
targetPort: 9393
selector:
app: hasura-w-proxy-internal
type: support
type: ClusterIP
+121
View File
@@ -0,0 +1,121 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: hasura-w-proxy-internal
labels:
app: hasura-w-proxy-internal
type: support
spec:
replicas: 2
selector:
matchLabels:
app: hasura-w-proxy-internal
type: support
template:
metadata:
labels:
app: hasura-w-proxy-internal
type: support
spec:
securityContext:
runAsUser: 65534 # nobody
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/worker
operator: Exists
containers:
- name: hasura
image: hasura/graphql-engine:v2.33.1-ce
ports:
- name: hasura-internal
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
resources:
limits:
cpu: "1"
memory: "640Mi"
requests:
cpu: "0.75"
memory: "512Mi"
env:
- name: HASURA_GRAPHQL_DATABASE_URL
value: postgres://postgres:xxx@yyy:5432/postgres
- name: HASURA_GRAPHQL_ENABLE_CONSOLE
value: "true"
- name: HASURA_GRAPHQL_DEV_MODE
value: "true"
- name: HASURA_GRAPHQL_ENABLE_TELEMETRY
value: "false"
- name: HASURA_GRAPHQL_EXPERIMENTAL_FEATURES
value: "inherited_roles"
- name: HASURA_GRAPHQL_PG_CONNECTIONS
value: "20"
- name: HASURA_GRAPHQL_LOG_LEVEL
value: "error"
- name: graphql-proxy
image: ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest
imagePullPolicy: Always
resources:
limits:
cpu: "1"
memory: "640Mi"
requests:
cpu: "0.75"
memory: "128Mi"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 5
ports:
- name: web
containerPort: 8181
- name: monitoring
containerPort: 9393
env:
- name: PORT_GRAPHQL
value: "8181"
- name: MONITORING_PORT
value: "9393"
- name: HOST_GRAPHQL
value: http://localhost:8080/
- name: ENABLE_GLOBAL_CACHE
value: "true"
- name: CACHE_TTL
value: "10"
---
apiVersion: v1
kind: Service
metadata:
name: hasura-w-proxy-internal
labels:
app: hasura-w-proxy-internal
type: support
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9393"
prometheus.io/path: "/metrics"
spec:
ports:
- name: hasura
port: 8080
targetPort: 8080
- name: proxy
port: 8181
targetPort: 8181
- name: monitoring
port: 9393
targetPort: 9393
selector:
app: hasura-w-proxy-internal
type: support
type: ClusterIP
Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

+62
View File
@@ -0,0 +1,62 @@
package main
import (
graphql "github.com/lukaszraczylo/go-simple-graphql"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/valyala/fasthttp"
)
// config is a struct that holds the configuration of the application.
type config struct {
Trace struct {
Client func()
TraceEndpoint string
Enable bool
}
Logger *libpack_logging.Logger
LogLevel string
Monitoring *libpack_monitoring.MetricsSetup
Api struct{ BannedUsersFile string }
Client struct {
GQLClient *graphql.BaseClient
FastProxyClient *fasthttp.Client
JWTUserClaimPath string
JWTRoleClaimPath string
RoleFromHeader string
proxy string
ClientTimeout int
RoleRateLimit bool
}
Security struct {
IntrospectionAllowed []string
BlockIntrospection bool
}
HasuraEventCleaner struct {
EventMetadataDb string
ClearOlderThan int
Enable bool
}
Cache struct {
CacheRedisURL string
CacheRedisPassword string
CacheTTL int
CacheRedisDB int
CacheEnable bool
CacheRedisEnable bool
}
Server struct {
HostGraphQL string
HostGraphQLReadOnly string
HealthcheckGraphQL string
AllowURLs []string
PortGraphQL int
PortMonitoring int
ApiPort int
PurgeEvery int
AccessLog bool
ReadOnlyMode bool
EnableApi bool
PurgeOnCrawl bool
}
}
+98
View File
@@ -0,0 +1,98 @@
package libpack_trace
import (
"context"
"fmt"
"time"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
oteltrace "go.opentelemetry.io/otel/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func NewClient(log *libpack_logging.Logger, otelGRPCCollector string, attr ...attribute.KeyValue) (func(), error) {
attr = append(attr, semconv.ServiceNameKey.String(libpack_config.PKG_NAME))
fmt.Printf("Starting OpenTelemetry tracer: otlp, configured with endpoint: %s\n", otelGRPCCollector)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
client := otlptracegrpc.NewClient(
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithEndpoint(otelGRPCCollector),
)
exporter, err := otlptrace.New(ctx, client)
if err != nil {
log.Error(&libpack_logging.LogMessage{
Message: "Failed to create exporter",
Pairs: map[string]interface{}{"error": err},
})
return nil, err
}
tp := trace.NewTracerProvider(
trace.WithSampler(trace.AlwaysSample()),
trace.WithBatcher(exporter, trace.WithMaxExportBatchSize(1), trace.WithBatchTimeout(30*time.Second)),
trace.WithResource(resource.NewWithAttributes(semconv.SchemaURL, attr...)),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
shutdownFunc := func() {
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
log.Info(&libpack_logging.LogMessage{
Message: "Shutting down tracer",
Pairs: nil,
})
if err := tp.Shutdown(shutdownCtx); err != nil {
log.Warning(&libpack_logging.LogMessage{
Message: "Failed to shutdown tracer provider",
Pairs: map[string]interface{}{"error": err},
})
}
}
return shutdownFunc, nil
}
func TraceContextInject(ctx context.Context) map[string]string {
carrier := propagation.MapCarrier{}
propagator := otel.GetTextMapPropagator()
propagator.Inject(ctx, carrier)
return map[string]string(carrier)
}
func TraceContextExtract(ctx context.Context, traceContext map[string]string) context.Context {
carrier := propagation.MapCarrier(traceContext)
propagator := otel.GetTextMapPropagator()
return propagator.Extract(ctx, carrier)
}
func StartSpanFromContext(ctx context.Context, operationName string) (context.Context, oteltrace.Span) {
tr := otel.GetTracerProvider().Tracer("")
return tr.Start(ctx, operationName, oteltrace.WithSpanKind(oteltrace.SpanKindServer))
}
func ContinueSpanFromContext(ctx context.Context, operationName string) (context.Context, oteltrace.Span) {
tr := otel.GetTracerProvider().Tracer("")
options := []oteltrace.SpanStartOption{
oteltrace.WithSpanKind(oteltrace.SpanKindInternal),
oteltrace.WithAttributes(attribute.String("cont", "true")),
}
return tr.Start(ctx, operationName, options...)
}
func AddAttributesToSpan(span oteltrace.Span, attributes ...attribute.KeyValue) {
span.SetAttributes(attributes...)
}
+109
View File
@@ -0,0 +1,109 @@
package libpack_trace
import (
"testing"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type TraceTestSuite struct {
suite.Suite
logger *libpack_logging.Logger
}
func (suite *TraceTestSuite) SetupTest() {
suite.logger = libpack_logging.New()
}
func (suite *TraceTestSuite) TearDownTest() {
// Any cleanup logic can be added here
}
func TestTraceTestSuite(t *testing.T) {
suite.Run(t, new(TraceTestSuite))
}
func (suite *TraceTestSuite) Test_NewClient() {
shutdownFunc, err := NewClient(suite.logger, "localhost:4317")
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), shutdownFunc)
shutdownFunc()
}
// func (suite *TraceTestSuite) Test_TraceContextInjectExtract() {
// ctx := context.Background()
// traceContext := TraceContextInject(ctx)
// assert.NotEmpty(suite.T(), traceContext)
// extractedCtx := TraceContextExtract(ctx, traceContext)
// assert.NotNil(suite.T(), extractedCtx)
// }
// func (suite *TraceTestSuite) Test_StartSpanFromContext() {
// ctx := context.Background()
// ctx, span := StartSpanFromContext(ctx, "operation")
// assert.NotNil(suite.T(), ctx)
// assert.NotNil(suite.T(), span)
// span.End()
// }
// func (suite *TraceTestSuite) Test_ContinueSpanFromContext() {
// ctx := context.Background()
// ctx, span := ContinueSpanFromContext(ctx, "operation")
// assert.NotNil(suite.T(), ctx)
// assert.NotNil(suite.T(), span)
// span.End()
// }
// func (suite *TraceTestSuite) Test_AddAttributesToSpan() {
// ctx := context.Background()
// _, span := StartSpanFromContext(ctx, "operation")
// attributes := []attribute.KeyValue{
// attribute.String("key1", "value1"),
// attribute.Int("key2", 2),
// }
// AddAttributesToSpan(span, attributes...)
// span.End()
// // Create an in-memory span exporter
// exporter := tracetest.NewSpanRecorder()
// tracerProvider := trace.NewTracerProvider(trace.WithSpanProcessor(exporter))
// otel.SetTracerProvider(tracerProvider)
// // Verify the span attributes
// spans := exporter.Ended()
// assert.Len(suite.T(), spans, 1)
// exportedSpan := spans[0]
// for _, attr := range attributes {
// assert.Contains(suite.T(), exportedSpan.Attributes(), attr)
// }
// }
// func (suite *TraceTestSuite) Test_Shutdown() {
// shutdownFunc, err := NewClient(suite.logger, "localhost:4317")
// assert.NoError(suite.T(), err)
// assert.NotNil(suite.T(), shutdownFunc)
// shutdownFunc()
// logOutput := captureStdOut(func() { suite.logger.Info(&libpack_logging.LogMessage{Message: "Shutting down tracer"}) })
// assert.Contains(suite.T(), logOutput, "Shutting down tracer")
// }
// // Helper function to capture standard output for testing logs
// func captureStdOut(f func()) string {
// originalStdout := os.Stdout
// r, w, _ := os.Pipe()
// os.Stdout = w
// f()
// w.Close()
// var buf bytes.Buffer
// buf.ReadFrom(r)
// os.Stdout = originalStdout
// return buf.String()
// }