skeleton added

This commit is contained in:
2026-02-08 19:56:16 +03:00
committed by KentoNion
commit f0b04b229a
19 changed files with 2021 additions and 0 deletions

99
pkg/config/config.go Executable file
View 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
View 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))
}