aboutsummaryrefslogtreecommitdiffstats
path: root/logging/syslog.go
blob: 00e3738c2b4e7a5213daebdf2f1e51f99e0b4d1e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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
	}
}