Skip to content

Navigation Menu

Sign in
Sign up

feat: added remote syslog logging and log annotation (domain+msgid) #850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
terminar wants to merge 1 commit into foxcpp:master
base: master
Choose a base branch
Loading
from terminar:feature/logging-remote-syslog
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions config.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package maddy
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"

Expand Down Expand Up @@ -53,13 +54,33 @@ func logOutput(_ *config.Map, node config.Node) (interface{}, error) {

func LogOutputOption(args []string) (log.Output, error) {
outs := make([]log.Output, 0, len(args))
for i, arg := range args {
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "stderr":
outs = append(outs, log.WriterOutput(os.Stderr, false))
case "stderr_ts":
outs = append(outs, log.WriterOutput(os.Stderr, true))
case "syslog":
case "syslog", "syslog+tcp", "syslog+udp":
network := "udp"
if arg == "syslog+tcp" {
network = "tcp"
}
// Optional next arg: remote address in host:port form.
if i+1 < len(args) {
if _, _, err := net.SplitHostPort(args[i+1]); err == nil {
i++
syslogOut, err := log.RemoteSyslogOutput(network, args[i])
if err != nil {
return nil, fmt.Errorf("failed to connect to remote syslog at %s: %v", args[i], err)
}
outs = append(outs, syslogOut)
break
}
}
if arg != "syslog" {
return nil, fmt.Errorf("'%s' requires a remote address (host:port)", arg)
}
syslogOut, err := log.SyslogOutput()
if err != nil {
return nil, fmt.Errorf("failed to connect to syslog daemon: %v", err)
Expand Down
31 changes: 31 additions & 0 deletions framework/log/log.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ type Logger struct {
// Additional fields that will be added
// to the Msg output.
Fields map[string]interface{}

// LogFields are rendered as [key=value ...] before the message text so
// that individual mail flows can be identified and filtered with grep.
// Use With() to populate this.
LogFields []string

@foxcpp foxcpp May 23, 2026
edited
Loading

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is a good solution since we already have Fields.

This is probably better done by customizing formatting at Logger level - msg_id is already added explicitly for most log records that are related to message delivery.

This, in turn, would require some further design work as current logger implementation is very simple and lacks extended configuration parsing.

Is there a specific reason why need fields formatted in this exact way?

}

func (l *Logger) Zap() *zap.Logger {
Expand Down Expand Up @@ -171,6 +176,17 @@ func fieldsToMap(fields []interface{}, out map[string]interface{}) {
func (l *Logger) formatMsg(msg string, fields map[string]interface{}) string {
formatted := strings.Builder{}

if len(l.LogFields) > 0 {
formatted.WriteRune('[')
for i, f := range l.LogFields {
if i > 0 {
formatted.WriteRune(' ')
}
formatted.WriteString(f)
}
formatted.WriteString("] ")
}

formatted.WriteString(msg)
formatted.WriteRune('\t')

Expand Down Expand Up @@ -252,6 +268,21 @@ func (l *Logger) Sublogger(name string) *Logger {
}
}

// With returns a copy of the logger with additional key=value pairs prepended
// to every log message as [key=value ...]. Multiple With calls accumulate.
// Use this to attach identifiers like domain or message-ID so log lines can
// be filtered with grep.
func (l *Logger) With(kvpairs ...interface{}) *Logger {
inherited := make([]string, len(l.LogFields), len(l.LogFields)+len(kvpairs)/2)
copy(inherited, l.LogFields)
for i := 0; i+1 < len(kvpairs); i += 2 {
inherited = append(inherited, fmt.Sprintf("%v=%v", kvpairs[i], kvpairs[i+1]))
}
newL := *l
newL.LogFields = inherited
return &newL
}

// DefaultLogger is the global Logger object that is used by
// package-level logging functions.
//
Expand Down
8 changes: 8 additions & 0 deletions framework/log/syslog.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,11 @@ func SyslogOutput() (Output, error) {
w, err := syslog.New(syslog.LOG_MAIL|syslog.LOG_INFO, "maddy")
return syslogOut{w}, err
}

// RemoteSyslogOutput returns a log.Output implementation that sends
// messages to a remote syslog daemon at addr (host:port) using network
// ("udp" or "tcp").
func RemoteSyslogOutput(network, addr string) (Output, error) {
w, err := syslog.Dial(network, addr, syslog.LOG_MAIL|syslog.LOG_INFO, "maddy")
return syslogOut{w}, err
}
4 changes: 4 additions & 0 deletions framework/log/syslog_stub.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ import (
func SyslogOutput() (Output, error) {
return nil, errors.New("log: syslog output is not supported on windows")
}

func RemoteSyslogOutput(network, addr string) (Output, error) {
return nil, errors.New("log: syslog output is not supported on windows")
}
14 changes: 13 additions & 1 deletion internal/endpoint/smtp/session.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ type Session struct {
delivery module.Delivery
deliveryErr error

log *log.Logger
log *log.Logger
connLog *log.Logger // connection-scoped logger; log is temporarily replaced per message
}

func (s *Session) AuthMechanisms() []string {
Expand Down Expand Up @@ -149,6 +150,9 @@ func (s *Session) abort(ctx context.Context) {
func (s *Session) cleanSession() {
s.releaseLimits()

if s.connLog != nil {
s.log = s.connLog
}
s.mailFrom = ""
s.opts = smtp.MailOptions{}
s.msgMeta = nil
Expand Down Expand Up @@ -271,6 +275,14 @@ func (s *Session) startDelivery(ctx context.Context, from string, opts smtp.Mail
s.mailFrom = cleanFrom
s.delivery = delivery

if s.connLog != nil {
if domain != "" {
s.log = s.connLog.With("msg_id", msgMeta.ID, "from_domain", domain)
} else {
s.log = s.connLog.With("msg_id", msgMeta.ID)
}
}

return msgMeta.ID, nil
}

Expand Down
1 change: 1 addition & 0 deletions internal/endpoint/smtp/smtp.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ func (endp *Endpoint) newSession(conn *smtp.Conn) *Session {
s := &Session{
endp: endp,
log: endp.log,
connLog: endp.log,
sessionCtx: context.Background(),
}

Expand Down
17 changes: 10 additions & 7 deletions internal/target/delivery.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
package target

import (
"strings"

"github.com/foxcpp/maddy/framework/log"
"github.com/foxcpp/maddy/framework/module"
)

// DeliveryLogger returns a logger annotated with msg_id and from_domain so
// every log line can be filtered by message ID or sender domain.
func DeliveryLogger(parent *log.Logger, msgMeta *module.MsgMetadata) *log.Logger {
l := parent.Sublogger("")
fields := make(map[string]interface{}, len(l.Fields)+1)
for k, v := range l.Fields {
fields[k] = v
kvpairs := []interface{}{"msg_id", msgMeta.ID}
if msgMeta.OriginalFrom != "" {
if _, domain, ok := strings.Cut(msgMeta.OriginalFrom, "@"); ok && domain != "" {
kvpairs = append(kvpairs, "from_domain", domain)
}
}
fields["msg_id"] = msgMeta.ID
l.Fields = fields
return l
return parent.Sublogger("").With(kvpairs...)
}

AltStyle によって変換されたページ (->オリジナル) /