1// SPDX-License-Identifier: ISC
2// Copyright (c) 2014-2020 Bitmark Inc.
3// Use of this source code is governed by an ISC
4// license that can be found in the LICENSE file.
5
6package logger
7
8import (
9	"encoding/json"
10	"errors"
11	"fmt"
12	"os"
13	"path"
14	"strings"
15	"sync"
16	"time"
17
18	"github.com/cihub/seelog"
19
20	"github.com/bitmark-inc/logger/level"
21)
22
23// initial configuration for the logger
24//
25// example of use (with structure tags for config file parsing)
26//   type AppConfiguration struct {
27//     …
28//     Logging logger.Configuration `libucl:"logging" hcl:"logging" json:"logging"`
29//     …
30//   }
31//
32//   err := logger.Initialise(conf.Logging)
33//   if nil != err {  // if failed then display message and exit
34//     exitwithstatus.Message("logger error: %s", err)
35//   }
36//   defer logger.Finalise()
37//
38// example of ucl/hcl configuration section
39//   logging {
40//     directory = "/var/lib/app/log"
41//     file = "app.log"
42//     size = 1048576
43//     count = 50
44//     #console = true # to duplicate messages to console (default false)
45//     levels {
46//       DEFAULT = "info"
47//       system = "error"
48//       main = "warn"
49//     }
50//   }
51type Configuration struct {
52	Directory string            `libucl:"directory" hcl:"directory" json:"directory"`
53	File      string            `libucl:"file" hcl:"file" json:"file"`
54	Size      int               `libucl:"size" hcl:"size" json:"size"`
55	Count     int               `libucl:"count" hcl:"count" json:"count"`
56	Levels    map[string]string `libucl:"levels" hcl:"levels" json:"levels"`
57	Console   bool              `libucl:"console" hcl:"console" json:"console"`
58}
59
60// some restrictions on sizes
61const (
62	minimumSize  = 20000
63	minimumCount = 10
64
65	// The log messages will be "<tag><tagSuffix><message>"
66	tagSuffix = ": "
67
68	// the tagname reserved to set the default level for unknown tags
69	DefaultTag = "DEFAULT"
70
71	// the initial level for unknown tags
72	DefaultLevel = "error"
73)
74
75// Holds a set of default values for future NewChannel calls
76var levelMap = map[string]string{DefaultTag: DefaultLevel}
77
78// The logging channel structure
79// example of use
80//
81//   var log *logger.L
82//   log := logger.New("sometag")
83//
84//   log.Debugf("value: %d", value)
85type L struct {
86	sync.Mutex
87	tag          string
88	formatPrefix string
89	textPrefix   string
90	level        string
91	levelNumber  int
92	log          seelog.LoggerInterface
93}
94
95// LogLevels - log levels
96type LogLevels struct {
97	Levels []Level `json:"levels"`
98}
99
100// Level - log level info
101type Level struct {
102	Tag      string `json:"tag"`
103	LogLevel string `json:"category"`
104}
105
106type outputTarget int
107
108const (
109	stdOut outputTarget = iota + 1
110	fileOut
111)
112
113// loggers - holds all logger info
114type loggers struct {
115	sync.Mutex
116	globalLog   *L
117	initialised bool
118	data        []*L
119	output      outputTarget
120}
121
122var globalData loggers
123
124// default set output to standard out
125func init() {
126	globalData.output = stdOut
127	stdLogger := defaultLogger()
128
129	_ = seelog.ReplaceLogger(stdLogger)
130	// ensure that the global critical/panic functions always written
131	globalData.globalLog = New("PANIC")
132	globalData.globalLog.level = level.Critical
133	globalData.globalLog.levelNumber = level.ValidLevels[level.Critical]
134}
135
136func defaultLogger() seelog.LoggerInterface {
137	config := fmt.Sprintf(`
138          <seelog type="adaptive"
139                  mininterval="2000000"
140                  maxinterval="100000000"
141                  critmsgcount="500"
142                  minlevel="trace">
143              <outputs formatid="all">
144			      <console />
145              </outputs>
146              <formats>
147                  <format id="all" format="%%Date %%Time [%%LEVEL] %%Msg%%n" />
148              </formats>
149          </seelog>`)
150
151	logger, _ := seelog.LoggerFromConfigAsString(config)
152	return logger
153}
154
155// Set up the logging system
156func Initialise(configuration Configuration) error {
157	if globalData.initialised {
158		return errors.New("logger is already initialised")
159	}
160
161	globalData.output = fileOut
162
163	if "" == configuration.Directory {
164		return errors.New("Directory cannot be empty")
165	}
166
167	if "" == configuration.File {
168		return errors.New("File cannot be empty")
169	}
170
171	d, f := path.Split(configuration.File)
172	if "" != d && f != configuration.File {
173		return fmt.Errorf("File: %q cannot be a path name", configuration.File)
174	}
175
176	if configuration.Size < minimumSize {
177		return fmt.Errorf("Size: %d cannot be less than: %d", configuration.Size, minimumSize)
178	}
179
180	if configuration.Count < minimumCount {
181		return fmt.Errorf("Count: %d cannot be less than: %d", configuration.Count, minimumCount)
182	}
183
184	info, err := os.Lstat(configuration.Directory)
185	if nil != err {
186		return err
187	}
188	if !info.IsDir() {
189		return fmt.Errorf("Directory: %q does not exist", configuration.Directory)
190	}
191	//permission := info.Mode().Perm()
192
193	lockFile := path.Join(configuration.Directory, "log.test")
194	os.Remove(lockFile)
195	fd, err := os.Create(lockFile)
196	if nil != err {
197		return err
198	}
199	defer fd.Close()
200	defer os.Remove(lockFile)
201	n, err := fd.Write([]byte("0123456789"))
202	if nil != err {
203		return err
204	}
205	if 10 != n {
206		return errors.New("unable to write to logging files")
207	}
208
209	for tag, l := range configuration.Levels {
210		// make sure that levelMap only contains correct data
211		// by ignoring invalid levels
212		if _, ok := level.ValidLevels[l]; ok {
213			levelMap[tag] = l
214		}
215	}
216
217	optionalConsole := ""
218	if configuration.Console {
219		optionalConsole = "<console />"
220	}
221
222	filepath := path.Join(configuration.Directory, configuration.File)
223	config := fmt.Sprintf(`
224          <seelog type="adaptive"
225                  mininterval="2000000"
226                  maxinterval="100000000"
227                  critmsgcount="500"
228                  minlevel="trace">
229              <outputs formatid="all">
230                  <rollingfile type="size" filename="%s" maxsize="%d" maxrolls="%d" />
231                  %s
232              </outputs>
233              <formats>
234                  <format id="all" format="%%Date %%Time [%%LEVEL] %%Msg%%n" />
235              </formats>
236          </seelog>`, filepath, configuration.Size, configuration.Count, optionalConsole)
237
238	logger, err := seelog.LoggerFromConfigAsString(config)
239	if err != nil {
240		return err
241	}
242	err = seelog.ReplaceLogger(logger)
243	if nil == err {
244		seelog.Current.Warn("LOGGER: ===== Logging system started =====")
245		globalData.initialised = true
246
247		// ensure that the global critical/panic functions always write to the log file
248		globalData.globalLog = New("PANIC")
249		globalData.globalLog.level = level.Critical
250		globalData.globalLog.levelNumber = level.ValidLevels[level.Critical]
251	}
252	return err
253}
254
255// flush all channels and let log message goes to standard out
256func Finalise() {
257	_ = seelog.Current.Warn("LOGGER: ===== Logging system stopped =====")
258	seelog.Flush()
259
260	// if log message goes to file, make it back to standard output
261	if globalData.output == fileOut {
262		globalData.initialised = false
263		globalData.output = stdOut
264		_ = seelog.ReplaceLogger(defaultLogger())
265
266		globalData.globalLog = New("PANIC")
267		globalData.globalLog.level = level.Critical
268		globalData.globalLog.levelNumber = level.ValidLevels[level.Critical]
269	}
270
271	globalData.data = globalData.data[:0]
272}
273
274// flush all channels
275func Flush() {
276	seelog.Flush()
277}
278
279// Open a new logging channel with a specified tag
280func New(tag string) *L {
281	// if log message goes to file, then Initialise should be called
282	if globalData.output == fileOut && !globalData.initialised {
283		panic("logger.New Initialise was not called")
284	}
285
286	// map % -> %% to be printf safe
287	s := strings.Split(tag, "%")
288	j := strings.Join(s, "%%")
289
290	// determine the level
291	l, ok := levelMap[tag]
292	if !ok {
293		l, ok = levelMap[DefaultTag]
294	}
295	if !ok {
296		l = DefaultLevel
297	}
298
299	// create a logger channel
300	ptr := &L{
301		tag:          tag, // for referencing default level
302		formatPrefix: j + tagSuffix,
303		textPrefix:   tag + tagSuffix,
304		level:        l,
305		levelNumber:  level.ValidLevels[l], // level is validated so get a non-zero value
306		log:          seelog.Current,
307	}
308
309	globalData.Lock()
310	globalData.data = append(globalData.data, ptr)
311	defer globalData.Unlock()
312
313	return ptr
314}
315
316// flush messages
317func (l *L) Flush() {
318	Flush()
319}
320
321// global logging message
322func Critical(message string) {
323	globalData.globalLog.Critical(message)
324}
325
326// global logging formatted message
327func Criticalf(format string, arguments ...interface{}) {
328	globalData.globalLog.Criticalf(format, arguments...)
329}
330
331// global logging message + panic
332func Panic(message string) {
333	globalData.globalLog.Critical(message)
334	Flush()
335	time.Sleep(100 * time.Millisecond) // to allow logging outputTarget
336	panic(message)
337}
338
339// global logging formatted message + panic
340func Panicf(format string, arguments ...interface{}) {
341	globalData.globalLog.Criticalf(format, arguments...)
342	Flush()
343	time.Sleep(100 * time.Millisecond) // to allow logging outputTarget
344	panic(fmt.Sprintf(format, arguments...))
345}
346
347// conditional panic
348func PanicIfError(message string, err error) {
349	if nil == err {
350		return
351	}
352	Panicf("%s failed with error: %v", message, err)
353}
354
355// ListLevels - return log level info in json format
356// it's not lock protected, because it should be low frequency to list log
357// levels
358func ListLevels() ([]byte, error) {
359	levels := make([]Level, 0)
360
361	for _, l := range globalData.data {
362		levels = append(levels, Level{
363			Tag:      l.tag,
364			LogLevel: l.level,
365		})
366	}
367
368	ll := LogLevels{Levels: levels}
369	bs, err := json.Marshal(ll)
370	if nil != err {
371		return []byte{}, err
372	}
373
374	return bs, nil
375}
376
377// UpdateTagLogLevel - update log level for specific tag
378func UpdateTagLogLevel(tag, newLevel string) error {
379	globalData.Lock()
380	defer globalData.Unlock()
381
382	for i, l := range globalData.data {
383		if l.tag == tag {
384			if num, ok := level.ValidLevels[newLevel]; !ok {
385				return fmt.Errorf("level %s invalid", newLevel)
386			} else {
387				globalData.data[i].levelNumber = num
388				globalData.data[i].level = newLevel
389				return nil
390			}
391		}
392	}
393
394	return fmt.Errorf("tag %s not found", tag)
395}
396
397func validLogger(l *L) bool {
398	if globalData.output == stdOut {
399		return true
400	}
401
402	if !globalData.initialised || nil == l {
403		return false
404	}
405
406	return true
407}
408