skeleton added
This commit is contained in:
99
pkg/config/config.go
Executable file
99
pkg/config/config.go
Executable file
@@ -0,0 +1,99 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
User string `yaml:"user" env-required:"true"`
|
||||
Pass string `yaml:"password" env-required:"true"`
|
||||
Host string `yaml:"host"`
|
||||
Ssl string `yaml:"sslmode" env-required:"true"`
|
||||
MigrationsPath string `yaml:"migrations_path" env-required:"true"`
|
||||
}
|
||||
|
||||
type APIKeys struct {
|
||||
Telegram string `yaml:"telegram" env-required:"true"`
|
||||
TelegramPhone string `yaml:"telegram_phone" env-default:"+79934771502"` // номер для userbot (MTProto), код/2FA — в сообщениях боту
|
||||
Youtube string `yaml:"youtube" env-required:"true"`
|
||||
TelegramAppId int `yaml:"telegram_app_id" env-required:"true"`
|
||||
TelegramAppHash string `yaml:"telegram_app_hash" env-required:"true"`
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
FilePath string `yaml:"file_path"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
StartupLag time.Duration `yaml:"startup_lag"`
|
||||
Cooldown time.Duration `yaml:"cooldown" default:"3600"`
|
||||
Timeout time.Duration `yaml:"timeout" default:"60"`
|
||||
MaxConnections int `yaml:"max_watcher_connections" default:"100"`
|
||||
}
|
||||
|
||||
type Downloader struct {
|
||||
Host string `yaml:"host" env-required:"true"`
|
||||
Max_downloads int `yaml:"max_parallel_downloads" default:"10"`
|
||||
VideoPath string `yaml:"video_path"`
|
||||
}
|
||||
|
||||
type UploaderConfig struct {
|
||||
MaxRetries int `env:"UPLOADER_MAX_RETRIES" envDefault:"5"`
|
||||
RetryCooldown time.Duration `env:"UPLOADER_RETRY_COOLDOWN" envDefault:"5m"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Env string `yaml:"env"`
|
||||
DB DB `yaml:"postgres_db"`
|
||||
APIKeys APIKeys `yaml:"API_keys"`
|
||||
Log Log `yaml:"logger"`
|
||||
Service Service `yaml:"service"`
|
||||
Downloader Downloader `yaml:"downloader"`
|
||||
Uploader UploaderConfig
|
||||
}
|
||||
|
||||
func MustLoad() Config {
|
||||
configPath := os.Getenv("CONFIG_PATH")
|
||||
if configPath == "" {
|
||||
configPath = "../config.yaml"
|
||||
}
|
||||
|
||||
//проверка существует ли файл
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
log.Fatal("cannot read config file")
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
err := cleanenv.ReadConfig(configPath, &cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//-----------------------------------------------------------------------------------------------
|
||||
dbhost := os.Getenv("DB_HOST") //получаем переменную из окружения (она есть если запущен в докер контейнере)
|
||||
if dbhost != "" {
|
||||
//time.Sleep(30 * time.Second) //если мы в докер контейнере, дадим время бд чтоб она поднялась
|
||||
cfg.DB.Host = dbhost
|
||||
}
|
||||
downloaderHost := os.Getenv("DOWNLOADER_HOST")
|
||||
if downloaderHost != "" {
|
||||
cfg.Downloader.Host = downloaderHost
|
||||
}
|
||||
migrationsPath := os.Getenv("MIGRATIONS_PATH")
|
||||
if migrationsPath != "" {
|
||||
cfg.DB.MigrationsPath = migrationsPath
|
||||
}
|
||||
dbUser := os.Getenv("DB_USER")
|
||||
if dbUser != "" {
|
||||
cfg.DB.User = dbUser
|
||||
}
|
||||
if cfg.Downloader.VideoPath == "" {
|
||||
cfg.Downloader.VideoPath = os.TempDir()
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
136
pkg/logger/logger.go
Executable file
136
pkg/logger/logger.go
Executable file
@@ -0,0 +1,136 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"tgVideoCall/pkg/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
const (
|
||||
envLocal = "local"
|
||||
envDev = "dev"
|
||||
envProd = "prod"
|
||||
)
|
||||
|
||||
func MustInitLogger(cfg config.Config) slog.Logger {
|
||||
var logFile *os.File
|
||||
var err error
|
||||
|
||||
if cfg.Log.FilePath != "" { //Если строка в конфиге пустая, это будет означать что нам не нужно сохранение логов в файл
|
||||
logFile, err = os.OpenFile(cfg.Log.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Fatal("error opening file:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var log *slog.Logger
|
||||
|
||||
switch cfg.Env {
|
||||
case envLocal:
|
||||
if cfg.Log.FilePath == "" {
|
||||
log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
return *log
|
||||
}
|
||||
log = slog.New(slog.NewTextHandler(io.MultiWriter(os.Stdout, logFile), &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
case envProd:
|
||||
if cfg.Log.FilePath == "" {
|
||||
log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
return *log
|
||||
}
|
||||
log = slog.New(slog.NewJSONHandler(io.MultiWriter(os.Stdout, logFile), &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
}
|
||||
if cfg.Log.FilePath != "" {
|
||||
log.Info(fmt.Sprintf("Logs are saving to: %s", cfg.Log.FilePath))
|
||||
}
|
||||
|
||||
return *log
|
||||
}
|
||||
|
||||
// ZapSlogHandler реализует slog.Handler для zap.Logger
|
||||
type ZapSlogHandler struct {
|
||||
zapLogger *zap.Logger
|
||||
}
|
||||
|
||||
func NewZapSlogHandler(zapLogger *zap.Logger) *ZapSlogHandler {
|
||||
return &ZapSlogHandler{zapLogger: zapLogger}
|
||||
}
|
||||
|
||||
func (h *ZapSlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
// Преобразуем slog.Level в zapcore.Level
|
||||
var zapLevel zapcore.Level
|
||||
switch {
|
||||
case level < slog.LevelDebug:
|
||||
zapLevel = zapcore.DebugLevel
|
||||
case level < slog.LevelInfo:
|
||||
zapLevel = zapcore.InfoLevel
|
||||
case level < slog.LevelWarn:
|
||||
zapLevel = zapcore.WarnLevel
|
||||
case level < slog.LevelError:
|
||||
zapLevel = zapcore.ErrorLevel
|
||||
default:
|
||||
zapLevel = zapcore.DPanicLevel
|
||||
}
|
||||
return h.zapLogger.Core().Enabled(zapLevel)
|
||||
}
|
||||
|
||||
func (h *ZapSlogHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
// Преобразуем slog.Record в поля zap
|
||||
fields := make([]zap.Field, 0, r.NumAttrs()+1)
|
||||
fields = append(fields, zap.String("message", r.Message))
|
||||
|
||||
// Добавляем атрибуты
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
fields = append(fields, zap.Any(attr.Key, attr.Value.Any()))
|
||||
return true
|
||||
})
|
||||
|
||||
// Добавляем источник (source), если есть
|
||||
if r.PC != 0 {
|
||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||
f, _ := fs.Next()
|
||||
fields = append(fields, zap.String("source", f.File+":"+strconv.FormatUint(uint64(r.PC), 10)))
|
||||
}
|
||||
|
||||
// Логируем с соответствующим уровнем
|
||||
switch r.Level {
|
||||
case slog.LevelDebug:
|
||||
h.zapLogger.Debug(r.Message, fields...)
|
||||
case slog.LevelInfo:
|
||||
h.zapLogger.Info(r.Message, fields...)
|
||||
case slog.LevelWarn:
|
||||
h.zapLogger.Warn(r.Message, fields...)
|
||||
case slog.LevelError:
|
||||
h.zapLogger.Error(r.Message, fields...)
|
||||
default:
|
||||
h.zapLogger.DPanic(r.Message, fields...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ZapSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
fields := make([]zap.Field, len(attrs))
|
||||
for i, attr := range attrs {
|
||||
fields[i] = zap.Any(attr.Key, attr.Value.Any())
|
||||
}
|
||||
return &ZapSlogHandler{zapLogger: h.zapLogger.With(fields...)}
|
||||
}
|
||||
|
||||
func (h *ZapSlogHandler) WithGroup(name string) slog.Handler {
|
||||
// Для простоты игнорируем группы, но в production нужно реализовать
|
||||
return h
|
||||
}
|
||||
|
||||
// WrapZapToSlog оборачивает zap.Logger в slog.Logger
|
||||
func WrapZapToSlog(zapLogger *zap.Logger) *slog.Logger {
|
||||
return slog.New(NewZapSlogHandler(zapLogger))
|
||||
}
|
||||
Reference in New Issue
Block a user