diff --git a/README.md b/README.md index dfa435e..039023a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ - [Client](./client/README.md) - [Handler](./handler/README.md) +- [Logger](./logger/README.md) - [Mock](./mock/README.md) - [Query](./query/README.md) - [Response](./response/README.md) diff --git a/handler/handler.go b/handler/handler.go index 9a643b1..bba322d 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/Moranilt/http-utils/logger" "github.com/Moranilt/http-utils/response" "github.com/Moranilt/http-utils/tiny_errors" "github.com/gorilla/mux" @@ -23,20 +24,11 @@ type HandlerMaker[ReqT any, RespT any] struct { request *http.Request response http.ResponseWriter requestBody ReqT - logger Logger + logger logger.Logger caller CallerFunc[ReqT, RespT] err error } -type Logger interface { - WithRequestInfo(r *http.Request) Logger - With(args ...any) Logger - - Debug(msg string, args ...any) - Info(msg string, args ...any) - Error(msg string, args ...any) -} - // A function that is called to process request. // // ReqT - type of request body @@ -46,7 +38,7 @@ type CallerFunc[ReqT any, RespT any] func(ctx context.Context, req ReqT) (RespT, // Create new handler instance // // **caller** should be a function that implements type CallerFunc[ReqT, RespT] -func New[ReqT any, RespT any](w http.ResponseWriter, r *http.Request, logger Logger, caller CallerFunc[ReqT, RespT]) *HandlerMaker[ReqT, RespT] { +func New[ReqT any, RespT any](w http.ResponseWriter, r *http.Request, logger logger.Logger, caller CallerFunc[ReqT, RespT]) *HandlerMaker[ReqT, RespT] { log := logger.WithRequestInfo(r) return &HandlerMaker[ReqT, RespT]{ logger: log, diff --git a/handler/handler_test.go b/handler/handler_test.go index c4746db..2bdda13 100644 --- a/handler/handler_test.go +++ b/handler/handler_test.go @@ -13,31 +13,12 @@ import ( "strings" "testing" + "github.com/Moranilt/http-utils/logger" "github.com/Moranilt/http-utils/response" "github.com/Moranilt/http-utils/tiny_errors" "github.com/gorilla/mux" ) -type mockedLogger struct{} - -func NewLogger() Logger { - return &mockedLogger{} -} - -func (l *mockedLogger) WithRequestInfo(r *http.Request) Logger { - return l -} - -func (l *mockedLogger) With(args ...any) Logger { - return l -} - -func (l *mockedLogger) Debug(msg string, args ...any) {} - -func (l *mockedLogger) Info(msg string, args ...any) {} - -func (l *mockedLogger) Error(msg string, args ...any) {} - type mockRequest struct { Name string `json:"name,omitempty" mapstructure:"name"` Phone string `json:"phone,omitempty" mapstructure:"phone"` @@ -70,7 +51,7 @@ func makeMockedFunction[ReqT any, RespT any](requestValidator func(request ReqT) } func TestHandler(t *testing.T) { - logger := NewLogger() + logger := logger.NewMock() t.Run("default handler Run", func(t *testing.T) { routePath := "/test-route" @@ -396,7 +377,7 @@ func TestHandler(t *testing.T) { } func BenchmarkMultipart(b *testing.B) { - logger := NewLogger() + logger := logger.NewMock() routePath := "/test-files" testHandler := makeMockedFunction(func(request mockMultipartRequest) *mockResponse { @@ -490,7 +471,7 @@ func newTestHandleController[ReqT any, RespT any]( } } -func (cntr *testHandleFuncController[ReqT, RespT]) Run(t testing.TB, logger Logger) { +func (cntr *testHandleFuncController[ReqT, RespT]) Run(t testing.TB, logger logger.Logger) { router := mux.NewRouter() requestPath := cntr.routePath if cntr.vars != nil { diff --git a/logger/README.md b/logger/README.md new file mode 100644 index 0000000..d7c112c --- /dev/null +++ b/logger/README.md @@ -0,0 +1,31 @@ +# Logger +Default logger for your service. + +# Examples +## Default +```go +import ( + "github.com/Moranilt/http-utils/logger" +) + +func main() { + log := logger.New(os.Stdout, logger.TYPE_JSON) + log.Info("Hello World") + // Output: {"level":"INFO","message":"Hello World","time":"2020-07-20T17:22:54+03:00"} +} +``` + +## Global +```go +import ( + "github.com/Moranilt/http-utils/logger" +) + +func main(){ + log := logger.New(os.Stdout, logger.TYPE_JSON) + logger.SetDefault(log) + + logger.Default().Info("Hello World") + // Output: {"level":"INFO","message":"Hello World","time":"2020-07-20T17:22:54+03:00"} +} +``` \ No newline at end of file diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..fd75ed0 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,214 @@ +package logger + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "sync/atomic" + + "log/slog" +) + +type LoggerType string + +const ( + TYPE_JSON LoggerType = "json" + TYPE_DEFAULT LoggerType = "default" +) + +type ContextKey string + +var defaultLogger atomic.Value + +func init() { + defaultLogger.Store(New(os.Stdout, TYPE_JSON)) +} + +func SetDefault(l Logger) { + defaultLogger.Store(l) +} + +func Default() Logger { + return defaultLogger.Load().(Logger) +} + +const ( + CtxRequestId ContextKey = "request_id" +) + +const ( + LevelTrace = slog.Level(-8) + LevelNotice = slog.Level(2) + LevelFatal = slog.Level(12) + LevelError = slog.Level(4) + LevelDebug = slog.Level(1) + LevelInfo = slog.Level(0) +) + +var LevelNames = map[slog.Leveler]string{ + LevelTrace: "TRACE", + LevelNotice: "NOTICE", + LevelFatal: "FATAL", + LevelError: "ERROR", + LevelDebug: "DEBUG", + LevelInfo: "INFO", +} + +type SLogger struct { + root *slog.Logger +} + +type Logger interface { + Trace(msg string, args ...any) + + Notice(msg string, args ...any) + + Error(msg string, args ...any) + Fatal(msg string, args ...any) + Fatalf(format string, args ...any) + Errorf(format string, args ...any) + + Debug(msg string, args ...any) + Debugf(format string, args ...any) + + Info(msg string, args ...any) + Infof(format string, args ...any) + + Log(ctx context.Context, level slog.Level, msg string, args ...any) + + With(args ...any) Logger + WithRequestInfo(r *http.Request) Logger + WithField(key string, value any) Logger + WithFields(fields ...any) Logger +} + +func New(output io.Writer, t LoggerType) Logger { + if output == nil { + output = os.Stdout + } + + var l *slog.Logger + if t == TYPE_JSON { + l = slog.New(slog.NewJSONHandler(output, &slog.HandlerOptions{ + Level: LevelTrace, + ReplaceAttr: renameLevel, + })) + } else { + l = slog.New(slog.NewTextHandler(output, &slog.HandlerOptions{ + Level: LevelTrace, + ReplaceAttr: renameLevel, + })) + } + + logger := &SLogger{ + l, + } + return logger +} + +func (s *SLogger) Error(msg string, args ...any) { + s.root.Log(context.Background(), LevelError, msg, args...) +} + +func (s *SLogger) Debug(msg string, args ...any) { + s.root.Log(context.Background(), LevelDebug, msg, args...) +} + +func (s *SLogger) Trace(msg string, args ...any) { + s.root.Log(context.Background(), LevelTrace, msg, args...) +} + +func (s *SLogger) Notice(msg string, args ...any) { + s.root.Log(context.Background(), LevelNotice, msg, args...) +} + +func (s *SLogger) Fatal(msg string, args ...any) { + s.root.Log(context.Background(), LevelFatal, msg, args...) + os.Exit(1) +} + +func (s *SLogger) Fatalf(format string, args ...any) { + s.root.Log(context.Background(), LevelFatal, fmt.Sprintf(format, args...)) + os.Exit(1) +} + +func (s *SLogger) Errorf(format string, args ...any) { + s.root.Log(context.Background(), LevelError, fmt.Sprintf(format, args...)) +} + +func (s *SLogger) Debugf(format string, args ...any) { + s.root.Log(context.Background(), LevelDebug, fmt.Sprintf(format, args...)) +} + +func (s *SLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) { + s.root.Log(ctx, level, msg, args...) +} + +func (s *SLogger) With(args ...any) Logger { + return &SLogger{ + root: s.root.With(args...), + } +} + +func (s *SLogger) WithField(key string, value any) Logger { + return &SLogger{ + root: s.root.With(key, value), + } +} + +func (s *SLogger) WithFields(fields ...any) Logger { + return &SLogger{ + root: s.root.With(fields...), + } +} + +func (l *SLogger) Infof(format string, args ...any) { + l.root.Log(context.Background(), LevelInfo, fmt.Sprintf(format, args...)) +} + +func (l *SLogger) Info(msg string, args ...any) { + l.root.Log(context.Background(), LevelInfo, msg, args...) +} + +func (l *SLogger) WithRequestInfo(r *http.Request) Logger { + l = l.WithRequestId(r.Context()) + var clientIP string + + if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + clientIP = ip + } + + return &SLogger{ + root: l.root.With( + "path", r.URL.Path, + "method", r.Method, + "ip", clientIP, + ), + } +} +func (l *SLogger) WithRequestId(ctx context.Context) *SLogger { + requestId := ctx.Value(CtxRequestId) + if requestId != "" { + return &SLogger{ + root: l.root.With("request_id", requestId), + } + } + return l +} + +func renameLevel(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + levelLabel, exists := LevelNames[level] + if !exists { + levelLabel = level.String() + } + + a.Value = slog.StringValue(levelLabel) + } + + return a +} diff --git a/logger/mock_logger.go b/logger/mock_logger.go new file mode 100644 index 0000000..8f717c3 --- /dev/null +++ b/logger/mock_logger.go @@ -0,0 +1,51 @@ +package logger + +import ( + "context" + "log/slog" + "net/http" +) + +func NewMock() Logger { + return &MockLogger{} +} + +type MockLogger struct{} + +func (m *MockLogger) Trace(msg string, args ...any) {} + +func (m *MockLogger) Notice(msg string, args ...any) {} + +func (m *MockLogger) Error(msg string, args ...any) {} + +func (m *MockLogger) Fatal(msg string, args ...any) {} + +func (m *MockLogger) Fatalf(format string, args ...any) {} + +func (m *MockLogger) Errorf(format string, args ...any) {} + +func (m *MockLogger) Debug(msg string, args ...any) {} + +func (m *MockLogger) Debugf(format string, args ...any) {} + +func (m *MockLogger) Info(msg string, args ...any) {} + +func (m *MockLogger) Infof(format string, args ...any) {} + +func (m *MockLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) {} + +func (m *MockLogger) With(args ...any) Logger { + return m +} + +func (m *MockLogger) WithRequestInfo(r *http.Request) Logger { + return m +} + +func (m *MockLogger) WithField(key string, value any) Logger { + return m +} + +func (m *MockLogger) WithFields(fields ...any) Logger { + return m +}