Files
libredesk/cmd/init.go
Abhinav Raut 5361bcb24f feat: dockerize libredesk
- feat: new flag for idempotent installation `--idempotent-install`
- feat: new flag to skip yes/no prompt `--yes`
- feat: new flag for upgrades `--upgrade`
- refactor: update doc strings and sample toml file.
- chore: update .gitignore.
2025-02-22 23:33:18 +05:30

810 lines
22 KiB
Go

package main
import (
"cmp"
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"time"
"html/template"
"github.com/abhinavxd/libredesk/internal/ai"
auth_ "github.com/abhinavxd/libredesk/internal/auth"
"github.com/abhinavxd/libredesk/internal/authz"
"github.com/abhinavxd/libredesk/internal/autoassigner"
"github.com/abhinavxd/libredesk/internal/automation"
businesshours "github.com/abhinavxd/libredesk/internal/business_hours"
"github.com/abhinavxd/libredesk/internal/colorlog"
"github.com/abhinavxd/libredesk/internal/conversation"
"github.com/abhinavxd/libredesk/internal/conversation/priority"
"github.com/abhinavxd/libredesk/internal/conversation/status"
"github.com/abhinavxd/libredesk/internal/csat"
"github.com/abhinavxd/libredesk/internal/inbox"
"github.com/abhinavxd/libredesk/internal/inbox/channel/email"
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
"github.com/abhinavxd/libredesk/internal/macro"
"github.com/abhinavxd/libredesk/internal/media"
fs "github.com/abhinavxd/libredesk/internal/media/stores/localfs"
"github.com/abhinavxd/libredesk/internal/media/stores/s3"
notifier "github.com/abhinavxd/libredesk/internal/notification"
emailnotifier "github.com/abhinavxd/libredesk/internal/notification/providers/email"
"github.com/abhinavxd/libredesk/internal/oidc"
"github.com/abhinavxd/libredesk/internal/role"
"github.com/abhinavxd/libredesk/internal/search"
"github.com/abhinavxd/libredesk/internal/setting"
"github.com/abhinavxd/libredesk/internal/sla"
"github.com/abhinavxd/libredesk/internal/tag"
"github.com/abhinavxd/libredesk/internal/team"
tmpl "github.com/abhinavxd/libredesk/internal/template"
"github.com/abhinavxd/libredesk/internal/user"
"github.com/abhinavxd/libredesk/internal/view"
"github.com/abhinavxd/libredesk/internal/ws"
"github.com/jmoiron/sqlx"
"github.com/knadh/go-i18n"
kjson "github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
_ "github.com/lib/pq"
"github.com/redis/go-redis/v9"
flag "github.com/spf13/pflag"
"github.com/zerodha/logf"
)
// constants holds the app constants.
type constants struct {
AppBaseURL string
FaviconURL string
LogoURL string
SiteName string
UploadProvider string
AllowedUploadFileExtensions []string
MaxFileUploadSizeMB int
}
// Config loads config files into koanf.
func initConfig(ko *koanf.Koanf) {
for _, f := range ko.Strings("config") {
log.Println("reading config file:", f)
if err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
if os.IsNotExist(err) {
log.Fatal("error config file not found.")
}
log.Fatalf("error loading config from file: %v.", err)
}
}
}
// initFlags initializes the commandline flags.
func initFlags() {
f := flag.NewFlagSet("config", flag.ContinueOnError)
// Registering `--help` handler.
f.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
f.PrintDefaults()
}
// Register the commandline flags and parse them.
f.StringSlice("config", []string{"config.toml"},
"path to one or more config files (will be merged in order)")
f.Bool("version", false, "show current version of the build")
f.Bool("install", false, "setup database")
f.Bool("idempotent-install", false, "run idempotent installation, i.e., skip installion if schema is already installed useful for the first time setup")
f.Bool("yes", false, "skip confirmation prompt")
f.Bool("upgrade", false, "upgrade the database schema")
f.Bool("set-system-user-password", false, "set password for the system user")
if err := f.Parse(os.Args[1:]); err != nil {
log.Fatalf("loading flags: %v", err)
}
if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil {
log.Fatalf("loading config: %v", err)
}
}
// initConstants initializes the app constants.
func initConstants() *constants {
return &constants{
AppBaseURL: ko.String("app.root_url"),
FaviconURL: ko.String("app.favicon_url"),
LogoURL: ko.String("app.logo_url"),
SiteName: ko.String("app.site_name"),
UploadProvider: ko.MustString("upload.provider"),
AllowedUploadFileExtensions: ko.Strings("app.allowed_file_upload_extensions"),
MaxFileUploadSizeMB: ko.Int("app.max_file_upload_size"),
}
}
// initFS initializes the stuffbin FileSystem.
func initFS() stuffbin.FileSystem {
var files = []string{
"frontend/dist",
"i18n",
"static",
}
// Get self executable path.
path, err := os.Executable()
if err != nil {
log.Fatalf("error initializing FS: %v", err)
}
// Load embedded files in the executable.
fs, err := stuffbin.UnStuff(path)
if err != nil {
if err == stuffbin.ErrNoID {
// The embed failed or the binary's already unstuffed or running in local / dev mode, use the local filesystem.
colorlog.Red("binary unstuff failed, using local filesystem for static files")
fs, err = stuffbin.NewLocalFS("/", files...)
if err != nil {
log.Fatalf("error initializing local FS: %v", err)
}
} else {
log.Fatalf("error initializing FS: %v", err)
}
}
return fs
}
// loadSettings loads settings from the DB into Koanf map.
func loadSettings(m *setting.Manager) {
j, err := m.GetAllJSON()
if err != nil {
log.Fatalf("error parsing settings from DB: %v", err)
}
// Setting keys are dot separated, eg: app.favicon_url. Unflatten them into
// nested maps {app: {favicon_url}}.
var out map[string]interface{}
if err := json.Unmarshal(j, &out); err != nil {
log.Fatalf("error unmarshalling settings from DB: %v", err)
}
if err := ko.Load(confmap.Provider(out, "."), nil); err != nil {
log.Fatalf("error parsing settings from DB: %v", err)
}
}
// initSettings inits setting manager.
func initSettings(db *sqlx.DB) *setting.Manager {
s, err := setting.New(setting.Opts{
DB: db,
Lo: initLogger("settings"),
})
if err != nil {
log.Fatalf("error initializing setting manager: %v", err)
}
return s
}
// initUser inits user manager.
func initUser(i18n *i18n.I18n, DB *sqlx.DB) *user.Manager {
mgr, err := user.New(i18n, user.Opts{
DB: DB,
Lo: initLogger("user_manager"),
})
if err != nil {
log.Fatalf("error initializing user manager: %v", err)
}
return mgr
}
// initConversations inits conversation manager.
func initConversations(
i18n *i18n.I18n,
sla *sla.Manager,
status *status.Manager,
priority *priority.Manager,
hub *ws.Hub,
notif *notifier.Service,
db *sqlx.DB,
inboxStore *inbox.Manager,
userStore *user.Manager,
teamStore *team.Manager,
mediaStore *media.Manager,
settings *setting.Manager,
csat *csat.Manager,
automationEngine *automation.Engine,
template *tmpl.Manager,
) *conversation.Manager {
c, err := conversation.New(hub, i18n, notif, sla, status, priority, inboxStore, userStore, teamStore, mediaStore, settings, csat, automationEngine, template, conversation.Opts{
DB: db,
Lo: initLogger("conversation_manager"),
OutgoingMessageQueueSize: ko.MustInt("message.outgoing_queue_size"),
IncomingMessageQueueSize: ko.MustInt("message.incoming_queue_size"),
})
if err != nil {
log.Fatalf("error initializing conversation manager: %v", err)
}
return c
}
// initTag inits tag manager.
func initTag(db *sqlx.DB) *tag.Manager {
var lo = initLogger("tag_manager")
mgr, err := tag.New(tag.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing tags: %v", err)
}
return mgr
}
// initViews inits view manager.
func initView(db *sqlx.DB) *view.Manager {
var lo = initLogger("view_manager")
m, err := view.New(view.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing view manager: %v", err)
}
return m
}
// initMacro inits macro manager.
func initMacro(db *sqlx.DB) *macro.Manager {
var lo = initLogger("macro")
m, err := macro.New(macro.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing macro manager: %v", err)
}
return m
}
// initBusinessHours inits business hours manager.
func initBusinessHours(db *sqlx.DB) *businesshours.Manager {
var lo = initLogger("business-hours")
m, err := businesshours.New(businesshours.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing business hours manager: %v", err)
}
return m
}
// initSLA inits SLA manager.
func initSLA(db *sqlx.DB, teamManager *team.Manager, settings *setting.Manager, businessHours *businesshours.Manager) *sla.Manager {
var lo = initLogger("sla")
m, err := sla.New(sla.Opts{
DB: db,
Lo: lo,
}, teamManager, settings, businessHours)
if err != nil {
log.Fatalf("error initializing SLA manager: %v", err)
}
return m
}
// initCSAT inits CSAT manager.
func initCSAT(db *sqlx.DB) *csat.Manager {
var lo = initLogger("csat")
m, err := csat.New(csat.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing CSAT manager: %v", err)
}
return m
}
// initTemplates inits template manager.
func initTemplate(db *sqlx.DB, fs stuffbin.FileSystem, consts *constants) *tmpl.Manager {
var (
lo = initLogger("template")
funcMap = getTmplFuncs(consts)
)
tpls, err := stuffbin.ParseTemplatesGlob(funcMap, fs, "/static/email-templates/*.html")
if err != nil {
log.Fatalf("error parsing e-mail templates: %v", err)
}
webTpls, err := stuffbin.ParseTemplatesGlob(funcMap, fs, "/static/public/web-templates/*.html")
if err != nil {
log.Fatalf("error parsing web templates: %v", err)
}
m, err := tmpl.New(lo, db, webTpls, tpls, funcMap)
if err != nil {
log.Fatalf("error initializing template manager: %v", err)
}
return m
}
// getTmplFuncs returns the template functions.
func getTmplFuncs(consts *constants) template.FuncMap {
return template.FuncMap{
"RootURL": func() string {
return consts.AppBaseURL
},
"FaviconURL": func() string {
return consts.FaviconURL
},
"Date": func(layout string) string {
if layout == "" {
layout = time.ANSIC
}
return time.Now().Format(layout)
},
"LogoURL": func() string {
return consts.LogoURL
},
"SiteName": func() string {
return consts.SiteName
},
}
}
// reloadSettings reloads the settings from the database into the Koanf instance.
func reloadSettings(app *App) error {
app.lo.Info("reloading settings")
j, err := app.setting.GetAllJSON()
if err != nil {
app.lo.Error("error parsing settings from DB", "error", err)
return err
}
var out map[string]interface{}
if err := json.Unmarshal(j, &out); err != nil {
app.lo.Error("error unmarshalling settings from DB", "error", err)
return err
}
if err := ko.Load(confmap.Provider(out, "."), nil); err != nil {
app.lo.Error("error loading settings into koanf", "error", err)
return err
}
newConsts := initConstants()
app.consts.Store(newConsts)
return nil
}
// reloadTemplates reloads the templates from the filesystem.
func reloadTemplates(app *App) error {
app.lo.Info("reloading templates")
funcMap := getTmplFuncs(app.consts.Load().(*constants))
tpls, err := stuffbin.ParseTemplatesGlob(funcMap, app.fs, "/static/email-templates/*.html")
if err != nil {
app.lo.Error("error parsing email templates", "error", err)
return err
}
webTpls, err := stuffbin.ParseTemplatesGlob(funcMap, app.fs, "/static/public/web-templates/*.html")
if err != nil {
app.lo.Error("error parsing web templates", "error", err)
return err
}
return app.tmpl.Reload(webTpls, tpls, funcMap)
}
// initTeam inits team manager.
func initTeam(db *sqlx.DB) *team.Manager {
var lo = initLogger("team-manager")
mgr, err := team.New(team.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing team manager: %v", err)
}
return mgr
}
// initMedia inits media manager.
func initMedia(db *sqlx.DB) *media.Manager {
var (
store media.Store
err error
appRootURL = ko.String("app.root_url")
lo = initLogger("media")
)
switch s := ko.MustString("upload.provider"); s {
case "s3":
store, err = s3.New(s3.Opt{
URL: ko.String("upload.s3.url"),
PublicURL: ko.String("upload.s3.public_url"),
AccessKey: ko.String("upload.s3.access_key"),
SecretKey: ko.String("upload.s3.secret_key"),
Region: ko.String("upload.s3.region"),
Bucket: ko.String("upload.s3.bucket"),
BucketPath: ko.String("upload.s3.bucket_path"),
// All files are private by default.
BucketType: "private",
Expiry: ko.Duration("upload.s3.expiry"),
})
if err != nil {
log.Fatalf("error initializing s3 media store: %v", err)
}
case "fs":
store, err = fs.New(fs.Opts{
UploadURI: "/uploads",
UploadPath: filepath.Clean(ko.String("upload.fs.upload_path")),
RootURL: appRootURL,
})
if err != nil {
log.Fatalf("error initializing fs media store: %v", err)
}
default:
log.Fatalf("unknown media store: %s", s)
}
media, err := media.New(media.Opts{
Store: store,
Lo: lo,
DB: db,
})
if err != nil {
log.Fatalf("error initializing media: %v", err)
}
return media
}
// initInbox initializes the inbox manager without registering inboxes.
func initInbox(db *sqlx.DB) *inbox.Manager {
var lo = initLogger("inbox-manager")
mgr, err := inbox.New(lo, db)
if err != nil {
log.Fatalf("error initializing inbox manager: %v", err)
}
return mgr
}
// initAutomationEngine initializes the automation engine.
func initAutomationEngine(db *sqlx.DB) *automation.Engine {
var lo = initLogger("automation_engine")
engine, err := automation.New(automation.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing automation engine: %v", err)
}
return engine
}
// initAutoAssigner initializes the auto assigner.
func initAutoAssigner(teamManager *team.Manager, userManager *user.Manager, conversationManager *conversation.Manager) *autoassigner.Engine {
systemUser, err := userManager.GetSystemUser()
if err != nil {
log.Fatalf("error fetching system user: %v", err)
}
e, err := autoassigner.New(teamManager, conversationManager, systemUser, initLogger("autoassigner"))
if err != nil {
log.Fatalf("error initializing auto assigner: %v", err)
}
return e
}
// initNotifier initializes the notifier service with available providers.
func initNotifier(userStore notifier.UserStore) *notifier.Service {
smtpCfg := email.SMTPConfig{}
if err := ko.UnmarshalWithConf("notification.email", &smtpCfg, koanf.UnmarshalConf{Tag: "json"}); err != nil {
log.Fatalf("error unmarshalling email notification provider config: %v", err)
}
emailNotifier, err := emailnotifier.New([]email.SMTPConfig{smtpCfg}, userStore, emailnotifier.Opts{
Lo: initLogger("email-notifier"),
FromEmail: ko.String("notification.email.email_address"),
})
if err != nil {
log.Fatalf("error initializing email notifier: %v", err)
}
notifierProviders := map[string]notifier.Notifier{
emailNotifier.Name(): emailNotifier,
}
return notifier.NewService(notifierProviders, ko.MustInt("notification.concurrency"), ko.MustInt("notification.queue_size"), initLogger("notifier"))
}
// initEmailInbox initializes the email inbox.
func initEmailInbox(inboxRecord imodels.Inbox, store inbox.MessageStore) (inbox.Inbox, error) {
var config email.Config
// Load JSON data into Koanf.
if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
return nil, fmt.Errorf("loading config: %w", err)
}
if err := ko.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
return nil, fmt.Errorf("unmarshalling `%s` %s config: %w", inboxRecord.Channel, inboxRecord.Name, err)
}
if len(config.SMTP) == 0 {
log.Printf("WARNING: Zero SMTP servers configured for `%s` inbox: Name: `%s`", inboxRecord.Channel, inboxRecord.Name)
}
if len(config.IMAP) == 0 {
log.Printf("WARNING: Zero IMAP clients configured for `%s` inbox: Name: `%s`", inboxRecord.Channel, inboxRecord.Name)
}
config.From = inboxRecord.From
if len(config.From) == 0 {
log.Printf("WARNING: No `from` email address set for `%s` inbox: Name: `%s`", inboxRecord.Channel, inboxRecord.Name)
}
inbox, err := email.New(store, email.Opts{
ID: inboxRecord.ID,
Config: config,
Lo: initLogger("email_inbox"),
})
if err != nil {
return nil, fmt.Errorf("initializing `%s` inbox: `%s` error : %w", inboxRecord.Channel, inboxRecord.Name, err)
}
log.Printf("`%s` inbox successfully initialized. %d SMTP servers. %d IMAP clients.", inboxRecord.Name, len(config.SMTP), len(config.IMAP))
return inbox, nil
}
// initializeInboxes handles inbox initialization.
func initializeInboxes(inboxR imodels.Inbox, store inbox.MessageStore) (inbox.Inbox, error) {
switch inboxR.Channel {
case "email":
return initEmailInbox(inboxR, store)
default:
return nil, fmt.Errorf("unknown inbox channel: %s", inboxR.Channel)
}
}
// reloadInboxes reloads all inboxes.
func reloadInboxes(app *App) error {
app.lo.Info("reloading inboxes")
return app.inbox.Reload(ctx, initializeInboxes)
}
// startInboxes registers the active inboxes and starts receiver for each.
func startInboxes(ctx context.Context, mgr *inbox.Manager, store inbox.MessageStore) {
mgr.SetMessageStore(store)
if err := mgr.InitInboxes(initializeInboxes); err != nil {
log.Fatalf("error initializing inboxes: %v", err)
}
if err := mgr.Start(ctx); err != nil {
log.Fatalf("error starting inboxes: %v", err)
}
}
// initAuthz initializes authorization enforcer.
func initAuthz() *authz.Enforcer {
enforcer, err := authz.NewEnforcer(initLogger("authz"))
if err != nil {
log.Fatalf("error initializing authz: %v", err)
}
return enforcer
}
// initAuth initializes the authentication manager.
func initAuth(o *oidc.Manager, rd *redis.Client) *auth_.Auth {
lo := initLogger("auth")
providers, err := buildProviders(o)
if err != nil {
log.Fatalf("error initializing auth: %v", err)
}
auth, err := auth_.New(auth_.Config{Providers: providers}, rd, lo)
if err != nil {
log.Fatalf("error initializing auth: %v", err)
}
return auth
}
// reloadAuth reloads the auth providers.
func reloadAuth(app *App) error {
app.lo.Info("reloading auth manager")
providers, err := buildProviders(app.oidc)
if err != nil {
log.Fatalf("error reloading auth: %v", err)
}
if err := app.auth.Reload(auth_.Config{Providers: providers}); err != nil {
app.lo.Error("error reloading auth", "error", err)
return err
}
return nil
}
// buildProviders creates a list of auth providers from the OIDC manager.
func buildProviders(o *oidc.Manager) ([]auth_.Provider, error) {
oidcConfigs, err := o.GetAll()
if err != nil {
return nil, err
}
providers := make([]auth_.Provider, 0, len(oidcConfigs))
for _, config := range oidcConfigs {
if !config.Enabled {
continue
}
providers = append(providers, auth_.Provider{
ID: config.ID,
Provider: config.Provider,
ProviderURL: config.ProviderURL,
RedirectURL: config.RedirectURI,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
})
}
return providers, nil
}
// initOIDC initializes open id connect config manager.
func initOIDC(db *sqlx.DB, settings *setting.Manager) *oidc.Manager {
lo := initLogger("oidc")
o, err := oidc.New(oidc.Opts{
DB: db,
Lo: lo,
}, settings)
if err != nil {
log.Fatalf("error initializing oidc: %v", err)
}
return o
}
// initI18n inits i18n.
func initI18n(fs stuffbin.FileSystem) *i18n.I18n {
file, err := fs.Get("i18n/" + cmp.Or(ko.String("app.lang"), defLang) + ".json")
if err != nil {
log.Fatalf("error reading i18n language file")
}
i18n, err := i18n.New(file.ReadBytes())
if err != nil {
log.Fatalf("error initializing i18n: %v", err)
}
return i18n
}
// initRedis inits redis DB.
func initRedis() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: ko.MustString("redis.address"),
Password: ko.String("redis.password"),
DB: ko.Int("redis.db"),
})
}
// initRedis inits postgres DB.
func initDB() *sqlx.DB {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s %s",
ko.MustString("db.host"),
ko.MustInt("db.port"),
ko.MustString("db.user"),
ko.MustString("db.password"),
ko.MustString("db.database"),
ko.String("db.ssl_mode"),
ko.String("db.params"),
)
db, err := sqlx.Connect("postgres", dsn)
if err != nil {
log.Fatalf("error connecting to DB: %v", err)
}
db.SetMaxOpenConns(ko.MustInt("db.max_open"))
db.SetMaxIdleConns(ko.MustInt("db.max_idle"))
db.SetConnMaxLifetime(ko.MustDuration("db.max_lifetime"))
return db
}
// initRedis inits role manager.
func initRole(db *sqlx.DB) *role.Manager {
var lo = initLogger("role_manager")
r, err := role.New(role.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing role manager: %v", err)
}
return r
}
// initStatus inits conversation status manager.
func initStatus(db *sqlx.DB) *status.Manager {
manager, err := status.New(status.Opts{
DB: db,
Lo: initLogger("status-manager"),
})
if err != nil {
log.Fatalf("error initializing status manager: %v", err)
}
return manager
}
// initPriority inits conversation priority manager.
func initPriority(db *sqlx.DB) *priority.Manager {
manager, err := priority.New(priority.Opts{
DB: db,
Lo: initLogger("priority-manager"),
})
if err != nil {
log.Fatalf("error initializing priority manager: %v", err)
}
return manager
}
// initAI inits AI manager.
func initAI(db *sqlx.DB) *ai.Manager {
lo := initLogger("ai")
m, err := ai.New(ai.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing AI manager: %v", err)
}
return m
}
// initSearch inits search manager.
func initSearch(db *sqlx.DB) *search.Manager {
lo := initLogger("search")
m, err := search.New(search.Opts{
DB: db,
Lo: lo,
})
if err != nil {
log.Fatalf("error initializing search manager: %v", err)
}
return m
}
// initLogger initializes a logf logger.
func initLogger(src string) *logf.Logger {
lvl, env := ko.MustString("app.log_level"), ko.MustString("app.env")
lo := logf.New(logf.Opts{
Level: getLogLevel(lvl),
EnableColor: getColor(env),
EnableCaller: true,
CallerSkipFrameCount: 3,
DefaultFields: []any{"sc", src},
})
return &lo
}
func getColor(env string) bool {
color := false
if env == "dev" {
color = true
}
return color
}
func getLogLevel(lvl string) logf.Level {
switch lvl {
case "info":
return logf.InfoLevel
case "debug":
return logf.DebugLevel
case "warn":
return logf.WarnLevel
case "error":
return logf.ErrorLevel
case "fatal":
return logf.FatalLevel
default:
return logf.InfoLevel
}
}