diff --git a/ctl.go b/ctl.go index 3ac146f..33ced36 100644 --- a/ctl.go +++ b/ctl.go @@ -613,16 +613,20 @@ func servectlcmd(ctx context.Context, log *mlog.Log, ctl *ctl, xcmd *string, shu /* protocol: > "setloglevels" > pkg - > level + > level (if empty, log level for pkg will be unset) < "ok" or error */ pkg := ctl.xread() levelstr := ctl.xread() - level, ok := mlog.Levels[levelstr] - if !ok { - ctl.xerror("bad level") + if levelstr == "" { + mox.Conf.LogLevelRemove(pkg) + } else { + level, ok := mlog.Levels[levelstr] + if !ok { + ctl.xerror("bad level") + } + mox.Conf.LogLevelSet(pkg, level) } - mox.Conf.SetLogLevel(pkg, level) ctl.xwriteok() default: diff --git a/doc.go b/doc.go index a60c573..1ba0690 100644 --- a/doc.go +++ b/doc.go @@ -146,6 +146,8 @@ By default, a single log level applies to all logging in mox. But for each smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message, etc. +Specify a pkg and an empty level to clear the configured level for a package. + Valid labels: error, info, debug, trace, traceauth, tracedata. usage: mox loglevels [level [pkg]] diff --git a/http/admin.go b/http/admin.go index ccd2052..307936c 100644 --- a/http/admin.go +++ b/http/admin.go @@ -1462,3 +1462,26 @@ func (Admin) QueueDrop(ctx context.Context, id int64) { } xcheckf(ctx, err, "drop message from queue") } + +// LogLevels returns the current log levels. +func (Admin) LogLevels(ctx context.Context) map[string]string { + m := map[string]string{} + for pkg, level := range mox.Conf.LogLevels() { + m[pkg] = level.String() + } + return m +} + +// LogLevelSet sets a log level for a package. +func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) { + level, ok := mlog.Levels[levelStr] + if !ok { + xcheckf(ctx, errors.New("unknown"), "lookup level") + } + mox.Conf.LogLevelSet(pkg, level) +} + +// LogLevelRemove removes a log level for a package, which cannot be the empty string. +func (Admin) LogLevelRemove(ctx context.Context, pkg string) { + mox.Conf.LogLevelRemove(pkg) +} diff --git a/http/admin.html b/http/admin.html index b5c05e2..5fc2b0e 100644 --- a/http/admin.html +++ b/http/admin.html @@ -260,6 +260,7 @@ const index = async () => { dom.br(), dom.h2('Configuration'), dom.div(dom.a('See configuration', attr({href: '#config'}))), + dom.div(dom.a('Log levels', attr({href: '#loglevels'}))), footer, ) } @@ -280,6 +281,116 @@ const config = async () => { ) } +const loglevels = async () => { + const loglevels = await api.LogLevels() + + const levels = ['error', 'info', 'debug', 'trace', 'traceauth', 'tracedata'] + + let form, fieldset, pkg, level + + const page = document.getElementById('page') + dom._kids(page, + crumbs( + crumblink('Mox Admin', '#'), + 'Log levels', + ), + dom.p('Note: changing a log level here only changes it for the current process. When mox restarts, it sets the log levels from the configuration file. Change mox.conf to keep the changes.'), + dom.table( + dom.thead( + dom.tr( + dom.th('Package', attr({title: 'Log levels can be configured per package. E.g. smtpserver, imapserver, dkim, dmarc, tlsrpt, etc.'})), + dom.th('Level', attr({title: 'If you set the log level to "trace", imap and smtp protocol transcripts will be logged. Sensitive authentication is replaced with "***" unless the level is >= "traceauth". Data is masked with "..." unless the level is "tracedata".'})), + dom.th('Action'), + ), + ), + dom.tbody( + Object.entries(loglevels).map(t => { + let lvl + return dom.tr( + dom.td(t[0] || '(default)'), + dom.td( + lvl=dom.select(levels.map(l => dom.option(l, t[1] === l ? attr({selected: ''}) : []))), + ), + dom.td( + dom.button('Save', attr({title: 'Set new log level for package.'}), async function click(e) { + e.preventDefault() + try { + e.target.disabled = true + await api.LogLevelSet(t[0], lvl.value) + } catch (err) { + console.log({err}) + window.alert('Error: ' + err) + return + } finally { + e.target.disabled = false + } + window.location.reload() // todo: reload just the current loglevels + }), + ' ', + dom.button('Remove', attr({title: 'Remove this log level, the default log level will apply.'}), t[0] === '' ? attr({disabled: ''}) : [], async function click(e) { + e.preventDefault() + try { + e.target.disabled = true + await api.LogLevelRemove(t[0]) + } catch (err) { + console.log({err}) + window.alert('Error: ' + err) + return + } finally { + e.target.disabled = false + } + window.location.reload() // todo: reload just the current loglevels + }), + ), + ) + }), + ), + ), + dom.br(), + dom.h2('Add log level setting'), + form=dom.form( + async function submit(e) { + e.preventDefault() + e.stopPropagation() + fieldset.disabled = true + try { + await api.LogLevelSet(pkg.value, level.value) + } catch (err) { + console.log({err}) + window.alert('Error: ' + err.message) + return + } finally { + fieldset.disabled = false + } + form.reset() + window.location.reload() // todo: reload just the current loglevels + }, + fieldset=dom.fieldset( + dom.label( + style({display: 'inline-block'}), + 'Package', + dom.br(), + pkg=dom.input(attr({required: ''})), + ), + ' ', + dom.label( + style({display: 'inline-block'}), + 'Level', + dom.br(), + level=dom.select( + attr({required: ''}), + levels.map(l => dom.option(l, l === 'debug' ? attr({selected: ''}) : [])), + ), + ), + ' ', + dom.button('Add'), + ), + dom.br(), + dom.p('Suggestions for packages: autotls dkim dmarc dmarcdb dns dnsbl dsn http imapserver iprev junk message metrics mox moxio mtasts mtastsdb publicsuffix queue sendmail serve smtpserver spf store subjectpass tlsrpt tlsrptdb updates'), + ), + ) +} + const box = (color, ...l) => [ dom.div( style({ @@ -1438,6 +1549,8 @@ const init = async () => { await index() } else if (h === 'config') { await config() + } else if (h === 'loglevels') { + await loglevels() } else if (h === 'accounts') { await accounts() } else if (t[0] === 'accounts' && t.length === 2) { diff --git a/http/adminapi.json b/http/adminapi.json index a5f588a..4bbef9b 100644 --- a/http/adminapi.json +++ b/http/adminapi.json @@ -577,6 +577,52 @@ } ], "Returns": [] + }, + { + "Name": "LogLevels", + "Docs": "LogLevels returns the current log levels.", + "Params": [], + "Returns": [ + { + "Name": "r0", + "Typewords": [ + "{}", + "string" + ] + } + ] + }, + { + "Name": "LogLevelSet", + "Docs": "LogLevelSet sets a log level for a package.", + "Params": [ + { + "Name": "pkg", + "Typewords": [ + "string" + ] + }, + { + "Name": "levelStr", + "Typewords": [ + "string" + ] + } + ], + "Returns": [] + }, + { + "Name": "LogLevelRemove", + "Docs": "LogLevelRemove removes a log level for a package, which cannot be the empty string.", + "Params": [ + { + "Name": "pkg", + "Typewords": [ + "string" + ] + } + ], + "Returns": [] } ], "Sections": [], diff --git a/main.go b/main.go index 57cb6e8..6e46499 100644 --- a/main.go +++ b/main.go @@ -745,6 +745,8 @@ By default, a single log level applies to all logging in mox. But for each smtpserver, smtpclient, queue, imapserver, spf, dkim, dmarc, junk, message, etc. +Specify a pkg and an empty level to clear the configured level for a package. + Valid labels: error, info, debug, trace, traceauth, tracedata. ` args := c.Parse() diff --git a/mlog/log.go b/mlog/log.go index 49d5eb5..5fba407 100644 --- a/mlog/log.go +++ b/mlog/log.go @@ -38,6 +38,10 @@ var Logfmt bool type Level int +func (l Level) String() string { + return LevelStrings[l] +} + var LevelStrings = map[Level]string{ LevelPrint: "print", LevelFatal: "fatal", diff --git a/mox-/config.go b/mox-/config.go index 8d3dbb7..a4afad2 100644 --- a/mox-/config.go +++ b/mox-/config.go @@ -66,10 +66,10 @@ type AccountDestination struct { Destination config.Destination } -// SetLogLevel sets a new log level for pkg. An empty pkg sets the default log +// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log // value that is used if no explicit log level is configured for a package. // This change is ephemeral, no config file is changed. -func (c *Config) SetLogLevel(pkg string, level mlog.Level) { +func (c *Config) LogLevelSet(pkg string, level mlog.Level) { c.logMutex.Lock() defer c.logMutex.Unlock() l := c.copyLogLevels() @@ -79,6 +79,17 @@ func (c *Config) SetLogLevel(pkg string, level mlog.Level) { mlog.SetConfig(c.Log) } +// LogLevelRemove removes a configured log level for a package. +func (c *Config) LogLevelRemove(pkg string) { + c.logMutex.Lock() + defer c.logMutex.Unlock() + l := c.copyLogLevels() + delete(l, pkg) + c.Log = l + xlog.Print("log level cleared", mlog.Field("pkg", pkg)) + mlog.SetConfig(c.Log) +} + // copyLogLevels returns a copy of c.Log, for modifications. // must be called with log lock held. func (c *Config) copyLogLevels() map[string]mlog.Level {