Compare commits

...
9 Commits
9 changed files with 594 additions and 103 deletions
+46 -21
View File
@@ -9,7 +9,9 @@ Ps. It's Hasura friendly.
- [Features](#features)
- [Usage example](#usage-example)
- [Environment variables](#environment-variables)
- [Cache](#cache)
- [Example reader code](#example-reader-code)
- [Tips](#tips)
- [Working with results](#working-with-results)
## Reasoning
@@ -37,6 +39,22 @@ Therefore, I present you the simple client to which you can copy & paste your gr
* `RETRIES_NUMBER` - Number of retries: Default: `1`
* `RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
### Cache
You have two options to enable the cache:
* Use `GRAPHQL_CACHE` environment variable which will enable the cache globally. It may be desired if you want to use the cache for all queries.
* Add `gqlcache: true` header for your query which will enable the cache for this query only with `GRAPHQL_CACHE_TTL` TTL.
Example:
```go
headers := map[string]interface{}{
...
"gqlcache": true,
}
```
### Example reader code
@@ -46,32 +64,39 @@ import (
graphql "github.com/lukaszraczylo/go-simple-graphql"
)
headers := map[string]interface{}{
"x-hasura-user-id": 37,
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
}
variables := map[string]interface{}{
"fileHash": "123deadc0w321",
}
query := `query searchFileKnown($fileHash: String) {
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
racy
violence
virus
func main() {
headers := map[string]interface{}{
"x-hasura-user-id": 37,
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
}
}`
gql := graphql.NewConnection()
result, err := gql.Query(query, variables, headers)
if err != nil {
fmt.Println("Query error", err)
return
variables := map[string]interface{}{
"fileHash": "123deadc0w321",
}
query := `query searchFileKnown($fileHash: String) {
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
racy
violence
virus
}
}`
gql := graphql.NewConnection()
result, err := gql.Query(query, variables, headers)
if err != nil {
fmt.Println("Query error", err)
return
}
fmt.Println(result)
}
fmt.Println(result)
```
### Tips
* Connection handler ( `gql := graphql.NewConnection()` ) should be created once and reused in the application especially if you run dozens of queries per second. It will allow you also to use cache and http2 to its full potential.
**Result**
```json
+10 -9
View File
@@ -1,23 +1,24 @@
module github.com/lukaszraczylo/go-simple-graphql
go 1.18
go 1.20
require (
github.com/allegro/bigcache/v3 v3.1.0
github.com/avast/retry-go v3.0.0+incompatible
github.com/buger/jsonparser v1.1.1
github.com/json-iterator/go v1.1.12
github.com/lukaszraczylo/pandati v0.0.15
github.com/rs/zerolog v1.28.0
github.com/lukaszraczylo/pandati v0.0.20
github.com/rs/zerolog v1.29.0
github.com/sethvargo/go-retry v0.2.4
github.com/stretchr/testify v1.7.1
golang.org/x/net v0.4.0
golang.org/x/net v0.8.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -25,7 +26,7 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/wI2L/jsondiff v0.3.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+20 -15
View File
@@ -2,24 +2,29 @@ github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3b
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/lukaszraczylo/pandati v0.0.15 h1:1BNtbmhIQbKgs1Gm9WlH3CMms+TgkgxZcU7utHbjloY=
github.com/lukaszraczylo/pandati v0.0.15/go.mod h1:5E0mqlNgdOps6FW2X4tPilr+3B8s9JGWFgA6ZVFixyQ=
github.com/lukaszraczylo/pandati v0.0.18 h1:6khtDY6QXkiyh4a3dzHtIzx/9UqUGfHjzKBCA0SIurI=
github.com/lukaszraczylo/pandati v0.0.18/go.mod h1:Aqlc0KH+ssvrscYYbEKMv7n5cFwe+0MjsJbikNz1h60=
github.com/lukaszraczylo/pandati v0.0.20 h1:LqF7CS3z9Zi04KwEY8wd/YUpfWKSwi0nc7pagKQLXHc=
github.com/lukaszraczylo/pandati v0.0.20/go.mod h1:ZegK9ll9UebKV1uNMF73qdLb43G/O4CSOdPglu6NniQ=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -29,8 +34,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
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/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -46,17 +51,17 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+28 -16
View File
@@ -18,6 +18,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
bigcache "github.com/allegro/bigcache/v3"
@@ -32,15 +33,15 @@ var (
)
type GraphQL struct {
Endpoint string
BackoffSetup retry.Backoff
HttpClient *http.Client
Log *logging.LogConfig
Cache bool // Enable caching for read queries
CacheStore *bigcache.BigCache
RetriesEnable bool
Endpoint string
RetriesNumber int
RetriesDelay time.Duration
BackoffSetup retry.Backoff
Cache bool
RetriesEnable bool
}
func pickGraphqlEndpoint() (graphqlEndpoint string) {
@@ -81,19 +82,30 @@ func setCacheEnabled() bool {
}
func NewConnection() *GraphQL {
endpoint := pickGraphqlEndpoint()
var httpClient *http.Client
if strings.HasPrefix(endpoint, "http://") {
httpClient = &http.Client{
Transport: http.DefaultTransport,
}
} else {
tlsClientConfig := &tls.Config{}
if strings.HasPrefix(endpoint, "https://") {
tlsClientConfig.InsecureSkipVerify = true
}
http2Transport := &http2.Transport{
AllowHTTP: true,
TLSClientConfig: tlsClientConfig,
}
httpClient = &http.Client{
Transport: http2Transport,
}
}
g := GraphQL{
Endpoint: pickGraphqlEndpoint(),
HttpClient: &http.Client{
Transport: &http2.Transport{
ReadIdleTimeout: 30 * time.Second,
DisableCompression: false,
AllowHTTP: true,
PingTimeout: 5 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
Endpoint: endpoint,
HttpClient: httpClient,
Log: logging.NewLogger(),
Cache: setCacheEnabled(),
CacheStore: setupCache(),
+1 -1
View File
@@ -35,9 +35,9 @@ func (suite *TestSuite) TestNewConnection() {
endpoint string
}
tests := []struct {
want *GraphQL
name string
args args
want *GraphQL
}{
{
name: "New connection: Env variable endpoint",
+33 -28
View File
@@ -6,6 +6,7 @@ import (
"os"
"time"
"github.com/lukaszraczylo/pandati"
"github.com/rs/zerolog"
)
@@ -16,14 +17,14 @@ type LogConfig struct {
func NewLogger() *LogConfig {
log := new(LogConfig)
dll := os.Getenv("LOG_LEVEL")
if dll == "debug" {
switch dll := os.Getenv("LOG_LEVEL"); dll {
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else if dll == "warn" {
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
} else if dll == "error" {
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
} else {
default:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
zerolog.TimeFieldFormat = time.RFC3339
@@ -36,12 +37,36 @@ func NewLogger() *LogConfig {
return log
}
func (l *LogConfig) Log(level zerolog.Level, message string, v ...map[string]interface{}) {
contextLog := l.Logger.WithLevel(level)
func (l *LogConfig) Critical(message string, v ...map[string]interface{}) {
w := l.Logger.Output(os.Stderr)
log(w.Fatal(), message, v...)
}
func (l *LogConfig) Error(message string, v ...map[string]interface{}) {
w := l.Logger.Output(os.Stderr)
log(w.Error(), message, v...)
}
func (l *LogConfig) Warning(message string, v ...map[string]interface{}) {
w := l.Logger.Output(os.Stdout)
log(w.Warn(), message, v...)
}
func (l *LogConfig) Info(message string, v ...map[string]interface{}) {
w := l.Logger.Output(os.Stdout)
log(w.Info(), message, v...)
}
func (l *LogConfig) Debug(message string, v ...map[string]interface{}) {
w := l.Logger.Output(os.Stdout)
log(w.Debug(), message, v...)
}
func log(contextLog *zerolog.Event, message string, v ...map[string]interface{}) {
if len(v) > 0 {
for _, m := range v {
for k, v := range m {
if v != nil && k != "" {
if !pandati.IsZero(v) && !pandati.IsZero(k) {
contextLog.Str(k, fmt.Sprintf("%v", v))
}
}
@@ -50,23 +75,3 @@ func (l *LogConfig) Log(level zerolog.Level, message string, v ...map[string]int
// contextLog.Str("_stack_trace", pandati.Trace())
contextLog.Msg(message)
}
func (l *LogConfig) Critical(message string, v ...map[string]interface{}) {
l.Log(zerolog.FatalLevel, message, v...)
}
func (l *LogConfig) Error(message string, v ...map[string]interface{}) {
l.Log(zerolog.ErrorLevel, message, v...)
}
func (l *LogConfig) Warning(message string, v ...map[string]interface{}) {
l.Log(zerolog.WarnLevel, message, v...)
}
func (l *LogConfig) Info(message string, v ...map[string]interface{}) {
l.Log(zerolog.InfoLevel, message, v...)
}
func (l *LogConfig) Debug(message string, v ...map[string]interface{}) {
l.Log(zerolog.DebugLevel, message, v...)
}
+373
View File
@@ -0,0 +1,373 @@
package logging
import (
"errors"
"io/ioutil"
"os"
"reflect"
"testing"
"github.com/buger/jsonparser"
"github.com/stretchr/testify/suite"
)
type LoggingTestSuite struct {
suite.Suite
}
var (
testsLogger *LogConfig
)
type stdoutCapture struct {
oldStdout *os.File
readPipe *os.File
}
func (sc *stdoutCapture) StartCapture() {
sc.oldStdout = os.Stdout
sc.readPipe, os.Stdout, _ = os.Pipe()
}
func (sc *stdoutCapture) StopCapture() (string, error) {
if sc.oldStdout == nil || sc.readPipe == nil {
return "", errors.New("StartCapture not called before StopCapture on Stdout")
}
os.Stdout.Close()
os.Stdout = sc.oldStdout
bytes, err := ioutil.ReadAll(sc.readPipe)
if err != nil {
return "", err
}
return string(bytes), nil
}
type stderrCapture struct {
oldStderr *os.File
readPipe *os.File
}
func (sc *stderrCapture) StartCapture() {
sc.oldStderr = os.Stderr
sc.readPipe, os.Stderr, _ = os.Pipe()
}
func (sc *stderrCapture) StopCapture() (string, error) {
if sc.oldStderr == nil || sc.readPipe == nil {
return "", errors.New("StartCapture not called before StopCapture on Stderr")
}
os.Stderr.Close()
os.Stderr = sc.oldStderr
bytes, err := ioutil.ReadAll(sc.readPipe)
if err != nil {
return "", err
}
return string(bytes), nil
}
func (suite *LoggingTestSuite) SetupTest() {
}
func TestLoggingTestSuite(t *testing.T) {
suite.Run(t, new(LoggingTestSuite))
}
func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
type args struct {
message string
}
tests := []struct {
name string
args args
wantLevel string
wantMessage string
envMinLogLevel string
loggerType string
stdOutExpect bool
stdErrExpect bool
}{
{
name: "Test log: Error",
loggerType: "Error",
args: args{
message: "This is a error message",
},
wantLevel: "error",
wantMessage: "This is a error message",
stdErrExpect: true,
stdOutExpect: false,
},
{
name: "Test log: Warning",
loggerType: "Warning",
args: args{
message: "This is a warning message",
},
wantLevel: "warn",
wantMessage: "This is a warning message",
stdErrExpect: false,
stdOutExpect: true,
envMinLogLevel: "info",
},
{
name: "Test log: Warning | Min level: Debug",
loggerType: "Warning",
args: args{
message: "This is a warning message",
},
wantLevel: "warn",
wantMessage: "This is a warning message",
stdErrExpect: false,
stdOutExpect: true,
envMinLogLevel: "debug",
},
{
name: "Test log: Info",
loggerType: "Info",
args: args{
message: "This is a info message",
},
wantLevel: "info",
wantMessage: "This is a info message",
stdErrExpect: false,
stdOutExpect: true,
},
{
name: "Test log: Info | Min level: Warn",
loggerType: "Info",
args: args{
message: "This is a info message",
},
wantLevel: "",
wantMessage: "",
stdErrExpect: false,
stdOutExpect: false,
envMinLogLevel: "warn",
},
{
name: "Test log: Warning | Min level: Warn",
loggerType: "Warning",
args: args{
message: "This is a warning message",
},
wantLevel: "warn",
wantMessage: "This is a warning message",
stdErrExpect: false,
stdOutExpect: true,
envMinLogLevel: "warn",
},
{
name: "Test log: Warning | Min level: Error",
loggerType: "Warning",
args: args{
message: "This is an error message",
},
wantLevel: "",
wantMessage: "",
stdErrExpect: false,
stdOutExpect: false,
envMinLogLevel: "error",
},
{
name: "Test log: Debug | Min level: Debug",
loggerType: "Debug",
args: args{
message: "This is a debug message",
},
wantLevel: "debug",
wantMessage: "This is a debug message",
stdErrExpect: false,
stdOutExpect: true,
envMinLogLevel: "debug",
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
if tt.envMinLogLevel != "" {
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
defer os.Unsetenv("LOG_LEVEL")
}
testsLogger = NewLogger()
captureStdout := stdoutCapture{}
captureStdout.StartCapture()
captureStderr := stderrCapture{}
captureStderr.StartCapture()
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{reflect.ValueOf(tt.args.message)})
stdoutOut, err := captureStdout.StopCapture()
if err != nil {
suite.T().Fatal(err)
}
stderrOut, err := captureStderr.StopCapture()
if err != nil {
suite.T().Fatal(err)
}
if tt.stdErrExpect && !tt.stdOutExpect {
gotLvl, gotMsg, err := getResponseValues(stderrOut, "short_message")
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDERR]: "+tt.name)
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
suite.Equal("", stdoutOut, "Failed in [STDERR]: "+tt.name)
}
if tt.stdOutExpect && !tt.stdErrExpect {
gotLvl, gotMsg, err := getResponseValues(stdoutOut, "short_message")
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDOUT]: "+tt.name)
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
suite.Equal("", stderrOut, "Failed in [STDOUT]: "+tt.name)
}
if !tt.stdErrExpect && !tt.stdOutExpect {
suite.Equal("", stderrOut, "Failed in [NEITHER]: "+tt.name)
suite.Equal("", stdoutOut, "Failed in [NEITHER]: "+tt.name)
}
os.Unsetenv("LOG_LEVEL")
})
}
}
func (suite *LoggingTestSuite) TestFullMessage() {
type args struct {
extraFields map[string]interface{}
message string
}
extraFields := make(map[string]interface{})
extraFields["_full_message"] = "full message"
tests := []struct {
args args
name string
wantLevel string
wantMessage string
envMinLogLevel string
loggerType string
stdOutExpect bool
stdErrExpect bool
}{
{
name: "Test log: Error",
loggerType: "Error",
args: args{
message: "This is a error message",
extraFields: extraFields,
},
wantLevel: "error",
wantMessage: extraFields["_full_message"].(string),
stdErrExpect: true,
stdOutExpect: false,
},
{
name: "Test log: Info",
loggerType: "Info",
args: args{
message: "This is a info message",
extraFields: extraFields,
},
wantMessage: extraFields["_full_message"].(string),
stdErrExpect: false,
stdOutExpect: true,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
if tt.envMinLogLevel != "" {
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
defer os.Unsetenv("LOG_LEVEL")
}
testsLogger = NewLogger()
captureStdout := stdoutCapture{}
captureStdout.StartCapture()
captureStderr := stderrCapture{}
captureStderr.StartCapture()
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{
reflect.ValueOf(tt.args.message),
reflect.ValueOf(tt.args.extraFields),
})
stdoutOut, err := captureStdout.StopCapture()
if err != nil {
suite.T().Fatal(err)
}
stderrOut, err := captureStderr.StopCapture()
if err != nil {
suite.T().Fatal(err)
}
if tt.stdErrExpect && !tt.stdOutExpect {
_, gotMsg, err := getResponseValues(stderrOut, "_full_message")
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
}
if tt.stdOutExpect && !tt.stdErrExpect {
_, gotMsg, err := getResponseValues(stdoutOut, "_full_message")
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
}
os.Unsetenv("LOG_LEVEL")
})
}
}
func Test_getResponseValues(t *testing.T) {
type args struct {
sourceJson string
}
tests := []struct {
name string
args args
wantGotLvl string
wantGotMsg string
wantErr bool
}{
{
name: "Test with json",
args: args{
sourceJson: `{"level": "debug", "short_message": "hello world"`,
},
wantGotLvl: "debug",
wantGotMsg: "hello world",
wantErr: false,
},
{
name: "Test with json, wrong message field",
args: args{
sourceJson: `{"level": "debug", "message": "hello world"`,
},
wantGotLvl: "debug",
wantGotMsg: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotGotLvl, gotGotMsg, err := getResponseValues(tt.args.sourceJson, "short_message")
if (err != nil) != tt.wantErr {
t.Errorf("getResponseValues() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotGotLvl != tt.wantGotLvl {
t.Errorf("getResponseValues() gotGotLvl = %v, want %v", gotGotLvl, tt.wantGotLvl)
}
if gotGotMsg != tt.wantGotMsg {
t.Errorf("getResponseValues() gotGotMsg = %v, want %v", gotGotMsg, tt.wantGotMsg)
}
})
}
}
func getResponseValues(sourceJson string, key string) (gotLvl, gotMsg string, err error) {
gotLvl, err = jsonparser.GetString([]byte(sourceJson), "level")
if err != nil {
return
}
gotMsg, err = jsonparser.GetString([]byte(sourceJson), key)
return
}
+14 -7
View File
@@ -13,8 +13,8 @@ import (
)
type requestBase struct {
Query string `json:"query"`
Variables interface{} `json:"variables"`
Query string `json:"query"`
}
type queryResults struct {
@@ -47,15 +47,21 @@ func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHe
}
var body []byte
var queryResult *queryResults
queryHash := fmt.Sprintf("%x", md5.Sum(query))
var queryResult queryResults
var queryHash string
cacheEnabled := queryHeaders["gqlcache"] == true
// If header 'gqlcache' is set to true, we will enforce the cache for the query
if cacheEnabled || g.Cache {
g.Log.Debug("Forced cache for query", map[string]interface{}{"_query": queryContent, "_headers": queryHeaders})
g.Cache = true
queryHash = fmt.Sprintf("%x", md5.Sum(query))
if g.Cache {
g.Log.Debug("Checking the cache for the query", map[string]interface{}{"_query": queryHash})
if entry, entryInfo, err := g.CacheStore.GetWithInfo(queryHash); err == nil {
g.Log.Debug("Found the query in the cache", map[string]interface{}{"_query": queryHash})
if pandati.IsZero(entryInfo.EntryStatus) {
return string(entry), nil
return pandati.BytesToString(entry), nil
}
} else {
g.Log.Debug("Unable to find the query in the cache", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
@@ -70,13 +76,14 @@ func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHe
}
for header, value := range queryHeaders {
httpRequest.Header.Add(fmt.Sprintf("%v", header), fmt.Sprintf("%v", value))
httpRequest.Header.Add(header, fmt.Sprintf("%v", value))
}
var httpResponse *http.Response
err = retry.Do(
func() error {
g.Log.Debug("Sending the query")
httpResponse, err = g.HttpClient.Do(httpRequest)
if err != nil {
@@ -123,7 +130,7 @@ func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHe
if g.Cache {
g.Log.Debug("Caching the query", map[string]interface{}{"_query": queryHash})
if queryContent[0:5] == "query" {
err = g.CacheStore.Set(queryHash, []byte(responseContent))
err = g.CacheStore.Set(queryHash, pandati.StringToBytes(responseContent))
if err != nil {
g.Log.Error("Unable to cache the query", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
}
+69 -6
View File
@@ -11,8 +11,8 @@ import (
func (suite *TestSuite) Test_GraphQL_queryBuilder() {
type args struct {
queryContent string
queryVariables interface{}
queryContent string
}
tests := []struct {
name string
@@ -26,7 +26,7 @@ func (suite *TestSuite) Test_GraphQL_queryBuilder() {
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
queryVariables: nil,
},
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":null}`),
want: []byte(`{"variables":null,"query":"query listUserBots { tbl_bots { bot_name } }"}`),
wantErr: false,
},
{
@@ -35,7 +35,7 @@ func (suite *TestSuite) Test_GraphQL_queryBuilder() {
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
queryVariables: map[string]interface{}{"user_id": 1},
},
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":{"user_id":1}}`),
want: []byte(`{"variables":{"user_id":1},"query":"query listUserBots { tbl_bots { bot_name } }"}`),
wantErr: false,
},
}
@@ -58,17 +58,17 @@ func (suite *TestSuite) Test_GraphQL_Query() {
g := NewConnection()
type args struct {
queryContent string
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
args args
name string
endpoint string
wantResult string
isLocal bool
cache_enabled bool
args args
wantResult string
wantErr bool
}{
{
@@ -125,6 +125,69 @@ func (suite *TestSuite) Test_GraphQL_Query() {
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query, with cache per query empty",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: true,
cache_enabled: false,
args: args{
queryContent: `query packages_prices_no_inlinecache {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}
}`,
queryVariables: nil,
queryHeaders: map[string]interface{}{
"gqlcache": false,
},
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query, with cache per query",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: true,
cache_enabled: false,
args: args{
queryContent: `query packages_prices_inlinecache {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}
}`,
queryVariables: nil,
queryHeaders: map[string]interface{}{
"gqlcache": true,
},
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query, with cache per query [verify]",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: true,
cache_enabled: false,
args: args{
queryContent: `query packages_prices_inlinecache {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}
}`,
queryVariables: nil,
queryHeaders: map[string]interface{}{
"gqlcache": true,
},
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query, with cache filled",
endpoint: "https://telegram-bot.app/v1/graphql",