diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/changeAPI.iml b/.idea/changeAPI.iml new file mode 100644 index 0000000..728392f --- /dev/null +++ b/.idea/changeAPI.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..fdcfa42 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8e4e17d --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/cmd/converter/main.go b/cmd/converter/main.go new file mode 100644 index 0000000..b0ed0d3 --- /dev/null +++ b/cmd/converter/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "converter/internal/app" + "converter/internal/config" + "converter/internal/lib/logger/handlers/slogpretty" + "fmt" + "log/slog" + "os" +) + +const ( + envlocal = "local" + envdev = "dev" + envprod = "prod" +) + +func main() { + + cfg := config.MustLoad() + + fmt.Println(cfg) + + log := setupLogger(cfg.Env) + log.Info("starting application", slog.Any("cfg", cfg)) + applicaton := app.New(log, cfg.GRPC.Port, cfg.TokenTTL) + applicaton.GRPCSrv.MustRun() + // TODO: инициализировать приложение app + + // TODO: запустить gRPC сервер приложения +} + +func setupLogger(env string) *slog.Logger { + var log *slog.Logger + + switch env { + case envlocal: + log = setupPrettySlog() + case envdev: + log = slog.New( + slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}), + ) + case envprod: + log = slog.New( + slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}), + ) + } + return log +} + +func setupPrettySlog() *slog.Logger { + opts := slogpretty.PrettyHandlerOptions{ + SlogOpts: &slog.HandlerOptions{ + Level: slog.LevelDebug, + }, + } + + handler := opts.NewPrettyHandler(os.Stdout) + + return slog.New(handler) +} diff --git a/config/local.yaml b/config/local.yaml new file mode 100644 index 0000000..1db89b1 --- /dev/null +++ b/config/local.yaml @@ -0,0 +1,5 @@ +env: "local" +token_ttl: 1h +grpc: + port: 44044 + timeout: 10h \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2bce6cc --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module converter + +go 1.22 + +require ( + github.com/netscrawler/protos v0.0.5 + google.golang.org/grpc v1.64.0 +) + +require ( + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aeb3402 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/netscrawler/protos v0.0.0-20240629165054-1454eca14db0 h1:YKByh7dtXFBif3xemj/OFyWbAcw3j/M8tD4U4XF229A= +github.com/netscrawler/protos v0.0.0-20240629165054-1454eca14db0/go.mod h1:jgfFUikzJpivreejTHY7s1BWvVpM9/rJdvTntTzcPzc= +github.com/netscrawler/protos v0.0.4 h1:D+Cb4cELT9YSIyUsyXL233Zb90ClzOR290aE4i+KI8Y= +github.com/netscrawler/protos v0.0.4/go.mod h1:jgfFUikzJpivreejTHY7s1BWvVpM9/rJdvTntTzcPzc= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..a4cb23b --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,23 @@ +package app + +import ( + grpcapp "converter/internal/app/grpc" + "log/slog" + "time" +) + +type App struct { + GRPCSrv *grpcapp.App +} + +func New( + log *slog.Logger, + grpcPort int, + tokenTTl time.Duration) *App { + //TODO: инициализировать хранилище + //TODO: init convert service + grpcApp := grpcapp.New(log, grpcPort) + return &App{ + GRPCSrv: grpcApp, + } +} diff --git a/internal/app/grpc/app.go b/internal/app/grpc/app.go new file mode 100644 index 0000000..bdf25d2 --- /dev/null +++ b/internal/app/grpc/app.go @@ -0,0 +1,62 @@ +package grpcapp + +import ( + convertgrpc "converter/internal/grpc/cnvrt" + "fmt" + "google.golang.org/grpc" + "log/slog" + "net" +) + +type App struct { + log *slog.Logger + gRPCServer *grpc.Server + port int +} + +func New( + log *slog.Logger, + port int) *App { + gRPCServer := grpc.NewServer() + + convertgrpc.Register(gRPCServer) + + return &App{ + log: log, + gRPCServer: gRPCServer, + port: port, + } + +} + +func (a *App) MustRun() { + if err := a.Run(); err != nil { + panic(err) + } +} + +func (a *App) Run() error { + const op = "grpcapp.Run" + log := a.log.With( + slog.String("op", op), + slog.Int("port", a.port)) + + l, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port)) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + log.Info("gRPC server is running", slog.String("addr", l.Addr().String())) + + if err := a.gRPCServer.Serve(l); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + return nil +} + +func (a *App) Stop() { + const op = "grpcapp.Stop" + + a.log.With(slog.String("op", op)). + Info("stopping gRPC server", slog.Int("port", a.port)) + a.gRPCServer.GracefulStop() +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..565af1b --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,52 @@ +package config + +import ( + "flag" + "github.com/ilyakaznacheev/cleanenv" + "os" + "time" +) + +type Config struct { + Env string `yaml:"env" env-default:"local"` + TokenTTL time.Duration `yaml:"token_ttl" env-required:"true"` + GRPC GRPCConfig `yaml:"grpc"` +} + +type GRPCConfig struct { + Port int `yaml:"port"` + Timeout time.Duration `yaml:"timeout"` +} + +func MustLoad() *Config { + path := fetchConfigPath() + if path == "" { + panic("config path is empty") + + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + panic("config file not found" + path) + } + + var cfg Config + + if err := cleanenv.ReadConfig(path, &cfg); err != nil { + panic("failed to read config" + err.Error()) + } + + return &cfg +} + +func fetchConfigPath() string { + var res string + + flag.StringVar(&res, "config", "", "path to config file") + flag.Parse() + + if res == "" { + res = os.Getenv("CONFIG_PATH") + } + + return res +} diff --git a/internal/grpc/cnvrt/server.go b/internal/grpc/cnvrt/server.go new file mode 100644 index 0000000..ab62b44 --- /dev/null +++ b/internal/grpc/cnvrt/server.go @@ -0,0 +1,22 @@ +package cnvrt + +import ( + "context" + cnvrtv1 "github.com/netscrawler/protos/gen/go/changeAPI" + "google.golang.org/grpc" +) + +type serverAPI struct { + cnvrtv1.UnimplementedConverterServer +} + +func Register(gRPC *grpc.Server) { + cnvrtv1.RegisterConverterServer(gRPC, &serverAPI{}) +} + +func (s serverAPI) Convert( + ctx context.Context, + req *cnvrtv1.ConvertRequest) ( + *cnvrtv1.ConvertResponse, error) { + panic("implement me") +} diff --git a/internal/lib/logger/handlers/slogdiscard/slogdiscard.go b/internal/lib/logger/handlers/slogdiscard/slogdiscard.go new file mode 100644 index 0000000..1488376 --- /dev/null +++ b/internal/lib/logger/handlers/slogdiscard/slogdiscard.go @@ -0,0 +1,36 @@ +package slogdiscard + +import ( + "context" + "golang.org/x/exp/slog" +) + +func NewDiscardLogger() *slog.Logger { + return slog.New(NewDiscardHandler()) +} + +type DiscardHandler struct{} + +func NewDiscardHandler() *DiscardHandler { + return &DiscardHandler{} +} + +func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { + // Просто игнорируем запись журнала + return nil +} + +func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { + // Возвращает тот же обработчик, так как нет атрибутов для сохранения + return h +} + +func (h *DiscardHandler) WithGroup(_ string) slog.Handler { + // Возвращает тот же обработчик, так как нет группы для сохранения + return h +} + +func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { + // Всегда возвращает false, так как запись журнала игнорируется + return false +} diff --git a/internal/lib/logger/handlers/slogpretty/slogpretty.go b/internal/lib/logger/handlers/slogpretty/slogpretty.go new file mode 100644 index 0000000..f298f2f --- /dev/null +++ b/internal/lib/logger/handlers/slogpretty/slogpretty.go @@ -0,0 +1,98 @@ +package slogpretty + +import ( + "context" + "encoding/json" + "io" + stdLog "log" + "log/slog" + + "github.com/fatih/color" +) + +type PrettyHandlerOptions struct { + SlogOpts *slog.HandlerOptions +} + +type PrettyHandler struct { + opts PrettyHandlerOptions + slog.Handler + l *stdLog.Logger + attrs []slog.Attr +} + +func (opts PrettyHandlerOptions) NewPrettyHandler( + out io.Writer, +) *PrettyHandler { + h := &PrettyHandler{ + Handler: slog.NewJSONHandler(out, opts.SlogOpts), + l: stdLog.New(out, "", 0), + } + + return h +} + +func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { + level := r.Level.String() + ":" + + switch r.Level { + case slog.LevelDebug: + level = color.MagentaString(level) + case slog.LevelInfo: + level = color.BlueString(level) + case slog.LevelWarn: + level = color.YellowString(level) + case slog.LevelError: + level = color.RedString(level) + } + + fields := make(map[string]interface{}, r.NumAttrs()) + + r.Attrs(func(a slog.Attr) bool { + fields[a.Key] = a.Value.Any() + + return true + }) + + for _, a := range h.attrs { + fields[a.Key] = a.Value.Any() + } + + var b []byte + var err error + + if len(fields) > 0 { + b, err = json.MarshalIndent(fields, "", " ") + if err != nil { + return err + } + } + + timeStr := r.Time.Format("[15:05:05.000]") + msg := color.CyanString(r.Message) + + h.l.Println( + timeStr, + level, + msg, + color.WhiteString(string(b)), + ) + + return nil +} + +func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &PrettyHandler{ + Handler: h.Handler, + l: h.l, + attrs: attrs, + } +} + +func (h *PrettyHandler) WithGroup(name string) slog.Handler { + // TODO: implement + return &PrettyHandler{ + Handler: h.Handler.WithGroup(name), + l: h.l, + } +} diff --git a/internal/lib/logger/sl/sl.go b/internal/lib/logger/sl/sl.go new file mode 100644 index 0000000..b9fa762 --- /dev/null +++ b/internal/lib/logger/sl/sl.go @@ -0,0 +1,12 @@ +package sl + +import ( + "log/slog" +) + +func Err(err error) slog.Attr { + return slog.Attr{ + Key: "error", + Value: slog.StringValue(err.Error()), + } +}