diff options
Diffstat (limited to 'logging/syslog.go')
| -rw-r--r-- | logging/syslog.go | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/logging/syslog.go b/logging/syslog.go new file mode 100644 index 0000000..00e3738 --- /dev/null +++ b/logging/syslog.go @@ -0,0 +1,166 @@ +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 + } +} |
