diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..82998d0 --- /dev/null +++ b/config/config.go @@ -0,0 +1,6 @@ +package libpack_config + +var ( + PKG_NAME string = "not-specified" + PKG_VERSION string = "0.0.0-dev" +) diff --git a/details.go b/details.go index 94a654e..a9f0309 100644 --- a/details.go +++ b/details.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/lukaszraczylo/ask" - libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" + libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" ) func extractClaimsFromJWTHeader(authorization string) (usr string, role string) { diff --git a/go.mod b/go.mod index d3838bf..70d0b3c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/lukaszraczylo/graphql-monitoring-proxy go 1.21 require ( + github.com/VictoriaMetrics/metrics v1.24.0 github.com/akyoto/cache v1.0.6 + github.com/buger/jsonparser v1.1.1 github.com/gofiber/fiber/v2 v2.49.2 github.com/gookit/goutil v0.6.12 github.com/graphql-go/graphql v0.8.1 @@ -11,20 +13,20 @@ require ( github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 github.com/lukaszraczylo/go-ratecounter v0.1.8 github.com/lukaszraczylo/go-simple-graphql v1.1.31 + github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.8.4 - github.com/telegram-bot-app/libpack v0.0.0-20231008100411-9f7f8bf94315 + github.com/telegram-bot-app/lib-logging v0.0.19 ) require ( dario.cat/mergo v1.0.0 // indirect - github.com/VictoriaMetrics/metrics v1.24.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/avast/retry-go/v4 v4.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.3.1 // indirect github.com/gookit/color v1.5.4 // indirect github.com/klauspost/compress v1.17.0 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lukaszraczylo/pandati v0.0.29 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -33,8 +35,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/rs/zerolog v1.31.0 // indirect - github.com/telegram-bot-app/lib-logging v0.0.19 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.50.0 // indirect github.com/valyala/fastrand v1.1.0 // indirect @@ -42,10 +43,12 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect github.com/wI2L/jsondiff v0.4.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3f1c172..c8a8794 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 h1:lvI8Wlbg4PxkRcg2f10wgoaRpfN19v+YdRek3+dLtlM= @@ -55,12 +58,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -72,8 +77,6 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/telegram-bot-app/lib-logging v0.0.19 h1:zbyFr2ygeBY+yuaB9moXyOGk8dIBCn0jPJQjvx7YvLE= github.com/telegram-bot-app/lib-logging v0.0.19/go.mod h1:n8d29fRUTdgJhC4RZ8s4lP2RHiGCCRYEj2ENEClUGc8= -github.com/telegram-bot-app/libpack v0.0.0-20231008100411-9f7f8bf94315 h1:gf+3gFgtdh48RQNmLNdK1IcGqpuTuj6RAdHxDMd/YPY= -github.com/telegram-bot-app/libpack v0.0.0-20231008100411-9f7f8bf94315/go.mod h1:W2kWHcfNNS0r++dJ1T2XX/C4cTSxI3MsoiMbOtyqu+I= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= diff --git a/graphql.go b/graphql.go index 5b7c359..5c6a916 100644 --- a/graphql.go +++ b/graphql.go @@ -7,7 +7,7 @@ import ( fiber "github.com/gofiber/fiber/v2" "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/parser" - libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" + libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" ) var retrospection_queries = []string{ diff --git a/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000..b8610e0 --- /dev/null +++ b/logging/logging.go @@ -0,0 +1,90 @@ +package libpack_logging + +import ( + "io" + "os" + "time" + + "github.com/gookit/goutil/envutil" + "github.com/rs/zerolog" +) + +type LogConfig struct { + logger zerolog.Logger +} + +var baseLogger zerolog.Logger + +func init() { + zerolog.TimeFieldFormat = time.RFC3339 + zerolog.MessageFieldName = "short_message" + zerolog.TimestampFieldName = "timestamp" + zerolog.LevelFieldName = "level" + zerolog.LevelFatalValue = "critical" + baseLogger = zerolog.New(os.Stdout).With().Timestamp().Logger() +} + +func NewLogger() *LogConfig { + switch logLevel := envutil.Getenv("LOG_LEVEL", "info"); logLevel { + case "debug": + baseLogger = baseLogger.Level(zerolog.DebugLevel) + case "warn": + baseLogger = baseLogger.Level(zerolog.WarnLevel) + case "error": + baseLogger = baseLogger.Level(zerolog.ErrorLevel) + default: + baseLogger = baseLogger.Level(zerolog.InfoLevel) + } + + return &LogConfig{logger: baseLogger} +} + +func (lw *LogConfig) log(w io.Writer, level zerolog.Level, message string, v map[string]interface{}) { + e := lw.logger.With().Logger() + e = e.Output(w) + event := e.WithLevel(level).CallerSkipFrame(3) + for k, val := range v { + switch v := val.(type) { + case string: + event.Str(k, v) + case int: + event.Int(k, v) + case float64: + event.Float64(k, v) + default: + event.Interface(k, val) + } + } + event.Msg(message) +} + +func (lw *LogConfig) Debug(message string, v ...map[string]interface{}) { + lw.log(os.Stdout, zerolog.DebugLevel, message, mergeMaps(v)) +} + +func (lw *LogConfig) Info(message string, v ...map[string]interface{}) { + lw.log(os.Stdout, zerolog.InfoLevel, message, mergeMaps(v)) +} + +func (lw *LogConfig) Warning(message string, v ...map[string]interface{}) { + lw.log(os.Stdout, zerolog.WarnLevel, message, mergeMaps(v)) +} + +func (lw *LogConfig) Error(message string, v ...map[string]interface{}) { + lw.log(os.Stderr, zerolog.ErrorLevel, message, mergeMaps(v)) +} + +func (lw *LogConfig) Critical(message string, v ...map[string]interface{}) { + lw.log(os.Stderr, zerolog.FatalLevel, message, mergeMaps(v)) + os.Exit(1) +} + +func mergeMaps(maps []map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for _, m := range maps { + for k, v := range m { + result[k] = v + } + } + return result +} diff --git a/logging/logging_bench_test.go b/logging/logging_bench_test.go new file mode 100644 index 0000000..06af809 --- /dev/null +++ b/logging/logging_bench_test.go @@ -0,0 +1,31 @@ +package libpack_logging + +import ( + "os" + "testing" +) + +func BenchmarkNewLogger(b *testing.B) { + for i := 0; i < b.N; i++ { + NewLogger() + } +} + +func BenchmarkInfoLog(b *testing.B) { + oldEnv := os.Getenv("LOG_LEVEL") + os.Setenv("LOG_LEVEL", "info") + oldStdout := os.Stdout + oldStderr := os.Stderr + os.Stdout, _ = os.Open(os.DevNull) + os.Stderr, _ = os.Open(os.DevNull) + defer func() { + os.Stdout = oldStdout + os.Stderr = oldStderr + os.Setenv("LOG_LEVEL", oldEnv) + }() + + testsLogger := NewLogger() + for i := 0; i < b.N; i++ { + testsLogger.Info("test", map[string]interface{}{"test": "test"}) + } +} diff --git a/logging/logging_test.go b/logging/logging_test.go new file mode 100644 index 0000000..823eac6 --- /dev/null +++ b/logging/logging_test.go @@ -0,0 +1,373 @@ +package libpack_logging + +import ( + "errors" + "io" + "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 := io.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 := io.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 +} diff --git a/main.go b/main.go index 8bc1aab..07d9756 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,8 @@ package main import ( "github.com/gookit/goutil/envutil" graphql "github.com/lukaszraczylo/go-simple-graphql" - libpack_config "github.com/telegram-bot-app/libpack/config" - libpack_logging "github.com/telegram-bot-app/libpack/logging" + libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" + libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" ) var cfg *config diff --git a/monitoring.go b/monitoring.go index a563bff..4089935 100644 --- a/monitoring.go +++ b/monitoring.go @@ -1,7 +1,7 @@ package main import ( - libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" + libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" ) func StartMonitoringServer() { diff --git a/monitoring/defaults.go b/monitoring/defaults.go new file mode 100644 index 0000000..cb17744 --- /dev/null +++ b/monitoring/defaults.go @@ -0,0 +1,12 @@ +package libpack_monitoring + +func (ms *MetricsSetup) RegisterDefaultMetrics() { + ms.RegisterMetricsCounter(MetricsSucceeded, nil) + ms.RegisterMetricsCounter(MetricsFailed, nil) + ms.RegisterMetricsCounter(MetricsSkipped, nil) + ms.RegisterMetricsHistogram(MetricsDuration, nil) +} + +func (ms *MetricsSetup) RegisterGoMetrics() { + // TODO: metrics.WriteProcessMetrics(ms.metrics_set) +} diff --git a/monitoring/helpers.go b/monitoring/helpers.go new file mode 100644 index 0000000..9f41f72 --- /dev/null +++ b/monitoring/helpers.go @@ -0,0 +1,57 @@ +package libpack_monitoring + +import ( + "fmt" + "strings" + + libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" +) + +func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) (complete_name string) { + if labels == nil { + labels = make(map[string]string) + } + labels["microservice"] = libpack_config.PKG_NAME + + if ms.metrics_prefix != "" { + complete_name = ms.metrics_prefix + "_" + name + } else { + complete_name = name + } + if labels != nil { + complete_name += "{" + for k, v := range labels { + complete_name += k + "=\"" + v + "\"," + } + complete_name = strings.TrimSuffix(complete_name, ",") + complete_name += "}" + } + return +} + +// validate_metrics_name validates the name of the metric to adhere to the Prometheus naming conventions +// https://prometheus.io/docs/practices/naming/ +func validate_metrics_name(name string) error { + // replace all spaces with underscores and remove all other non-alphanumeric characters + name_new := strings.ReplaceAll(name, " ", "_") + name_new = strings.Map(func(r rune) rune { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { + return r + } + return -1 + }, name_new) + name_new = strings.ReplaceAll(name_new, "__", "_") + name_new = strings.Trim(name_new, "_") + if name_new != name { + return fmt.Errorf("Invalid metric name: %s, expected %s", name, name_new) + } + return nil +} + +func compile_metrics_with_labels(name string, labels map[string]string) string { + metric_name := name + for k, v := range labels { + metric_name += "_" + k + "_" + v + } + return metric_name +} diff --git a/monitoring/helpers_test.go b/monitoring/helpers_test.go new file mode 100644 index 0000000..41a3019 --- /dev/null +++ b/monitoring/helpers_test.go @@ -0,0 +1,87 @@ +package libpack_monitoring + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func (suite *MonitoringTestSuite) Test_validate_metrics_name() { + type args struct { + name string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test_validate_metrics_name - valid", + args: args{ + name: "test_metrics_name", + }, + wantErr: false, + }, + { + name: "Test_validate_metrics_name - invalid", + args: args{ + name: "test metrics name", + }, + wantErr: true, + }, + { + name: "Test_validate_metrics_name - invalid - special chars", + args: args{ + name: "test_metrics_name!", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + err := validate_metrics_name(tt.args.name) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func (suite *MonitoringTestSuite) TestValidateMetricsName() { + tests := []struct { + name string + expected error + }{ + { + name: "valid_name", + expected: nil, + }, + { + name: "name with spaces", + expected: fmt.Errorf("Invalid metric name: %s, expected %s", "name with spaces", "name_with_spaces"), + }, + { + name: "name with non-alphanumeric characters", + expected: fmt.Errorf("Invalid metric name: %s, expected %s", "name with non-alphanumeric characters", "name_with_nonalphanumeric_characters"), + }, + { + name: "name__with__consecutive__underscores", + expected: fmt.Errorf("Invalid metric name: %s, expected %s", "name__with__consecutive__underscores", "name_with_consecutive_underscores"), + }, + { + name: "_name_with_leading_or_trailing_underscores_", + expected: fmt.Errorf("Invalid metric name: %s, expected %s", "_name_with_leading_or_trailing_underscores_", "name_with_leading_or_trailing_underscores"), + }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + err := validate_metrics_name(tt.name) + assert.Equal(t, tt.expected, err) + }) + } +} diff --git a/monitoring/monitoring.go b/monitoring/monitoring.go new file mode 100644 index 0000000..5f7c0ad --- /dev/null +++ b/monitoring/monitoring.go @@ -0,0 +1,129 @@ +// Package `libpack_monitoring` provides and easy way to add prometheus metrics to your application. +// It also provides a way to add custom metrics to the already started prometheus registry. + +package libpack_monitoring + +import ( + "fmt" + "time" + + "github.com/VictoriaMetrics/metrics" + "github.com/gofiber/fiber/v2" + "github.com/gookit/goutil/envutil" + logging "github.com/telegram-bot-app/lib-logging" +) + +type MetricsSetup struct { + metrics_prefix string + metrics_set *metrics.Set +} + +var ( + log *logging.LogConfig +) + +func NewMonitoring() *MetricsSetup { + log = logging.NewLogger() + ms := &MetricsSetup{} + ms.metrics_set = metrics.NewSet() + go ms.startPrometheusEndpoint() + return ms +} + +func (ms *MetricsSetup) startPrometheusEndpoint() { + app := fiber.New() + app.Get("/metrics", ms.metricsEndpoint) + err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393))) + if err != nil { + fmt.Println("Can't start the service: ", err) + } +} + +func (ms *MetricsSetup) metricsEndpoint(c *fiber.Ctx) error { + ms.metrics_set.WritePrometheus(c.Response().BodyWriter()) + return nil +} + +func (ms *MetricsSetup) AddMetricsPrefix(prefix string) { + ms.metrics_prefix = prefix + return +} + +func (ms *MetricsSetup) ListActiveMetrics() []string { + return ms.metrics_set.ListMetricNames() +} + +func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge { + if validate_metrics_name(metric_name) != nil { + log.Critical("RegisterMetricsGauge() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}) + return nil + } + return ms.metrics_set.GetOrCreateGauge(ms.get_metrics_name(metric_name, labels), func() float64 { + // get current value of the gauge and add val to it + return val + }) +} + +func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter { + if validate_metrics_name(metric_name) != nil { + log.Critical("RegisterMetricsCounter() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}) + return nil + } + return ms.metrics_set.GetOrCreateCounter(ms.get_metrics_name(metric_name, labels)) +} + +func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter { + if validate_metrics_name(metric_name) != nil { + log.Critical("RegisterFloatCounter() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}) + return nil + } + return ms.metrics_set.GetOrCreateFloatCounter(ms.get_metrics_name(metric_name, labels)) +} + +func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary { + if validate_metrics_name(metric_name) != nil { + log.Critical("RegisterMetricsSummary() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}) + return nil + } + return ms.metrics_set.GetOrCreateSummary(ms.get_metrics_name(metric_name, labels)) +} + +func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram { + if validate_metrics_name(metric_name) != nil { + log.Critical("RegisterMetricsHistogram() error", map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}) + return nil + } + return ms.metrics_set.GetOrCreateHistogram(ms.get_metrics_name(metric_name, labels)) +} + +func (ms *MetricsSetup) Increment(metric_name string, labels map[string]string) { + ms.RegisterMetricsCounter(metric_name, labels).Inc() +} + +func (ms *MetricsSetup) IncrementFloat(metric_name string, labels map[string]string, value float64) { + ms.RegisterFloatCounter(metric_name, labels).Add(value) +} + +func (ms *MetricsSetup) Set(metric_name string, labels map[string]string, value uint64) { + ms.RegisterMetricsCounter(metric_name, labels).Set(value) +} + +func (ms *MetricsSetup) Update(metric_name string, labels map[string]string, value float64) { + ms.RegisterMetricsHistogram(metric_name, labels).Update(value) +} + +func (ms *MetricsSetup) UpdateDuration(metric_name string, labels map[string]string, value time.Time) { + ms.RegisterMetricsHistogram(metric_name, labels).UpdateDuration(value) +} + +func (ms *MetricsSetup) UpdateSummary(metric_name string, labels map[string]string, value float64) { + ms.RegisterMetricsSummary(metric_name, labels).Update(value) +} + +func (ms *MetricsSetup) RemoveMetrics(metric_name string, labels map[string]string) { + ms.metrics_set.UnregisterMetric(ms.get_metrics_name(metric_name, labels)) +} + +func (ms *MetricsSetup) PurgeMetrics() { + ms.metrics_set.UnregisterAllMetrics() +} diff --git a/monitoring/structs.go b/monitoring/structs.go new file mode 100644 index 0000000..99f7710 --- /dev/null +++ b/monitoring/structs.go @@ -0,0 +1,8 @@ +package libpack_monitoring + +const ( + MetricsSucceeded = "requests_succesful" + MetricsFailed = "requests_failed" + MetricsDuration = "requests_duration" + MetricsSkipped = "requests_skipped" +) diff --git a/monitoring/suite_test.go b/monitoring/suite_test.go new file mode 100644 index 0000000..0de4b5a --- /dev/null +++ b/monitoring/suite_test.go @@ -0,0 +1,140 @@ +package libpack_monitoring + +import ( + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// import ( +// "fmt" +// "io" +// "net/http" +// "testing" +// "time" + +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/suite" +// libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" +// ) + +type MonitoringTestSuite struct { + suite.Suite + metrics_endpoint string +} + +func (suite *MonitoringTestSuite) SetupTest() { + suite.metrics_endpoint = "http://localhost:9393/metrics" +} + +func (suite *MonitoringTestSuite) TearDownTest() { +} + +func TestMonitoringSuite(t *testing.T) { + suite.Run(t, new(MonitoringTestSuite)) +} + +func (suite *MonitoringTestSuite) testing_call_metrics_endpoint() *http.Response { + resp, err := http.Get(suite.metrics_endpoint) + if err != nil { + suite.T().Error("Can't call metrics endpoint", err) + suite.T().FailNow() + } + return resp +} + +func (suite *MonitoringTestSuite) testing_get_body_as_text(resp *http.Response) string { + body, err := io.ReadAll(resp.Body) + if err != nil { + suite.T().Error("Can't read response body", err) + suite.T().FailNow() + } + return string(body) +} + +func (suite *MonitoringTestSuite) TestNewMonitoring() { + metrics_prefix := "within_test" + + suite.T().Run("TestWholeEndpoint", func(t *testing.T) { + mon := NewMonitoring() + mon.AddMetricsPrefix(metrics_prefix) + mon.RegisterDefaultMetrics() + + resp := suite.testing_call_metrics_endpoint() + assert.Equal(t, 200, resp.StatusCode) + assert.Contains(t, suite.testing_get_body_as_text(resp), "within_test_requests_succesful", "Metrics endpoint should contain metrics with prefix %s", metrics_prefix) + + mon.RegisterMetricsGauge("test_gauge_metrics", nil, 14) + resp = suite.testing_call_metrics_endpoint() + assert.Equal(t, 200, resp.StatusCode) + assert.Contains(t, suite.testing_get_body_as_text(resp), "within_test_test_gauge_metrics", "Metrics endpoint should contain metrics with prefix %s", metrics_prefix) + + // Triggering it again will increase the gauge by 1 + mon.RegisterMetricsGauge("test_gauge_metrics", nil, 7) + time.Sleep(1 * time.Second) + resp = suite.testing_call_metrics_endpoint() + assert.Equal(t, 200, resp.StatusCode) + assert.Contains(t, suite.testing_get_body_as_text(resp), "within_test_test_gauge_metrics 7", "Metrics endpoint should contain incremented () gauge metrics with prefix %s", metrics_prefix) + + mon.RegisterMetricsCounter("test_counter_metrics", nil) + mon.Increment("test_counter_metrics", nil) + time.Sleep(1 * time.Second) + resp = suite.testing_call_metrics_endpoint() + assert.Equal(t, 200, resp.StatusCode) + assert.Contains(t, suite.testing_get_body_as_text(resp), "within_test_test_counter_metrics", "Metrics endpoint should contain metrics with prefix %s", metrics_prefix) + mon.Increment("test_counter_metrics", nil) + time.Sleep(1 * time.Second) + resp = suite.testing_call_metrics_endpoint() + assert.Equal(t, 200, resp.StatusCode) + assert.Contains(t, suite.testing_get_body_as_text(resp), "within_test_test_counter_metrics 2", "Metrics endpoint should contain metrics with prefix %s", metrics_prefix) + + // mon.AddCustomMetrics(&CustomMetrics{ + // Name: "test_custom_metrics", + // Help: "test custom metrics", + // Type: TypeHistogram, + // }, libpack_config.PKG_NAME) + // resp = suite.testing_call_metrics_endpoint() + // assert.Equal(t, 200, resp.StatusCode) + // assert.Contains(t, suite.testing_get_body_as_text(resp), "within_test_test_custom_metrics", "Metrics endpoint should contain metrics with prefix %s", metrics_prefix) + // fmt.Println(suite.testing_get_body_as_text(resp)) + + // assert.Containsf(t, mon.ListActiveMetrics(), "test_custom_metrics", "ListActiveMetrics() should contain metrics with prefix %s", metrics_prefix) + + // mon.AddCustomMetrics(&CustomMetrics{ + // Name: "test_gauge_metrics", + // Help: "test gauge metrics", + // Type: TypeGauge, + // }, libpack_config.PKG_NAME) + // mon.Increment("test_gauge_metrics") + // time.Sleep(2 * time.Second) + // resp = suite.testing_call_metrics_endpoint() + // assert.Equal(t, 200, resp.StatusCode) + // assert.Contains(t, suite.testing_get_body_as_text(resp), "test_gauge_metrics{microservice=\""+libpack_config.PKG_NAME+"\"} 1", "Metrics endpoint should contain incremented (1) gauge metrics with prefix %s", metrics_prefix) + + // mon.Increment("test_gauge_metrics") + // time.Sleep(2 * time.Second) + // resp = suite.testing_call_metrics_endpoint() + // assert.Equal(t, 200, resp.StatusCode) + // assert.Contains(t, suite.testing_get_body_as_text(resp), "test_gauge_metrics{microservice=\""+libpack_config.PKG_NAME+"\"} 2", "Metrics endpoint should contain incremented (2) gauge metrics with prefix %s", metrics_prefix) + + // mon.AddCustomMetrics(&CustomMetrics{ + // Name: "test_counter_metrics", + // Help: "test counter metrics", + // Type: TypeCounter, + // }, libpack_config.PKG_NAME) + // mon.Set("test_counter_metrics", 7) + // time.Sleep(2 * time.Second) + // resp = suite.testing_call_metrics_endpoint() + // assert.Equal(t, 200, resp.StatusCode) + // assert.Contains(t, suite.testing_get_body_as_text(resp), "test_counter_metrics{microservice=\""+libpack_config.PKG_NAME+"\"} 7", "Metrics endpoint should contain metrics with prefix %s", metrics_prefix) + + resp = suite.testing_call_metrics_endpoint() + assert.Equal(t, 200, resp.StatusCode) + assert.Contains(t, suite.testing_get_body_as_text(resp), "go_goroutines", "Metrics endpoint should contain metrics with prefix %s", metrics_prefix) + + }) +} diff --git a/proxy.go b/proxy.go index 2bd335d..4e9b66e 100644 --- a/proxy.go +++ b/proxy.go @@ -5,7 +5,7 @@ import ( fiber "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/proxy" - libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" + libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" ) func proxyTheRequest(c *fiber.Ctx) error { diff --git a/server.go b/server.go index adef4bf..3e82f1c 100644 --- a/server.go +++ b/server.go @@ -8,7 +8,7 @@ import ( "github.com/gofiber/fiber/v2/middleware/cors" jsoniter "github.com/json-iterator/go" - libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" + libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" ) var json = jsoniter.ConfigCompatibleWithStandardLibrary diff --git a/struct_config.go b/struct_config.go index 0bad4b3..82335e3 100644 --- a/struct_config.go +++ b/struct_config.go @@ -3,8 +3,8 @@ package main import ( "github.com/akyoto/cache" graphql "github.com/lukaszraczylo/go-simple-graphql" - libpack_logging "github.com/telegram-bot-app/libpack/logging" - libpack_monitoring "github.com/telegram-bot-app/libpack/monitoring" + libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" + libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" ) // config is a struct that holds the configuration of the application.