1package log
2
3import (
4	log "github.com/sirupsen/logrus"
5	"io"
6	"io/ioutil"
7	"net"
8	"os"
9	"sync"
10)
11
12// The following are taken from logrus
13const (
14	// PanicLevel level, highest level of severity. Logs and then calls panic with the
15	// message passed to Debug, Info, ...
16	PanicLevel Level = iota
17	// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
18	// logging level is set to Panic.
19	FatalLevel
20	// ErrorLevel level. Logs. Used for errors that should definitely be noted.
21	// Commonly used for hooks to send errors to an error tracking service.
22	ErrorLevel
23	// WarnLevel level. Non-critical entries that deserve eyes.
24	WarnLevel
25	// InfoLevel level. General operational entries about what's going on inside the
26	// application.
27	InfoLevel
28	// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
29	DebugLevel
30)
31
32type Level uint8
33
34// Convert the Level to a string. E.g. PanicLevel becomes "panic".
35func (level Level) String() string {
36	switch level {
37	case DebugLevel:
38		return "debug"
39	case InfoLevel:
40		return "info"
41	case WarnLevel:
42		return "warning"
43	case ErrorLevel:
44		return "error"
45	case FatalLevel:
46		return "fatal"
47	case PanicLevel:
48		return "panic"
49	}
50
51	return "unknown"
52}
53
54type Logger interface {
55	log.FieldLogger
56	WithConn(conn net.Conn) *log.Entry
57	Reopen() error
58	GetLogDest() string
59	SetLevel(level string)
60	GetLevel() string
61	IsDebug() bool
62	AddHook(h log.Hook)
63}
64
65// Implements the Logger interface
66// It's a logrus logger wrapper that contains an instance of our LoggerHook
67type HookedLogger struct {
68
69	// satisfy the log.FieldLogger interface
70	*log.Logger
71
72	h LoggerHook
73
74	// destination, file name or "stderr", "stdout" or "off"
75	dest string
76
77	oo OutputOption
78}
79
80type loggerKey struct {
81	dest, level string
82}
83
84type loggerCache map[loggerKey]Logger
85
86// loggers store the cached loggers created by NewLogger
87var loggers struct {
88	cache loggerCache
89	// mutex guards the cache
90	sync.Mutex
91}
92
93// GetLogger returns a struct that implements Logger (i.e HookedLogger) with a custom hook.
94// It may be new or already created, (ie. singleton factory pattern)
95// The hook has been initialized with dest
96// dest can can be a path to a file, or the following string values:
97// "off" - disable any log output
98// "stdout" - write to standard output
99// "stderr" - write to standard error
100// If the file doesn't exists, a new file will be created. Otherwise it will be appended
101// Each Logger returned is cached on dest, subsequent call will get the cached logger if dest matches
102// If there was an error, the log will revert to stderr instead of using a custom hook
103
104func GetLogger(dest string, level string) (Logger, error) {
105	loggers.Lock()
106	defer loggers.Unlock()
107	key := loggerKey{dest, level}
108	if loggers.cache == nil {
109		loggers.cache = make(loggerCache, 1)
110	} else {
111		if l, ok := loggers.cache[key]; ok {
112			// return the one we found in the cache
113			return l, nil
114		}
115	}
116	o := parseOutputOption(dest)
117	logrus, err := newLogrus(o, level)
118	if err != nil {
119		return nil, err
120	}
121	l := &HookedLogger{dest: dest}
122	l.Logger = logrus
123
124	// cache it
125	loggers.cache[key] = l
126
127	if o != OutputFile {
128		return l, nil
129	}
130	// we'll use the hook to output instead
131	logrus.Out = ioutil.Discard
132	// setup the hook
133	if h, err := NewLogrusHook(dest); err != nil {
134		// revert back to stderr
135		logrus.Out = os.Stderr
136		return l, err
137	} else {
138		logrus.Hooks.Add(h)
139		l.h = h
140	}
141
142	return l, nil
143
144}
145
146func newLogrus(o OutputOption, level string) (*log.Logger, error) {
147	logLevel, err := log.ParseLevel(level)
148	if err != nil {
149		return nil, err
150	}
151	var out io.Writer
152
153	if o != OutputFile {
154		if o == OutputNull || o == OutputStderr {
155			out = os.Stderr
156		} else if o == OutputStdout {
157			out = os.Stdout
158		} else if o == OutputOff {
159			out = ioutil.Discard
160		}
161	} else {
162		// we'll use a hook to output instead
163		out = ioutil.Discard
164	}
165
166	logger := &log.Logger{
167		Out:       out,
168		Formatter: new(log.TextFormatter),
169		Hooks:     make(log.LevelHooks),
170		Level:     logLevel,
171	}
172
173	return logger, nil
174}
175
176// AddHook adds a new logrus hook
177func (l *HookedLogger) AddHook(h log.Hook) {
178	log.AddHook(h)
179}
180
181func (l *HookedLogger) IsDebug() bool {
182	return l.GetLevel() == log.DebugLevel.String()
183}
184
185// SetLevel sets a log level, one of the LogLevels
186func (l *HookedLogger) SetLevel(level string) {
187	var logLevel log.Level
188	var err error
189	if logLevel, err = log.ParseLevel(level); err != nil {
190		return
191	}
192	log.SetLevel(logLevel)
193}
194
195// GetLevel gets the current log level
196func (l *HookedLogger) GetLevel() string {
197	return l.Level.String()
198}
199
200// Reopen closes the log file and re-opens it
201func (l *HookedLogger) Reopen() error {
202	if l.h == nil {
203		return nil
204	}
205	return l.h.Reopen()
206}
207
208// GetLogDest Gets the file name
209func (l *HookedLogger) GetLogDest() string {
210	return l.dest
211}
212
213// WithConn extends logrus to be able to log with a net.Conn
214func (l *HookedLogger) WithConn(conn net.Conn) *log.Entry {
215	var addr = "unknown"
216	if conn != nil {
217		addr = conn.RemoteAddr().String()
218	}
219	return l.WithField("addr", addr)
220}
221