aboutsummaryrefslogtreecommitdiffstats
path: root/logging/syslog.go
diff options
context:
space:
mode:
Diffstat (limited to 'logging/syslog.go')
-rw-r--r--logging/syslog.go166
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
+ }
+}