mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-13 02:17:35 +00:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c68dc2f20a | |||
| 11ff751001 | |||
| 0414473f15 | |||
| bc61557015 | |||
| 12ec00f697 | |||
| da4a179d66 | |||
| d0ecefce6c | |||
| c742530d2f | |||
| 7304559801 | |||
| aa46992497 | |||
| e968a48584 | |||
| c67dfe1827 | |||
| 55d86e34cf | |||
| cd4a1f16ed | |||
| 3352050bdb | |||
| bb2509e254 | |||
| d027122446 | |||
| 3abbaf66a1 | |||
| f8871a4fb7 | |||
| 420e63f383 | |||
| 9bd9f0b9ba | |||
| 31cb5930d5 | |||
| 454e1d2425 | |||
| 98afa39943 | |||
| 6605c59efd | |||
| f87f2ae5a2 | |||
| 04f6deb0a8 | |||
| 5ea41ea268 | |||
| b8b814a9be | |||
| 5b79b49b00 | |||
| bdbf829a59 | |||
| dcff327745 | |||
| f2997c4c9f | |||
| c3fe0471df | |||
| d62c718682 | |||
| 26cebee756 | |||
| acace4fe16 | |||
| f6fc338c8c | |||
| 9b792c3c64 | |||
| d3fe02aa52 | |||
| 82000bfb4c | |||
| 3aa83d4480 | |||
| caeae62236 | |||
| 0e1deab8ed | |||
| 67b0bebbc3 | |||
| 92c2c162d8 | |||
| 8367812a48 | |||
| 86fa0551df | |||
| 4be6b0f6cf | |||
| 6bc4cfd916 | |||
| a3093fe2d1 | |||
| c0f5f0830d | |||
| 623cbbcae3 | |||
| 05a07fde42 | |||
| c926d0d0a3 | |||
| 6c96880eae | |||
| 7f78869a8a | |||
| 794ec6a752 | |||
| 9678b8f7b9 | |||
| 7bb76893f5 | |||
| 4ef42e5781 | |||
| 996d29b57b | |||
| 7c80d6adaa | |||
| 31fc3ae3d9 | |||
| da8ec5f21d | |||
| 3d80f457d3 | |||
| 09c3e4cd95 | |||
| d07ee4090c | |||
| b1045b8bc2 |
@@ -5,69 +5,15 @@ on:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
env:
|
||||
GO_VERSION: ">=1.21"
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
pull-requests: write
|
||||
|
||||
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"
|
||||
runs-on: ubuntu-latest
|
||||
container: golang:1
|
||||
needs: [prepare]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Ensure full history is checked out
|
||||
token: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- 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
|
||||
git config --global --add safe.directory /__w/graphql-monitoring-proxy/graphql-monitoring-proxy
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "Update go.mod and go.sum"
|
||||
commit_options: "--no-verify --signoff"
|
||||
file_pattern: "go.mod go.sum"
|
||||
autoupdate:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
||||
with:
|
||||
go-version: ">=1.24"
|
||||
release-workflow: "release.yaml"
|
||||
secrets: inherit
|
||||
|
||||
+7
-100
@@ -1,109 +1,16 @@
|
||||
name: Run tests on PR
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- main
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**/**.md"
|
||||
- "**/**.yaml"
|
||||
- "static/**"
|
||||
branches:
|
||||
- "**"
|
||||
- "!main"
|
||||
|
||||
env:
|
||||
GO_VERSION: ">=1.21"
|
||||
|
||||
permissions:
|
||||
# deployments permission to deploy GitHub pages website
|
||||
deployments: write
|
||||
# contents permission to update benchmark contents in gh-pages branch
|
||||
contents: write
|
||||
|
||||
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
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
CI_RUN=${CI} make test
|
||||
|
||||
- name: Run benchmark
|
||||
run: |
|
||||
go test -bench=. -benchmem ./... -run=^# | tee output.txt
|
||||
|
||||
- name: Store benchmark result
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
tool: "go"
|
||||
output-file-path: output.txt
|
||||
fail-on-alert: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-on-alert: true
|
||||
summary-always: true
|
||||
# auto-push only if it's on main branch
|
||||
auto-push: false
|
||||
gh-pages-branch: "gh-pages"
|
||||
benchmark-data-dir-path: "docs"
|
||||
pr-checks:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**/release.yaml"
|
||||
- "static/**"
|
||||
- "docs/**"
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
packages: write
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||
with:
|
||||
go-version: "1.24"
|
||||
docker-enabled: true
|
||||
secrets: inherit
|
||||
|
||||
benchmark:
|
||||
name: Publish Benchmarks
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: main
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Run benchmarks
|
||||
run: go test -bench=. -benchmem ./... -run=^# | tee output.txt
|
||||
|
||||
- name: Store benchmark result
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
tool: "go"
|
||||
output-file-path: output.txt
|
||||
fail-on-alert: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-on-alert: true
|
||||
summary-always: true
|
||||
auto-push: false
|
||||
benchmark-data-dir-path: "docs/bench"
|
||||
|
||||
- name: Push benchmark results
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add docs/bench
|
||||
git diff --staged --quiet || git commit -m "Update benchmark results"
|
||||
git push origin main
|
||||
@@ -1,72 +0,0 @@
|
||||
name: Test and release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**/**.md"
|
||||
- "**/**.yaml"
|
||||
- "static/**"
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
env:
|
||||
GO_VERSION: ">=1.21"
|
||||
|
||||
permissions:
|
||||
# deployments permission to deploy GitHub pages website
|
||||
deployments: write
|
||||
# contents permission to update benchmark contents in gh-pages branch
|
||||
contents: write
|
||||
|
||||
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 }}
|
||||
|
||||
test:
|
||||
name: "Benchmarking the results"
|
||||
needs: [shared]
|
||||
runs-on: ubuntu-latest
|
||||
container: golang:1
|
||||
# container: github/super-linter:v4
|
||||
|
||||
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
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Run benchmark
|
||||
run: |
|
||||
go test -bench=. -benchmem ./... -run=^# | tee output.txt
|
||||
|
||||
- name: Store benchmark result
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
tool: "go"
|
||||
output-file-path: output.txt
|
||||
fail-on-alert: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-on-alert: true
|
||||
summary-always: true
|
||||
# auto-push only if it's on main branch
|
||||
auto-push: true
|
||||
gh-pages-branch: "gh-pages"
|
||||
benchmark-data-dir-path: "docs"
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
# Project-specific golangci-lint configuration (v2)
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
# Code quality
|
||||
- govet # Go vet (suspicious constructs)
|
||||
- staticcheck # Advanced static analysis
|
||||
- unused # Find unused code
|
||||
- errcheck # Check for unchecked errors
|
||||
|
||||
# Security
|
||||
- gosec # Security issues
|
||||
|
||||
settings:
|
||||
unused:
|
||||
field-writes-are-uses: true
|
||||
post-statements-are-reads: true
|
||||
exported-is-used: true
|
||||
exported-fields-are-used: true
|
||||
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# Field alignment is a micro-optimization that reduces readability
|
||||
- fieldalignment
|
||||
# Shadow warnings in this codebase are intentional and safe
|
||||
- shadow
|
||||
|
||||
staticcheck:
|
||||
checks:
|
||||
- "all"
|
||||
# Disable naming convention checks - existing codebase uses underscores
|
||||
# and ALL_CAPS which would require significant refactoring
|
||||
- "-ST1000" # Package comments
|
||||
- "-ST1003" # Naming conventions (underscores, ALL_CAPS)
|
||||
# Disable quickfix suggestions - these are style preferences, not errors
|
||||
- "-QF1001" # De Morgan's law
|
||||
- "-QF1012" # fmt.Fprintf suggestion
|
||||
|
||||
errcheck:
|
||||
# Don't check error returns on these functions (best-effort cleanup)
|
||||
exclude-functions:
|
||||
- (*github.com/gorilla/websocket.Conn).Close
|
||||
- (*github.com/gorilla/websocket.Conn).SetReadDeadline
|
||||
- (*github.com/gorilla/websocket.Conn).WriteMessage
|
||||
- (*github.com/redis/go-redis/v9.Client).Close
|
||||
- (*github.com/redis/go-redis/v9.Pipeline).Exec
|
||||
- (io.Closer).Close
|
||||
- (*os.File).Close
|
||||
- (*compress/gzip.Reader).Close
|
||||
- (net.Conn).Close
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
# G104: Errors unhandled - covered by errcheck with proper exclusions
|
||||
- G104
|
||||
# G115: Integer overflow conversion - safe in this codebase
|
||||
# These are uint64 counter values that will never exceed int64 max
|
||||
- G115
|
||||
# G402: TLS InsecureSkipVerify - this is a configurable option
|
||||
# Users explicitly enable this via GMP_DISABLE_TLS_VERIFY env var
|
||||
- G402
|
||||
|
||||
exclusions:
|
||||
presets:
|
||||
- common-false-positives
|
||||
rules:
|
||||
# Test files can have relaxed rules
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- unused
|
||||
- errcheck
|
||||
- gosec
|
||||
|
||||
# Specific file exclusions for known patterns
|
||||
- path: api\.go
|
||||
linters:
|
||||
- gosec
|
||||
text: "G306"
|
||||
# File permissions 0644 for banned users file is intentional
|
||||
# This is a non-sensitive configuration file that may be
|
||||
# read by deployment tools
|
||||
|
||||
# Exclude enableApi naming (would be a breaking change)
|
||||
- path: api\.go
|
||||
text: "ST1003"
|
||||
|
||||
# Generated files
|
||||
- path: \.pb\.go$
|
||||
linters:
|
||||
- all
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
|
||||
settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: true
|
||||
modules-download-mode: readonly
|
||||
build-tags:
|
||||
- ""
|
||||
go: "1.23"
|
||||
|
||||
output:
|
||||
formats:
|
||||
text:
|
||||
path: stdout
|
||||
colors: true
|
||||
sort-results: true
|
||||
@@ -0,0 +1,87 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: graphql-proxy
|
||||
main: .
|
||||
binary: graphql-proxy
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w
|
||||
|
||||
archives:
|
||||
- id: graphql-proxy
|
||||
formats: [tar.gz]
|
||||
name_template: "graphql-proxy-{{ .Os }}-{{ .Arch }}"
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
|
||||
checksum:
|
||||
name_template: "graphql-proxy-checksums.txt"
|
||||
algorithm: sha256
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^Merge'
|
||||
- '^WIP'
|
||||
- '^Update go.mod'
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: lukaszraczylo
|
||||
name: graphql-monitoring-proxy
|
||||
name_template: "version {{.Version}}"
|
||||
draft: false
|
||||
prerelease: auto
|
||||
|
||||
dockers_v2:
|
||||
- images:
|
||||
- "ghcr.io/lukaszraczylo/graphql-monitoring-proxy"
|
||||
tags:
|
||||
- "{{ .Version }}"
|
||||
- "latest"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
dockerfile: Dockerfile.goreleaser
|
||||
extra_files:
|
||||
- static/app
|
||||
|
||||
signs:
|
||||
- cmd: cosign
|
||||
signature: "${artifact}.sigstore.json"
|
||||
args:
|
||||
- sign-blob
|
||||
- "--bundle=${signature}"
|
||||
- "${artifact}"
|
||||
- "--yes"
|
||||
artifacts: checksum
|
||||
output: true
|
||||
|
||||
docker_signs:
|
||||
- cmd: cosign
|
||||
artifacts: manifests
|
||||
output: true
|
||||
args:
|
||||
- sign
|
||||
- "${artifact}@${digest}"
|
||||
- "--yes"
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM gcr.io/distroless/base-debian12:nonroot
|
||||
ARG TARGETPLATFORM
|
||||
WORKDIR /go/src/app
|
||||
COPY --chmod=777 --chown=nonroot:nonroot static/app /go/src/app
|
||||
COPY ${TARGETPLATFORM}/graphql-proxy /go/src/app/graphql-proxy
|
||||
ENTRYPOINT ["/go/src/app/graphql-proxy"]
|
||||
@@ -57,6 +57,25 @@ You should always try to stick to the latest and greatest version of the graphql
|
||||
|
||||
You can find the example of the Kubernetes manifest in the [example standalone deployment](static/kubernetes-deployment.yaml) or [example combined deployment](static/kubernetes-single-deployment.yaml) files. Observed advantage of multideployment is that it allows the network requests to travel via localhost, without leaving the deployment which brings quite significant network performance boost.
|
||||
|
||||
#### Verifying Release Signatures
|
||||
|
||||
All release checksums and Docker images are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. To verify:
|
||||
|
||||
```bash
|
||||
# Verify checksum signature
|
||||
cosign verify-blob \
|
||||
--certificate-identity-regexp "https://github.com/lukaszraczylo/graphql-monitoring-proxy/.*" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
--bundle "<checksums-file>.sigstore.json" \
|
||||
<checksums-file>
|
||||
|
||||
# Verify Docker image
|
||||
cosign verify \
|
||||
--certificate-identity-regexp "https://github.com/lukaszraczylo/graphql-monitoring-proxy/.*" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest
|
||||
```
|
||||
|
||||
#### Note on websocket support
|
||||
|
||||
**Native WebSocket Support Available!** Starting with version 0.27.0, the proxy includes native WebSocket support for GraphQL subscriptions. Enable it by setting `WEBSOCKET_ENABLE=true`.
|
||||
@@ -155,6 +174,7 @@ You can still use the non-prefixed environment variables in the spirit of the ba
|
||||
| `CACHE_TTL` | The cache TTL | `60` |
|
||||
| `CACHE_MAX_MEMORY_SIZE` | Maximum memory size for cache in MB | `100` |
|
||||
| `CACHE_MAX_ENTRIES` | Maximum number of entries in cache | `10000` |
|
||||
| `CACHE_USE_LRU` | Use LRU eviction algorithm (see [Cache Eviction](#cache-eviction-algorithms)) | `false` |
|
||||
| `CACHE_PER_USER_DISABLED` | **⚠️ SECURITY**: Disable per-user cache isolation | `false` (**DO NOT** set to `true` in multi-user apps) |
|
||||
| `ENABLE_REDIS_CACHE` | Enable distributed Redis cache | `false` |
|
||||
| `CACHE_REDIS_URL` | URL to redis server / cluster endpoint | `localhost:6379` |
|
||||
@@ -439,6 +459,32 @@ These features ensure the cache runs efficiently even under high load and with l
|
||||
Since version `0.5.30` the cache is gzipped in the memory, which should optimise the memory usage quite significantly.
|
||||
Since version `0.15.48` the you can also use the distributed Redis cache.
|
||||
|
||||
#### Cache Eviction Algorithms
|
||||
|
||||
The proxy supports two cache eviction strategies:
|
||||
|
||||
**Standard (default):** Uses Go's `sync.Map` with approximate eviction. When memory limits are reached, entries are evicted based on iteration order (pseudo-random). This is memory-efficient and has excellent concurrent read performance.
|
||||
|
||||
**LRU (Least Recently Used):** Uses a proper LRU algorithm with a linked list to track access order. When limits are reached, the least recently accessed entries are evicted first. Enable with `CACHE_USE_LRU=true`.
|
||||
|
||||
| Feature | Standard | LRU |
|
||||
|---------|----------|-----|
|
||||
| Eviction order | Pseudo-random | Least recently used |
|
||||
| Read performance | Excellent | Good |
|
||||
| Memory tracking | Approximate | Precise |
|
||||
| Best for | High read throughput | Cache hit optimization |
|
||||
|
||||
*LRU cache configuration:*
|
||||
```bash
|
||||
GMP_ENABLE_GLOBAL_CACHE=true
|
||||
GMP_CACHE_TTL=300
|
||||
GMP_CACHE_USE_LRU=true
|
||||
GMP_CACHE_MAX_MEMORY_SIZE=200
|
||||
GMP_CACHE_MAX_ENTRIES=5000
|
||||
```
|
||||
|
||||
Use LRU when cache hit rate is critical and you want to ensure frequently accessed data stays cached. Use Standard (default) for maximum read throughput with less memory overhead.
|
||||
|
||||
#### Read-only endpoint
|
||||
|
||||
You can now specify the read-only GraphQL endpoint by setting the `HOST_GRAPHQL_READONLY` environment variable. The default value is empty, preventing the proxy from using the read-only endpoint for the queries and directing all the requests to the main endpoint specified as `HOST_GRAPHQL`. If the `HOST_GRAPHQL_READONLY` is set, the proxy will use the read-only endpoint for the queries with the `query` type and the main endpoint for the `mutation` type queries. Format of the read-only endpoint is the same as `HOST_GRAPHQL` endpoint, for example `http://localhost:8080/`.
|
||||
|
||||
+59
-20
@@ -1277,15 +1277,34 @@
|
||||
stateEl.classList.remove('loading');
|
||||
|
||||
let badgeClass = 'badge-info';
|
||||
if (data.state === 'closed') badgeClass = 'badge-success';
|
||||
else if (data.state === 'open') badgeClass = 'badge-danger';
|
||||
else if (data.state === 'half-open') badgeClass = 'badge-warning';
|
||||
let stateText = data.state || 'unknown';
|
||||
|
||||
stateEl.innerHTML = `<span class="badge ${badgeClass}">${data.state || 'Unknown'}</span>`;
|
||||
// For cluster mode, determine state from instance counts
|
||||
if (data.instances_open !== undefined) {
|
||||
// Cluster mode data
|
||||
if (data.instances_open > 0) {
|
||||
stateText = `${data.instances_open} open`;
|
||||
badgeClass = 'badge-danger';
|
||||
} else if (data.instances_halfopen > 0) {
|
||||
stateText = `${data.instances_halfopen} half-open`;
|
||||
badgeClass = 'badge-warning';
|
||||
} else if (data.instances_closed > 0) {
|
||||
stateText = `${data.instances_closed} closed`;
|
||||
badgeClass = 'badge-success';
|
||||
}
|
||||
} else {
|
||||
// Single instance mode
|
||||
if (data.state === 'closed') badgeClass = 'badge-success';
|
||||
else if (data.state === 'open') badgeClass = 'badge-danger';
|
||||
else if (data.state === 'half-open') badgeClass = 'badge-warning';
|
||||
}
|
||||
|
||||
stateEl.innerHTML = `<span class="badge ${badgeClass}">${stateText}</span>`;
|
||||
|
||||
document.getElementById('cb-enabled').textContent = data.enabled ? 'Yes' : 'No';
|
||||
|
||||
if (data.counts) {
|
||||
// Single instance mode with detailed counts
|
||||
document.getElementById('cb-total-requests').textContent =
|
||||
(data.counts.requests || 0).toLocaleString();
|
||||
document.getElementById('cb-total-successes').textContent =
|
||||
@@ -1296,6 +1315,14 @@
|
||||
(data.counts.consecutive_successes || 0).toLocaleString();
|
||||
document.getElementById('cb-consecutive-failures').textContent =
|
||||
(data.counts.consecutive_failures || 0).toLocaleString();
|
||||
} else if (data.instances_open !== undefined) {
|
||||
// Cluster mode - show instance distribution instead
|
||||
const total = (data.instances_open || 0) + (data.instances_closed || 0) + (data.instances_halfopen || 0);
|
||||
document.getElementById('cb-total-requests').textContent = total + ' instances';
|
||||
document.getElementById('cb-total-successes').textContent = (data.instances_closed || 0).toLocaleString();
|
||||
document.getElementById('cb-total-failures').textContent = (data.instances_open || 0).toLocaleString();
|
||||
document.getElementById('cb-consecutive-successes').textContent = '--';
|
||||
document.getElementById('cb-consecutive-failures').textContent = '--';
|
||||
}
|
||||
|
||||
if (data.config) {
|
||||
@@ -1307,23 +1334,31 @@
|
||||
function updateCoalescing(data) {
|
||||
document.getElementById('coalescing-rate').textContent =
|
||||
(data.backend_savings_pct || 0).toFixed(1) + '%';
|
||||
document.getElementById('coalescing-total').textContent =
|
||||
(data.total_requests || 0).toLocaleString();
|
||||
document.getElementById('coalescing-primary').textContent =
|
||||
(data.primary_requests || 0).toLocaleString();
|
||||
document.getElementById('coalescing-coalesced').textContent =
|
||||
(data.coalesced_requests || 0).toLocaleString();
|
||||
|
||||
// Handle both single instance (total_requests) and cluster mode (total_coalesced + total_primary)
|
||||
const totalRequests = data.total_requests ||
|
||||
((data.total_coalesced_requests || 0) + (data.total_primary_requests || 0));
|
||||
document.getElementById('coalescing-total').textContent = totalRequests.toLocaleString();
|
||||
|
||||
// Handle both single instance and cluster mode field names
|
||||
const primaryRequests = data.primary_requests || data.total_primary_requests || 0;
|
||||
document.getElementById('coalescing-primary').textContent = primaryRequests.toLocaleString();
|
||||
|
||||
const coalescedRequests = data.coalesced_requests || data.total_coalesced_requests || 0;
|
||||
document.getElementById('coalescing-coalesced').textContent = coalescedRequests.toLocaleString();
|
||||
|
||||
document.getElementById('coalescing-savings').textContent =
|
||||
(data.backend_savings_pct || 0).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
function updateRetryBudget(data) {
|
||||
document.getElementById('retry-tokens').textContent =
|
||||
data.current_tokens || '--';
|
||||
document.getElementById('retry-current-tokens').textContent =
|
||||
data.current_tokens || '--';
|
||||
document.getElementById('retry-max-tokens').textContent =
|
||||
data.max_tokens || '--';
|
||||
// Use explicit undefined check to handle 0 values correctly
|
||||
const currentTokens = data.current_tokens !== undefined ? data.current_tokens : '--';
|
||||
const maxTokens = data.max_tokens !== undefined ? data.max_tokens : '--';
|
||||
|
||||
document.getElementById('retry-tokens').textContent = currentTokens;
|
||||
document.getElementById('retry-current-tokens').textContent = currentTokens;
|
||||
document.getElementById('retry-max-tokens').textContent = maxTokens;
|
||||
document.getElementById('retry-total').textContent =
|
||||
(data.total_attempts || 0).toLocaleString();
|
||||
document.getElementById('retry-denied').textContent =
|
||||
@@ -1333,13 +1368,17 @@
|
||||
}
|
||||
|
||||
function updateWebSocket(data) {
|
||||
document.getElementById('ws-connections').textContent =
|
||||
data.active_connections || 0;
|
||||
// Handle both single instance (active_connections) and cluster mode (total_connections)
|
||||
const connections = data.active_connections !== undefined ? data.active_connections :
|
||||
(data.total_connections !== undefined ? data.total_connections : 0);
|
||||
document.getElementById('ws-connections').textContent = connections;
|
||||
}
|
||||
|
||||
function updateConnections(data) {
|
||||
document.getElementById('pool-connections').textContent =
|
||||
data.active_connections || 0;
|
||||
// Handle both single instance (active_connections) and cluster mode (total_active)
|
||||
const connections = data.active_connections !== undefined ? data.active_connections :
|
||||
(data.total_active !== undefined ? data.total_active : 0);
|
||||
document.getElementById('pool-connections').textContent = connections;
|
||||
}
|
||||
|
||||
async function resetCoalescing() {
|
||||
|
||||
+80
-71
@@ -9,9 +9,18 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
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"
|
||||
)
|
||||
|
||||
// Admin dashboard constants
|
||||
const (
|
||||
// WebSocketReadDeadline is the read deadline for WebSocket connections
|
||||
WebSocketReadDeadline = 60 * time.Second
|
||||
// StatsStreamInterval is the interval for streaming stats updates
|
||||
StatsStreamInterval = 2 * time.Second
|
||||
)
|
||||
|
||||
//go:embed admin/dashboard.html
|
||||
var dashboardHTML embed.FS
|
||||
|
||||
@@ -60,7 +69,7 @@ func (ad *AdminDashboard) RegisterRoutes(app *fiber.App) {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Admin dashboard routes registered",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": "/admin",
|
||||
},
|
||||
})
|
||||
@@ -88,18 +97,18 @@ func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to get aggregated metrics, falling back to local stats",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
// Fall through to local stats on error
|
||||
} else {
|
||||
// Return aggregated cluster stats
|
||||
response := map[string]interface{}{
|
||||
response := map[string]any{
|
||||
"cluster_mode": true,
|
||||
"total_instances": metrics.TotalInstances,
|
||||
"healthy_instances": metrics.HealthyInstances,
|
||||
"timestamp": metrics.LastUpdate.Format(time.RFC3339),
|
||||
"version": "0.27.0",
|
||||
"version": libpack_config.PKG_VERSION,
|
||||
}
|
||||
|
||||
// Add combined stats from aggregation
|
||||
@@ -115,12 +124,12 @@ func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
|
||||
|
||||
// Local instance stats (fallback or non-cluster mode)
|
||||
uptimeSeconds := time.Since(startTime).Seconds()
|
||||
stats := map[string]interface{}{
|
||||
stats := map[string]any{
|
||||
"cluster_mode": false,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"uptime_seconds": uptimeSeconds,
|
||||
"uptime_human": formatDuration(time.Since(startTime)),
|
||||
"version": "0.27.0", // TODO: Get from build info
|
||||
"version": libpack_config.PKG_VERSION,
|
||||
}
|
||||
|
||||
if cfg != nil && cfg.Monitoring != nil {
|
||||
@@ -130,7 +139,7 @@ func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
|
||||
total := succeeded + failed + skipped
|
||||
|
||||
// Request statistics
|
||||
requestStats := map[string]interface{}{
|
||||
requestStats := map[string]any{
|
||||
"total": total,
|
||||
"succeeded": succeeded,
|
||||
"failed": failed,
|
||||
@@ -172,7 +181,7 @@ func (ad *AdminDashboard) getStats(c *fiber.Ctx) error {
|
||||
if totalCacheRequests > 0 {
|
||||
hitRate = float64(cacheStats.CacheHits) / float64(totalCacheRequests) * 100
|
||||
}
|
||||
stats["cache_summary"] = map[string]interface{}{
|
||||
stats["cache_summary"] = map[string]any{
|
||||
"hits": cacheStats.CacheHits,
|
||||
"misses": cacheStats.CacheMisses,
|
||||
"hit_rate_pct": hitRate,
|
||||
@@ -205,16 +214,16 @@ func formatDuration(d time.Duration) string {
|
||||
func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
|
||||
healthMgr := GetBackendHealthManager()
|
||||
|
||||
health := map[string]interface{}{
|
||||
health := map[string]any{
|
||||
"status": "unknown",
|
||||
"backend": map[string]interface{}{
|
||||
"backend": map[string]any{
|
||||
"healthy": false,
|
||||
},
|
||||
}
|
||||
|
||||
if healthMgr != nil {
|
||||
isHealthy := healthMgr.IsHealthy()
|
||||
health["backend"] = map[string]interface{}{
|
||||
health["backend"] = map[string]any{
|
||||
"healthy": isHealthy,
|
||||
"consecutive_failures": healthMgr.GetConsecutiveFailures(),
|
||||
"last_check": healthMgr.GetLastHealthCheck().Format(time.RFC3339),
|
||||
@@ -232,7 +241,7 @@ func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
|
||||
|
||||
// getCircuitBreakerStatus returns circuit breaker status
|
||||
func (ad *AdminDashboard) getCircuitBreakerStatus(c *fiber.Ctx) error {
|
||||
status := map[string]interface{}{
|
||||
status := map[string]any{
|
||||
"enabled": false,
|
||||
"state": "unknown",
|
||||
}
|
||||
@@ -247,14 +256,14 @@ func (ad *AdminDashboard) getCircuitBreakerStatus(c *fiber.Ctx) error {
|
||||
cbMutex.RUnlock()
|
||||
|
||||
status["state"] = state.String()
|
||||
status["counts"] = map[string]interface{}{
|
||||
status["counts"] = map[string]any{
|
||||
"requests": counts.Requests,
|
||||
"total_successes": counts.TotalSuccesses,
|
||||
"total_failures": counts.TotalFailures,
|
||||
"consecutive_successes": counts.ConsecutiveSuccesses,
|
||||
"consecutive_failures": counts.ConsecutiveFailures,
|
||||
}
|
||||
status["config"] = map[string]interface{}{
|
||||
status["config"] = map[string]any{
|
||||
"max_failures": cfg.CircuitBreaker.MaxFailures,
|
||||
"failure_ratio": cfg.CircuitBreaker.FailureRatio,
|
||||
"timeout": cfg.CircuitBreaker.Timeout,
|
||||
@@ -277,13 +286,13 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to get aggregated cache metrics, falling back to local stats",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
// Fall through to local stats on error
|
||||
} else {
|
||||
// Build aggregated cache stats from combined stats
|
||||
response := map[string]interface{}{
|
||||
response := map[string]any{
|
||||
"cluster_mode": true,
|
||||
"total_instances": metrics.TotalInstances,
|
||||
}
|
||||
@@ -321,7 +330,7 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Local instance stats (fallback or non-cluster mode)
|
||||
stats := map[string]interface{}{
|
||||
stats := map[string]any{
|
||||
"cluster_mode": false,
|
||||
"enabled": false,
|
||||
}
|
||||
@@ -376,7 +385,7 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getConnectionStats(c *fiber.Ctx) error {
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
|
||||
stats := map[string]interface{}{
|
||||
stats := map[string]any{
|
||||
"available": false,
|
||||
}
|
||||
|
||||
@@ -393,7 +402,7 @@ func (ad *AdminDashboard) getRetryBudgetStats(c *fiber.Ctx) error {
|
||||
rb := GetRetryBudget()
|
||||
|
||||
if rb == nil {
|
||||
return c.JSON(map[string]interface{}{
|
||||
return c.JSON(map[string]any{
|
||||
"enabled": false,
|
||||
})
|
||||
}
|
||||
@@ -406,7 +415,7 @@ func (ad *AdminDashboard) getCoalescingStats(c *fiber.Ctx) error {
|
||||
rc := GetRequestCoalescer()
|
||||
|
||||
if rc == nil {
|
||||
return c.JSON(map[string]interface{}{
|
||||
return c.JSON(map[string]any{
|
||||
"enabled": false,
|
||||
})
|
||||
}
|
||||
@@ -419,7 +428,7 @@ func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
|
||||
wsp := GetWebSocketProxy()
|
||||
|
||||
if wsp == nil {
|
||||
return c.JSON(map[string]interface{}{
|
||||
return c.JSON(map[string]any{
|
||||
"enabled": false,
|
||||
})
|
||||
}
|
||||
@@ -429,8 +438,8 @@ func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
|
||||
|
||||
// clearCache clears the cache
|
||||
func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
|
||||
// TODO: Implement cache clearing
|
||||
return c.JSON(map[string]interface{}{
|
||||
libpack_cache.CacheClear()
|
||||
return c.JSON(map[string]any{
|
||||
"success": true,
|
||||
"message": "Cache cleared successfully",
|
||||
})
|
||||
@@ -443,7 +452,7 @@ func (ad *AdminDashboard) resetRetryBudget(c *fiber.Ctx) error {
|
||||
rb.Reset()
|
||||
}
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
return c.JSON(map[string]any{
|
||||
"success": true,
|
||||
"message": "Retry budget statistics reset",
|
||||
})
|
||||
@@ -456,7 +465,7 @@ func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error {
|
||||
rc.Reset()
|
||||
}
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
return c.JSON(map[string]any{
|
||||
"success": true,
|
||||
"message": "Coalescing statistics reset",
|
||||
})
|
||||
@@ -466,7 +475,7 @@ func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getClusterStats(c *fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
if aggregator == nil {
|
||||
return c.Status(503).JSON(map[string]interface{}{
|
||||
return c.Status(503).JSON(map[string]any{
|
||||
"error": "Cluster mode not available",
|
||||
"message": "Redis-based metrics aggregation is not enabled",
|
||||
"cluster_mode": false,
|
||||
@@ -478,17 +487,17 @@ func (ad *AdminDashboard) getClusterStats(c *fiber.Ctx) error {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to get aggregated metrics",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
return c.Status(500).JSON(map[string]interface{}{
|
||||
return c.Status(500).JSON(map[string]any{
|
||||
"error": "Failed to retrieve cluster metrics",
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Format response similar to regular stats endpoint
|
||||
response := map[string]interface{}{
|
||||
response := map[string]any{
|
||||
"cluster_mode": true,
|
||||
"total_instances": metrics.TotalInstances,
|
||||
"healthy_instances": metrics.HealthyInstances,
|
||||
@@ -503,7 +512,7 @@ func (ad *AdminDashboard) getClusterStats(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getClusterInstances(c *fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
if aggregator == nil {
|
||||
return c.Status(503).JSON(map[string]interface{}{
|
||||
return c.Status(503).JSON(map[string]any{
|
||||
"error": "Cluster mode not available",
|
||||
"message": "Redis-based metrics aggregation is not enabled",
|
||||
"cluster_mode": false,
|
||||
@@ -515,16 +524,16 @@ func (ad *AdminDashboard) getClusterInstances(c *fiber.Ctx) error {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to get instance metrics",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
return c.Status(500).JSON(map[string]interface{}{
|
||||
return c.Status(500).JSON(map[string]any{
|
||||
"error": "Failed to retrieve instance metrics",
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
return c.JSON(map[string]any{
|
||||
"cluster_mode": true,
|
||||
"total_instances": metrics.TotalInstances,
|
||||
"healthy_instances": metrics.HealthyInstances,
|
||||
@@ -537,7 +546,7 @@ func (ad *AdminDashboard) getClusterInstances(c *fiber.Ctx) error {
|
||||
func (ad *AdminDashboard) getClusterDebug(c *fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
|
||||
debug := map[string]interface{}{
|
||||
debug := map[string]any{
|
||||
"aggregator_initialized": aggregator != nil,
|
||||
"redis_cache_enabled": false,
|
||||
}
|
||||
@@ -562,7 +571,7 @@ func (ad *AdminDashboard) getClusterDebug(c *fiber.Ctx) error {
|
||||
// Show first instance structure as example
|
||||
if len(metrics.Instances) > 0 {
|
||||
first := metrics.Instances[0]
|
||||
debug["sample_instance"] = map[string]interface{}{
|
||||
debug["sample_instance"] = map[string]any{
|
||||
"instance_id": first.InstanceID,
|
||||
"hostname": first.Hostname,
|
||||
"uptime_seconds": first.UptimeSeconds,
|
||||
@@ -573,7 +582,7 @@ func (ad *AdminDashboard) getClusterDebug(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Show requests structure if it exists
|
||||
if requests, ok := first.Stats["requests"].(map[string]interface{}); ok {
|
||||
if requests, ok := first.Stats["requests"].(map[string]any); ok {
|
||||
debug["sample_requests"] = requests
|
||||
}
|
||||
}
|
||||
@@ -584,7 +593,7 @@ func (ad *AdminDashboard) getClusterDebug(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Helper to get map keys
|
||||
func getMapKeys(m map[string]interface{}) []string {
|
||||
func getMapKeys(m map[string]any) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
@@ -596,7 +605,7 @@ func getMapKeys(m map[string]interface{}) []string {
|
||||
func (ad *AdminDashboard) forcePublish(c *fiber.Ctx) error {
|
||||
aggregator := GetMetricsAggregator()
|
||||
if aggregator == nil {
|
||||
return c.Status(503).JSON(map[string]interface{}{
|
||||
return c.Status(503).JSON(map[string]any{
|
||||
"error": "Aggregator not initialized",
|
||||
"success": false,
|
||||
})
|
||||
@@ -605,7 +614,7 @@ func (ad *AdminDashboard) forcePublish(c *fiber.Ctx) error {
|
||||
// Trigger publish in goroutine to avoid blocking
|
||||
go aggregator.publishMetrics()
|
||||
|
||||
return c.JSON(map[string]interface{}{
|
||||
return c.JSON(map[string]any{
|
||||
"success": true,
|
||||
"triggered": true,
|
||||
"message": "Publish triggered in background",
|
||||
@@ -634,7 +643,7 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "WebSocket client connected to stats stream",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"remote_addr": c.RemoteAddr().String(),
|
||||
},
|
||||
})
|
||||
@@ -645,18 +654,18 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "WebSocket client disconnected from stats stream",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"remote_addr": c.RemoteAddr().String(),
|
||||
},
|
||||
})
|
||||
}
|
||||
c.Close()
|
||||
_ = c.Close() // Best-effort cleanup
|
||||
}()
|
||||
|
||||
// Set up ping/pong handlers
|
||||
c.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
_ = c.SetReadDeadline(time.Now().Add(WebSocketReadDeadline))
|
||||
c.SetPongHandler(func(string) error {
|
||||
c.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
_ = c.SetReadDeadline(time.Now().Add(WebSocketReadDeadline))
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -674,14 +683,14 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Stream statistics every 2 seconds
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
// Stream statistics at configured interval
|
||||
ticker := time.NewTicker(StatsStreamInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Send initial stats immediately (cluster-aware for dashboard)
|
||||
if stats := ad.gatherAllStatsClusterAware(); stats != nil {
|
||||
if data, err := json.Marshal(stats); err == nil {
|
||||
c.WriteMessage(websocket.TextMessage, data)
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +707,7 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to marshal stats for WebSocket",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
return
|
||||
@@ -709,7 +718,7 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
||||
if ad.logger != nil {
|
||||
ad.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Failed to write to WebSocket (client likely disconnected)",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
return
|
||||
@@ -724,34 +733,34 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
||||
|
||||
// gatherAllStats collects all statistics into a single structure
|
||||
// This always returns LOCAL stats for this instance (used by metrics aggregator)
|
||||
func (ad *AdminDashboard) gatherAllStats() map[string]interface{} {
|
||||
func (ad *AdminDashboard) gatherAllStats() map[string]any {
|
||||
return ad.gatherAllStatsWithMode(false)
|
||||
}
|
||||
|
||||
// gatherAllStatsClusterAware collects statistics with cluster awareness
|
||||
// If cluster mode is available, returns aggregated stats from all instances
|
||||
func (ad *AdminDashboard) gatherAllStatsClusterAware() map[string]interface{} {
|
||||
func (ad *AdminDashboard) gatherAllStatsClusterAware() map[string]any {
|
||||
return ad.gatherAllStatsWithMode(true)
|
||||
}
|
||||
|
||||
// gatherAllStatsWithMode collects statistics with optional cluster mode
|
||||
func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string]interface{} {
|
||||
func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string]any {
|
||||
// Check if cluster mode is requested and available
|
||||
if useClusterMode {
|
||||
if aggregator := GetMetricsAggregator(); aggregator != nil {
|
||||
metrics, err := aggregator.GetAggregatedMetrics()
|
||||
if err == nil && metrics != nil {
|
||||
// Return aggregated cluster stats
|
||||
result := map[string]interface{}{
|
||||
result := map[string]any{
|
||||
"cluster_mode": true,
|
||||
"total_instances": metrics.TotalInstances,
|
||||
"healthy_instances": metrics.HealthyInstances,
|
||||
}
|
||||
|
||||
// Build stats section from combined stats
|
||||
stats := map[string]interface{}{
|
||||
stats := map[string]any{
|
||||
"timestamp": metrics.LastUpdate.Format(time.RFC3339),
|
||||
"version": "0.27.0",
|
||||
"version": libpack_config.PKG_VERSION,
|
||||
}
|
||||
|
||||
// Copy all combined stats
|
||||
@@ -771,16 +780,16 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
}
|
||||
|
||||
// Fall back to local stats
|
||||
result := make(map[string]interface{})
|
||||
result := make(map[string]any)
|
||||
result["cluster_mode"] = false
|
||||
|
||||
// Main stats
|
||||
uptimeSeconds := time.Since(startTime).Seconds()
|
||||
stats := map[string]interface{}{
|
||||
stats := map[string]any{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"uptime_seconds": uptimeSeconds,
|
||||
"uptime_human": formatDuration(time.Since(startTime)),
|
||||
"version": "0.27.0",
|
||||
"version": libpack_config.PKG_VERSION,
|
||||
}
|
||||
|
||||
if cfg != nil && cfg.Monitoring != nil {
|
||||
@@ -789,7 +798,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
skipped := getAdminMetricValue("requests_skipped")
|
||||
total := succeeded + failed + skipped
|
||||
|
||||
requestStats := map[string]interface{}{
|
||||
requestStats := map[string]any{
|
||||
"total": total,
|
||||
"succeeded": succeeded,
|
||||
"failed": failed,
|
||||
@@ -828,7 +837,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
if totalCacheRequests > 0 {
|
||||
hitRate = float64(cacheStats.CacheHits) / float64(totalCacheRequests) * 100
|
||||
}
|
||||
stats["cache_summary"] = map[string]interface{}{
|
||||
stats["cache_summary"] = map[string]any{
|
||||
"hits": cacheStats.CacheHits,
|
||||
"misses": cacheStats.CacheMisses,
|
||||
"hit_rate_pct": hitRate,
|
||||
@@ -841,16 +850,16 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
|
||||
// Health
|
||||
healthMgr := GetBackendHealthManager()
|
||||
health := map[string]interface{}{
|
||||
health := map[string]any{
|
||||
"status": "unknown",
|
||||
"backend": map[string]interface{}{
|
||||
"backend": map[string]any{
|
||||
"healthy": false,
|
||||
},
|
||||
}
|
||||
|
||||
if healthMgr != nil {
|
||||
isHealthy := healthMgr.IsHealthy()
|
||||
health["backend"] = map[string]interface{}{
|
||||
health["backend"] = map[string]any{
|
||||
"healthy": isHealthy,
|
||||
"consecutive_failures": healthMgr.GetConsecutiveFailures(),
|
||||
"last_check": healthMgr.GetLastHealthCheck().Format(time.RFC3339),
|
||||
@@ -865,7 +874,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
result["health"] = health
|
||||
|
||||
// Circuit breaker
|
||||
cbStatus := map[string]interface{}{
|
||||
cbStatus := map[string]any{
|
||||
"enabled": false,
|
||||
"state": "unknown",
|
||||
}
|
||||
@@ -880,14 +889,14 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
cbMutex.RUnlock()
|
||||
|
||||
cbStatus["state"] = state.String()
|
||||
cbStatus["counts"] = map[string]interface{}{
|
||||
cbStatus["counts"] = map[string]any{
|
||||
"requests": counts.Requests,
|
||||
"total_successes": counts.TotalSuccesses,
|
||||
"total_failures": counts.TotalFailures,
|
||||
"consecutive_successes": counts.ConsecutiveSuccesses,
|
||||
"consecutive_failures": counts.ConsecutiveFailures,
|
||||
}
|
||||
cbStatus["config"] = map[string]interface{}{
|
||||
cbStatus["config"] = map[string]any{
|
||||
"max_failures": cfg.CircuitBreaker.MaxFailures,
|
||||
"failure_ratio": cfg.CircuitBreaker.FailureRatio,
|
||||
"timeout": cfg.CircuitBreaker.Timeout,
|
||||
@@ -899,7 +908,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
result["circuit_breaker"] = cbStatus
|
||||
|
||||
// Cache stats
|
||||
cacheStats := map[string]interface{}{
|
||||
cacheStats := map[string]any{
|
||||
"enabled": false,
|
||||
}
|
||||
|
||||
@@ -947,7 +956,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
|
||||
// Connection stats
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
connStats := map[string]interface{}{
|
||||
connStats := map[string]any{
|
||||
"available": false,
|
||||
}
|
||||
|
||||
@@ -960,7 +969,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
// Retry budget
|
||||
rb := GetRetryBudget()
|
||||
if rb == nil {
|
||||
result["retry_budget"] = map[string]interface{}{"enabled": false}
|
||||
result["retry_budget"] = map[string]any{"enabled": false}
|
||||
} else {
|
||||
result["retry_budget"] = rb.GetStats()
|
||||
}
|
||||
@@ -968,7 +977,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
// Coalescing
|
||||
rc := GetRequestCoalescer()
|
||||
if rc == nil {
|
||||
result["coalescing"] = map[string]interface{}{"enabled": false}
|
||||
result["coalescing"] = map[string]any{"enabled": false}
|
||||
} else {
|
||||
result["coalescing"] = rc.GetStats()
|
||||
}
|
||||
@@ -976,7 +985,7 @@ func (ad *AdminDashboard) gatherAllStatsWithMode(useClusterMode bool) map[string
|
||||
// WebSocket
|
||||
wsp := GetWebSocketProxy()
|
||||
if wsp == nil {
|
||||
result["websocket"] = map[string]interface{}{"enabled": false}
|
||||
result["websocket"] = map[string]any{"enabled": false}
|
||||
} else {
|
||||
result["websocket"] = wsp.GetStats()
|
||||
}
|
||||
|
||||
+17
-15
@@ -103,7 +103,7 @@ func TestAdminDashboard_GetStats(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var stats map[string]interface{}
|
||||
var stats map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &stats)
|
||||
assert.NoError(t, err)
|
||||
@@ -116,7 +116,7 @@ func TestAdminDashboard_GetStats(t *testing.T) {
|
||||
assert.NotNil(t, stats["requests"])
|
||||
|
||||
// Verify request stats structure
|
||||
requests := stats["requests"].(map[string]interface{})
|
||||
requests := stats["requests"].(map[string]any)
|
||||
assert.NotNil(t, requests["total"])
|
||||
assert.NotNil(t, requests["succeeded"])
|
||||
assert.NotNil(t, requests["failed"])
|
||||
@@ -139,7 +139,7 @@ func TestAdminDashboard_GetHealth(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var health map[string]interface{}
|
||||
var health map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &health)
|
||||
assert.NoError(t, err)
|
||||
@@ -188,7 +188,7 @@ func TestAdminDashboard_GetCircuitBreakerStatus(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var status map[string]interface{}
|
||||
var status map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &status)
|
||||
assert.NoError(t, err)
|
||||
@@ -214,6 +214,7 @@ func TestAdminDashboard_GetCacheStats(t *testing.T) {
|
||||
CacheRedisEnable bool
|
||||
CacheMaxMemorySize int
|
||||
CacheMaxEntries int
|
||||
CacheUseLRU bool
|
||||
GraphQLQueryCacheSize int
|
||||
PerUserCacheDisabled bool
|
||||
}{
|
||||
@@ -221,6 +222,7 @@ func TestAdminDashboard_GetCacheStats(t *testing.T) {
|
||||
CacheTTL: 60,
|
||||
CacheMaxMemorySize: 100,
|
||||
CacheMaxEntries: 10000,
|
||||
CacheUseLRU: false,
|
||||
PerUserCacheDisabled: false,
|
||||
},
|
||||
}
|
||||
@@ -234,7 +236,7 @@ func TestAdminDashboard_GetCacheStats(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var stats map[string]interface{}
|
||||
var stats map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &stats)
|
||||
assert.NoError(t, err)
|
||||
@@ -258,7 +260,7 @@ func TestAdminDashboard_GetConnectionStats(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var stats map[string]interface{}
|
||||
var stats map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &stats)
|
||||
assert.NoError(t, err)
|
||||
@@ -281,7 +283,7 @@ func TestAdminDashboard_GetRetryBudgetStats(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var stats map[string]interface{}
|
||||
var stats map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &stats)
|
||||
assert.NoError(t, err)
|
||||
@@ -304,7 +306,7 @@ func TestAdminDashboard_GetCoalescingStats(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var stats map[string]interface{}
|
||||
var stats map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &stats)
|
||||
assert.NoError(t, err)
|
||||
@@ -327,7 +329,7 @@ func TestAdminDashboard_GetWebSocketStats(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var stats map[string]interface{}
|
||||
var stats map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &stats)
|
||||
assert.NoError(t, err)
|
||||
@@ -350,7 +352,7 @@ func TestAdminDashboard_ClearCache(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var result map[string]interface{}
|
||||
var result map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &result)
|
||||
assert.NoError(t, err)
|
||||
@@ -381,7 +383,7 @@ func TestAdminDashboard_ResetRetryBudget(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var result map[string]interface{}
|
||||
var result map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &result)
|
||||
assert.NoError(t, err)
|
||||
@@ -408,7 +410,7 @@ func TestAdminDashboard_ResetCoalescing(t *testing.T) {
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// Parse response
|
||||
var result map[string]interface{}
|
||||
var result map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(body, &result)
|
||||
assert.NoError(t, err)
|
||||
@@ -473,7 +475,7 @@ func TestAdminDashboard_IntegrationWithFeatures(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var rbStats map[string]interface{}
|
||||
var rbStats map[string]any
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
json.Unmarshal(body, &rbStats)
|
||||
assert.Equal(t, true, rbStats["enabled"])
|
||||
@@ -484,7 +486,7 @@ func TestAdminDashboard_IntegrationWithFeatures(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var coalStats map[string]interface{}
|
||||
var coalStats map[string]any
|
||||
body, _ = io.ReadAll(resp.Body)
|
||||
json.Unmarshal(body, &coalStats)
|
||||
assert.Equal(t, true, coalStats["enabled"])
|
||||
@@ -495,7 +497,7 @@ func TestAdminDashboard_IntegrationWithFeatures(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var wsStats map[string]interface{}
|
||||
var wsStats map[string]any
|
||||
body, _ = io.ReadAll(resp.Body)
|
||||
json.Unmarshal(body, &wsStats)
|
||||
assert.Equal(t, true, wsStats["enabled"])
|
||||
|
||||
@@ -37,7 +37,7 @@ func authMiddleware(c *fiber.Ctx) error {
|
||||
if expectedKey == "" {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Admin API authentication disabled - endpoints protected by network segmentation",
|
||||
Pairs: map[string]interface{}{"endpoint": c.Path()},
|
||||
Pairs: map[string]any{"endpoint": c.Path()},
|
||||
})
|
||||
return c.Next()
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func authMiddleware(c *fiber.Ctx) error {
|
||||
if subtle.ConstantTimeCompare([]byte(apiKey), []byte(expectedKey)) != 1 {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Unauthorized API access attempt",
|
||||
Pairs: map[string]interface{}{"endpoint": c.Path(), "ip": c.IP()},
|
||||
Pairs: map[string]any{"endpoint": c.Path(), "ip": c.IP()},
|
||||
})
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Unauthorized",
|
||||
@@ -61,6 +61,23 @@ func enableApi(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SECURITY WARNING: Check if API authentication is configured
|
||||
adminAPIKey := os.Getenv("GMP_ADMIN_API_KEY")
|
||||
if adminAPIKey == "" {
|
||||
adminAPIKey = os.Getenv("ADMIN_API_KEY")
|
||||
}
|
||||
if adminAPIKey == "" {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "⚠️ Admin API enabled WITHOUT authentication - all endpoints are publicly accessible!",
|
||||
Pairs: map[string]any{
|
||||
"security_risk": "HIGH - Admin API endpoints can be accessed without credentials",
|
||||
"affected_ops": "user-ban, user-unban, cache-clear, circuit-breaker controls",
|
||||
"recommendation": "Set GMP_ADMIN_API_KEY environment variable or use network segmentation",
|
||||
"api_port": cfg.Server.ApiPort,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
apiserver := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
||||
@@ -115,7 +132,7 @@ func periodicallyReloadBannedUsers(ctx context.Context) {
|
||||
loadBannedUsers()
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Banned users reloaded",
|
||||
Pairs: map[string]interface{}{"users": bannedUsersIDs},
|
||||
Pairs: map[string]any{"users": bannedUsersIDs},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -128,18 +145,18 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Checking if user is banned",
|
||||
Pairs: map[string]interface{}{"user_id": userID, "banned": found},
|
||||
Pairs: map[string]any{"user_id": userID, "banned": found},
|
||||
})
|
||||
|
||||
if found {
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "User is banned",
|
||||
Pairs: map[string]interface{}{"user_id": userID},
|
||||
Pairs: map[string]any{"user_id": userID},
|
||||
})
|
||||
if err := c.Status(fiber.StatusForbidden).SendString("User is banned"); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to send banned user response",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -225,7 +242,7 @@ func apiBanUser(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't parse the ban user request",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Invalid request payload")
|
||||
}
|
||||
@@ -240,7 +257,7 @@ func apiBanUser(c *fiber.Ctx) error {
|
||||
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Banned user",
|
||||
Pairs: map[string]interface{}{"user_id": req.UserID, "reason": req.Reason},
|
||||
Pairs: map[string]any{"user_id": req.UserID, "reason": req.Reason},
|
||||
})
|
||||
|
||||
if err := storeBannedUsers(); err != nil {
|
||||
@@ -255,7 +272,7 @@ func apiUnbanUser(c *fiber.Ctx) error {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't parse the unban user request",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Invalid request payload")
|
||||
}
|
||||
@@ -270,7 +287,7 @@ func apiUnbanUser(c *fiber.Ctx) error {
|
||||
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Unbanned user",
|
||||
Pairs: map[string]interface{}{"user_id": req.UserID},
|
||||
Pairs: map[string]any{"user_id": req.UserID},
|
||||
})
|
||||
|
||||
if err := storeBannedUsers(); err != nil {
|
||||
@@ -289,7 +306,7 @@ func storeBannedUsers() error {
|
||||
if err := fileLock.Unlock(); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to unlock file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
}()
|
||||
@@ -301,7 +318,7 @@ func storeBannedUsers() error {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't marshal banned users",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -309,7 +326,7 @@ func storeBannedUsers() error {
|
||||
if err := os.WriteFile(cfg.Api.BannedUsersFile, data, 0o644); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't write banned users to file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -321,12 +338,12 @@ 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},
|
||||
Pairs: map[string]any{"file": cfg.Api.BannedUsersFile},
|
||||
})
|
||||
if err := os.WriteFile(cfg.Api.BannedUsersFile, []byte("{}"), 0o644); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't create and write to the file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -336,7 +353,7 @@ func loadBannedUsers() {
|
||||
if err := lockFileRead(fileLock); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't lock the file [load]",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -344,7 +361,7 @@ func loadBannedUsers() {
|
||||
if err := fileLock.Unlock(); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to unlock file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
}()
|
||||
@@ -353,7 +370,7 @@ func loadBannedUsers() {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't read banned users from file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -362,7 +379,7 @@ func loadBannedUsers() {
|
||||
if err := json.Unmarshal(data, &newBannedUsers); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't unmarshal banned users",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -388,7 +405,7 @@ func lockFile(fileLock *flock.Flock) error {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't lock the file",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -396,7 +413,7 @@ func lockFile(fileLock *flock.Flock) error {
|
||||
case <-ctx.Done():
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "File lock timeout",
|
||||
Pairs: map[string]interface{}{"timeout": "30s"},
|
||||
Pairs: map[string]any{"timeout": "30s"},
|
||||
})
|
||||
return fmt.Errorf("file lock timeout after 30 seconds")
|
||||
}
|
||||
@@ -418,7 +435,7 @@ func lockFileRead(fileLock *flock.Flock) error {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't lock the file for reading",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -426,7 +443,7 @@ func lockFileRead(fileLock *flock.Flock) error {
|
||||
case <-ctx.Done():
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "File read lock timeout",
|
||||
Pairs: map[string]interface{}{"timeout": "30s"},
|
||||
Pairs: map[string]any{"timeout": "30s"},
|
||||
})
|
||||
return fmt.Errorf("file read lock timeout after 30 seconds")
|
||||
}
|
||||
|
||||
+15
-15
@@ -87,7 +87,7 @@ func (suite *APIAuthSecurityTestSuite) TestOptionalAuthentication() {
|
||||
os.Unsetenv("ADMIN_API_KEY")
|
||||
|
||||
tests := []struct {
|
||||
body map[string]interface{}
|
||||
body map[string]any
|
||||
name string
|
||||
endpoint string
|
||||
method string
|
||||
@@ -131,7 +131,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
os.Setenv("GMP_ADMIN_API_KEY", suite.validAPIKey)
|
||||
defer os.Unsetenv("GMP_ADMIN_API_KEY")
|
||||
tests := []struct {
|
||||
body map[string]interface{}
|
||||
body map[string]any
|
||||
name string
|
||||
apiKey string
|
||||
endpoint string
|
||||
@@ -144,7 +144,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: "",
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject requests without API key",
|
||||
},
|
||||
@@ -153,7 +153,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: "wrong-key",
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject requests with invalid API key",
|
||||
},
|
||||
@@ -162,7 +162,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: "' OR '1'='1",
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject SQL injection attempts in API key",
|
||||
},
|
||||
@@ -171,7 +171,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: "<script>alert('xss')</script>",
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject XSS attempts in API key",
|
||||
},
|
||||
@@ -180,7 +180,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: "key; rm -rf /",
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject command injection attempts in API key",
|
||||
},
|
||||
@@ -189,7 +189,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: suite.validAPIKey,
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 200,
|
||||
description: "Should accept valid API key for user-ban endpoint",
|
||||
},
|
||||
@@ -198,7 +198,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: suite.validAPIKey,
|
||||
endpoint: "/api/user-unban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test unban"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test unban"},
|
||||
expectedStatus: 200,
|
||||
description: "Should accept valid API key for user-unban endpoint",
|
||||
},
|
||||
@@ -225,7 +225,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: strings.ToUpper(suite.validAPIKey),
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject case-modified API key (case sensitive)",
|
||||
},
|
||||
@@ -234,7 +234,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: suite.validAPIKey + "extra",
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject API key with extra characters",
|
||||
},
|
||||
@@ -243,7 +243,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: suite.validAPIKey[5:],
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject partial API key",
|
||||
},
|
||||
@@ -262,7 +262,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
apiKey: suite.validAPIKey + "тест",
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test reason"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test reason"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject API key with unicode characters",
|
||||
},
|
||||
@@ -298,7 +298,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthentication() {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
var response map[string]interface{}
|
||||
var response map[string]any
|
||||
err = json.Unmarshal(body, &response)
|
||||
suite.NoError(err)
|
||||
|
||||
@@ -559,7 +559,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthenticationErrorMessages() {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
var response map[string]interface{}
|
||||
var response map[string]any
|
||||
err = json.Unmarshal(body, &response)
|
||||
suite.NoError(err)
|
||||
|
||||
|
||||
+8
-11
@@ -56,7 +56,7 @@ func (bhm *BackendHealthManager) WaitForBackendReady(timeout time.Duration) erro
|
||||
|
||||
bhm.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Waiting for GraphQL backend to become ready",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"backend_url": bhm.backendURL,
|
||||
"timeout": timeout.String(),
|
||||
},
|
||||
@@ -70,7 +70,7 @@ func (bhm *BackendHealthManager) WaitForBackendReady(timeout time.Duration) erro
|
||||
bhm.mu.Unlock()
|
||||
bhm.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "GraphQL backend is ready",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"retry_count": retryCount,
|
||||
"time_taken": time.Since(deadline.Add(-timeout)).String(),
|
||||
},
|
||||
@@ -83,7 +83,7 @@ func (bhm *BackendHealthManager) WaitForBackendReady(timeout time.Duration) erro
|
||||
if retryCount%5 == 0 {
|
||||
bhm.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Still waiting for GraphQL backend",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"retry_count": retryCount,
|
||||
"time_remaining": time.Until(deadline).String(),
|
||||
},
|
||||
@@ -185,7 +185,7 @@ func (bhm *BackendHealthManager) checkBackendHealth() bool {
|
||||
if err != nil {
|
||||
bhm.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Backend health check failed",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"check_url": healthCheckURL,
|
||||
},
|
||||
@@ -199,7 +199,7 @@ func (bhm *BackendHealthManager) checkBackendHealth() bool {
|
||||
if !isHealthy {
|
||||
bhm.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Backend returned unhealthy status",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"status_code": statusCode,
|
||||
"check_url": healthCheckURL,
|
||||
},
|
||||
@@ -226,14 +226,11 @@ func (bhm *BackendHealthManager) updateHealthStatus(isHealthy bool) {
|
||||
if !previouslyHealthy {
|
||||
bhm.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "GraphQL backend recovered",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"consecutive_failures": bhm.consecutiveFails.Load(),
|
||||
},
|
||||
})
|
||||
// Trigger circuit breaker reset if needed
|
||||
if cfg != nil && cfg.CircuitBreaker.Enable && cb != nil {
|
||||
// The circuit breaker will automatically reset based on its timeout
|
||||
}
|
||||
// Note: Circuit breaker resets automatically based on its configured timeout
|
||||
}
|
||||
bhm.consecutiveFails.Store(0)
|
||||
} else {
|
||||
@@ -241,7 +238,7 @@ func (bhm *BackendHealthManager) updateHealthStatus(isHealthy bool) {
|
||||
if previouslyHealthy {
|
||||
bhm.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "GraphQL backend became unhealthy",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"consecutive_failures": fails,
|
||||
},
|
||||
})
|
||||
|
||||
Vendored
+39
-35
@@ -1,3 +1,6 @@
|
||||
// Package libpack_cache provides a unified caching interface that supports
|
||||
// both in-memory and Redis backends. It handles response caching for GraphQL
|
||||
// queries with automatic compression and TTL management.
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
@@ -27,6 +30,7 @@ type CacheConfig struct {
|
||||
Memory struct {
|
||||
MaxMemorySize int64 `json:"max_memory_size"` // Maximum memory size in bytes
|
||||
MaxEntries int64 `json:"max_entries"` // Maximum number of entries
|
||||
UseLRU bool `json:"use_lru"` // Use LRU eviction algorithm instead of random eviction
|
||||
}
|
||||
TTL int `json:"ttl"`
|
||||
IncludeUserContext bool `json:"include_user_context"` // Include user ID and role in cache key
|
||||
@@ -96,16 +100,6 @@ func CalculateHash(c *fiber.Ctx, userID string, userRole string) string {
|
||||
return strutil.Md5(cacheKeyData)
|
||||
}
|
||||
|
||||
// CalculateHashLegacy generates a cache hash using only the request body (DEPRECATED).
|
||||
// This function exists for backward compatibility only and should NOT be used
|
||||
// in production multi-user applications as it creates a security vulnerability
|
||||
// where users can see each other's cached data.
|
||||
//
|
||||
// Deprecated: Use CalculateHash with user context instead.
|
||||
func CalculateHashLegacy(c *fiber.Ctx) string {
|
||||
return strutil.Md5(c.Body())
|
||||
}
|
||||
|
||||
func EnableCache(cfg *CacheConfig) {
|
||||
if cfg.Logger == nil {
|
||||
cfg.Logger = libpack_logger.New()
|
||||
@@ -126,7 +120,7 @@ func EnableCache(cfg *CacheConfig) {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to create Redis client",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
// Fall back to memory cache
|
||||
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
|
||||
@@ -134,34 +128,41 @@ func EnableCache(cfg *CacheConfig) {
|
||||
cfg.Client = libpack_cache_redis.NewCacheWrapper(redisClient, cfg.Logger)
|
||||
}
|
||||
} else {
|
||||
// Calculate memory and entry limits
|
||||
maxMemory := cfg.Memory.MaxMemorySize
|
||||
if maxMemory <= 0 {
|
||||
maxMemory = libpack_cache_memory.DefaultMaxMemorySize
|
||||
}
|
||||
|
||||
maxEntries := cfg.Memory.MaxEntries
|
||||
if maxEntries <= 0 {
|
||||
maxEntries = libpack_cache_memory.DefaultMaxCacheSize
|
||||
}
|
||||
|
||||
cacheType := "standard"
|
||||
if cfg.Memory.UseLRU {
|
||||
cacheType = "LRU"
|
||||
}
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Using in-memory cache",
|
||||
Pairs: map[string]interface{}{
|
||||
"max_memory_size_bytes": cfg.Memory.MaxMemorySize,
|
||||
"max_entries": cfg.Memory.MaxEntries,
|
||||
Pairs: map[string]any{
|
||||
"type": cacheType,
|
||||
"max_memory_size_bytes": maxMemory,
|
||||
"max_entries": maxEntries,
|
||||
},
|
||||
})
|
||||
|
||||
// Use memory size and entry limits if configured, otherwise use defaults
|
||||
if cfg.Memory.MaxMemorySize > 0 || cfg.Memory.MaxEntries > 0 {
|
||||
maxMemory := cfg.Memory.MaxMemorySize
|
||||
if maxMemory <= 0 {
|
||||
maxMemory = libpack_cache_memory.DefaultMaxMemorySize
|
||||
}
|
||||
|
||||
maxEntries := cfg.Memory.MaxEntries
|
||||
if maxEntries <= 0 {
|
||||
maxEntries = libpack_cache_memory.DefaultMaxCacheSize
|
||||
}
|
||||
|
||||
if cfg.Memory.UseLRU {
|
||||
// Use LRU cache with proper eviction algorithm
|
||||
cfg.Client = libpack_cache_memory.NewLRUMemoryCache(maxMemory, maxEntries)
|
||||
} else {
|
||||
// Use standard sync.Map-based cache
|
||||
cfg.Client = libpack_cache_memory.NewWithSize(
|
||||
time.Duration(cfg.TTL)*time.Second,
|
||||
maxMemory,
|
||||
maxEntries,
|
||||
)
|
||||
} else {
|
||||
// Backward compatibility
|
||||
cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second)
|
||||
}
|
||||
}
|
||||
config = cfg
|
||||
@@ -181,7 +182,7 @@ func CacheLookup(hash string) []byte {
|
||||
if err != nil {
|
||||
config.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to create gzip reader for cached data",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "hash": hash},
|
||||
Pairs: map[string]any{"error": err.Error(), "hash": hash},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -190,7 +191,7 @@ func CacheLookup(hash string) []byte {
|
||||
if closeErr := reader.Close(); closeErr != nil {
|
||||
config.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to close gzip reader",
|
||||
Pairs: map[string]interface{}{"error": closeErr.Error(), "hash": hash},
|
||||
Pairs: map[string]any{"error": closeErr.Error(), "hash": hash},
|
||||
})
|
||||
}
|
||||
}()
|
||||
@@ -199,7 +200,7 @@ func CacheLookup(hash string) []byte {
|
||||
if err != nil {
|
||||
config.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to decompress cached data",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "hash": hash},
|
||||
Pairs: map[string]any{"error": err.Error(), "hash": hash},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -217,7 +218,7 @@ func CacheDelete(hash string) {
|
||||
}
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Deleting data from cache",
|
||||
Pairs: map[string]interface{}{"hash": hash},
|
||||
Pairs: map[string]any{"hash": hash},
|
||||
})
|
||||
// Use atomic operations with validation to prevent inconsistent statistics
|
||||
for {
|
||||
@@ -242,7 +243,7 @@ func CacheStore(hash string, data []byte) {
|
||||
}
|
||||
config.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Storing data in cache",
|
||||
Pairs: map[string]interface{}{"hash": hash},
|
||||
Pairs: map[string]any{"hash": hash},
|
||||
})
|
||||
atomic.AddInt64(&cacheStats.CachedQueries, 1)
|
||||
config.Client.Set(hash, data, time.Duration(config.TTL)*time.Second)
|
||||
@@ -254,7 +255,7 @@ 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},
|
||||
Pairs: map[string]any{"hash": hash, "ttl": ttl},
|
||||
})
|
||||
atomic.AddInt64(&cacheStats.CachedQueries, 1)
|
||||
config.Client.Set(hash, data, ttl)
|
||||
@@ -271,6 +272,9 @@ func CacheGetQueries() int64 {
|
||||
}
|
||||
|
||||
func CacheClear() {
|
||||
if !IsCacheInitialized() {
|
||||
return
|
||||
}
|
||||
config.Client.Clear()
|
||||
cacheStats = &CacheStats{}
|
||||
}
|
||||
|
||||
Vendored
+9
-4
@@ -38,12 +38,12 @@ func NewLRUMemoryCache(maxMemorySize, maxEntries int64) *LRUMemoryCache {
|
||||
entries: make(map[string]*lruEntry),
|
||||
evictList: list.New(),
|
||||
gzipWriterPool: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return gzip.NewWriter(nil)
|
||||
},
|
||||
},
|
||||
gzipReaderPool: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return &gzip.Reader{}
|
||||
},
|
||||
},
|
||||
@@ -257,11 +257,11 @@ func (c *LRUMemoryCache) decompress(data []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// GetStats returns cache statistics
|
||||
func (c *LRUMemoryCache) GetStats() map[string]interface{} {
|
||||
func (c *LRUMemoryCache) GetStats() map[string]any {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"entries": atomic.LoadInt64(&c.currentCount),
|
||||
"memory_bytes": atomic.LoadInt64(&c.currentMemory),
|
||||
"max_entries": c.maxEntries,
|
||||
@@ -279,3 +279,8 @@ func (c *LRUMemoryCache) GetMemoryUsage() int64 {
|
||||
func (c *LRUMemoryCache) GetMaxMemorySize() int64 {
|
||||
return c.maxMemorySize
|
||||
}
|
||||
|
||||
// CountQueries returns the number of entries in the cache
|
||||
func (c *LRUMemoryCache) CountQueries() int64 {
|
||||
return atomic.LoadInt64(&c.currentCount)
|
||||
}
|
||||
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package libpack_cache_memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LRUMemoryCacheTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestLRUMemoryCacheTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(LRUMemoryCacheTestSuite))
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestNewLRUMemoryCache() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100) // 1MB, 100 entries
|
||||
suite.NotNil(cache)
|
||||
suite.Equal(int64(0), cache.CountQueries())
|
||||
suite.Equal(int64(0), cache.GetMemoryUsage())
|
||||
suite.Equal(int64(1024*1024), cache.GetMaxMemorySize())
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestSetAndGet() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
// Set a value
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
|
||||
// Get the value
|
||||
val, found := cache.Get("key1")
|
||||
suite.True(found)
|
||||
suite.Equal([]byte("value1"), val)
|
||||
|
||||
// Get non-existent key
|
||||
val, found = cache.Get("nonexistent")
|
||||
suite.False(found)
|
||||
suite.Nil(val)
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestUpdateExisting() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
cache.Set("key1", []byte("value2"), 5*time.Second)
|
||||
|
||||
val, found := cache.Get("key1")
|
||||
suite.True(found)
|
||||
suite.Equal([]byte("value2"), val)
|
||||
suite.Equal(int64(1), cache.CountQueries())
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestDelete() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
suite.Equal(int64(1), cache.CountQueries())
|
||||
|
||||
cache.Delete("key1")
|
||||
suite.Equal(int64(0), cache.CountQueries())
|
||||
|
||||
val, found := cache.Get("key1")
|
||||
suite.False(found)
|
||||
suite.Nil(val)
|
||||
|
||||
// Delete non-existent key should not panic
|
||||
cache.Delete("nonexistent")
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestClear() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||
suite.Equal(int64(3), cache.CountQueries())
|
||||
|
||||
cache.Clear()
|
||||
suite.Equal(int64(0), cache.CountQueries())
|
||||
suite.Equal(int64(0), cache.GetMemoryUsage())
|
||||
|
||||
_, found := cache.Get("key1")
|
||||
suite.False(found)
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestExpiration() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
cache.Set("key1", []byte("value1"), 100*time.Millisecond)
|
||||
|
||||
// Should exist immediately
|
||||
val, found := cache.Get("key1")
|
||||
suite.True(found)
|
||||
suite.Equal([]byte("value1"), val)
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// Should be expired
|
||||
val, found = cache.Get("key1")
|
||||
suite.False(found)
|
||||
suite.Nil(val)
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestEvictionByCount() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 3) // Max 3 entries
|
||||
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||
|
||||
// All 3 should exist
|
||||
_, found := cache.Get("key1")
|
||||
suite.True(found)
|
||||
_, found = cache.Get("key2")
|
||||
suite.True(found)
|
||||
_, found = cache.Get("key3")
|
||||
suite.True(found)
|
||||
|
||||
// Add 4th entry - should evict oldest (key1)
|
||||
cache.Set("key4", []byte("value4"), 5*time.Second)
|
||||
|
||||
suite.Equal(int64(3), cache.CountQueries())
|
||||
|
||||
// key1 should be evicted (it was least recently used)
|
||||
_, found = cache.Get("key1")
|
||||
suite.False(found)
|
||||
|
||||
// Others should still exist
|
||||
_, found = cache.Get("key2")
|
||||
suite.True(found)
|
||||
_, found = cache.Get("key3")
|
||||
suite.True(found)
|
||||
_, found = cache.Get("key4")
|
||||
suite.True(found)
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestLRUOrder() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 3) // Max 3 entries
|
||||
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||
|
||||
// Access key1 to make it recently used
|
||||
cache.Get("key1")
|
||||
|
||||
// Add key4 - should evict key2 (now least recently used)
|
||||
cache.Set("key4", []byte("value4"), 5*time.Second)
|
||||
|
||||
// key2 should be evicted
|
||||
_, found := cache.Get("key2")
|
||||
suite.False(found)
|
||||
|
||||
// key1 should still exist (was accessed recently)
|
||||
_, found = cache.Get("key1")
|
||||
suite.True(found)
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestEvictionByMemory() {
|
||||
// Small memory limit - 500 bytes
|
||||
cache := NewLRUMemoryCache(500, 100)
|
||||
|
||||
// Each entry has ~64 bytes overhead + key + value
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||
cache.Set("key3", []byte("value3"), 5*time.Second)
|
||||
|
||||
// Add large entry that should trigger eviction
|
||||
largeValue := make([]byte, 200)
|
||||
cache.Set("large", largeValue, 5*time.Second)
|
||||
|
||||
// Memory should be under limit
|
||||
suite.LessOrEqual(cache.GetMemoryUsage(), int64(500))
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestCompression() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
// Create a compressible value (> 1KB to trigger compression)
|
||||
largeValue := make([]byte, 2048)
|
||||
for i := range largeValue {
|
||||
largeValue[i] = 'A' // Highly compressible
|
||||
}
|
||||
|
||||
cache.Set("compressed", largeValue, 5*time.Second)
|
||||
|
||||
// Should be able to retrieve it correctly
|
||||
val, found := cache.Get("compressed")
|
||||
suite.True(found)
|
||||
suite.Equal(largeValue, val)
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestGetStats() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||
|
||||
stats := cache.GetStats()
|
||||
suite.Equal(int64(2), stats["entries"])
|
||||
suite.Equal(int64(1024*1024), stats["max_memory"])
|
||||
suite.Equal(int64(100), stats["max_entries"])
|
||||
suite.NotNil(stats["memory_bytes"])
|
||||
suite.NotNil(stats["fill_percent"])
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestConcurrentAccess() {
|
||||
cache := NewLRUMemoryCache(10*1024*1024, 1000)
|
||||
const numGoroutines = 50
|
||||
const numOperations = 500
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines * 3) // readers, writers, deleters
|
||||
|
||||
// Writers
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numOperations; j++ {
|
||||
key := fmt.Sprintf("key-%d-%d", id, j)
|
||||
value := []byte(fmt.Sprintf("value-%d-%d", id, j))
|
||||
cache.Set(key, value, 5*time.Second)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Readers
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numOperations; j++ {
|
||||
key := fmt.Sprintf("key-%d-%d", id, j)
|
||||
cache.Get(key)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Deleters
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < numOperations; j++ {
|
||||
key := fmt.Sprintf("key-%d-%d", id, j%100)
|
||||
cache.Delete(key)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestCleanExpiredEntries() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
cache.Set("expire1", []byte("value1"), 50*time.Millisecond)
|
||||
cache.Set("expire2", []byte("value2"), 50*time.Millisecond)
|
||||
cache.Set("keep", []byte("value3"), 5*time.Second)
|
||||
|
||||
suite.Equal(int64(3), cache.CountQueries())
|
||||
|
||||
// Wait for some to expire
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Clean expired entries
|
||||
cache.CleanExpiredEntries()
|
||||
|
||||
// Only "keep" should remain
|
||||
suite.Equal(int64(1), cache.CountQueries())
|
||||
|
||||
_, found := cache.Get("keep")
|
||||
suite.True(found)
|
||||
}
|
||||
|
||||
func (suite *LRUMemoryCacheTestSuite) TestCountQueries() {
|
||||
cache := NewLRUMemoryCache(1024*1024, 100)
|
||||
|
||||
suite.Equal(int64(0), cache.CountQueries())
|
||||
|
||||
cache.Set("key1", []byte("value1"), 5*time.Second)
|
||||
suite.Equal(int64(1), cache.CountQueries())
|
||||
|
||||
cache.Set("key2", []byte("value2"), 5*time.Second)
|
||||
suite.Equal(int64(2), cache.CountQueries())
|
||||
|
||||
cache.Delete("key1")
|
||||
suite.Equal(int64(1), cache.CountQueries())
|
||||
|
||||
cache.Clear()
|
||||
suite.Equal(int64(0), cache.CountQueries())
|
||||
}
|
||||
|
||||
// Benchmarks
|
||||
|
||||
func BenchmarkLRUMemoryCacheSet(b *testing.B) {
|
||||
cache := NewLRUMemoryCache(100*1024*1024, 100000)
|
||||
value := []byte("benchmark-value")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
cache.Set(key, value, 5*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUMemoryCacheGet(b *testing.B) {
|
||||
cache := NewLRUMemoryCache(100*1024*1024, 100000)
|
||||
value := []byte("benchmark-value")
|
||||
|
||||
// Pre-populate
|
||||
for i := 0; i < 10000; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
cache.Set(key, value, 5*time.Minute)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
key := fmt.Sprintf("key-%d", i%10000)
|
||||
cache.Get(key)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUMemoryCacheConcurrent(b *testing.B) {
|
||||
cache := NewLRUMemoryCache(100*1024*1024, 100000)
|
||||
value := []byte("benchmark-value")
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
if i%2 == 0 {
|
||||
cache.Set(key, value, 5*time.Second)
|
||||
} else {
|
||||
cache.Get(key)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
Vendored
+9
-6
@@ -1,3 +1,6 @@
|
||||
// Package libpack_cache_memory provides an in-memory LRU cache implementation
|
||||
// with automatic compression for large values, memory limits, and background
|
||||
// eviction of expired entries.
|
||||
package libpack_cache_memory
|
||||
|
||||
import (
|
||||
@@ -61,12 +64,12 @@ func NewWithSize(globalTTL time.Duration, maxMemorySize int64, maxCacheSize int6
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
compressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return gzip.NewWriter(nil)
|
||||
},
|
||||
},
|
||||
decompressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
r, _ := gzip.NewReader(bytes.NewReader([]byte{}))
|
||||
return r
|
||||
},
|
||||
@@ -204,7 +207,7 @@ func (c *Cache) Delete(key string) {
|
||||
}
|
||||
|
||||
func (c *Cache) Clear() {
|
||||
c.entries.Range(func(key, value interface{}) bool {
|
||||
c.entries.Range(func(key, value any) bool {
|
||||
c.entries.Delete(key)
|
||||
return true
|
||||
})
|
||||
@@ -255,7 +258,7 @@ func (c *Cache) decompress(data []byte) ([]byte, error) {
|
||||
|
||||
func (c *Cache) CleanExpiredEntries() {
|
||||
now := time.Now()
|
||||
c.entries.Range(func(key, value interface{}) bool {
|
||||
c.entries.Range(func(key, value any) bool {
|
||||
entry := value.(CacheEntry)
|
||||
if entry.ExpiresAt.Before(now) {
|
||||
if _, exists := c.entries.LoadAndDelete(key); exists {
|
||||
@@ -276,7 +279,7 @@ func (c *Cache) evictOldest(n int) {
|
||||
|
||||
// Collect all entries with their expiry times
|
||||
entries := make([]keyExpiry, 0, n*2)
|
||||
c.entries.Range(func(k, v interface{}) bool {
|
||||
c.entries.Range(func(k, v any) bool {
|
||||
key := k.(string)
|
||||
entry := v.(CacheEntry)
|
||||
entries = append(entries, keyExpiry{entry.ExpiresAt, key})
|
||||
@@ -316,7 +319,7 @@ func (c *Cache) evictToFreeMemory(bytesToFree int64) {
|
||||
|
||||
// Collect entries to consider for eviction
|
||||
entries := make([]keyMemorySize, 0, int(c.maxCacheSize/5))
|
||||
c.entries.Range(func(k, v interface{}) bool {
|
||||
c.entries.Range(func(k, v any) bool {
|
||||
key := k.(string)
|
||||
entry := v.(CacheEntry)
|
||||
entries = append(entries, keyMemorySize{entry.ExpiresAt, key, entry.MemorySize})
|
||||
|
||||
Vendored
+4
-1
@@ -1,3 +1,6 @@
|
||||
// Package libpack_cache_redis provides a Redis-backed cache implementation
|
||||
// for distributed caching across multiple proxy instances. Supports key
|
||||
// prefixing for multi-tenant isolation.
|
||||
package libpack_cache_redis
|
||||
|
||||
import (
|
||||
@@ -42,7 +45,7 @@ func New(redisClientConfig *RedisClientConfig) (*RedisConfig, error) {
|
||||
ctx: context.Background(),
|
||||
prefix: redisClientConfig.Prefix,
|
||||
builderPool: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return &strings.Builder{}
|
||||
},
|
||||
},
|
||||
|
||||
Vendored
+5
-5
@@ -29,7 +29,7 @@ func (w *CacheWrapper) Set(key string, value []byte, ttl time.Duration) {
|
||||
if err := w.redis.Set(key, value, ttl); err != nil {
|
||||
w.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Redis set error",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"key": key,
|
||||
},
|
||||
@@ -43,7 +43,7 @@ func (w *CacheWrapper) Get(key string) ([]byte, bool) {
|
||||
if err != nil {
|
||||
w.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Redis get error",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"key": key,
|
||||
},
|
||||
@@ -58,7 +58,7 @@ func (w *CacheWrapper) Delete(key string) {
|
||||
if err := w.redis.Delete(key); err != nil {
|
||||
w.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Redis delete error",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"key": key,
|
||||
},
|
||||
@@ -71,7 +71,7 @@ func (w *CacheWrapper) Clear() {
|
||||
if err := w.redis.Clear(); err != nil {
|
||||
w.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Redis clear error",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
@@ -84,7 +84,7 @@ func (w *CacheWrapper) CountQueries() int64 {
|
||||
if err != nil {
|
||||
w.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Redis count queries error",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerCacheFallback() {
|
||||
// Trip the circuit by generating failures
|
||||
testErr := errors.New("test error")
|
||||
for i := 0; i < cfg.CircuitBreaker.MaxFailures; i++ {
|
||||
_, err := cb.Execute(func() (interface{}, error) {
|
||||
_, err := cb.Execute(func() (any, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
assert.Error(suite.T(), err, "Execute should return error")
|
||||
@@ -108,7 +108,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerNoCacheFallback() {
|
||||
// Trip the circuit by generating failures
|
||||
testErr := errors.New("test error")
|
||||
for i := 0; i < cfg.CircuitBreaker.MaxFailures; i++ {
|
||||
_, err := cb.Execute(func() (interface{}, error) {
|
||||
_, err := cb.Execute(func() (any, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
assert.Error(suite.T(), err, "Execute should return error")
|
||||
@@ -168,7 +168,7 @@ func (suite *CircuitBreakerTestSuite) TestCacheDisabledFallback() {
|
||||
// Trip the circuit by generating failures
|
||||
testErr := errors.New("test error")
|
||||
for i := 0; i < cfg.CircuitBreaker.MaxFailures; i++ {
|
||||
_, err := cb.Execute(func() (interface{}, error) {
|
||||
_, err := cb.Execute(func() (any, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
assert.Error(suite.T(), err, "Execute should return error")
|
||||
|
||||
@@ -25,7 +25,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerStateTransitions() {
|
||||
// 2. Generate failures to trip the circuit
|
||||
testErr := errors.New("test error")
|
||||
for i := 0; i < cfg.CircuitBreaker.MaxFailures; i++ {
|
||||
_, err := cb.Execute(func() (interface{}, error) {
|
||||
_, err := cb.Execute(func() (any, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
assert.Error(suite.T(), err, "Execute should return error")
|
||||
@@ -35,7 +35,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerStateTransitions() {
|
||||
assert.Equal(suite.T(), gobreaker.StateOpen.String(), cb.State().String(), "Circuit should transition to open state after failures")
|
||||
|
||||
// Verify that requests are rejected during open state
|
||||
_, err := cb.Execute(func() (interface{}, error) {
|
||||
_, err := cb.Execute(func() (any, error) {
|
||||
return "success", nil
|
||||
})
|
||||
assert.Equal(suite.T(), gobreaker.ErrOpenState.Error(), err.Error(), "Should return ErrOpenState when circuit is open")
|
||||
@@ -55,7 +55,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerStateTransitions() {
|
||||
// (Sony's gobreaker transitions to half-open on the next request after timeout)
|
||||
tmpState := cb.State()
|
||||
// Execute a successful request to check state
|
||||
_, _ = cb.Execute(func() (interface{}, error) {
|
||||
_, _ = cb.Execute(func() (any, error) {
|
||||
return "success", nil
|
||||
})
|
||||
|
||||
@@ -73,7 +73,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerStateTransitions() {
|
||||
|
||||
// 6. Execute successful requests in half-open state to transition back to closed
|
||||
for i := 0; i < cfg.CircuitBreaker.MaxRequestsInHalfOpen; i++ {
|
||||
_, err = cb.Execute(func() (interface{}, error) {
|
||||
_, err = cb.Execute(func() (any, error) {
|
||||
return "success", nil
|
||||
})
|
||||
assert.NoError(suite.T(), err, "Execute should not return error")
|
||||
@@ -104,7 +104,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerHalfOpenToOpen() {
|
||||
// 1. Generate failures to trip the circuit
|
||||
testErr := errors.New("test error")
|
||||
for i := 0; i < cfg.CircuitBreaker.MaxFailures; i++ {
|
||||
_, err := cb.Execute(func() (interface{}, error) {
|
||||
_, err := cb.Execute(func() (any, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
assert.Error(suite.T(), err, "Execute should return error")
|
||||
@@ -119,7 +119,7 @@ func (suite *CircuitBreakerTestSuite) TestCircuitBreakerHalfOpenToOpen() {
|
||||
// The next request should transition the circuit to half-open
|
||||
tmpState := cb.State()
|
||||
// Try a request that will fail
|
||||
_, _ = cb.Execute(func() (interface{}, error) {
|
||||
_, _ = cb.Execute(func() (any, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ func (suite *CircuitBreakerTestSuite) TestExecuteFunctionBehavior() {
|
||||
|
||||
// Test with success
|
||||
result := "success"
|
||||
execResult, err := cb.Execute(func() (interface{}, error) {
|
||||
execResult, err := cb.Execute(func() (any, error) {
|
||||
return result, nil
|
||||
})
|
||||
|
||||
@@ -202,7 +202,7 @@ func (suite *CircuitBreakerTestSuite) TestExecuteFunctionBehavior() {
|
||||
|
||||
// Test with error
|
||||
testErr := errors.New("test error")
|
||||
_, err = cb.Execute(func() (interface{}, error) {
|
||||
_, err = cb.Execute(func() (any, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package libpack_config provides build-time configuration variables
|
||||
// for package name and version, which are set during the build process
|
||||
// using ldflags.
|
||||
package libpack_config
|
||||
|
||||
var (
|
||||
|
||||
+7
-7
@@ -118,7 +118,7 @@ func (cpm *ConnectionPoolManager) cleanIdleConnections() {
|
||||
if cpm.logger != nil {
|
||||
cpm.logger.Debug(&libpack_logging.LogMessage{
|
||||
Message: "Cleaned idle HTTP connections",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"active_connections": cpm.activeConnections.Load(),
|
||||
"total_connections": cpm.totalConnections.Load(),
|
||||
},
|
||||
@@ -172,7 +172,7 @@ func (cpm *ConnectionPoolManager) performKeepAlive() {
|
||||
if cpm.logger != nil {
|
||||
cpm.logger.Debug(&libpack_logging.LogMessage{
|
||||
Message: "Keep-alive request failed",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
@@ -202,7 +202,7 @@ func (cpm *ConnectionPoolManager) checkAndRecover() {
|
||||
if cpm.logger != nil {
|
||||
cpm.logger.Warning(&libpack_logging.LogMessage{
|
||||
Message: "Connection pool health degraded, attempting recovery",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"consecutive_failures": failures,
|
||||
},
|
||||
})
|
||||
@@ -246,8 +246,8 @@ func (cpm *ConnectionPoolManager) RecordConnectionFailure() {
|
||||
}
|
||||
|
||||
// GetConnectionStats returns current connection statistics
|
||||
func (cpm *ConnectionPoolManager) GetConnectionStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (cpm *ConnectionPoolManager) GetConnectionStats() map[string]any {
|
||||
return map[string]any{
|
||||
"active_connections": cpm.activeConnections.Load(),
|
||||
"total_connections": cpm.totalConnections.Load(),
|
||||
"connection_failures": cpm.connectionFailures.Load(),
|
||||
@@ -296,7 +296,7 @@ func InitializeConnectionPool(client *fasthttp.Client) {
|
||||
connectionPoolMutex.Lock()
|
||||
defer connectionPoolMutex.Unlock()
|
||||
if connectionPoolManager != nil {
|
||||
connectionPoolManager.Shutdown()
|
||||
_ = connectionPoolManager.Shutdown() // Best-effort cleanup
|
||||
}
|
||||
connectionPoolManager = NewConnectionPoolManager(client)
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func ShutdownConnectionPool() {
|
||||
connectionPoolMutex.Lock()
|
||||
defer connectionPoolMutex.Unlock()
|
||||
if connectionPoolManager != nil {
|
||||
connectionPoolManager.Shutdown()
|
||||
_ = connectionPoolManager.Shutdown() // Best-effort cleanup
|
||||
connectionPoolManager = nil
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -27,7 +27,7 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "=== DEBUG: Parsing GraphQL Query ===",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"query_length": len(query),
|
||||
"query_preview": truncateString(query, 100),
|
||||
},
|
||||
@@ -43,14 +43,14 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "DEBUG: Failed to parse query",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "DEBUG: Query parsed successfully",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"definitions_count": len(p.Definitions),
|
||||
},
|
||||
})
|
||||
@@ -72,7 +72,7 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: fmt.Sprintf("DEBUG: Definition #%d (OperationDefinition)", i),
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"operation_type": operationType,
|
||||
"operation_name": operationName,
|
||||
"selection_count": selectionCount,
|
||||
@@ -87,7 +87,7 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
if field, ok := sel.(*ast.Field); ok {
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: fmt.Sprintf("DEBUG: Mutation field #%d", j),
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"field_name": field.Name.Value,
|
||||
},
|
||||
})
|
||||
@@ -97,7 +97,7 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
} else if frag, ok := d.(*ast.FragmentDefinition); ok {
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: fmt.Sprintf("DEBUG: Definition #%d (FragmentDefinition)", i),
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"fragment_name": frag.Name.Value,
|
||||
},
|
||||
})
|
||||
@@ -109,7 +109,7 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "DEBUG: Final routing decision",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"operation_type": result.operationType,
|
||||
"operation_name": result.operationName,
|
||||
"active_endpoint": result.activeEndpoint,
|
||||
@@ -125,7 +125,7 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
if result.operationType == "mutation" && result.activeEndpoint != cfg.Server.HostGraphQL {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "DEBUG: ⚠️ BUG DETECTED: Mutation routed to wrong endpoint!",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"expected_endpoint": cfg.Server.HostGraphQL,
|
||||
"actual_endpoint": result.activeEndpoint,
|
||||
},
|
||||
@@ -135,7 +135,7 @@ func debugParseGraphQLQuery(c *fiber.Ctx, query string) {
|
||||
if result.operationType == "mutation" && strings.Contains(strings.ToLower(result.activeEndpoint), "read") {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "DEBUG: ⚠️ CRITICAL: Mutation endpoint contains 'read' in URL!",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"endpoint": result.activeEndpoint,
|
||||
},
|
||||
})
|
||||
|
||||
+10
-10
@@ -20,19 +20,19 @@ func extractClaimsFromJWTHeader(authorization string) (usr, role string) {
|
||||
|
||||
tokenParts := strings.SplitN(authorization, ".", 3)
|
||||
if len(tokenParts) != 3 {
|
||||
handleError("Can't split the token", map[string]interface{}{"token": maskToken(authorization)})
|
||||
handleError("Can't split the token", map[string]any{"token": maskToken(authorization)})
|
||||
return
|
||||
}
|
||||
|
||||
claim, err := base64.RawURLEncoding.DecodeString(tokenParts[1])
|
||||
if err != nil {
|
||||
handleError("Can't decode the token", map[string]interface{}{"token": maskToken(authorization)})
|
||||
handleError("Can't decode the token", map[string]any{"token": maskToken(authorization)})
|
||||
return
|
||||
}
|
||||
|
||||
var claimMap map[string]interface{}
|
||||
var claimMap map[string]any
|
||||
if err = json.Unmarshal(claim, &claimMap); err != nil {
|
||||
handleError("Can't unmarshal the claim", map[string]interface{}{"token": maskToken(authorization)})
|
||||
handleError("Can't unmarshal the claim", map[string]any{"token": maskToken(authorization)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,20 +42,20 @@ func extractClaimsFromJWTHeader(authorization string) (usr, role string) {
|
||||
return
|
||||
}
|
||||
|
||||
func extractClaim(claimMap map[string]interface{}, claimPath, name string) string {
|
||||
func extractClaim(claimMap map[string]any, claimPath, name string) string {
|
||||
if claimPath == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Validate claim path to prevent injection attacks
|
||||
if !isValidClaimPath(claimPath) {
|
||||
handleError(fmt.Sprintf("Invalid claim path for %s", name), map[string]interface{}{"path": claimPath})
|
||||
handleError(fmt.Sprintf("Invalid claim path for %s", name), map[string]any{"path": claimPath})
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
value, ok := ask.For(claimMap, claimPath).String(defaultValue)
|
||||
if !ok {
|
||||
handleError(fmt.Sprintf("Can't find the %s", name), map[string]interface{}{"claim_map": sanitizeClaimMap(claimMap), "path": claimPath})
|
||||
handleError(fmt.Sprintf("Can't find the %s", name), map[string]any{"claim_map": sanitizeClaimMap(claimMap), "path": claimPath})
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ func isValidClaimPath(path string) bool {
|
||||
}
|
||||
|
||||
// sanitizeClaimMap removes sensitive data from claim map for logging
|
||||
func sanitizeClaimMap(claimMap map[string]interface{}) map[string]interface{} {
|
||||
sanitized := make(map[string]interface{})
|
||||
func sanitizeClaimMap(claimMap map[string]any) map[string]any {
|
||||
sanitized := make(map[string]any)
|
||||
sensitiveKeys := map[string]bool{
|
||||
"password": true, "secret": true, "token": true, "key": true,
|
||||
"auth": true, "credential": true, "private": true,
|
||||
@@ -110,7 +110,7 @@ func sanitizeClaimMap(claimMap map[string]interface{}) map[string]interface{} {
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func handleError(msg string, details map[string]interface{}) {
|
||||
func handleError(msg string, details map[string]any) {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, emptyMetrics)
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: msg,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
graphql-monitoring-proxy.raczylo.com
|
||||
+713
@@ -0,0 +1,713 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GraphQL Monitoring Proxy - High-Performance GraphQL Gateway</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="High-performance GraphQL proxy with monitoring, caching, circuit breaker, rate limiting, and security features. Zero cost monitoring at 100k+ req/s."
|
||||
/>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class'
|
||||
}
|
||||
</script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body { font-family: "Inter", sans-serif; }
|
||||
code, pre { font-family: "JetBrains Mono", monospace; }
|
||||
.theme-transition {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
.animate-fade-in-up { animation: fadeInUp 0.6s ease-out; }
|
||||
.animate-float { animation: float 3s ease-in-out infinite; }
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.dark .glass {
|
||||
background: rgba(17, 24, 39, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #e879f9 0%, #818cf8 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.dark .gradient-text {
|
||||
background: linear-gradient(135deg, #f0abfc 0%, #a5b4fc 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); }
|
||||
.dark .shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.4); }
|
||||
html { scroll-behavior: smooth; }
|
||||
</style>
|
||||
<script>
|
||||
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 theme-transition">
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed w-full glass shadow-modern z-50 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<a href="#" class="flex items-center hover:opacity-80 transition-opacity duration-300 gap-2">
|
||||
<i class="fas fa-diagram-project text-2xl gradient-text"></i>
|
||||
<span class="text-xl font-bold gradient-text">graphql-monitoring-proxy</span>
|
||||
</a>
|
||||
<div class="hidden md:flex space-x-6">
|
||||
<a href="#features" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Features</a>
|
||||
<a href="#monitoring" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Monitoring</a>
|
||||
<a href="#speed" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Speed</a>
|
||||
<a href="#security" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Security</a>
|
||||
<a href="#resilience" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Resilience</a>
|
||||
<a href="#installation" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Install</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle theme">
|
||||
<i class="fas fa-moon dark:hidden text-xl"></i>
|
||||
<i class="fas fa-sun hidden dark:inline text-xl"></i>
|
||||
</button>
|
||||
<a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="View on GitHub">
|
||||
<i class="fab fa-github text-xl"></i>
|
||||
</a>
|
||||
<button id="mobile-menu-toggle" class="md:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle menu">
|
||||
<i class="fas fa-bars text-xl" id="menu-open-icon"></i>
|
||||
<i class="fas fa-times text-xl hidden" id="menu-close-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="px-4 py-3 space-y-1 bg-white dark:bg-gray-800">
|
||||
<a href="#features" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Features</a>
|
||||
<a href="#monitoring" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Monitoring</a>
|
||||
<a href="#speed" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Speed</a>
|
||||
<a href="#security" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Security</a>
|
||||
<a href="#resilience" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Resilience</a>
|
||||
<a href="#installation" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Install</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative pt-24 sm:pt-32 pb-12 sm:pb-20 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-fuchsia-50 via-violet-50 to-indigo-50 dark:from-gray-900 dark:via-fuchsia-900/20 dark:to-indigo-900/20 theme-transition"></div>
|
||||
<div class="absolute top-0 -left-4 w-72 h-72 bg-fuchsia-300 dark:bg-fuchsia-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float"></div>
|
||||
<div class="absolute top-0 -right-4 w-72 h-72 bg-violet-300 dark:bg-violet-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-indigo-300 dark:bg-indigo-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 2s;"></div>
|
||||
|
||||
<div class="relative max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center">
|
||||
<div class="mb-8 sm:mb-10 flex justify-center animate-fade-in-up">
|
||||
<div class="text-8xl sm:text-9xl animate-float">
|
||||
<i class="fas fa-diagram-project gradient-text"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-4 sm:mb-6 leading-tight animate-fade-in-up" style="animation-delay: 0.1s;">
|
||||
GraphQL Monitoring<br /><span class="gradient-text">Proxy</span>
|
||||
</h1>
|
||||
<p class="text-base sm:text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8 sm:mb-10 max-w-3xl mx-auto leading-relaxed px-4 animate-fade-in-up" style="animation-delay: 0.2s;">
|
||||
Enterprise-grade GraphQL gateway with Prometheus metrics, smart caching, circuit breaker, rate limiting, request coalescing, WebSocket subscriptions, and comprehensive security - all at zero cost.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center mb-8 sm:mb-12 px-4 animate-fade-in-up" style="animation-delay: 0.3s;">
|
||||
<a href="#installation" class="group relative bg-gradient-to-r from-fuchsia-500 to-indigo-600 hover:from-fuchsia-600 hover:to-indigo-700 text-white px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center shadow-lg hover:shadow-xl hover:scale-105">
|
||||
<span class="relative z-10">Get Started</span>
|
||||
</a>
|
||||
<a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy" class="group glass hover:shadow-lg text-gray-900 dark:text-gray-100 px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center hover:scale-105">
|
||||
<i class="fab fa-github mr-2"></i>View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-2 sm:gap-4 text-sm px-4">
|
||||
<img src="https://img.shields.io/github/v/release/lukaszraczylo/graphql-monitoring-proxy" alt="Version" class="h-5" />
|
||||
<img src="https://img.shields.io/github/license/lukaszraczylo/graphql-monitoring-proxy" alt="License" class="h-5" />
|
||||
<img src="https://goreportcard.com/badge/github.com/lukaszraczylo/graphql-monitoring-proxy" alt="Go Report" class="h-5" />
|
||||
</div>
|
||||
<div class="mt-12 sm:mt-16 max-w-3xl mx-auto px-4 animate-fade-in-up" style="animation-delay: 0.4s;">
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-fuchsia-500 to-indigo-600 rounded-xl blur opacity-25 group-hover:opacity-50 transition duration-500"></div>
|
||||
<div class="relative bg-gray-900 rounded-xl p-6 text-left">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
<span class="ml-2 text-gray-400 text-sm">terminal</span>
|
||||
</div>
|
||||
<pre class="text-gray-100 text-sm sm:text-base overflow-x-auto"><code><span class="text-gray-400"># Run with Docker</span>
|
||||
<span class="text-fuchsia-400">$</span> docker run -p 8080:8080 -p 9393:9393 \
|
||||
-e GMP_HOST_GRAPHQL=http://your-graphql:4000/ \
|
||||
-e GMP_ENABLE_GLOBAL_CACHE=true \
|
||||
-e GMP_ENABLE_CIRCUIT_BREAKER=true \
|
||||
ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance Stats -->
|
||||
<section class="py-12 sm:py-16 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="grid sm:grid-cols-4 gap-4 text-center">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<div class="text-4xl font-bold gradient-text mb-2">100k+</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Requests/second</p>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<div class="text-4xl font-bold gradient-text mb-2">10MB</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">RAM usage</p>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<div class="text-4xl font-bold gradient-text mb-2">0.1%</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">CPU usage</p>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<div class="text-4xl font-bold gradient-text mb-2">$0</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Cost</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 text-center">
|
||||
<a href="bench/" class="inline-flex items-center text-fuchsia-600 dark:text-fuchsia-400 hover:underline font-medium">
|
||||
View benchmarks
|
||||
<i class="fas fa-arrow-right ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Overview -->
|
||||
<section id="features" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Feature Overview</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Everything you need for production GraphQL</p>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-fuchsia-500 to-fuchsia-600 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-chart-line text-white"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-2">Monitoring</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Prometheus metrics, OpenTelemetry tracing, admin dashboard</p>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-violet-500 to-violet-600 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-bolt text-white"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-2">Speed</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Smart caching, request coalescing, read-only replicas</p>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-shield-halved text-white"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-2">Security</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Rate limiting, introspection blocking, user banning</p>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-rose-500 to-rose-600 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-heart-pulse text-white"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-2">Resilience</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Circuit breaker, retry budget, connection recovery</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Monitoring Section -->
|
||||
<section id="monitoring" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">
|
||||
<i class="fas fa-chart-line gradient-text mr-3"></i>Monitoring
|
||||
</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Complete observability for your GraphQL API</p>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-fire mr-2 text-orange-500"></i>
|
||||
Prometheus Metrics
|
||||
</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Query execution timing with histograms</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>User ID extraction from JWT tokens</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Operation name and type tracking</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Cache hit/miss ratios</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Success/failure/skipped counters</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Configurable metrics purging</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-satellite-dish mr-2 text-blue-500"></i>
|
||||
OpenTelemetry Tracing
|
||||
</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Distributed tracing support</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Configurable OTLP collector endpoint</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Trace context propagation via headers</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Child span creation for each request</li>
|
||||
</ul>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg mt-4 text-xs overflow-x-auto"><code>GMP_ENABLE_TRACE=true
|
||||
GMP_TRACE_ENDPOINT=localhost:4317</code></pre>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl md:col-span-2">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-desktop mr-2 text-pink-500"></i>
|
||||
Real-Time Admin Dashboard
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Web-based UI at <code class="text-fuchsia-600 dark:text-fuchsia-400">/admin</code> with auto-refresh every 5 seconds:</p>
|
||||
<div class="grid sm:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">System Health</h4>
|
||||
<ul class="space-y-1 text-gray-600 dark:text-gray-400">
|
||||
<li>Backend GraphQL status</li>
|
||||
<li>Redis connectivity</li>
|
||||
<li>Response times</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">Live Statistics</h4>
|
||||
<ul class="space-y-1 text-gray-600 dark:text-gray-400">
|
||||
<li>Request coalescing rate</li>
|
||||
<li>Retry budget tokens</li>
|
||||
<li>Active WebSocket connections</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">Controls</h4>
|
||||
<ul class="space-y-1 text-gray-600 dark:text-gray-400">
|
||||
<li>Circuit breaker state</li>
|
||||
<li>Cache statistics</li>
|
||||
<li>Reset/clear actions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Speed Section -->
|
||||
<section id="speed" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">
|
||||
<i class="fas fa-bolt gradient-text mr-3"></i>Speed
|
||||
</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Maximize throughput, minimize latency</p>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-layer-group mr-2 text-amber-500"></i>
|
||||
Request Coalescing
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Deduplicate concurrent identical queries - only one request hits the backend, response is shared with all waiting clients.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Reduces backend load 50-80%</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Prevents thundering herd on cache expiry</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Zero latency for primary request</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Enabled by default</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-database mr-2 text-violet-500"></i>
|
||||
Smart Caching
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Memory-aware caching with per-user isolation, compression, and flexible TTL control.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>In-memory with LRU eviction</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Distributed Redis cache support</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Per-query TTL via <code>@cached(ttl: 90)</code></li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Force refresh via <code>@cached(refresh: true)</code></li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Automatic gzip compression</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Per-user cache isolation (security)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-plug mr-2 text-emerald-500"></i>
|
||||
WebSocket Subscriptions
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Native GraphQL subscription support with bidirectional proxying.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Automatic ping/pong keep-alive</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Configurable message size limits</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Connection statistics in dashboard</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Graceful connection handling</li>
|
||||
</ul>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg mt-4 text-xs overflow-x-auto"><code>GMP_WEBSOCKET_ENABLE=true
|
||||
GMP_WEBSOCKET_PING_INTERVAL=30</code></pre>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-code-branch mr-2 text-cyan-500"></i>
|
||||
Read-Only Replica Support
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Route queries to read replicas, mutations to primary for maximum throughput.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Automatic query/mutation routing</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Scales read capacity horizontally</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Works with Hasura read replicas</li>
|
||||
</ul>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg mt-4 text-xs overflow-x-auto"><code>GMP_HOST_GRAPHQL=http://primary:8080/
|
||||
GMP_HOST_GRAPHQL_READONLY=http://replica:8080/</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Security Section -->
|
||||
<section id="security" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">
|
||||
<i class="fas fa-shield-halved gradient-text mr-3"></i>Security
|
||||
</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Protect your GraphQL API from abuse</p>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-gauge-high mr-2 text-rose-500"></i>
|
||||
Role-Based Rate Limiting
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Different rate limits per user role with burst control and dynamic config reload.</p>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg text-xs overflow-x-auto"><code>{
|
||||
"ratelimit": {
|
||||
"admin": { "req": 1000, "interval": "second", "burst": 2000 },
|
||||
"premium": { "req": 500, "interval": "second" },
|
||||
"guest": { "req": 10, "interval": "second" },
|
||||
"-": { "req": 5, "interval": "second" }
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-eye-slash mr-2 text-indigo-500"></i>
|
||||
Introspection Blocking
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Block schema introspection to prevent API discovery attacks, with configurable allowlists.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Blocks __schema, __type, etc.</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Deep nested query inspection</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Allowlist specific introspections</li>
|
||||
</ul>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg mt-4 text-xs overflow-x-auto"><code>GMP_BLOCK_SCHEMA_INTROSPECTION=true
|
||||
GMP_ALLOWED_INTROSPECTION="__typename"</code></pre>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-ban mr-2 text-red-500"></i>
|
||||
User Ban/Unban API
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Block misbehaving users detected by your monitoring system.</p>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg text-xs overflow-x-auto"><code>curl -X POST http://localhost:9090/api/user-ban \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"user_id": "1337", "reason": "Scraping"}'</code></pre>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-lock mr-2 text-amber-500"></i>
|
||||
Additional Security
|
||||
</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i><strong>Read-only mode:</strong> Block all mutations</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i><strong>URL allowlist:</strong> Restrict accessible endpoints</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i><strong>JWT claim extraction:</strong> User ID and role from tokens</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i><strong>API authentication:</strong> Optional X-API-Key for admin endpoints</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i><strong>Log sanitization:</strong> Automatic redaction of sensitive data</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i><strong>SQL injection prevention:</strong> Parameterized queries</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Resilience Section -->
|
||||
<section id="resilience" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">
|
||||
<i class="fas fa-heart-pulse gradient-text mr-3"></i>Resilience
|
||||
</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Handle failures gracefully</p>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-toggle-off mr-2 text-rose-500"></i>
|
||||
Circuit Breaker
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Prevent cascading failures with automatic detection and recovery.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Trip on consecutive failures or ratio</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Automatic recovery after timeout</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Serve cached responses when open</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Configurable for timeouts, 5XX, 4XX</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Exponential backoff support</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Health endpoint: <code>/api/circuit-breaker/health</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-coins mr-2 text-amber-500"></i>
|
||||
Retry Budget
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Prevent retry storms with token bucket rate limiting.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Token bucket algorithm</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Configurable refill rate</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Prevents overwhelming recovering backends</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Enabled by default</li>
|
||||
</ul>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg mt-4 text-xs overflow-x-auto"><code>GMP_RETRY_BUDGET_ENABLE=true
|
||||
GMP_RETRY_BUDGET_TOKENS_PER_SEC=10
|
||||
GMP_RETRY_BUDGET_MAX_TOKENS=100</code></pre>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-rotate mr-2 text-cyan-500"></i>
|
||||
Connection Recovery
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Automatic connection pool management and backend health monitoring.</p>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Backend startup readiness probe</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Keep-alive with health checks</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Automatic pool reset on failures</li>
|
||||
<li class="flex items-start gap-2"><i class="fas fa-check text-green-500 mt-1"></i>Intelligent retry with backoff</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-triangle-exclamation mr-2 text-orange-500"></i>
|
||||
Graceful Degradation
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Informative error responses with retry recommendations.</p>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg text-xs overflow-x-auto"><code>{
|
||||
"errors": [{
|
||||
"message": "Backend temporarily unavailable",
|
||||
"extensions": {
|
||||
"code": "SERVICE_UNAVAILABLE",
|
||||
"retryable": true,
|
||||
"retry_after": 60
|
||||
}
|
||||
}]
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Maintenance Section -->
|
||||
<section class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">
|
||||
<i class="fas fa-wrench gradient-text mr-3"></i>Maintenance
|
||||
</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Built-in tools for Hasura users</p>
|
||||
</div>
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
|
||||
<i class="fas fa-broom mr-2 text-emerald-500"></i>
|
||||
Hasura Event Cleaner
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Automatically clean up old event logs to prevent database bloat. Runs hourly.</p>
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2 text-sm">Tables Cleaned</h4>
|
||||
<ul class="space-y-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
<li><code>hdb_catalog.event_invocation_logs</code></li>
|
||||
<li><code>hdb_catalog.event_log</code></li>
|
||||
<li><code>hdb_catalog.hdb_action_log</code></li>
|
||||
<li><code>hdb_catalog.hdb_cron_event_invocation_logs</code></li>
|
||||
<li><code>hdb_catalog.hdb_scheduled_event_invocation_logs</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2 text-sm">Configuration</h4>
|
||||
<pre class="bg-gray-900 text-gray-100 p-3 rounded-lg text-xs overflow-x-auto"><code>GMP_HASURA_EVENT_CLEANER=true
|
||||
GMP_HASURA_EVENT_CLEANER_OLDER_THAN=14
|
||||
GMP_HASURA_EVENT_METADATA_DB=postgres://...</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Installation Section -->
|
||||
<section id="installation" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Installation</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Deploy in seconds</p>
|
||||
</div>
|
||||
<div class="max-w-3xl mx-auto space-y-6">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center">
|
||||
<i class="fab fa-docker mr-2 text-blue-500"></i>
|
||||
Docker
|
||||
</h3>
|
||||
<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto"><code>docker pull ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest</code></pre>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center">
|
||||
<i class="fas fa-download mr-2 text-fuchsia-500"></i>
|
||||
Binary Download
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-3">Download from the <a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy/releases/latest" class="text-fuchsia-600 dark:text-fuchsia-400 hover:underline">releases page</a>.</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Supported: Darwin ARM64/AMD64, Linux ARM64/AMD64, Windows AMD64</p>
|
||||
</div>
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center">
|
||||
<i class="fas fa-dharmachakra mr-2 text-indigo-500"></i>
|
||||
Kubernetes
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-3">Example manifests available:</p>
|
||||
<ul class="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<li><a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy/blob/main/static/kubernetes-deployment.yaml" class="text-fuchsia-600 dark:text-fuchsia-400 hover:underline">Standalone deployment</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy/blob/main/static/kubernetes-single-deployment.yaml" class="text-fuchsia-600 dark:text-fuchsia-400 hover:underline">Combined deployment (proxy + Hasura)</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy/blob/main/static/kubernetes-single-deployment-with-ro.yaml" class="text-fuchsia-600 dark:text-fuchsia-400 hover:underline">Combined with read-only replica</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Endpoints Section -->
|
||||
<section class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Endpoints</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Available HTTP endpoints</p>
|
||||
</div>
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="glass p-6 rounded-xl">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<code class="text-fuchsia-600 dark:text-fuchsia-400 font-medium whitespace-nowrap">:8080/*</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">GraphQL passthrough endpoint</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<code class="text-fuchsia-600 dark:text-fuchsia-400 font-medium whitespace-nowrap">:8080/admin</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">Admin dashboard UI</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<code class="text-fuchsia-600 dark:text-fuchsia-400 font-medium whitespace-nowrap">:9393/metrics</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">Prometheus metrics</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<code class="text-fuchsia-600 dark:text-fuchsia-400 font-medium whitespace-nowrap">:8080/healthz</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">Health check (with optional backend verification)</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<code class="text-fuchsia-600 dark:text-fuchsia-400 font-medium whitespace-nowrap">:8080/livez</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">Liveness probe</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<code class="text-fuchsia-600 dark:text-fuchsia-400 font-medium whitespace-nowrap">:9090/api/*</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">Management API (user-ban, cache-clear, circuit-breaker)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="py-8 bg-gray-100 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-diagram-project text-xl gradient-text"></i>
|
||||
<span class="font-semibold gradient-text">graphql-monitoring-proxy</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<i class="fab fa-github text-xl"></i>
|
||||
</a>
|
||||
<a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy/issues" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 text-sm">
|
||||
Issues
|
||||
</a>
|
||||
<a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy/releases" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 text-sm">
|
||||
Releases
|
||||
</a>
|
||||
<a href="https://github.com/lukaszraczylo/graphql-monitoring-proxy#configuration" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 text-sm">
|
||||
Full Docs
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm">MIT License</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Theme toggle
|
||||
document.getElementById('theme-toggle').addEventListener('click', function() {
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
document.getElementById('mobile-menu-toggle').addEventListener('click', function() {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
const openIcon = document.getElementById('menu-open-icon');
|
||||
const closeIcon = document.getElementById('menu-close-icon');
|
||||
|
||||
menu.classList.toggle('hidden');
|
||||
openIcon.classList.toggle('hidden');
|
||||
closeIcon.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking a link
|
||||
document.querySelectorAll('#mobile-menu a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
document.getElementById('mobile-menu').classList.add('hidden');
|
||||
document.getElementById('menu-open-icon').classList.remove('hidden');
|
||||
document.getElementById('menu-close-icon').classList.add('hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -29,15 +29,15 @@ const (
|
||||
|
||||
// ProxyError represents a structured error response
|
||||
type ProxyError struct {
|
||||
Code string `json:"code"` // Machine-readable error code
|
||||
Message string `json:"message"` // Human-readable error message
|
||||
Details string `json:"details,omitempty"` // Additional error details
|
||||
Retryable bool `json:"retryable"` // Whether the request can be retried
|
||||
StatusCode int `json:"status_code"` // HTTP status code
|
||||
Timestamp time.Time `json:"timestamp"` // When the error occurred
|
||||
TraceID string `json:"trace_id,omitempty"` // Trace ID for correlation
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional context
|
||||
Cause error `json:"-"` // Original error (not serialized)
|
||||
Code string `json:"code"` // Machine-readable error code
|
||||
Message string `json:"message"` // Human-readable error message
|
||||
Details string `json:"details,omitempty"` // Additional error details
|
||||
Retryable bool `json:"retryable"` // Whether the request can be retried
|
||||
StatusCode int `json:"status_code"` // HTTP status code
|
||||
Timestamp time.Time `json:"timestamp"` // When the error occurred
|
||||
TraceID string `json:"trace_id,omitempty"` // Trace ID for correlation
|
||||
Metadata map[string]any `json:"metadata,omitempty"` // Additional context
|
||||
Cause error `json:"-"` // Original error (not serialized)
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
@@ -78,7 +78,7 @@ func NewProxyError(code, message string, statusCode int, retryable bool) *ProxyE
|
||||
StatusCode: statusCode,
|
||||
Retryable: retryable,
|
||||
Timestamp: time.Now(),
|
||||
Metadata: make(map[string]interface{}),
|
||||
Metadata: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,122 +101,13 @@ func (e *ProxyError) WithTraceID(traceID string) *ProxyError {
|
||||
}
|
||||
|
||||
// WithMetadata adds metadata
|
||||
func (e *ProxyError) WithMetadata(key string, value interface{}) *ProxyError {
|
||||
func (e *ProxyError) WithMetadata(key string, value any) *ProxyError {
|
||||
e.Metadata[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// Common error constructors
|
||||
|
||||
// NewConnectionError creates a connection-related error
|
||||
func NewConnectionError(err error) *ProxyError {
|
||||
code := ErrCodeConnectionRefused
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if contains(errStr, "reset") {
|
||||
code = ErrCodeConnectionReset
|
||||
}
|
||||
}
|
||||
|
||||
return NewProxyError(code, "Failed to connect to backend", 502, true).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
// NewTimeoutError creates a timeout error
|
||||
func NewTimeoutError(err error) *ProxyError {
|
||||
return NewProxyError(ErrCodeTimeout, "Request timed out", 504, false).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
// NewCircuitOpenError creates a circuit breaker open error
|
||||
func NewCircuitOpenError() *ProxyError {
|
||||
return NewProxyError(ErrCodeCircuitOpen, "Service temporarily unavailable due to circuit breaker", 503, false).
|
||||
WithDetails("The backend service is currently experiencing issues. Please try again later.")
|
||||
}
|
||||
|
||||
// NewRateLimitError creates a rate limit error
|
||||
func NewRateLimitError(userID, role string) *ProxyError {
|
||||
return NewProxyError(ErrCodeRateLimited, "Rate limit exceeded", 429, false).
|
||||
WithDetails("You have exceeded the rate limit for your role").
|
||||
WithMetadata("user_id", userID).
|
||||
WithMetadata("role", role)
|
||||
}
|
||||
|
||||
// NewBackendError creates a backend error from status code
|
||||
func NewBackendError(statusCode int, body string) *ProxyError {
|
||||
code := ErrCodeBackendError
|
||||
message := "Backend returned an error"
|
||||
retryable := false
|
||||
|
||||
switch {
|
||||
case statusCode == 429:
|
||||
code = ErrCodeRateLimited
|
||||
message = "Backend rate limit exceeded"
|
||||
retryable = true
|
||||
case statusCode == 503:
|
||||
code = ErrCodeServiceUnavailable
|
||||
message = "Backend service unavailable"
|
||||
retryable = true
|
||||
case statusCode == 502 || statusCode == 504:
|
||||
code = ErrCodeBadGateway
|
||||
message = "Bad gateway"
|
||||
retryable = true
|
||||
case statusCode >= 500:
|
||||
code = ErrCodeBackendError
|
||||
message = "Backend server error"
|
||||
retryable = true
|
||||
case statusCode == 404:
|
||||
code = ErrCodeNotFound
|
||||
message = "Resource not found"
|
||||
case statusCode == 403:
|
||||
code = ErrCodeForbidden
|
||||
message = "Access forbidden"
|
||||
case statusCode == 401:
|
||||
code = ErrCodeUnauthorized
|
||||
message = "Unauthorized"
|
||||
case statusCode >= 400:
|
||||
code = ErrCodeInvalidRequest
|
||||
message = "Invalid request"
|
||||
}
|
||||
|
||||
return NewProxyError(code, message, statusCode, retryable).
|
||||
WithMetadata("backend_status", statusCode).
|
||||
WithMetadata("backend_body", truncateString(body, 500))
|
||||
}
|
||||
|
||||
// NewInvalidResponseError creates an invalid response error
|
||||
func NewInvalidResponseError(details string) *ProxyError {
|
||||
return NewProxyError(ErrCodeInvalidResponse, "Backend returned invalid response", 502, false).
|
||||
WithDetails(details)
|
||||
}
|
||||
|
||||
// NewInternalError creates an internal error
|
||||
func NewInternalError(err error) *ProxyError {
|
||||
return NewProxyError(ErrCodeInternalError, "Internal proxy error", 500, false).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
// NewContextCanceledError creates a context canceled error
|
||||
func NewContextCanceledError() *ProxyError {
|
||||
return NewProxyError(ErrCodeContextCanceled, "Request canceled", 499, false).
|
||||
WithDetails("The request was canceled by the client")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) > 0 && len(substr) > 0 && len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
|
||||
}
|
||||
|
||||
func containsMiddle(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
|
||||
@@ -48,7 +48,7 @@ func enableHasuraEventCleaner(ctx context.Context) error {
|
||||
|
||||
logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Event cleaner enabled",
|
||||
Pairs: map[string]interface{}{"interval_in_days": clearOlderThan},
|
||||
Pairs: map[string]any{"interval_in_days": clearOlderThan},
|
||||
})
|
||||
|
||||
// Parse pool configuration
|
||||
@@ -67,7 +67,7 @@ func enableHasuraEventCleaner(ctx context.Context) error {
|
||||
if err != nil {
|
||||
logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to create connection pool",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func cleanEvents(ctx context.Context, pool *pgxpool.Pool, clearOlderThan int, lo
|
||||
} else {
|
||||
logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Successfully executed query",
|
||||
Pairs: map[string]interface{}{"query": query, "interval": interval},
|
||||
Pairs: map[string]any{"query": query, "interval": interval},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func cleanEvents(ctx context.Context, pool *pgxpool.Pool, clearOlderThan int, lo
|
||||
}
|
||||
logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to execute some queries",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"failed_queries": failedQueries,
|
||||
"errors": errMsgs,
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestEventsSecurityTestSuite(t *testing.T) {
|
||||
// TestEventCleanerSQLInjection tests various SQL injection attempts in the event cleaner
|
||||
func (suite *EventsSecurityTestSuite) TestEventCleanerSQLInjection() {
|
||||
tests := []struct {
|
||||
clearDays interface{}
|
||||
clearDays any
|
||||
name string
|
||||
description string
|
||||
expectError bool
|
||||
@@ -175,7 +175,7 @@ func (suite *EventsSecurityTestSuite) TestEventCleanerParameterizedQueries() {
|
||||
|
||||
// TestEventCleanerConcurrentSQLInjection tests SQL injection under concurrent conditions
|
||||
func (suite *EventsSecurityTestSuite) TestEventCleanerConcurrentSQLInjection() {
|
||||
maliciousInputs := []interface{}{
|
||||
maliciousInputs := []any{
|
||||
"1'; DROP TABLE events; --",
|
||||
"1 OR 1=1",
|
||||
"'; TRUNCATE events; --",
|
||||
@@ -185,7 +185,7 @@ func (suite *EventsSecurityTestSuite) TestEventCleanerConcurrentSQLInjection() {
|
||||
done := make(chan error, len(maliciousInputs))
|
||||
|
||||
for _, input := range maliciousInputs {
|
||||
go func(val interface{}) {
|
||||
go func(val any) {
|
||||
err := validateClearDaysInput(val)
|
||||
done <- err
|
||||
}(input)
|
||||
@@ -202,7 +202,7 @@ func (suite *EventsSecurityTestSuite) TestEventCleanerConcurrentSQLInjection() {
|
||||
// TestEventCleanerInputSanitization tests input sanitization effectiveness
|
||||
func (suite *EventsSecurityTestSuite) TestEventCleanerInputSanitization() {
|
||||
tests := []struct {
|
||||
input interface{}
|
||||
input any
|
||||
name string
|
||||
expected int
|
||||
hasError bool
|
||||
@@ -279,7 +279,7 @@ func (suite *EventsSecurityTestSuite) TestEventCleanerDatabaseInteraction() {
|
||||
// Helper functions that should be implemented in the main codebase
|
||||
|
||||
// validateClearDaysInput validates and sanitizes the clearDays input
|
||||
func validateClearDaysInput(input interface{}) error {
|
||||
func validateClearDaysInput(input any) error {
|
||||
// This function should be implemented in the main codebase
|
||||
// to validate clearDays input before using it in SQL queries
|
||||
|
||||
@@ -319,7 +319,7 @@ func validateClearDaysInput(input interface{}) error {
|
||||
}
|
||||
|
||||
// sanitizeAndValidateClearDays sanitizes and validates the input, returning the clean integer
|
||||
func sanitizeAndValidateClearDays(input interface{}) (int, error) {
|
||||
func sanitizeAndValidateClearDays(input any) (int, error) {
|
||||
err := validateClearDaysInput(input)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
module github.com/lukaszraczylo/graphql-monitoring-proxy
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.6
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/VictoriaMetrics/metrics v1.40.2
|
||||
github.com/VictoriaMetrics/metrics v1.42.0
|
||||
github.com/alicebob/miniredis/v2 v2.33.0
|
||||
github.com/avast/retry-go/v4 v4.7.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/gofiber/fiber/v2 v2.52.10
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/gofiber/fiber/v2 v2.52.12
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/goutil v0.7.2
|
||||
github.com/gookit/goutil v0.7.4
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/jackc/pgx/v5 v5.7.6
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
|
||||
github.com/lukaszraczylo/go-ratecounter v0.1.12
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.89
|
||||
github.com/redis/go-redis/v9 v9.17.1
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
github.com/sony/gobreaker v1.0.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
github.com/valyala/fasthttp v1.69.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
|
||||
go.opentelemetry.io/otel/sdk v1.42.0
|
||||
go.opentelemetry.io/otel/trace v1.42.0
|
||||
google.golang.org/grpc v1.79.3
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -36,21 +34,20 @@ require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
@@ -58,17 +55,17 @@ require (
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260330182312-d5a96adf58d8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260330182312-d5a96adf58d8 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac=
|
||||
github.com/VictoriaMetrics/metrics v1.40.2/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA=
|
||||
github.com/VictoriaMetrics/metrics v1.42.0 h1:t/OGs3BjMUYhxw/h83Z28qAss8DuA4QEVwO4NwJ9hZc=
|
||||
github.com/VictoriaMetrics/metrics v1.42.0/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc=
|
||||
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=
|
||||
@@ -16,10 +16,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
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/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
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=
|
||||
@@ -32,12 +30,12 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
|
||||
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
||||
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
|
||||
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
|
||||
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
@@ -48,24 +46,26 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/goutil v0.7.2 h1:NSiqWWY+BT0MwIlKDeSVPfQmr9xTkkAqwDjhplobdgo=
|
||||
github.com/gookit/goutil v0.7.2/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM=
|
||||
github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
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.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
||||
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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=
|
||||
@@ -80,12 +80,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
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.17.1 h1:7tl732FjYPRT9H9aNfyTwKg9iTETjWjGKEJ2t/5iWTs=
|
||||
github.com/redis/go-redis/v9 v9.17.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=
|
||||
@@ -99,8 +99,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
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=
|
||||
@@ -109,49 +109,51 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260330182312-d5a96adf58d8 h1:udju5p8o61FW6K2fxHWPIZhChk4FHl2Hjk8+uuLNnpM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260330182312-d5a96adf58d8/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260330182312-d5a96adf58d8 h1:OHkuo1i98/05rzpm9NBbfEtpJH/k3abEgZUKaAuCI7Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260330182312-d5a96adf58d8/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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=
|
||||
|
||||
+5
-5
@@ -103,14 +103,14 @@ type parseGraphQLQueryResult struct {
|
||||
var (
|
||||
// Pool for request/response maps during unmarshaling
|
||||
queryPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make(map[string]interface{}, 48)
|
||||
New: func() any {
|
||||
return make(map[string]any, 48)
|
||||
},
|
||||
}
|
||||
|
||||
// Pool for parse result objects
|
||||
resultPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return &parseGraphQLQueryResult{}
|
||||
},
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func initGraphQLParsing() {
|
||||
if cfg != nil && cfg.Logger != nil {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "GraphQL query cache initialized",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"max_entries": maxQueryCacheSize,
|
||||
"max_size_mb": 50,
|
||||
},
|
||||
@@ -244,7 +244,7 @@ func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult {
|
||||
res.activeEndpoint = cfg.Server.HostGraphQL
|
||||
|
||||
// Get a map from the pool for JSON unmarshaling
|
||||
m := queryPool.Get().(map[string]interface{})
|
||||
m := queryPool.Get().(map[string]any)
|
||||
defer func() {
|
||||
// Clear and return the map to the pool
|
||||
for k := range m {
|
||||
|
||||
+1
-1
@@ -486,7 +486,7 @@ func (suite *Tests) Test_DeepIntrospectionQueries() {
|
||||
for _, q := range tt.allowed {
|
||||
introspectionAllowedQueries[strings.ToLower(q)] = struct{}{}
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
body := map[string]any{
|
||||
"query": tt.query,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
@@ -116,9 +116,9 @@ func (suite *IntegrationSecurityTestSuite) setupTestApps() {
|
||||
}
|
||||
|
||||
// Mock GraphQL response
|
||||
response := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
response := map[string]any{
|
||||
"data": map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": "12345",
|
||||
"name": "Test User",
|
||||
"email": "test@example.com",
|
||||
@@ -156,7 +156,7 @@ func (suite *IntegrationSecurityTestSuite) TestEndToEndSecurity() {
|
||||
defer func() { cfg.LogLevel = originalLogLevel }()
|
||||
|
||||
// Create GraphQL request with sensitive data
|
||||
graphqlQuery := map[string]interface{}{
|
||||
graphqlQuery := map[string]any{
|
||||
"query": `
|
||||
mutation LoginUser($input: LoginInput!) {
|
||||
login(input: $input) {
|
||||
@@ -165,8 +165,8 @@ func (suite *IntegrationSecurityTestSuite) TestEndToEndSecurity() {
|
||||
}
|
||||
}
|
||||
`,
|
||||
"variables": map[string]interface{}{
|
||||
"input": map[string]interface{}{
|
||||
"variables": map[string]any{
|
||||
"input": map[string]any{
|
||||
"email": "user@example.com",
|
||||
"password": "secret123password",
|
||||
"api_key": "sk-sensitive-key-123",
|
||||
@@ -194,7 +194,7 @@ func (suite *IntegrationSecurityTestSuite) TestEndToEndSecurity() {
|
||||
// TestAPISecurityFlow tests complete API security workflow
|
||||
func (suite *IntegrationSecurityTestSuite) TestAPISecurityFlow() {
|
||||
tests := []struct {
|
||||
body map[string]interface{}
|
||||
body map[string]any
|
||||
name string
|
||||
endpoint string
|
||||
method string
|
||||
@@ -207,7 +207,7 @@ func (suite *IntegrationSecurityTestSuite) TestAPISecurityFlow() {
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
apiKey: "",
|
||||
body: map[string]interface{}{"user_id": "malicious-user", "reason": "test ban"},
|
||||
body: map[string]any{"user_id": "malicious-user", "reason": "test ban"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject unauthorized ban attempts",
|
||||
},
|
||||
@@ -216,7 +216,7 @@ func (suite *IntegrationSecurityTestSuite) TestAPISecurityFlow() {
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
apiKey: "' OR '1'='1 --",
|
||||
body: map[string]interface{}{"user_id": "test-user", "reason": "test ban"},
|
||||
body: map[string]any{"user_id": "test-user", "reason": "test ban"},
|
||||
expectedStatus: 401,
|
||||
description: "Should reject SQL injection in API key",
|
||||
},
|
||||
@@ -225,7 +225,7 @@ func (suite *IntegrationSecurityTestSuite) TestAPISecurityFlow() {
|
||||
endpoint: "/api/user-ban",
|
||||
method: "POST",
|
||||
apiKey: suite.validAPIKey,
|
||||
body: map[string]interface{}{"user_id": "test-user-ban", "reason": "test ban reason"},
|
||||
body: map[string]any{"user_id": "test-user-ban", "reason": "test ban reason"},
|
||||
expectedStatus: 200,
|
||||
description: "Should accept valid ban request",
|
||||
},
|
||||
@@ -488,9 +488,9 @@ func (suite *IntegrationSecurityTestSuite) TestDataSanitizationIntegration() {
|
||||
defer func() { cfg.LogLevel = originalLogLevel }()
|
||||
|
||||
// Create request with sensitive data
|
||||
sensitiveData := map[string]interface{}{
|
||||
sensitiveData := map[string]any{
|
||||
"query": "{ user { id name } }",
|
||||
"variables": map[string]interface{}{
|
||||
"variables": map[string]any{
|
||||
"password": "secret123",
|
||||
"api_key": "sk-sensitive-123",
|
||||
"credit_card": "4111111111111111",
|
||||
@@ -513,7 +513,7 @@ func (suite *IntegrationSecurityTestSuite) TestDataSanitizationIntegration() {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
var response map[string]interface{}
|
||||
var response map[string]any
|
||||
err = json.Unmarshal(body, &response)
|
||||
suite.NoError(err)
|
||||
|
||||
@@ -587,7 +587,7 @@ func (suite *IntegrationSecurityTestSuite) TestErrorHandlingSecurityIntegration(
|
||||
func (suite *IntegrationSecurityTestSuite) TestComprehensiveSecurityScenario() {
|
||||
suite.Run("Complete security workflow", func() {
|
||||
// 1. Attempt SQL injection via GraphQL
|
||||
maliciousGraphQL := map[string]interface{}{
|
||||
maliciousGraphQL := map[string]any{
|
||||
"query": "{ user(id: \"'; DROP TABLE users; --\") { id } }",
|
||||
}
|
||||
|
||||
@@ -660,7 +660,7 @@ func BenchmarkSecurityOperations(b *testing.B) {
|
||||
})
|
||||
|
||||
b.Run("Log Sanitization", func(b *testing.B) {
|
||||
testData := map[string]interface{}{
|
||||
testData := map[string]any{
|
||||
"password": "secret123",
|
||||
"api_key": "sk-123456",
|
||||
"data": "normal data",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -783,3 +784,251 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRetryBudgetIntegration tests that retry budget correctly limits retry attempts
|
||||
func (suite *Tests) TestRetryBudgetIntegration() {
|
||||
// Initialize a retry budget with limited tokens for testing
|
||||
budgetCtx := context.Background()
|
||||
testBudget := NewRetryBudgetWithContext(budgetCtx, RetryBudgetConfig{
|
||||
MaxTokens: 3, // Only allow 3 retries total
|
||||
TokensPerSecond: 0, // Don't refill during test
|
||||
Enabled: true,
|
||||
}, cfg.Logger)
|
||||
|
||||
// Replace global retry budget
|
||||
originalBudget := retryBudget
|
||||
retryBudget = testBudget
|
||||
defer func() {
|
||||
testBudget.Shutdown()
|
||||
retryBudget = originalBudget
|
||||
}()
|
||||
|
||||
suite.Run("retry_budget_limits_retries", func() {
|
||||
testBudget.Reset()
|
||||
|
||||
// Verify retry budget is set and works correctly
|
||||
rb := GetRetryBudget()
|
||||
suite.NotNil(rb, "Retry budget should be set")
|
||||
suite.True(rb.enabled, "Retry budget should be enabled")
|
||||
suite.T().Logf("Retry budget: enabled=%v, tokens=%d", rb.enabled, rb.currentTokens.Load())
|
||||
|
||||
// Test that AllowRetry consumes tokens correctly
|
||||
initialTokens := rb.currentTokens.Load()
|
||||
suite.Equal(int64(3), initialTokens, "Should start with 3 tokens")
|
||||
|
||||
// First 3 retries should be allowed
|
||||
suite.True(rb.AllowRetry(), "First retry should be allowed")
|
||||
suite.True(rb.AllowRetry(), "Second retry should be allowed")
|
||||
suite.True(rb.AllowRetry(), "Third retry should be allowed")
|
||||
|
||||
// Fourth retry should be denied (tokens exhausted)
|
||||
suite.False(rb.AllowRetry(), "Fourth retry should be denied - budget exhausted")
|
||||
|
||||
// Verify stats
|
||||
stats := rb.GetStats()
|
||||
suite.Equal(int64(4), stats["total_attempts"].(int64), "Should have 4 total attempts")
|
||||
suite.Equal(int64(3), stats["allowed_retries"].(int64), "Should have 3 allowed retries")
|
||||
suite.Equal(int64(1), stats["denied_retries"].(int64), "Should have 1 denied retry")
|
||||
|
||||
suite.T().Logf("Retry budget stats: total=%d, allowed=%d, denied=%d",
|
||||
stats["total_attempts"], stats["allowed_retries"], stats["denied_retries"])
|
||||
})
|
||||
|
||||
suite.Run("retry_budget_exhaustion", func() {
|
||||
// Create a new budget with only 1 token
|
||||
testBudget.Shutdown()
|
||||
budgetCtx2 := context.Background()
|
||||
testBudget2 := NewRetryBudgetWithContext(budgetCtx2, RetryBudgetConfig{
|
||||
MaxTokens: 1, // Only allow 1 retry
|
||||
TokensPerSecond: 0, // Don't refill
|
||||
Enabled: true,
|
||||
}, cfg.Logger)
|
||||
retryBudget = testBudget2
|
||||
defer func() {
|
||||
testBudget2.Shutdown()
|
||||
}()
|
||||
|
||||
// Test budget exhaustion with 1 token
|
||||
rb := GetRetryBudget()
|
||||
suite.NotNil(rb, "Retry budget should be set")
|
||||
suite.Equal(int64(1), rb.currentTokens.Load(), "Should start with 1 token")
|
||||
|
||||
// First retry should be allowed
|
||||
suite.True(rb.AllowRetry(), "First retry should be allowed")
|
||||
|
||||
// Second retry should be denied (only 1 token available)
|
||||
suite.False(rb.AllowRetry(), "Second retry should be denied - budget exhausted")
|
||||
|
||||
// Verify stats
|
||||
stats := rb.GetStats()
|
||||
suite.Equal(int64(2), stats["total_attempts"].(int64), "Should have 2 total attempts")
|
||||
suite.Equal(int64(1), stats["allowed_retries"].(int64), "Should have 1 allowed retry")
|
||||
suite.Equal(int64(1), stats["denied_retries"].(int64), "Should have 1 denied retry")
|
||||
|
||||
suite.T().Logf("Retry budget stats: total=%d, allowed=%d, denied=%d",
|
||||
stats["total_attempts"], stats["allowed_retries"], stats["denied_retries"])
|
||||
})
|
||||
}
|
||||
|
||||
// TestConnectionPoolStatsIntegration tests that connection pool stats are tracked
|
||||
func (suite *Tests) TestConnectionPoolStatsIntegration() {
|
||||
// Save original config
|
||||
originalClient := cfg.Client.FastProxyClient
|
||||
originalHostGraphQL := cfg.Server.HostGraphQL
|
||||
originalCoalescing := cfg.RequestCoalescing.Enable
|
||||
|
||||
// Restore after test
|
||||
defer func() {
|
||||
cfg.Client.FastProxyClient = originalClient
|
||||
cfg.Server.HostGraphQL = originalHostGraphQL
|
||||
cfg.RequestCoalescing.Enable = originalCoalescing
|
||||
}()
|
||||
|
||||
// Disable request coalescing for accurate tracking
|
||||
cfg.RequestCoalescing.Enable = false
|
||||
|
||||
suite.Run("connection_success_tracked", func() {
|
||||
// Create test server that succeeds
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"data":{"test":"success"}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg.Server.HostGraphQL = server.URL
|
||||
cfg.Client.ClientTimeout = 5
|
||||
cfg.Client.FastProxyClient = createFasthttpClient(cfg)
|
||||
|
||||
// Initialize connection pool
|
||||
InitializeConnectionPool(cfg.Client.FastProxyClient)
|
||||
defer ShutdownConnectionPool()
|
||||
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
suite.NotNil(poolMgr, "Connection pool manager should be initialized")
|
||||
|
||||
// Get stats before
|
||||
statsBefore := poolMgr.GetConnectionStats()
|
||||
successBefore := statsBefore["total_connections"].(int64)
|
||||
|
||||
// Make a successful request
|
||||
reqCtx := &fasthttp.RequestCtx{}
|
||||
reqCtx.Request.SetRequestURI("/graphql")
|
||||
reqCtx.Request.Header.SetMethod("POST")
|
||||
reqCtx.Request.Header.Set("Content-Type", "application/json")
|
||||
reqCtx.Request.SetBody([]byte(`{"query": "query { test }"}`))
|
||||
|
||||
ctx := suite.app.AcquireCtx(reqCtx)
|
||||
err := proxyTheRequest(ctx, cfg.Server.HostGraphQL)
|
||||
suite.app.ReleaseCtx(ctx)
|
||||
|
||||
suite.Nil(err, "Request should succeed")
|
||||
|
||||
// Get stats after
|
||||
statsAfter := poolMgr.GetConnectionStats()
|
||||
successAfter := statsAfter["total_connections"].(int64)
|
||||
|
||||
suite.Greater(successAfter, successBefore,
|
||||
"Total connections should increase after successful request")
|
||||
})
|
||||
|
||||
suite.Run("connection_failure_tracked_on_5xx", func() {
|
||||
// Create test server that returns 503
|
||||
// Note: 503 triggers retry which records failures
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_, _ = w.Write([]byte(`{"errors":[{"message":"Service unavailable"}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg.Server.HostGraphQL = server.URL
|
||||
cfg.Client.ClientTimeout = 2
|
||||
cfg.Client.FastProxyClient = createFasthttpClient(cfg)
|
||||
|
||||
// Initialize connection pool
|
||||
InitializeConnectionPool(cfg.Client.FastProxyClient)
|
||||
defer ShutdownConnectionPool()
|
||||
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
suite.NotNil(poolMgr, "Connection pool manager should be initialized")
|
||||
|
||||
// Get stats before
|
||||
statsBefore := poolMgr.GetConnectionStats()
|
||||
failuresBefore := statsBefore["connection_failures"].(int64)
|
||||
|
||||
// Make a failing request (503 is retryable, so it will retry and track failures)
|
||||
reqCtx := &fasthttp.RequestCtx{}
|
||||
reqCtx.Request.SetRequestURI("/graphql")
|
||||
reqCtx.Request.Header.SetMethod("POST")
|
||||
reqCtx.Request.Header.Set("Content-Type", "application/json")
|
||||
reqCtx.Request.SetBody([]byte(`{"query": "query { fail }"}`))
|
||||
|
||||
ctx := suite.app.AcquireCtx(reqCtx)
|
||||
_ = proxyTheRequest(ctx, cfg.Server.HostGraphQL)
|
||||
suite.app.ReleaseCtx(ctx)
|
||||
|
||||
// Get stats after - should have failures from retry attempts
|
||||
statsAfter := poolMgr.GetConnectionStats()
|
||||
failuresAfter := statsAfter["connection_failures"].(int64)
|
||||
|
||||
suite.Greater(failuresAfter, failuresBefore,
|
||||
"Connection failures should increase after 5xx responses that trigger retries")
|
||||
|
||||
suite.T().Logf("Connection failures: before=%d, after=%d",
|
||||
failuresBefore, failuresAfter)
|
||||
})
|
||||
|
||||
suite.Run("stats_reflect_request_outcomes", func() {
|
||||
// This test verifies that connection stats properly reflect the
|
||||
// combination of successes and failures over multiple requests
|
||||
|
||||
// Start with a fresh server
|
||||
var requestCount atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count := requestCount.Add(1)
|
||||
// First 2 requests succeed, rest fail
|
||||
if count <= 2 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"data":{"test":"success"}}`))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`{"errors":[{"message":"Error"}]}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg.Server.HostGraphQL = server.URL
|
||||
cfg.Client.ClientTimeout = 2
|
||||
cfg.Client.FastProxyClient = createFasthttpClient(cfg)
|
||||
|
||||
// Initialize connection pool
|
||||
InitializeConnectionPool(cfg.Client.FastProxyClient)
|
||||
defer ShutdownConnectionPool()
|
||||
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
suite.NotNil(poolMgr, "Connection pool manager should be initialized")
|
||||
|
||||
// Make 2 successful requests
|
||||
for i := 0; i < 2; i++ {
|
||||
reqCtx := &fasthttp.RequestCtx{}
|
||||
reqCtx.Request.SetRequestURI("/graphql")
|
||||
reqCtx.Request.Header.SetMethod("POST")
|
||||
reqCtx.Request.Header.Set("Content-Type", "application/json")
|
||||
reqCtx.Request.SetBody([]byte(`{"query": "query { test }"}`))
|
||||
|
||||
ctx := suite.app.AcquireCtx(reqCtx)
|
||||
_ = proxyTheRequest(ctx, cfg.Server.HostGraphQL)
|
||||
suite.app.ReleaseCtx(ctx)
|
||||
}
|
||||
|
||||
// Get stats after successes
|
||||
statsAfterSuccess := poolMgr.GetConnectionStats()
|
||||
totalConnections := statsAfterSuccess["total_connections"].(int64)
|
||||
|
||||
suite.GreaterOrEqual(totalConnections, int64(2),
|
||||
"Should have at least 2 successful connections tracked")
|
||||
|
||||
suite.T().Logf("Total connections after 2 successful requests: %d", totalConnections)
|
||||
})
|
||||
}
|
||||
|
||||
+6
-3
@@ -1,3 +1,6 @@
|
||||
// Package libpack_logger provides structured JSON logging with configurable
|
||||
// log levels, caller information, and automatic sensitive data redaction.
|
||||
// Supports debug, info, warning, and error log levels.
|
||||
package libpack_logger
|
||||
|
||||
import (
|
||||
@@ -47,13 +50,13 @@ type Logger struct {
|
||||
|
||||
// LogMessage represents a log message with optional pairs.
|
||||
type LogMessage struct {
|
||||
Pairs map[string]interface{}
|
||||
Pairs map[string]any
|
||||
Message string
|
||||
}
|
||||
|
||||
// bufferPool is used to reuse bytes.Buffer for efficiency.
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
@@ -132,7 +135,7 @@ func (l *Logger) shouldLog(level int) bool {
|
||||
// log writes the log message with the given level.
|
||||
func (l *Logger) log(level int, m *LogMessage) {
|
||||
if m.Pairs == nil {
|
||||
m.Pairs = make(map[string]interface{})
|
||||
m.Pairs = make(map[string]any)
|
||||
}
|
||||
|
||||
m.Pairs[fieldNames["timestamp"]] = time.Now().Format(l.timeFormat)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestLogConcurrentAccess(t *testing.T) {
|
||||
defer wg.Done()
|
||||
msg := &LogMessage{
|
||||
Message: "concurrent log test",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"goroutine_id": id,
|
||||
},
|
||||
}
|
||||
|
||||
+5
-5
@@ -9,7 +9,7 @@ import (
|
||||
// LRUCacheEntry represents a cache entry with metadata
|
||||
type LRUCacheEntry struct {
|
||||
timestamp time.Time
|
||||
value interface{}
|
||||
value any
|
||||
element *list.Element
|
||||
key string
|
||||
size int64
|
||||
@@ -44,7 +44,7 @@ func NewLRUCache(maxEntries int, maxSize int64) *LRUCache {
|
||||
}
|
||||
|
||||
// Get retrieves a value from the cache
|
||||
func (c *LRUCache) Get(key string) (interface{}, bool) {
|
||||
func (c *LRUCache) Get(key string) (any, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -61,7 +61,7 @@ func (c *LRUCache) Get(key string) (interface{}, bool) {
|
||||
}
|
||||
|
||||
// Set adds or updates a value in the cache
|
||||
func (c *LRUCache) Set(key string, value interface{}, size int64) {
|
||||
func (c *LRUCache) Set(key string, value any, size int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -211,11 +211,11 @@ func (c *LRUCache) CleanupExpired(maxAge time.Duration) int {
|
||||
}
|
||||
|
||||
// GetStats returns cache statistics
|
||||
func (c *LRUCache) GetStats() map[string]interface{} {
|
||||
func (c *LRUCache) GetStats() map[string]any {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"entries": c.evictList.Len(),
|
||||
"size_bytes": c.currentSize,
|
||||
"max_entries": c.maxEntries,
|
||||
|
||||
@@ -131,6 +131,7 @@ func parseConfig() {
|
||||
c.Cache.CacheTTL = getDetailsFromEnv("CACHE_TTL", 60)
|
||||
c.Cache.CacheMaxMemorySize = getDetailsFromEnv("CACHE_MAX_MEMORY_SIZE", 100) // Default 100MB
|
||||
c.Cache.CacheMaxEntries = getDetailsFromEnv("CACHE_MAX_ENTRIES", 10000) // Default 10000 entries
|
||||
c.Cache.CacheUseLRU = getDetailsFromEnv("CACHE_USE_LRU", false) // Use LRU eviction algorithm
|
||||
// GraphQL query parsing cache - auto-calculate based on CPU cores if not set
|
||||
c.Cache.GraphQLQueryCacheSize = getDetailsFromEnv("GRAPHQL_QUERY_CACHE_SIZE", runtime.GOMAXPROCS(0)*250)
|
||||
|
||||
@@ -145,7 +146,7 @@ func parseConfig() {
|
||||
if c.Logger != nil {
|
||||
c.Logger.Warning(&libpack_logging.LogMessage{
|
||||
Message: "⚠️ Per-user cache isolation is DISABLED - Users may see each other's cached data!",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"security_risk": "CRITICAL - Do not use in multi-user applications",
|
||||
"recommendation": "Remove CACHE_PER_USER_DISABLED or set it to false",
|
||||
},
|
||||
@@ -192,7 +193,7 @@ func parseConfig() {
|
||||
if clientTimeout < 1 || clientTimeout > 3600 { // 1 second to 1 hour max
|
||||
c.Logger.Warning(&libpack_logging.LogMessage{
|
||||
Message: "Invalid client timeout, using default",
|
||||
Pairs: map[string]interface{}{"requested": clientTimeout, "default": 120},
|
||||
Pairs: map[string]any{"requested": clientTimeout, "default": 120},
|
||||
})
|
||||
clientTimeout = 120
|
||||
}
|
||||
@@ -204,7 +205,7 @@ func parseConfig() {
|
||||
if maxConns < 1 || maxConns > 10000 { // Reasonable bounds
|
||||
c.Logger.Warning(&libpack_logging.LogMessage{
|
||||
Message: "Invalid max connections per host, using default",
|
||||
Pairs: map[string]interface{}{"requested": maxConns, "default": 1024},
|
||||
Pairs: map[string]any{"requested": maxConns, "default": 1024},
|
||||
})
|
||||
maxConns = 1024
|
||||
}
|
||||
@@ -240,7 +241,7 @@ func parseConfig() {
|
||||
if c.Logger != nil {
|
||||
c.Logger.Warning(&libpack_logging.LogMessage{
|
||||
Message: "⚠️ TLS certificate verification is DISABLED - This is a security risk in production!",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"recommendation": "Enable TLS verification by removing CLIENT_DISABLE_TLS_VERIFY or setting it to false",
|
||||
},
|
||||
})
|
||||
@@ -260,7 +261,7 @@ func parseConfig() {
|
||||
if validatedPath, err := validateFilePath(bannedUsersFile); err != nil {
|
||||
c.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Invalid banned users file path, using default",
|
||||
Pairs: map[string]interface{}{"requested": bannedUsersFile, "error": err.Error()},
|
||||
Pairs: map[string]any{"requested": bannedUsersFile, "error": err.Error()},
|
||||
})
|
||||
c.Api.BannedUsersFile = "/go/src/app/banned_users.json"
|
||||
} else {
|
||||
@@ -330,12 +331,12 @@ func parseConfig() {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Failed to initialize tracing",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
} else {
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "Tracing initialized",
|
||||
Pairs: map[string]interface{}{"endpoint": cfg.Tracing.Endpoint},
|
||||
Pairs: map[string]any{"endpoint": cfg.Tracing.Endpoint},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -345,7 +346,7 @@ func parseConfig() {
|
||||
if cfg.Cache.CacheRedisEnable {
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "Initializing metrics aggregator for cluster mode",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"redis_url": cfg.Cache.CacheRedisURL,
|
||||
"redis_db": cfg.Cache.CacheRedisDB,
|
||||
},
|
||||
@@ -359,14 +360,14 @@ func parseConfig() {
|
||||
); err != nil {
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "FAILED to initialize metrics aggregator - cluster mode will not work",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "✓ Metrics aggregator successfully initialized",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": GetMetricsAggregator().GetInstanceID(),
|
||||
},
|
||||
})
|
||||
@@ -390,9 +391,16 @@ func parseConfig() {
|
||||
// Memory cache configurations
|
||||
cacheConfig.Memory.MaxMemorySize = int64(cfg.Cache.CacheMaxMemorySize) * 1024 * 1024 // Convert MB to bytes
|
||||
cacheConfig.Memory.MaxEntries = int64(cfg.Cache.CacheMaxEntries)
|
||||
cacheConfig.Memory.UseLRU = cfg.Cache.CacheUseLRU
|
||||
|
||||
cacheType := "standard"
|
||||
if cfg.Cache.CacheUseLRU {
|
||||
cacheType = "LRU"
|
||||
}
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "Configuring memory cache with limits",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"type": cacheType,
|
||||
"max_memory_mb": cfg.Cache.CacheMaxMemorySize,
|
||||
"max_entries": cfg.Cache.CacheMaxEntries,
|
||||
},
|
||||
@@ -442,7 +450,7 @@ func parseConfig() {
|
||||
detailedError := err.Error()
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Failed to start service due to rate limit configuration error",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": detailedError,
|
||||
},
|
||||
})
|
||||
@@ -509,7 +517,7 @@ func main() {
|
||||
if err := enableApi(ctx); err != nil {
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "API server error",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -519,7 +527,7 @@ func main() {
|
||||
if err := enableHasuraEventCleaner(ctx); err != nil {
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Event cleaner error",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -559,7 +567,7 @@ func main() {
|
||||
// Start monitoring server
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "Starting monitoring server...",
|
||||
Pairs: map[string]interface{}{"port": cfg.Server.PortMonitoring},
|
||||
Pairs: map[string]any{"port": cfg.Server.PortMonitoring},
|
||||
})
|
||||
|
||||
// Start monitoring server in a goroutine
|
||||
@@ -577,7 +585,7 @@ func main() {
|
||||
case err := <-monitoringErrCh:
|
||||
cfg.Logger.Critical(&libpack_logging.LogMessage{
|
||||
Message: "Failed to start monitoring server",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"port": cfg.Server.PortMonitoring,
|
||||
},
|
||||
@@ -592,7 +600,7 @@ func main() {
|
||||
startupTimeout := time.Duration(getDetailsFromEnv("BACKEND_STARTUP_TIMEOUT", 300)) * time.Second
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "Waiting for GraphQL backend to be ready",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"timeout_seconds": int(startupTimeout.Seconds()),
|
||||
},
|
||||
})
|
||||
@@ -600,7 +608,7 @@ func main() {
|
||||
if err := healthMgr.WaitForBackendReady(startupTimeout); err != nil {
|
||||
cfg.Logger.Critical(&libpack_logging.LogMessage{
|
||||
Message: "GraphQL backend did not become ready in time",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"timeout": startupTimeout.String(),
|
||||
},
|
||||
@@ -615,7 +623,7 @@ func main() {
|
||||
// Start HTTP proxy
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "Starting HTTP proxy server...",
|
||||
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL},
|
||||
Pairs: map[string]any{"port": cfg.Server.PortGraphQL},
|
||||
})
|
||||
|
||||
// Start HTTP proxy in a goroutine
|
||||
@@ -633,7 +641,7 @@ func main() {
|
||||
case err := <-proxyErrCh:
|
||||
cfg.Logger.Critical(&libpack_logging.LogMessage{
|
||||
Message: "Failed to start HTTP proxy server",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"port": cfg.Server.PortGraphQL,
|
||||
},
|
||||
@@ -662,7 +670,7 @@ func main() {
|
||||
if err := shutdownManager.Shutdown(30 * time.Second); err != nil {
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Error during shutdown",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -743,7 +751,7 @@ func startCacheMemoryMonitoring(ctx context.Context) {
|
||||
if percentUsed > 80.0 {
|
||||
cfg.Logger.Warning(&libpack_logging.LogMessage{
|
||||
Message: "Memory cache usage is high",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"memory_usage_bytes": memoryUsage,
|
||||
"memory_limit_bytes": memoryLimit,
|
||||
"percent_used": percentUsed,
|
||||
|
||||
+2
-2
@@ -146,8 +146,8 @@ func (suite *Tests) Test_envVariableSetting() {
|
||||
|
||||
func (suite *Tests) Test_getDetailsFromEnv() {
|
||||
tests := []struct {
|
||||
defaultValue interface{}
|
||||
expected interface{}
|
||||
defaultValue any
|
||||
expected any
|
||||
name string
|
||||
key string
|
||||
envValue string
|
||||
|
||||
+72
-55
@@ -29,19 +29,19 @@ type MetricsAggregator struct {
|
||||
|
||||
// InstanceMetrics represents metrics for a single proxy instance
|
||||
type InstanceMetrics struct {
|
||||
InstanceID string `json:"instance_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
UptimeSeconds float64 `json:"uptime_seconds"`
|
||||
Stats map[string]interface{} `json:"stats"`
|
||||
Cache map[string]interface{} `json:"cache,omitempty"` // Full cache details including memory
|
||||
CacheSummary map[string]interface{} `json:"cache_summary,omitempty"` // Deprecated: kept for compatibility
|
||||
Health map[string]interface{} `json:"health"`
|
||||
CircuitBreaker map[string]interface{} `json:"circuit_breaker,omitempty"`
|
||||
RetryBudget map[string]interface{} `json:"retry_budget,omitempty"`
|
||||
Coalescing map[string]interface{} `json:"coalescing,omitempty"`
|
||||
WebSocketStats map[string]interface{} `json:"websocket,omitempty"`
|
||||
Connections map[string]interface{} `json:"connections,omitempty"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
UptimeSeconds float64 `json:"uptime_seconds"`
|
||||
Stats map[string]any `json:"stats"`
|
||||
Cache map[string]any `json:"cache,omitempty"` // Full cache details including memory
|
||||
CacheSummary map[string]any `json:"cache_summary,omitempty"` // Deprecated: kept for compatibility
|
||||
Health map[string]any `json:"health"`
|
||||
CircuitBreaker map[string]any `json:"circuit_breaker,omitempty"`
|
||||
RetryBudget map[string]any `json:"retry_budget,omitempty"`
|
||||
Coalescing map[string]any `json:"coalescing,omitempty"`
|
||||
WebSocketStats map[string]any `json:"websocket,omitempty"`
|
||||
Connections map[string]any `json:"connections,omitempty"`
|
||||
}
|
||||
|
||||
// AggregatedMetrics represents combined metrics from all instances
|
||||
@@ -49,7 +49,7 @@ type AggregatedMetrics struct {
|
||||
TotalInstances int `json:"total_instances"`
|
||||
HealthyInstances int `json:"healthy_instances"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
CombinedStats map[string]interface{} `json:"combined_stats"`
|
||||
CombinedStats map[string]any `json:"combined_stats"`
|
||||
Instances []InstanceMetrics `json:"instances"`
|
||||
PerInstanceStats map[string]InstanceMetrics `json:"per_instance_stats"`
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func InitializeMetricsAggregator(redisURL, redisPassword string, redisDB int, lo
|
||||
if logger != nil {
|
||||
logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "❌ CRITICAL: Redis connection test FAILED during initialization",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"redis_url": redisURL,
|
||||
"redis_db": redisDB,
|
||||
@@ -111,7 +111,7 @@ func InitializeMetricsAggregator(redisURL, redisPassword string, redisDB int, lo
|
||||
if logger != nil {
|
||||
logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "✓ Redis connection test PASSED",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"redis_url": redisURL,
|
||||
"redis_db": redisDB,
|
||||
},
|
||||
@@ -146,7 +146,7 @@ func InitializeMetricsAggregator(redisURL, redisPassword string, redisDB int, lo
|
||||
if logger != nil {
|
||||
logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Metrics aggregator initialized",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": instanceID,
|
||||
"redis_url": redisURL,
|
||||
"publish_key": aggregator.publishKey,
|
||||
@@ -199,7 +199,7 @@ func (ma *MetricsAggregator) publishMetrics() {
|
||||
if ma.logger != nil {
|
||||
ma.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Cannot publish metrics - global config not initialized yet",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": ma.instanceID,
|
||||
},
|
||||
})
|
||||
@@ -215,7 +215,7 @@ func (ma *MetricsAggregator) publishMetrics() {
|
||||
if ma.logger != nil {
|
||||
ma.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "gatherAllStats returned empty/nil result",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": ma.instanceID,
|
||||
},
|
||||
})
|
||||
@@ -238,11 +238,11 @@ func (ma *MetricsAggregator) publishMetrics() {
|
||||
|
||||
// Extract specific sections - CRITICAL: we must set the correct structure
|
||||
// Stats should contain the inner stats object with requests, cache_summary, etc.
|
||||
if stats, ok := allStats["stats"].(map[string]interface{}); ok {
|
||||
if stats, ok := allStats["stats"].(map[string]any); ok {
|
||||
metrics.Stats = stats
|
||||
|
||||
// Also extract cache summary separately for easier access (deprecated but kept for compatibility)
|
||||
if cacheSummary, ok := stats["cache_summary"].(map[string]interface{}); ok {
|
||||
if cacheSummary, ok := stats["cache_summary"].(map[string]any); ok {
|
||||
metrics.CacheSummary = cacheSummary
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ func (ma *MetricsAggregator) publishMetrics() {
|
||||
if ma.logger != nil {
|
||||
ma.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to extract stats from allStats - using empty stats",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": ma.instanceID,
|
||||
"allStats_keys": func() []string {
|
||||
keys := make([]string, 0, len(allStats))
|
||||
@@ -263,32 +263,32 @@ func (ma *MetricsAggregator) publishMetrics() {
|
||||
},
|
||||
})
|
||||
}
|
||||
metrics.Stats = make(map[string]interface{})
|
||||
metrics.Stats = make(map[string]any)
|
||||
}
|
||||
|
||||
// Extract full cache details (includes memory usage)
|
||||
if cache, ok := allStats["cache"].(map[string]interface{}); ok {
|
||||
if cache, ok := allStats["cache"].(map[string]any); ok {
|
||||
metrics.Cache = cache
|
||||
}
|
||||
|
||||
if health, ok := allStats["health"].(map[string]interface{}); ok {
|
||||
if health, ok := allStats["health"].(map[string]any); ok {
|
||||
metrics.Health = health
|
||||
} else {
|
||||
metrics.Health = make(map[string]interface{})
|
||||
metrics.Health = make(map[string]any)
|
||||
}
|
||||
if cb, ok := allStats["circuit_breaker"].(map[string]interface{}); ok {
|
||||
if cb, ok := allStats["circuit_breaker"].(map[string]any); ok {
|
||||
metrics.CircuitBreaker = cb
|
||||
}
|
||||
if rb, ok := allStats["retry_budget"].(map[string]interface{}); ok {
|
||||
if rb, ok := allStats["retry_budget"].(map[string]any); ok {
|
||||
metrics.RetryBudget = rb
|
||||
}
|
||||
if coal, ok := allStats["coalescing"].(map[string]interface{}); ok {
|
||||
if coal, ok := allStats["coalescing"].(map[string]any); ok {
|
||||
metrics.Coalescing = coal
|
||||
}
|
||||
if ws, ok := allStats["websocket"].(map[string]interface{}); ok {
|
||||
if ws, ok := allStats["websocket"].(map[string]any); ok {
|
||||
metrics.WebSocketStats = ws
|
||||
}
|
||||
if conn, ok := allStats["connections"].(map[string]interface{}); ok {
|
||||
if conn, ok := allStats["connections"].(map[string]any); ok {
|
||||
metrics.Connections = conn
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ func (ma *MetricsAggregator) publishMetrics() {
|
||||
if ma.logger != nil {
|
||||
ma.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to marshal metrics for Redis",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
return
|
||||
@@ -321,7 +321,7 @@ func (ma *MetricsAggregator) publishMetrics() {
|
||||
if ma.logger != nil {
|
||||
ma.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "❌ CRITICAL: Failed to publish metrics to Redis - cluster mode will not work!",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"error": err.Error(),
|
||||
"instance_id": ma.instanceID,
|
||||
"key": key,
|
||||
@@ -348,7 +348,7 @@ func (ma *MetricsAggregator) removeInstanceMetrics() {
|
||||
if err != nil && ma.logger != nil {
|
||||
ma.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Failed to remove instance metrics from Redis during shutdown",
|
||||
Pairs: map[string]interface{}{"instance_id": ma.instanceID, "error": err.Error()},
|
||||
Pairs: map[string]any{"instance_id": ma.instanceID, "error": err.Error()},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -356,7 +356,7 @@ func (ma *MetricsAggregator) removeInstanceMetrics() {
|
||||
if ma.logger != nil {
|
||||
ma.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Removed instance metrics from Redis",
|
||||
Pairs: map[string]interface{}{"instance_id": ma.instanceID},
|
||||
Pairs: map[string]any{"instance_id": ma.instanceID},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -378,7 +378,7 @@ func (ma *MetricsAggregator) GetAggregatedMetrics() (*AggregatedMetrics, error)
|
||||
TotalInstances: 0,
|
||||
HealthyInstances: 0,
|
||||
LastUpdate: time.Now(),
|
||||
CombinedStats: make(map[string]interface{}),
|
||||
CombinedStats: make(map[string]any),
|
||||
Instances: []InstanceMetrics{},
|
||||
PerInstanceStats: make(map[string]InstanceMetrics),
|
||||
}, nil
|
||||
@@ -391,7 +391,7 @@ func (ma *MetricsAggregator) GetAggregatedMetrics() (*AggregatedMetrics, error)
|
||||
key := fmt.Sprintf("%s:%s", ma.publishKey, instanceID)
|
||||
cmds[i] = pipe.Get(ctx, key)
|
||||
}
|
||||
pipe.Exec(ctx)
|
||||
_, _ = pipe.Exec(ctx) // Errors handled per-command below
|
||||
|
||||
// Parse metrics
|
||||
instances := make([]InstanceMetrics, 0, len(instanceIDs))
|
||||
@@ -422,7 +422,7 @@ func (ma *MetricsAggregator) GetAggregatedMetrics() (*AggregatedMetrics, error)
|
||||
if ma.logger != nil {
|
||||
ma.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Failed to unmarshal instance metrics",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
}
|
||||
continue
|
||||
@@ -440,7 +440,7 @@ func (ma *MetricsAggregator) GetAggregatedMetrics() (*AggregatedMetrics, error)
|
||||
if ma.logger != nil {
|
||||
ma.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Removed inactive instance",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": instID,
|
||||
"inactive_seconds": age.Seconds(),
|
||||
},
|
||||
@@ -463,7 +463,7 @@ func (ma *MetricsAggregator) GetAggregatedMetrics() (*AggregatedMetrics, error)
|
||||
if ma.logger != nil && (staleCount > 0 || errorCount > 0) {
|
||||
ma.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Cleaned up stale instance IDs from Redis",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"total_in_set": len(instanceIDs),
|
||||
"valid_instances": len(instances),
|
||||
"stale_cleaned": staleCount,
|
||||
@@ -486,14 +486,14 @@ func (ma *MetricsAggregator) GetAggregatedMetrics() (*AggregatedMetrics, error)
|
||||
}
|
||||
|
||||
// aggregateStats combines statistics from multiple instances
|
||||
func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[string]interface{} {
|
||||
func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[string]any {
|
||||
if len(instances) == 0 {
|
||||
if ma.logger != nil {
|
||||
ma.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "No instances to aggregate",
|
||||
})
|
||||
}
|
||||
return make(map[string]interface{})
|
||||
return make(map[string]any)
|
||||
}
|
||||
|
||||
// Initialize aggregated values
|
||||
@@ -519,7 +519,10 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
totalRetryAllowed int64
|
||||
totalRetryDenied int64
|
||||
totalRetryAttempts int64
|
||||
totalCurrentTokens int64
|
||||
totalMaxTokens int64
|
||||
retryBudgetEnabled = false
|
||||
retryTokensPerSec float64 // Use max tokens_per_sec from any instance
|
||||
|
||||
// Circuit breaker stats
|
||||
cbOpenCount int
|
||||
@@ -539,7 +542,7 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
if ma.logger != nil {
|
||||
ma.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Instance has nil Stats",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": instance.InstanceID,
|
||||
"index": idx,
|
||||
},
|
||||
@@ -548,7 +551,7 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
continue
|
||||
}
|
||||
|
||||
if stats, ok := instance.Stats["requests"].(map[string]interface{}); ok {
|
||||
if stats, ok := instance.Stats["requests"].(map[string]any); ok {
|
||||
if total, ok := stats["total"].(float64); ok {
|
||||
totalRequests += int64(total)
|
||||
}
|
||||
@@ -576,7 +579,7 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
}
|
||||
ma.logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Instance Stats missing 'requests' key",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"instance_id": instance.InstanceID,
|
||||
"stats_keys": keys,
|
||||
"index": idx,
|
||||
@@ -645,6 +648,17 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
if attempts, ok := instance.RetryBudget["total_attempts"].(float64); ok {
|
||||
totalRetryAttempts += int64(attempts)
|
||||
}
|
||||
if currentTokens, ok := instance.RetryBudget["current_tokens"].(float64); ok {
|
||||
totalCurrentTokens += int64(currentTokens)
|
||||
}
|
||||
if maxTokens, ok := instance.RetryBudget["max_tokens"].(float64); ok {
|
||||
totalMaxTokens += int64(maxTokens)
|
||||
}
|
||||
if tokensPerSec, ok := instance.RetryBudget["tokens_per_sec"].(float64); ok {
|
||||
if tokensPerSec > retryTokensPerSec {
|
||||
retryTokensPerSec = tokensPerSec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate circuit breaker stats
|
||||
@@ -701,11 +715,11 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
result := map[string]any{
|
||||
"cluster_mode": true,
|
||||
"total_instances": len(instances),
|
||||
"cluster_uptime": oldestUptime,
|
||||
"requests": map[string]interface{}{
|
||||
"requests": map[string]any{
|
||||
"total": totalRequests,
|
||||
"succeeded": totalSucceeded,
|
||||
"failed": totalFailed,
|
||||
@@ -714,13 +728,13 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
"current_requests_per_second": totalCurrentRPS,
|
||||
"avg_requests_per_second": totalAvgRPS,
|
||||
},
|
||||
"cache_summary": map[string]interface{}{
|
||||
"cache_summary": map[string]any{
|
||||
"hits": totalCacheHits,
|
||||
"misses": totalCacheMisses,
|
||||
"hit_rate_pct": cacheHitRate,
|
||||
"total_cached": totalCachedQueries,
|
||||
},
|
||||
"memory": map[string]interface{}{
|
||||
"memory": map[string]any{
|
||||
"total_usage_mb": func() float64 {
|
||||
if hasValidMemoryStats {
|
||||
return totalMemoryUsageMB
|
||||
@@ -729,27 +743,30 @@ func (ma *MetricsAggregator) aggregateStats(instances []InstanceMetrics) map[str
|
||||
}(),
|
||||
"available": hasValidMemoryStats,
|
||||
},
|
||||
"connections": map[string]interface{}{
|
||||
"connections": map[string]any{
|
||||
"total_active": totalActiveConnections,
|
||||
},
|
||||
"websocket": map[string]interface{}{
|
||||
"websocket": map[string]any{
|
||||
"total_connections": totalWSConnections,
|
||||
},
|
||||
"coalescing": map[string]interface{}{
|
||||
"coalescing": map[string]any{
|
||||
"enabled": len(instances) > 0, // enabled if we have instances with data
|
||||
"total_coalesced_requests": totalCoalescedRequests,
|
||||
"total_primary_requests": totalPrimaryRequests,
|
||||
"backend_savings_pct": backendSavings,
|
||||
"coalescing_rate_pct": backendSavings,
|
||||
},
|
||||
"retry_budget": map[string]interface{}{
|
||||
"retry_budget": map[string]any{
|
||||
"enabled": retryBudgetEnabled,
|
||||
"allowed_retries": totalRetryAllowed,
|
||||
"denied_retries": totalRetryDenied,
|
||||
"total_attempts": totalRetryAttempts,
|
||||
"denial_rate_pct": retryDenialRate,
|
||||
"current_tokens": totalCurrentTokens,
|
||||
"max_tokens": totalMaxTokens,
|
||||
"tokens_per_sec": retryTokensPerSec,
|
||||
},
|
||||
"circuit_breaker": map[string]interface{}{
|
||||
"circuit_breaker": map[string]any{
|
||||
"enabled": circuitBreakerEnabled,
|
||||
"state": cbState,
|
||||
"instances_open": cbOpenCount,
|
||||
@@ -771,7 +788,7 @@ func (ma *MetricsAggregator) Shutdown() {
|
||||
}
|
||||
|
||||
if ma.redisClient != nil {
|
||||
ma.redisClient.Close()
|
||||
_ = ma.redisClient.Close() // Best-effort cleanup
|
||||
}
|
||||
|
||||
if ma.logger != nil {
|
||||
|
||||
@@ -9,7 +9,3 @@ func (ms *MetricsSetup) RegisterDefaultMetrics() {
|
||||
ms.RegisterMetricsCounter(MetricsCacheMiss, nil)
|
||||
ms.RegisterMetricsCounter(MetricsQueriesCached, nil)
|
||||
}
|
||||
|
||||
func (ms *MetricsSetup) RegisterGoMetrics() {
|
||||
// TODO: metrics.WriteProcessMetrics(ms.metrics_set)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package libpack_monitoring provides Prometheus-compatible metrics collection
|
||||
// and exposure using VictoriaMetrics. Supports counters, gauges, histograms,
|
||||
// and custom metrics with labels.
|
||||
package libpack_monitoring
|
||||
|
||||
import (
|
||||
@@ -82,7 +85,7 @@ func (ms *MetricsSetup) startPrometheusEndpoint() {
|
||||
if err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393))); err != nil {
|
||||
log.Critical(&libpack_logger.LogMessage{
|
||||
Message: "Can't start the MONITORING service",
|
||||
Pairs: map[string]interface{}{"error": err},
|
||||
Pairs: map[string]any{"error": err},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -109,7 +112,7 @@ func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[stri
|
||||
if err := validate_metrics_name(metric_name); err != nil {
|
||||
log.Error(&libpack_logger.LogMessage{
|
||||
Message: "RegisterMetricsGauge() error - invalid metric name",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "metric_name": metric_name},
|
||||
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
||||
})
|
||||
// Return a dummy gauge instead of nil to prevent panics
|
||||
return &metrics.Gauge{}
|
||||
@@ -125,7 +128,7 @@ func (ms *MetricsSetup) RegisterMetricsGaugeFunc(metric_name string, labels map[
|
||||
if err := validate_metrics_name(metric_name); err != nil {
|
||||
log.Error(&libpack_logger.LogMessage{
|
||||
Message: "RegisterMetricsGaugeFunc() error - invalid metric name",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "metric_name": metric_name},
|
||||
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
||||
})
|
||||
// Return a dummy gauge instead of nil to prevent panics
|
||||
return &metrics.Gauge{}
|
||||
@@ -137,7 +140,7 @@ func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[st
|
||||
if err := validate_metrics_name(metric_name); err != nil {
|
||||
log.Error(&libpack_logger.LogMessage{
|
||||
Message: "RegisterMetricsCounter() error - invalid metric name",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "metric_name": metric_name},
|
||||
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
||||
})
|
||||
// Return a dummy counter instead of nil to prevent panics
|
||||
return &metrics.Counter{}
|
||||
@@ -152,7 +155,7 @@ func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[stri
|
||||
if err := validate_metrics_name(metric_name); err != nil {
|
||||
log.Error(&libpack_logger.LogMessage{
|
||||
Message: "RegisterFloatCounter() error - invalid metric name",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "metric_name": metric_name},
|
||||
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
||||
})
|
||||
// Return a dummy float counter instead of nil to prevent panics
|
||||
return &metrics.FloatCounter{}
|
||||
@@ -164,7 +167,7 @@ func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[st
|
||||
if err := validate_metrics_name(metric_name); err != nil {
|
||||
log.Error(&libpack_logger.LogMessage{
|
||||
Message: "RegisterMetricsSummary() error - invalid metric name",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "metric_name": metric_name},
|
||||
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
||||
})
|
||||
// Return a dummy summary instead of nil to prevent panics
|
||||
return &metrics.Summary{}
|
||||
@@ -176,7 +179,7 @@ func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[
|
||||
if err := validate_metrics_name(metric_name); err != nil {
|
||||
log.Error(&libpack_logger.LogMessage{
|
||||
Message: "RegisterMetricsHistogram() error - invalid metric name",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "metric_name": metric_name},
|
||||
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
||||
})
|
||||
// Return a dummy histogram instead of nil to prevent panics
|
||||
return &metrics.Histogram{}
|
||||
|
||||
+6
-3
@@ -1,3 +1,6 @@
|
||||
// Package pools provides memory-efficient buffer and gzip reader pools
|
||||
// for reducing allocations in high-throughput request processing.
|
||||
// Buffers are automatically sized and recycled to minimize GC pressure.
|
||||
package pools
|
||||
|
||||
import (
|
||||
@@ -16,21 +19,21 @@ const (
|
||||
|
||||
// bufferPool is the global pool for reusable buffers
|
||||
var bufferPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return bytes.NewBuffer(make([]byte, 0, InitialBufferSize))
|
||||
},
|
||||
}
|
||||
|
||||
// gzipWriterPool is the global pool for reusable gzip writers
|
||||
var gzipWriterPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return gzip.NewWriter(nil)
|
||||
},
|
||||
}
|
||||
|
||||
// gzipReaderPool is the global pool for reusable gzip readers
|
||||
var gzipReaderPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return new(gzip.Reader)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
||||
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
||||
@@ -90,7 +88,7 @@ func initCircuitBreaker(config *config) {
|
||||
|
||||
config.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Circuit breaker initialized",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"max_failures": config.CircuitBreaker.MaxFailures,
|
||||
"timeout_seconds": config.CircuitBreaker.Timeout,
|
||||
"max_half_open_reqs": config.CircuitBreaker.MaxRequestsInHalfOpen,
|
||||
@@ -105,7 +103,7 @@ func createTripFunc(config *config) func(counts gobreaker.Counts) bool {
|
||||
if counts.ConsecutiveFailures >= safeUint32(config.CircuitBreaker.MaxFailures) {
|
||||
config.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Circuit breaker tripped due to consecutive failures",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"consecutive_failures": counts.ConsecutiveFailures,
|
||||
"max_failures": config.CircuitBreaker.MaxFailures,
|
||||
"total_requests": counts.Requests,
|
||||
@@ -122,7 +120,7 @@ func createTripFunc(config *config) func(counts gobreaker.Counts) bool {
|
||||
if failureRatio >= config.CircuitBreaker.FailureRatio {
|
||||
config.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Circuit breaker tripped due to failure ratio",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"failure_ratio": failureRatio,
|
||||
"threshold": config.CircuitBreaker.FailureRatio,
|
||||
"total_failures": counts.TotalFailures,
|
||||
@@ -162,7 +160,7 @@ func createStateChangeFunc(config *config) func(name string, from gobreaker.Stat
|
||||
// Log state change
|
||||
config.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Circuit breaker state changed",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"from": from.String(),
|
||||
"to": to.String(),
|
||||
"name": name,
|
||||
@@ -315,7 +313,7 @@ func setupTracing(c *fiber.Ctx) context.Context {
|
||||
if err != nil {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Failed to parse trace header",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
} else if spanCtx, err := tracer.ExtractSpanContext(spanInfo); err == nil {
|
||||
ctx = trace.ContextWithSpanContext(ctx, spanCtx)
|
||||
@@ -392,7 +390,7 @@ func performProxyRequestCore(c *fiber.Ctx, proxyURL string, cacheKey string) err
|
||||
}
|
||||
|
||||
// Execute request through circuit breaker
|
||||
_, err := cb.Execute(func() (interface{}, error) {
|
||||
_, err := cb.Execute(func() (any, error) {
|
||||
// Execute the request with retries
|
||||
err := performProxyRequestWithRetries(c, proxyURL)
|
||||
// Check if the error or status code should trip the circuit breaker
|
||||
@@ -400,7 +398,7 @@ func performProxyRequestCore(c *fiber.Ctx, proxyURL string, cacheKey string) err
|
||||
// Log error that could potentially trip the circuit
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Error in circuit-protected request",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": c.Path(),
|
||||
"error": err.Error(),
|
||||
},
|
||||
@@ -454,24 +452,41 @@ func executeProxyAttempt(c *fiber.Ctx, proxyURL string) error {
|
||||
return retry.Unrecoverable(fmt.Errorf("fiber context became nil during retry"))
|
||||
}
|
||||
|
||||
// Get connection pool manager for stats tracking
|
||||
poolMgr := GetConnectionPoolManager()
|
||||
|
||||
// Execute the proxy request
|
||||
if err := doProxyRequestWithTimeout(c, proxyURL, cfg.Client.FastProxyClient); err != nil {
|
||||
proxyErr := doProxyRequestWithTimeout(c, proxyURL, cfg.Client.FastProxyClient)
|
||||
if proxyErr != nil {
|
||||
// Check if this is a connection error
|
||||
if isConnectionError(err) {
|
||||
if isConnectionError(proxyErr) {
|
||||
notifyHealthManager(false)
|
||||
return err // Connection errors are retryable
|
||||
// Track connection failure
|
||||
if poolMgr != nil {
|
||||
poolMgr.RecordConnectionFailure()
|
||||
}
|
||||
return proxyErr // Connection errors are retryable
|
||||
}
|
||||
|
||||
// Check if this is a timeout error - don't retry timeouts
|
||||
if isTimeoutError(err) {
|
||||
return retry.Unrecoverable(err)
|
||||
if isTimeoutError(proxyErr) {
|
||||
return retry.Unrecoverable(proxyErr)
|
||||
}
|
||||
return err
|
||||
|
||||
// Check if this is a retryable HTTP error (e.g., 503)
|
||||
// These indicate the server responded but with an error status
|
||||
if strings.Contains(proxyErr.Error(), "non-200 response") {
|
||||
// Track as a failure for retryable HTTP errors
|
||||
if poolMgr != nil {
|
||||
poolMgr.RecordConnectionFailure()
|
||||
}
|
||||
}
|
||||
return proxyErr
|
||||
}
|
||||
|
||||
// Safety check before accessing response
|
||||
if c == nil || c.Response() == nil {
|
||||
return retry.Unrecoverable(fmt.Errorf("fiber context or response became nil"))
|
||||
// Safety check before accessing response (c is already validated at function entry)
|
||||
if c.Response() == nil {
|
||||
return retry.Unrecoverable(fmt.Errorf("fiber response became nil"))
|
||||
}
|
||||
|
||||
// Check status code and determine retry strategy
|
||||
@@ -481,10 +496,18 @@ func executeProxyAttempt(c *fiber.Ctx, proxyURL string) error {
|
||||
if err == nil {
|
||||
// Success case
|
||||
notifyHealthManager(true)
|
||||
// Track successful connection
|
||||
if poolMgr != nil {
|
||||
poolMgr.RecordConnectionSuccess()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if shouldRetry {
|
||||
// Track connection failure for retryable errors (5xx, etc)
|
||||
if poolMgr != nil {
|
||||
poolMgr.RecordConnectionFailure()
|
||||
}
|
||||
return err // Retryable error
|
||||
}
|
||||
|
||||
@@ -526,7 +549,7 @@ func performProxyRequestWithEnhancedRetries(c *fiber.Ctx, proxyURL string, backe
|
||||
retry.OnRetry(func(n uint, err error) {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Retrying the request",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": c.Path(),
|
||||
"attempt": n + 1,
|
||||
"max_attempts": attempts,
|
||||
@@ -541,31 +564,51 @@ func performProxyRequestWithEnhancedRetries(c *fiber.Ctx, proxyURL string, backe
|
||||
retry.LastErrorOnly(true),
|
||||
retry.RetryIf(func(err error) bool {
|
||||
// Don't retry if context is cancelled or context is nil
|
||||
defer func() {
|
||||
// Recover from any panic when accessing context
|
||||
if r := recover(); r != nil {
|
||||
// If we panic, don't retry
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to safely access the context
|
||||
ctx := c.Context()
|
||||
if ctx == nil {
|
||||
// Safely check if context is done/cancelled
|
||||
// Note: fasthttp.RequestCtx.Done() can panic if not properly initialized
|
||||
// If we panic, don't retry (maintains backward compatibility with test behavior)
|
||||
shouldRetry := true
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// If we panic accessing context, don't retry
|
||||
// This typically happens in test scenarios with mock contexts
|
||||
shouldRetry = false
|
||||
}
|
||||
}()
|
||||
ctx := c.Context()
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
shouldRetry = false
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
if !shouldRetry {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if context is done/cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
// Check retry budget before allowing retry
|
||||
if rb := GetRetryBudget(); rb != nil {
|
||||
if !rb.AllowRetry() {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Retry denied by budget",
|
||||
Pairs: map[string]any{
|
||||
"path": c.Path(),
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -649,7 +692,7 @@ func handleCircuitOpenGracefulDegradation(c *fiber.Ctx, cacheKey string) error {
|
||||
if cachedResponse := libpack_cache.CacheLookup(cacheKey); cachedResponse != nil {
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Circuit open - serving from cache",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": c.Path(),
|
||||
},
|
||||
})
|
||||
@@ -669,7 +712,7 @@ func handleCircuitOpenGracefulDegradation(c *fiber.Ctx, cacheKey string) error {
|
||||
// No cached response available - provide helpful error response
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Circuit open - no cached response available",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": c.Path(),
|
||||
},
|
||||
})
|
||||
@@ -725,7 +768,7 @@ func handleGzippedResponse(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to create gzip reader",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -743,7 +786,7 @@ func handleGzippedResponse(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to decompress response",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -757,157 +800,6 @@ func handleGzippedResponse(c *fiber.Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sanitizeForLogging removes sensitive data from request/response bodies before logging
|
||||
func sanitizeForLogging(body []byte, contentType string) string {
|
||||
// List of sensitive field patterns to redact
|
||||
sensitiveFields := []string{
|
||||
"password", "passwd", "pwd",
|
||||
"token", "api_key", "apikey", "api-key",
|
||||
"secret", "private_key", "privatekey", "private-key",
|
||||
"authorization", "auth", "bearer",
|
||||
"session", "sessionid", "session_id", "cookie",
|
||||
"ssn", "social_security",
|
||||
"credit_card", "card_number", "cardnumber", "cvv", "cvc",
|
||||
"email", "phone", "address",
|
||||
}
|
||||
|
||||
// Try to parse as JSON if content type suggests it
|
||||
if strings.Contains(strings.ToLower(contentType), "json") {
|
||||
var data map[string]interface{}
|
||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||
decoder.UseNumber() // Preserve number precision and type
|
||||
if err := decoder.Decode(&data); err == nil {
|
||||
redactSensitiveFields(data, sensitiveFields)
|
||||
sanitized, _ := json.Marshal(data)
|
||||
return string(sanitized)
|
||||
}
|
||||
}
|
||||
|
||||
// For non-JSON or failed parsing, truncate to prevent logging large bodies
|
||||
bodyStr := string(body)
|
||||
if len(bodyStr) > 1000 {
|
||||
return bodyStr[:1000] + "... [truncated]"
|
||||
}
|
||||
|
||||
// For small non-JSON bodies, do basic string replacement
|
||||
for _, field := range sensitiveFields {
|
||||
// Simple pattern matching for key-value pairs
|
||||
bodyStr = redactPatternInString(bodyStr, field)
|
||||
}
|
||||
|
||||
return bodyStr
|
||||
}
|
||||
|
||||
// redactSensitiveFields recursively redacts sensitive fields in a map
|
||||
func redactSensitiveFields(data map[string]interface{}, fields []string) {
|
||||
for key, value := range data {
|
||||
keyLower := strings.ToLower(key)
|
||||
// Check if the key matches any sensitive field
|
||||
for _, field := range fields {
|
||||
if strings.Contains(keyLower, field) {
|
||||
data[key] = "[REDACTED]"
|
||||
break
|
||||
}
|
||||
}
|
||||
// Recurse for nested objects
|
||||
if nested, ok := value.(map[string]interface{}); ok {
|
||||
redactSensitiveFields(nested, fields)
|
||||
}
|
||||
// Handle arrays of objects
|
||||
if arr, ok := value.([]interface{}); ok {
|
||||
for _, item := range arr {
|
||||
if nestedItem, ok := item.(map[string]interface{}); ok {
|
||||
redactSensitiveFields(nestedItem, fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// redactPatternInString performs basic pattern redaction in strings
|
||||
func redactPatternInString(text string, pattern string) string {
|
||||
// Use proper regex to capture and redact complete sensitive values
|
||||
// Order matters: process most specific patterns first
|
||||
|
||||
// 1. JSON pattern: "field":"value" → "field":"[REDACTED]"
|
||||
jsonPattern := regexp.MustCompile(`(?i)"` + regexp.QuoteMeta(pattern) + `"\s*:\s*"[^"]*"`)
|
||||
text = jsonPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
return regexp.MustCompile(`:\s*"[^"]*"`).ReplaceAllString(match, `:"[REDACTED]"`)
|
||||
})
|
||||
|
||||
// 2. XML pattern: <field>value</field> → <field>[REDACTED]</field>
|
||||
xmlPattern := regexp.MustCompile(`(?i)<` + regexp.QuoteMeta(pattern) + `>[^<]*</` + regexp.QuoteMeta(pattern) + `>`)
|
||||
xmlMatched := xmlPattern.MatchString(text)
|
||||
text = xmlPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
return regexp.MustCompile(`>[^<]*<`).ReplaceAllString(match, ">[REDACTED]<")
|
||||
})
|
||||
|
||||
// If XML pattern was matched, also add a standardized redaction marker for test compatibility
|
||||
if xmlMatched {
|
||||
// Append a form-style marker to indicate redaction occurred
|
||||
if !strings.Contains(text, pattern+"=[REDACTED]") {
|
||||
text = text + " " + pattern + "=[REDACTED]"
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Double quoted pattern: field="value" → field="[REDACTED]"
|
||||
quotedPattern := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(pattern) + `="[^"]*"`)
|
||||
text = quotedPattern.ReplaceAllString(text, pattern+`="[REDACTED]"`)
|
||||
|
||||
// 4. Single quoted pattern: field='value' → field='[REDACTED]'
|
||||
singleQuotedPattern := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(pattern) + `='[^']*'`)
|
||||
text = singleQuotedPattern.ReplaceAllString(text, pattern+`='[REDACTED]'`)
|
||||
|
||||
// 5. Form/URL pattern: field=value& or field=value$ → field=[REDACTED]& or field=[REDACTED]$
|
||||
// This must be last and should only match unquoted values
|
||||
formPattern := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(pattern) + `=([^&\s"']+)(?:[&\s]|$)`)
|
||||
text = formPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
// Only replace if the value is not already [REDACTED]
|
||||
if strings.Contains(match, "[REDACTED]") {
|
||||
return match
|
||||
}
|
||||
return regexp.MustCompile(`=([^&\s"']+)`).ReplaceAllString(match, "=[REDACTED]")
|
||||
})
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// convertHeaders converts map[string][]string to map[string]string by taking first value
|
||||
func convertHeaders(headers map[string][]string) map[string]string {
|
||||
converted := make(map[string]string)
|
||||
for key, values := range headers {
|
||||
if len(values) > 0 {
|
||||
converted[key] = values[0]
|
||||
}
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
// sanitizeHeaders removes sensitive headers from logging
|
||||
func sanitizeHeaders(headers map[string]string) map[string]string {
|
||||
sanitized := make(map[string]string)
|
||||
sensitiveHeaders := []string{
|
||||
"authorization", "x-api-key", "x-auth-token", "cookie", "set-cookie",
|
||||
"x-api-secret", "x-access-token", "x-csrf-token",
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
keyLower := strings.ToLower(key)
|
||||
isRedacted := false
|
||||
for _, sensitive := range sensitiveHeaders {
|
||||
if strings.Contains(keyLower, sensitive) {
|
||||
sanitized[key] = "[REDACTED]"
|
||||
isRedacted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isRedacted {
|
||||
sanitized[key] = value
|
||||
}
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// logDebugRequest logs the request details when in debug mode with sanitization.
|
||||
func logDebugRequest(c *fiber.Ctx) {
|
||||
contentType := string(c.Request().Header.ContentType())
|
||||
@@ -916,7 +808,7 @@ func logDebugRequest(c *fiber.Ctx) {
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Proxying the request",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": c.Path(),
|
||||
"body": sanitizedBody,
|
||||
"headers": sanitizedHeaders,
|
||||
@@ -933,7 +825,7 @@ func logDebugResponse(c *fiber.Ctx) {
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Received proxied response",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": c.Path(),
|
||||
"response_body": sanitizedBody,
|
||||
"response_code": c.Response().StatusCode(),
|
||||
@@ -951,7 +843,7 @@ func safeMaxRequests(maxRequestsInHalfOpen int) uint32 {
|
||||
if cfg != nil && cfg.Logger != nil {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Invalid MaxRequestsInHalfOpen value, using default",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"requested_value": maxRequestsInHalfOpen,
|
||||
"default_value": defaultMaxRequestsInHalfOpen,
|
||||
},
|
||||
|
||||
@@ -21,19 +21,19 @@ func TestProxyLoggingSecurityTestSuite(t *testing.T) {
|
||||
func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
input map[string]any
|
||||
expected map[string]any
|
||||
contentType string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Password field redaction",
|
||||
input: map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"username": "user123",
|
||||
"password": "secret123",
|
||||
"email": "user@example.com",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"username": "user123",
|
||||
"password": "[REDACTED]",
|
||||
"email": "[REDACTED]",
|
||||
@@ -43,13 +43,13 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
},
|
||||
{
|
||||
name: "API key and token redaction",
|
||||
input: map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"data": "normal data",
|
||||
"api_key": "sk-123456789",
|
||||
"token": "bearer-token-123",
|
||||
"auth": "auth-value",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"data": "normal data",
|
||||
"api_key": "[REDACTED]",
|
||||
"token": "[REDACTED]",
|
||||
@@ -60,22 +60,22 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
},
|
||||
{
|
||||
name: "Nested sensitive fields",
|
||||
input: map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"user": map[string]any{
|
||||
"name": "John Doe",
|
||||
"password": "secret123",
|
||||
"profile": map[string]interface{}{
|
||||
"profile": map[string]any{
|
||||
"api_key": "sk-nested-key",
|
||||
"bio": "User bio",
|
||||
},
|
||||
},
|
||||
"public_data": "visible",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"user": map[string]any{
|
||||
"name": "John Doe",
|
||||
"password": "[REDACTED]",
|
||||
"profile": map[string]interface{}{
|
||||
"profile": map[string]any{
|
||||
"api_key": "[REDACTED]",
|
||||
"bio": "User bio",
|
||||
},
|
||||
@@ -87,25 +87,25 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
},
|
||||
{
|
||||
name: "Array with sensitive data",
|
||||
input: map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"users": []any{
|
||||
map[string]any{
|
||||
"name": "User1",
|
||||
"password": "pass1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"name": "User2",
|
||||
"token": "token2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"users": []any{
|
||||
map[string]any{
|
||||
"name": "User1",
|
||||
"password": "[REDACTED]",
|
||||
},
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"name": "User2",
|
||||
"token": "[REDACTED]",
|
||||
},
|
||||
@@ -116,13 +116,13 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
},
|
||||
{
|
||||
name: "Credit card and financial data",
|
||||
input: map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"order_id": "12345",
|
||||
"credit_card": "4111111111111111",
|
||||
"cvv": "123",
|
||||
"amount": 100.50,
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"order_id": "12345",
|
||||
"credit_card": "[REDACTED]",
|
||||
"cvv": "[REDACTED]",
|
||||
@@ -133,14 +133,14 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
},
|
||||
{
|
||||
name: "Personal identifiable information",
|
||||
input: map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"name": "John Doe",
|
||||
"ssn": "123-45-6789",
|
||||
"phone": "+1-555-123-4567",
|
||||
"address": "123 Main St",
|
||||
"age": 30,
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"name": "John Doe",
|
||||
"ssn": "[REDACTED]",
|
||||
"phone": "[REDACTED]",
|
||||
@@ -152,13 +152,13 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
},
|
||||
{
|
||||
name: "Mixed case field names",
|
||||
input: map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"UserName": "john",
|
||||
"PASSWORD": "secret",
|
||||
"Api_Key": "key123",
|
||||
"Bearer": "token",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"UserName": "john",
|
||||
"PASSWORD": "[REDACTED]",
|
||||
"Api_Key": "[REDACTED]",
|
||||
@@ -169,24 +169,24 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
},
|
||||
{
|
||||
name: "Various password patterns",
|
||||
input: map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"pwd": "secret1",
|
||||
"passwd": "secret2",
|
||||
"password": "secret3",
|
||||
"pass": "not-redacted", // Should NOT be redacted (not in list)
|
||||
"pass": "secret4", // Now redacted for better security coverage
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"pwd": "[REDACTED]",
|
||||
"passwd": "[REDACTED]",
|
||||
"password": "[REDACTED]",
|
||||
"pass": "not-redacted",
|
||||
"pass": "[REDACTED]",
|
||||
},
|
||||
contentType: "application/json",
|
||||
description: "Should handle various password field patterns",
|
||||
},
|
||||
{
|
||||
name: "Various auth patterns",
|
||||
input: map[string]interface{}{
|
||||
input: map[string]any{
|
||||
"authorization": "Bearer token123",
|
||||
"auth": "basic auth",
|
||||
"bearer": "token456",
|
||||
@@ -195,7 +195,7 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
"session_id": "session789",
|
||||
"cookie": "cookie_value",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
expected: map[string]any{
|
||||
"authorization": "[REDACTED]",
|
||||
"auth": "[REDACTED]",
|
||||
"bearer": "[REDACTED]",
|
||||
@@ -219,7 +219,7 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSensitiveDataSanitization() {
|
||||
result := sanitizeForLogging(inputBytes, tt.contentType)
|
||||
|
||||
// Parse the result back to compare
|
||||
var sanitized map[string]interface{}
|
||||
var sanitized map[string]any
|
||||
decoder := json.NewDecoder(strings.NewReader(result))
|
||||
decoder.UseNumber() // Preserve number precision and type
|
||||
err = decoder.Decode(&sanitized)
|
||||
@@ -398,10 +398,10 @@ func (suite *ProxyLoggingSecurityTestSuite) TestRedactSensitiveFields() {
|
||||
sensitiveFields := []string{"password", "token", "secret"}
|
||||
|
||||
suite.Run("Deep nested structure", func() {
|
||||
data := map[string]interface{}{
|
||||
"level1": map[string]interface{}{
|
||||
"level2": map[string]interface{}{
|
||||
"level3": map[string]interface{}{
|
||||
data := map[string]any{
|
||||
"level1": map[string]any{
|
||||
"level2": map[string]any{
|
||||
"level3": map[string]any{
|
||||
"password": "testdeepsecret",
|
||||
"public": "data",
|
||||
},
|
||||
@@ -415,28 +415,28 @@ func (suite *ProxyLoggingSecurityTestSuite) TestRedactSensitiveFields() {
|
||||
redactSensitiveFields(data, sensitiveFields)
|
||||
|
||||
// Verify deep nesting is handled
|
||||
level3 := data["level1"].(map[string]interface{})["level2"].(map[string]interface{})["level3"].(map[string]interface{})
|
||||
level3 := data["level1"].(map[string]any)["level2"].(map[string]any)["level3"].(map[string]any)
|
||||
suite.Equal("[REDACTED]", level3["password"])
|
||||
suite.Equal("data", level3["public"])
|
||||
|
||||
// Verify intermediate levels
|
||||
level2 := data["level1"].(map[string]interface{})["level2"].(map[string]interface{})
|
||||
level2 := data["level1"].(map[string]any)["level2"].(map[string]any)
|
||||
suite.Equal("[REDACTED]", level2["token"])
|
||||
|
||||
// Verify top level
|
||||
suite.Equal("[REDACTED]", data["secret"])
|
||||
level1 := data["level1"].(map[string]interface{})
|
||||
level1 := data["level1"].(map[string]any)
|
||||
suite.Equal("value", level1["normal"])
|
||||
})
|
||||
|
||||
suite.Run("Array of objects", func() {
|
||||
data := map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{
|
||||
data := map[string]any{
|
||||
"users": []any{
|
||||
map[string]any{
|
||||
"name": "User1",
|
||||
"password": "testpass1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"name": "User2",
|
||||
"token": "testtoken2",
|
||||
},
|
||||
@@ -446,9 +446,9 @@ func (suite *ProxyLoggingSecurityTestSuite) TestRedactSensitiveFields() {
|
||||
|
||||
redactSensitiveFields(data, sensitiveFields)
|
||||
|
||||
users := data["users"].([]interface{})
|
||||
user1 := users[0].(map[string]interface{})
|
||||
user2 := users[1].(map[string]interface{})
|
||||
users := data["users"].([]any)
|
||||
user1 := users[0].(map[string]any)
|
||||
user2 := users[1].(map[string]any)
|
||||
|
||||
suite.Equal("[REDACTED]", user1["password"])
|
||||
suite.Equal("User1", user1["name"])
|
||||
@@ -509,9 +509,9 @@ func (suite *ProxyLoggingSecurityTestSuite) TestRedactPatternInString() {
|
||||
// TestSanitizationPerformance tests performance of sanitization functions
|
||||
func (suite *ProxyLoggingSecurityTestSuite) TestSanitizationPerformance() {
|
||||
// Create a large JSON structure with sensitive data
|
||||
largeData := make(map[string]interface{})
|
||||
largeData := make(map[string]any)
|
||||
for i := 0; i < 1000; i++ {
|
||||
largeData[fmt.Sprintf("user_%d", i)] = map[string]interface{}{
|
||||
largeData[fmt.Sprintf("user_%d", i)] = map[string]any{
|
||||
"name": fmt.Sprintf("User%d", i),
|
||||
"password": fmt.Sprintf("secret%d", i),
|
||||
"email": fmt.Sprintf("user%d@example.com", i),
|
||||
@@ -526,12 +526,12 @@ func (suite *ProxyLoggingSecurityTestSuite) TestSanitizationPerformance() {
|
||||
result := sanitizeForLogging(largeJSON, "application/json")
|
||||
|
||||
// Verify the result is valid JSON
|
||||
var sanitized map[string]interface{}
|
||||
var sanitized map[string]any
|
||||
err = json.Unmarshal([]byte(result), &sanitized)
|
||||
suite.NoError(err)
|
||||
|
||||
// Verify sensitive data was redacted (spot check)
|
||||
user0 := sanitized["user_0"].(map[string]interface{})
|
||||
user0 := sanitized["user_0"].(map[string]any)
|
||||
suite.Equal("[REDACTED]", user0["password"])
|
||||
suite.Equal("[REDACTED]", user0["email"])
|
||||
suite.Equal("User0", user0["name"])
|
||||
@@ -557,7 +557,7 @@ func (suite *ProxyLoggingSecurityTestSuite) TestEdgeCases() {
|
||||
|
||||
// This should not panic
|
||||
suite.NotPanics(func() {
|
||||
data := make(map[string]interface{})
|
||||
data := make(map[string]any)
|
||||
data["test"] = nil
|
||||
redactSensitiveFields(data, sensitiveFields)
|
||||
})
|
||||
@@ -577,12 +577,12 @@ func (suite *ProxyLoggingSecurityTestSuite) TestEdgeCases() {
|
||||
|
||||
// BenchmarkSanitizeForLogging benchmarks the sanitization function
|
||||
func BenchmarkSanitizeForLogging(b *testing.B) {
|
||||
testData := map[string]interface{}{
|
||||
testData := map[string]any{
|
||||
"username": "testuser",
|
||||
"password": "secret123",
|
||||
"api_key": "sk-123456789",
|
||||
"data": "normal data",
|
||||
"nested": map[string]interface{}{
|
||||
"nested": map[string]any{
|
||||
"token": "nested-token",
|
||||
"value": "nested-value",
|
||||
},
|
||||
|
||||
+13
-13
@@ -25,8 +25,8 @@ type RateLimitConfig struct {
|
||||
func (r *RateLimitConfig) UnmarshalJSON(data []byte) error {
|
||||
// Use a temporary struct to unmarshal the JSON data
|
||||
type RateLimitConfigTemp struct {
|
||||
Interval interface{} `json:"interval"`
|
||||
Req int `json:"req"`
|
||||
Interval any `json:"interval"`
|
||||
Req int `json:"req"`
|
||||
}
|
||||
|
||||
var temp RateLimitConfigTemp
|
||||
@@ -96,7 +96,7 @@ func loadRatelimitConfig() error {
|
||||
// Log detailed error information
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to load rate limit configuration",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"paths": paths,
|
||||
"path_errors": configError.PathErrors,
|
||||
},
|
||||
@@ -120,7 +120,7 @@ func loadConfigFromPath(path string) error {
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Failed to load rate limit config",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": path,
|
||||
"error": errMsg,
|
||||
"error_details": err.Error(),
|
||||
@@ -137,7 +137,7 @@ func loadConfigFromPath(path string) error {
|
||||
errMsg := fmt.Sprintf("Invalid JSON format: %s", err.Error())
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Failed to parse rate limit config",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": path,
|
||||
"error": errMsg,
|
||||
},
|
||||
@@ -150,7 +150,7 @@ func loadConfigFromPath(path string) error {
|
||||
errMsg := "Empty rate limit configuration"
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Invalid rate limit config",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"path": path,
|
||||
"error": errMsg,
|
||||
},
|
||||
@@ -167,7 +167,7 @@ func loadConfigFromPath(path string) error {
|
||||
if cfg.LogLevel == "DEBUG" {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Setting ratelimit config for role",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"role": key,
|
||||
"interval_used": value.Interval,
|
||||
"ratelimit": value.Req,
|
||||
@@ -186,7 +186,7 @@ func loadConfigFromPath(path string) error {
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit config loaded",
|
||||
Pairs: map[string]interface{}{"ratelimit": rateLimits},
|
||||
Pairs: map[string]any{"ratelimit": rateLimits},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func rateLimitedRequest(userID, userRole string) bool {
|
||||
if !ok || roleConfig.RateCounterTicker == nil {
|
||||
cfg.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit role not found or ticker not initialized - defaulting to deny",
|
||||
Pairs: map[string]interface{}{"user_role": userRole},
|
||||
Pairs: map[string]any{"user_role": userRole},
|
||||
})
|
||||
// Default to deny when config not found (security fix)
|
||||
return false
|
||||
@@ -224,7 +224,7 @@ func checkRateLimit(userID, userRole string, roleConfig RateLimitConfig, endpoin
|
||||
roleConfig.RateCounterTicker.Incr(1)
|
||||
tickerRate := roleConfig.RateCounterTicker.GetRate()
|
||||
|
||||
logDetails := map[string]interface{}{
|
||||
logDetails := map[string]any{
|
||||
"user_role": userRole,
|
||||
"user_id": userID,
|
||||
"rate": tickerRate,
|
||||
@@ -235,14 +235,14 @@ func checkRateLimit(userID, userRole string, roleConfig RateLimitConfig, endpoin
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit ticker",
|
||||
Pairs: map[string]interface{}{"log_details": logDetails},
|
||||
Pairs: map[string]any{"log_details": logDetails},
|
||||
})
|
||||
|
||||
// Check burst limit if configured
|
||||
if roleConfig.Burst > 0 && tickerRate > float64(roleConfig.Burst) {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Burst limit exceeded",
|
||||
Pairs: map[string]interface{}{"log_details": logDetails},
|
||||
Pairs: map[string]any{"log_details": logDetails},
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -250,7 +250,7 @@ func checkRateLimit(userID, userRole string, roleConfig RateLimitConfig, endpoin
|
||||
if tickerRate > float64(roleConfig.Req) {
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Rate limit exceeded",
|
||||
Pairs: map[string]interface{}{"log_details": logDetails},
|
||||
Pairs: map[string]any{"log_details": logDetails},
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (rc *RequestCoalescer) Do(key string, fn func() (*CoalescedResponse, error)
|
||||
if rc.logger != nil {
|
||||
rc.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Request coalesced with in-flight request",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"key": key[:min(len(key), 32)] + "...",
|
||||
"waiters": waiters,
|
||||
},
|
||||
@@ -115,7 +115,7 @@ func (rc *RequestCoalescer) Do(key string, fn func() (*CoalescedResponse, error)
|
||||
if rc.logger != nil {
|
||||
rc.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Request coalesced (race condition)",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"key": key[:min(len(key), 32)] + "...",
|
||||
"waiters": waiters,
|
||||
},
|
||||
@@ -163,7 +163,7 @@ func (rc *RequestCoalescer) Do(key string, fn func() (*CoalescedResponse, error)
|
||||
if rc.logger != nil && waiters > 1 {
|
||||
rc.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Request completed, served coalesced waiters",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"key": key[:min(len(key), 32)] + "...",
|
||||
"waiters": waiters,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
@@ -183,7 +183,7 @@ func (rc *RequestCoalescer) Do(key string, fn func() (*CoalescedResponse, error)
|
||||
}
|
||||
|
||||
// GetStats returns coalescing statistics
|
||||
func (rc *RequestCoalescer) GetStats() map[string]interface{} {
|
||||
func (rc *RequestCoalescer) GetStats() map[string]any {
|
||||
totalRequests := rc.totalRequests.Load()
|
||||
coalescedRequests := rc.coalescedRequests.Load()
|
||||
|
||||
@@ -199,7 +199,7 @@ func (rc *RequestCoalescer) GetStats() map[string]interface{} {
|
||||
savings = float64(coalescedRequests) / float64(primaryRequests) * 100
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"enabled": rc.enabled,
|
||||
"total_requests": totalRequests,
|
||||
"primary_requests": primaryRequests,
|
||||
|
||||
+5
-5
@@ -81,7 +81,7 @@ func (rb *RetryBudget) AllowRetry() bool {
|
||||
if rb.logger != nil {
|
||||
rb.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Retry denied: budget exhausted",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"current_tokens": current,
|
||||
"denied_count": rb.deniedRetries.Load(),
|
||||
},
|
||||
@@ -150,7 +150,7 @@ func (rb *RetryBudget) refill() {
|
||||
}
|
||||
|
||||
// GetStats returns current statistics
|
||||
func (rb *RetryBudget) GetStats() map[string]interface{} {
|
||||
func (rb *RetryBudget) GetStats() map[string]any {
|
||||
totalAttempts := rb.totalAttempts.Load()
|
||||
allowedRetries := rb.allowedRetries.Load()
|
||||
deniedRetries := rb.deniedRetries.Load()
|
||||
@@ -160,7 +160,7 @@ func (rb *RetryBudget) GetStats() map[string]interface{} {
|
||||
denialRate = float64(deniedRetries) / float64(totalAttempts) * 100
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"enabled": rb.enabled,
|
||||
"current_tokens": rb.currentTokens.Load(),
|
||||
"max_tokens": rb.maxTokens,
|
||||
@@ -195,7 +195,7 @@ func (rb *RetryBudget) UpdateConfig(config RetryBudgetConfig) {
|
||||
if rb.logger != nil {
|
||||
rb.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Retry budget configuration updated",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"tokens_per_sec": config.TokensPerSecond,
|
||||
"max_tokens": config.MaxTokens,
|
||||
"enabled": config.Enabled,
|
||||
@@ -222,7 +222,7 @@ func InitializeRetryBudgetWithContext(ctx context.Context, config RetryBudgetCon
|
||||
if logger != nil && config.Enabled {
|
||||
logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Retry budget initialized",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"tokens_per_sec": config.TokensPerSecond,
|
||||
"max_tokens": config.MaxTokens,
|
||||
},
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// Sanitization constants
|
||||
const (
|
||||
// MaxLogBodySize is the maximum size of body content to include in logs
|
||||
MaxLogBodySize = 1000
|
||||
// RedactedPlaceholder is the string used to replace sensitive values
|
||||
RedactedPlaceholder = "[REDACTED]"
|
||||
// TruncatedSuffix is appended to truncated log content
|
||||
TruncatedSuffix = "... [truncated]"
|
||||
)
|
||||
|
||||
// sensitiveFieldPatterns contains common sensitive field names for redaction
|
||||
var sensitiveFieldPatterns = []string{
|
||||
// Passwords
|
||||
"password", "passwd", "pwd", "pass",
|
||||
// Tokens (expanded coverage)
|
||||
"token", "accesstoken", "access_token", "refreshtoken", "refresh_token",
|
||||
"api_key", "apikey", "api-key", "api_token",
|
||||
"jwt", "jwttoken", "jwt_token", "idtoken", "id_token",
|
||||
// Secrets & Keys
|
||||
"secret", "client_secret", "clientsecret",
|
||||
"private_key", "privatekey", "private-key",
|
||||
// Auth
|
||||
"authorization", "auth", "bearer", "basic",
|
||||
// Sessions
|
||||
"session", "sessionid", "session_id", "cookie", "csrf", "xsrf",
|
||||
// PII - Personal Identifiable Information
|
||||
"ssn", "social_security", "personal_id", "national_id",
|
||||
"credit_card", "card_number", "cardnumber", "cvv", "cvc", "cvv2",
|
||||
"track1", "track2", "pan",
|
||||
"email", "phone", "address", "postal", "zip",
|
||||
// MFA/2FA
|
||||
"otp", "2fa", "mfa", "pin", "totp",
|
||||
}
|
||||
|
||||
// sensitiveHeaderPatterns contains header names that should be redacted
|
||||
var sensitiveHeaderPatterns = []string{
|
||||
"authorization", "x-api-key", "x-auth-token", "cookie", "set-cookie",
|
||||
"x-api-secret", "x-access-token", "x-csrf-token",
|
||||
}
|
||||
|
||||
// sanitizeForLogging removes sensitive data from request/response bodies before logging
|
||||
func sanitizeForLogging(body []byte, contentType string) string {
|
||||
// Try to parse as JSON if content type suggests it
|
||||
if strings.Contains(strings.ToLower(contentType), "json") {
|
||||
var data map[string]any
|
||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||
decoder.UseNumber() // Preserve number precision and type
|
||||
if err := decoder.Decode(&data); err == nil {
|
||||
redactSensitiveFields(data, sensitiveFieldPatterns)
|
||||
sanitized, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
// Fall through to string-based sanitization on marshal error
|
||||
} else {
|
||||
return string(sanitized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For non-JSON or failed parsing, truncate to prevent logging large bodies
|
||||
bodyStr := string(body)
|
||||
if len(bodyStr) > MaxLogBodySize {
|
||||
return bodyStr[:MaxLogBodySize] + TruncatedSuffix
|
||||
}
|
||||
|
||||
// For small non-JSON bodies, do basic string replacement
|
||||
for _, field := range sensitiveFieldPatterns {
|
||||
bodyStr = redactPatternInString(bodyStr, field)
|
||||
}
|
||||
|
||||
return bodyStr
|
||||
}
|
||||
|
||||
// redactSensitiveFields recursively redacts sensitive fields in a map
|
||||
func redactSensitiveFields(data map[string]any, fields []string) {
|
||||
for key, value := range data {
|
||||
keyLower := strings.ToLower(key)
|
||||
// Check if the key matches any sensitive field
|
||||
for _, field := range fields {
|
||||
if strings.Contains(keyLower, field) {
|
||||
data[key] = RedactedPlaceholder
|
||||
break
|
||||
}
|
||||
}
|
||||
// Recurse for nested objects
|
||||
if nested, ok := value.(map[string]any); ok {
|
||||
redactSensitiveFields(nested, fields)
|
||||
}
|
||||
// Handle arrays of objects
|
||||
if arr, ok := value.([]any); ok {
|
||||
for _, item := range arr {
|
||||
if nestedItem, ok := item.(map[string]any); ok {
|
||||
redactSensitiveFields(nestedItem, fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// redactPatternInString performs basic pattern redaction in strings
|
||||
func redactPatternInString(text string, pattern string) string {
|
||||
// Use proper regex to capture and redact complete sensitive values
|
||||
// Order matters: process most specific patterns first
|
||||
|
||||
// 1. JSON pattern: "field":"value" → "field":"[REDACTED]"
|
||||
jsonPattern := regexp.MustCompile(`(?i)"` + regexp.QuoteMeta(pattern) + `"\s*:\s*"[^"]*"`)
|
||||
text = jsonPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
return regexp.MustCompile(`:\s*"[^"]*"`).ReplaceAllString(match, `:"[REDACTED]"`)
|
||||
})
|
||||
|
||||
// 2. XML pattern: <field>value</field> → <field>[REDACTED]</field>
|
||||
xmlPattern := regexp.MustCompile(`(?i)<` + regexp.QuoteMeta(pattern) + `>[^<]*</` + regexp.QuoteMeta(pattern) + `>`)
|
||||
xmlMatched := xmlPattern.MatchString(text)
|
||||
text = xmlPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
return regexp.MustCompile(`>[^<]*<`).ReplaceAllString(match, ">[REDACTED]<")
|
||||
})
|
||||
|
||||
// If XML pattern was matched, also add a standardized redaction marker for test compatibility
|
||||
if xmlMatched {
|
||||
// Append a form-style marker to indicate redaction occurred
|
||||
if !strings.Contains(text, pattern+"=[REDACTED]") {
|
||||
text = text + " " + pattern + "=[REDACTED]"
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Double quoted pattern: field="value" → field="[REDACTED]"
|
||||
quotedPattern := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(pattern) + `="[^"]*"`)
|
||||
text = quotedPattern.ReplaceAllString(text, pattern+`="[REDACTED]"`)
|
||||
|
||||
// 4. Single quoted pattern: field='value' → field='[REDACTED]'
|
||||
singleQuotedPattern := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(pattern) + `='[^']*'`)
|
||||
text = singleQuotedPattern.ReplaceAllString(text, pattern+`='[REDACTED]'`)
|
||||
|
||||
// 5. Form/URL pattern: field=value& or field=value$ → field=[REDACTED]& or field=[REDACTED]$
|
||||
// This must be last and should only match unquoted values
|
||||
formPattern := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(pattern) + `=([^&\s"']+)(?:[&\s]|$)`)
|
||||
text = formPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||||
// Only replace if the value is not already [REDACTED]
|
||||
if strings.Contains(match, "[REDACTED]") {
|
||||
return match
|
||||
}
|
||||
return regexp.MustCompile(`=([^&\s"']+)`).ReplaceAllString(match, "=[REDACTED]")
|
||||
})
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// convertHeaders converts map[string][]string to map[string]string by taking first value
|
||||
func convertHeaders(headers map[string][]string) map[string]string {
|
||||
converted := make(map[string]string)
|
||||
for key, values := range headers {
|
||||
if len(values) > 0 {
|
||||
converted[key] = values[0]
|
||||
}
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
// sanitizeHeaders removes sensitive headers from logging
|
||||
func sanitizeHeaders(headers map[string]string) map[string]string {
|
||||
sanitized := make(map[string]string)
|
||||
|
||||
for key, value := range headers {
|
||||
keyLower := strings.ToLower(key)
|
||||
isRedacted := false
|
||||
for _, sensitive := range sensitiveHeaderPatterns {
|
||||
if strings.Contains(keyLower, sensitive) {
|
||||
sanitized[key] = RedactedPlaceholder
|
||||
isRedacted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isRedacted {
|
||||
sanitized[key] = value
|
||||
}
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func StartHTTPProxy() error {
|
||||
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "GraphQL proxy starting",
|
||||
Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL},
|
||||
Pairs: map[string]any{"port": cfg.Server.PortGraphQL},
|
||||
})
|
||||
|
||||
if err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL)); err != nil {
|
||||
@@ -168,7 +168,7 @@ func healthCheck(c *fiber.Ctx) error {
|
||||
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Health check: Can't reach the GraphQL server",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"endpoint": endpoint,
|
||||
"error": errorMsg,
|
||||
"response_time_ms": graphqlStatus.ResponseTime,
|
||||
@@ -224,7 +224,7 @@ func healthCheck(c *fiber.Ctx) error {
|
||||
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Health check: Can't connect to Redis",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"server": cfg.Cache.CacheRedisURL,
|
||||
"error": errorMsg,
|
||||
"response_time_ms": redisStatus.ResponseTime,
|
||||
@@ -243,7 +243,7 @@ func healthCheck(c *fiber.Ctx) error {
|
||||
|
||||
cfg.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Health check completed",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"status": response.Status,
|
||||
"dependencies": response.Dependencies,
|
||||
},
|
||||
@@ -275,7 +275,7 @@ func processGraphQLRequest(c *fiber.Ctx) error {
|
||||
|
||||
// Debug logging for mutation routing analysis (enabled when LOG_LEVEL=DEBUG)
|
||||
if cfg.LogLevel == "DEBUG" {
|
||||
var m map[string]interface{}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(c.Body(), &m); err == nil {
|
||||
if query, ok := m["query"].(string); ok {
|
||||
debugParseGraphQLQuery(c, query)
|
||||
@@ -380,7 +380,7 @@ func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int,
|
||||
if err := proxyTheRequest(c, currentEndpoint); err != nil {
|
||||
cfg.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't proxy the request",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Can't proxy the request - try again later")
|
||||
@@ -403,7 +403,7 @@ func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached
|
||||
if cfg.Server.AccessLog {
|
||||
cfg.Logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "Request processed",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"ip": c.IP(),
|
||||
"fwd-ip": c.Get("X-Forwarded-For"),
|
||||
"user_id": userID,
|
||||
|
||||
+4
-4
@@ -54,7 +54,7 @@ func (sm *ShutdownManager) RunGoroutine(name string, fn func(context.Context)) {
|
||||
if logger != nil {
|
||||
logger.Debug(&libpack_logging.LogMessage{
|
||||
Message: "Starting managed goroutine",
|
||||
Pairs: map[string]interface{}{"name": name},
|
||||
Pairs: map[string]any{"name": name},
|
||||
})
|
||||
}
|
||||
fn(sm.ctx)
|
||||
@@ -64,7 +64,7 @@ func (sm *ShutdownManager) RunGoroutine(name string, fn func(context.Context)) {
|
||||
if logger != nil {
|
||||
logger.Debug(&libpack_logging.LogMessage{
|
||||
Message: "Managed goroutine finished",
|
||||
Pairs: map[string]interface{}{"name": name},
|
||||
Pairs: map[string]any{"name": name},
|
||||
})
|
||||
}
|
||||
}()
|
||||
@@ -114,7 +114,7 @@ func (sm *ShutdownManager) doShutdown(timeout time.Duration) error {
|
||||
if logger != nil {
|
||||
logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "Shutting down component",
|
||||
Pairs: map[string]interface{}{"component": c.Name},
|
||||
Pairs: map[string]any{"component": c.Name},
|
||||
})
|
||||
}
|
||||
if err := c.Shutdown(shutdownCtx); err != nil {
|
||||
@@ -124,7 +124,7 @@ func (sm *ShutdownManager) doShutdown(timeout time.Duration) error {
|
||||
if logger != nil {
|
||||
logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Error shutting down component",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"component": c.Name,
|
||||
"error": err.Error(),
|
||||
},
|
||||
|
||||
@@ -44,6 +44,7 @@ type config struct {
|
||||
CacheRedisEnable bool
|
||||
CacheMaxMemorySize int
|
||||
CacheMaxEntries int
|
||||
CacheUseLRU bool // Use LRU eviction algorithm instead of random eviction
|
||||
GraphQLQueryCacheSize int // Max number of parsed GraphQL queries to cache
|
||||
PerUserCacheDisabled bool // Disable per-user cache isolation (SECURITY RISK - not recommended)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package tracing provides OpenTelemetry distributed tracing integration
|
||||
// for the GraphQL proxy. Supports OTLP export to collectors like Jaeger,
|
||||
// Zipkin, or any OTLP-compatible backend.
|
||||
package tracing
|
||||
|
||||
import (
|
||||
|
||||
+23
-23
@@ -67,7 +67,7 @@ func NewWebSocketProxy(backendURL string, config WebSocketConfig, logger *libpac
|
||||
if logger != nil && config.Enabled {
|
||||
logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "WebSocket proxy enabled",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"backend_url": backendURL,
|
||||
"ping_interval": config.PingInterval,
|
||||
"max_message_size": config.MaxMessageSize,
|
||||
@@ -132,7 +132,7 @@ func (wsp *WebSocketProxy) handleConnection(ctx context.Context, clientConn *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "WebSocket connection established",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"active_connections": wsp.activeConnections.Load(),
|
||||
},
|
||||
@@ -150,13 +150,13 @@ func (wsp *WebSocketProxy) handleConnection(ctx context.Context, clientConn *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to read first message from client",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
clientConn.Close()
|
||||
_ = clientConn.Close() // Best-effort cleanup
|
||||
return
|
||||
}
|
||||
|
||||
@@ -170,16 +170,16 @@ func (wsp *WebSocketProxy) handleConnection(ctx context.Context, clientConn *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to connect to backend WebSocket",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
clientConn.Close()
|
||||
_ = clientConn.Close() // Best-effort cleanup
|
||||
return
|
||||
}
|
||||
defer backendConn.Close()
|
||||
defer func() { _ = backendConn.Close() }() // Best-effort cleanup
|
||||
|
||||
// Forward the first message (connection_init) to backend
|
||||
if err := backendConn.WriteMessage(messageType, message); err != nil {
|
||||
@@ -187,7 +187,7 @@ func (wsp *WebSocketProxy) handleConnection(ctx context.Context, clientConn *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Failed to forward connection_init to backend",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"error": err.Error(),
|
||||
},
|
||||
@@ -199,7 +199,7 @@ func (wsp *WebSocketProxy) handleConnection(ctx context.Context, clientConn *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Backend WebSocket connection established",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"subprotocol": backendConn.Subprotocol(),
|
||||
"has_authorization": headers.Get("Authorization") != "",
|
||||
@@ -231,7 +231,7 @@ func (wsp *WebSocketProxy) handleConnection(ctx context.Context, clientConn *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Info(&libpack_logger.LogMessage{
|
||||
Message: "WebSocket connection closed",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"duration_seconds": duration.Seconds(),
|
||||
"messages_sent": wsp.messagesSent.Load(),
|
||||
@@ -258,7 +258,7 @@ func (wsp *WebSocketProxy) proxyClientToBackend(ctx context.Context, client *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Client WebSocket closed normally",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
},
|
||||
})
|
||||
@@ -268,7 +268,7 @@ func (wsp *WebSocketProxy) proxyClientToBackend(ctx context.Context, client *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Error reading from client WebSocket",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"error": err.Error(),
|
||||
},
|
||||
@@ -286,7 +286,7 @@ func (wsp *WebSocketProxy) proxyClientToBackend(ctx context.Context, client *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Error writing to backend WebSocket",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"error": err.Error(),
|
||||
},
|
||||
@@ -298,7 +298,7 @@ func (wsp *WebSocketProxy) proxyClientToBackend(ctx context.Context, client *web
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Message proxied to backend",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"message_type": messageType,
|
||||
"message_size": len(message),
|
||||
@@ -322,7 +322,7 @@ func (wsp *WebSocketProxy) proxyBackendToClient(ctx context.Context, backend *go
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Backend WebSocket closed normally",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
},
|
||||
})
|
||||
@@ -332,7 +332,7 @@ func (wsp *WebSocketProxy) proxyBackendToClient(ctx context.Context, backend *go
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Error reading from backend WebSocket",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"error": err.Error(),
|
||||
},
|
||||
@@ -350,7 +350,7 @@ func (wsp *WebSocketProxy) proxyBackendToClient(ctx context.Context, backend *go
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Error writing to client WebSocket",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"error": err.Error(),
|
||||
},
|
||||
@@ -362,7 +362,7 @@ func (wsp *WebSocketProxy) proxyBackendToClient(ctx context.Context, backend *go
|
||||
if wsp.logger != nil {
|
||||
wsp.logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Message proxied to client",
|
||||
Pairs: map[string]interface{}{
|
||||
Pairs: map[string]any{
|
||||
"connection_id": connectionID,
|
||||
"message_type": messageType,
|
||||
"message_size": len(message),
|
||||
@@ -383,7 +383,7 @@ func (wsp *WebSocketProxy) extractAuthFromPayload(message []byte, originalHeader
|
||||
}
|
||||
|
||||
// Try to parse as JSON to extract headers from payload
|
||||
var msg map[string]interface{}
|
||||
var msg map[string]any
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
// Not JSON or parse error, return original headers
|
||||
return enrichedHeaders
|
||||
@@ -397,13 +397,13 @@ func (wsp *WebSocketProxy) extractAuthFromPayload(message []byte, originalHeader
|
||||
}
|
||||
|
||||
// Extract payload
|
||||
payload, ok := msg["payload"].(map[string]interface{})
|
||||
payload, ok := msg["payload"].(map[string]any)
|
||||
if !ok {
|
||||
return enrichedHeaders
|
||||
}
|
||||
|
||||
// Try to extract headers from payload.headers (graphql-ws format)
|
||||
if payloadHeaders, ok := payload["headers"].(map[string]interface{}); ok {
|
||||
if payloadHeaders, ok := payload["headers"].(map[string]any); ok {
|
||||
for key, value := range payloadHeaders {
|
||||
if strValue, ok := value.(string); ok {
|
||||
enrichedHeaders.Set(key, strValue)
|
||||
@@ -462,8 +462,8 @@ func (wsp *WebSocketProxy) dialBackend(ctx context.Context, headers http.Header)
|
||||
}
|
||||
|
||||
// GetStats returns WebSocket statistics
|
||||
func (wsp *WebSocketProxy) GetStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (wsp *WebSocketProxy) GetStats() map[string]any {
|
||||
return map[string]any{
|
||||
"enabled": wsp.enabled,
|
||||
"active_connections": wsp.activeConnections.Load(),
|
||||
"total_connections": wsp.totalConnections.Load(),
|
||||
|
||||
Reference in New Issue
Block a user