allow unsetting a log level through subcommand and add admin page for settng log level

This commit is contained in:
Mechiel Lukkien 2023-02-06 15:17:46 +01:00
parent 4202fbe108
commit 6cbe4d5d37
No known key found for this signature in database
8 changed files with 212 additions and 7 deletions

14
ctl.go
View file

@ -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:

2
doc.go
View file

@ -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]]

View file

@ -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)
}

View file

@ -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) {

View file

@ -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": [],

View file

@ -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()

View file

@ -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",

View file

@ -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 {