package logging import ( "context" "fmt" "io" "log/slog" "os" "path/filepath" "sync" "time" "go.sudomsg.com/kit/net" ) type Facility int const ( FacilityKern Facility = 0 // kernel messages FacilityUser Facility = 1 // user-level messages FacilityMail Facility = 2 // mail system FacilityDaemon Facility = 3 // system daemons FacilityAuth Facility = 4 // security/authorization messages FacilitySyslog Facility = 5 // messages generated internally by syslogd FacilityLPR Facility = 6 // line printer subsystem FacilityNews Facility = 7 // network news subsystem FacilityUUCP Facility = 8 // UUCP subsystem FacilityCron Facility = 9 // clock daemon FacilityAuthPriv Facility = 10 // security/authorization messages (private) FacilityFTP Facility = 11 // FTP daemon FacilityLocal0 Facility = 16 FacilityLocal7 Facility = 23 ) type SyslogSink struct { Writer io.Writer Facility Facility Tag string Hostname string Level slog.Level Encoder Encoder EmitHostname bool ReplaceAttr func(groups []string, a slog.Attr) slog.Attr mu sync.Mutex } var _ Sink = &SyslogSink{} type SyslogOptions struct { Facility Facility Tag string Hostname string Level slog.Leveler ReplaceAttr func(groups []string, a slog.Attr) slog.Attr } // DialSyslog creates a Sink connected to a local or remote syslog daemon. // raddr: "/dev/log", "localhost:514", etc. func DialSyslog(ctx context.Context, network net.NetType, raddr string, enc Encoder, options SyslogOptions) (*SyslogSink, error) { if raddr == "" { network, raddr = resolveLocalEndpoint() } conn, err := net.Dial(ctx, network, raddr) if err != nil { return nil, fmt.Errorf("syslog dial failed: %w", err) } isLocal := (network == net.NetUnixDatagram || network == net.NetUnix) sink := NewSyslogSink(conn, enc, options) sink.EmitHostname = !isLocal return sink, nil } func NewSyslogSink(w io.Writer, enc Encoder, options SyslogOptions) *SyslogSink { if options.Hostname == "" { options.Hostname, _ = os.Hostname() } if options.Hostname == "" { options.Hostname = "localhost" } if options.Tag == "" { options.Tag = filepath.Base(os.Args[0]) } if options.Tag == "" { options.Tag = "kit" } if len(options.Tag) > 32 { options.Tag = options.Tag[:32] } return &SyslogSink{ Writer: w, Encoder: enc, Tag: options.Tag, Facility: options.Facility, Hostname: options.Hostname, ReplaceAttr: options.ReplaceAttr, } } func (s *SyslogSink) Enabled(level slog.Level) bool { return s.Level <= level } func (s *SyslogSink) Append(r slog.Record) error { level := slog.Leveler(s.Level) var ts time.Time it := RecordAll(r, func(groups []string, attr slog.Attr) slog.Attr { if s.ReplaceAttr != nil { attr = s.ReplaceAttr(groups, attr) } if len(groups) == 0 { if attr.Key == slog.TimeKey { ts = attr.Value.Time() return slog.Attr{} } if attr.Key == slog.LevelKey { level = attr.Value.Any().(slog.Leveler) return slog.Attr{} } } return attr }) out, err := s.Encoder(it) if err != nil { return fmt.Errorf("failed to encode: %w", err) } pri := int(s.Facility)*8 + mapLevelToSyslog(level.Level()) // Note: RFC 3164 uses a space for leading zeros in days (e.g., "Jan 2") t := "" if !ts.IsZero() { t = r.Time.Format("Jan _2 15:04:05") } hostname := "" if s.EmitHostname { hostname = s.Hostname } packet := fmt.Sprintf("<%d>%s %s %s: %s", pri, t, hostname, s.Tag, out) s.mu.Lock() defer s.mu.Unlock() _, err = fmt.Fprint(s.Writer, packet) return err } func mapLevelToSyslog(l slog.Level) int { switch { case l >= slog.LevelError: return 3 case l >= slog.LevelWarn: return 4 case l >= slog.LevelInfo: return 6 default: return 7 } }