logging is now compatible with klog.
See https://git.kyleisom.net/lib/libklogger
This commit is contained in:
parent
419f23d655
commit
d1452f54c0
|
@ -0,0 +1,14 @@
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// Console is a Logger that writes to the console. It must be
|
||||||
|
// constructed with a call to NewConsole.
|
||||||
|
type Console struct {
|
||||||
|
*LogWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConsole returns a new console logger.
|
||||||
|
func NewConsole() *Console {
|
||||||
|
return &Console{LogWriter: NewLogWriter(os.Stdout, os.Stderr)}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Package logging implements attribute-based logging. Log entries
|
||||||
|
// consist of timestamps, an actor and event string, and a mapping of
|
||||||
|
// string key-value attribute pairs. For example,
|
||||||
|
//
|
||||||
|
// log.Error("serialiser", "failed to open file",
|
||||||
|
// map[string]string{
|
||||||
|
// "error": err.Error(),
|
||||||
|
// "path": "data.bin",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// This produces the output message
|
||||||
|
//
|
||||||
|
// [2016-04-01T15:04:30-0700] [ERROR] [actor:serialiser event:failed to open file] error=is a directory path=data.bin
|
||||||
|
//
|
||||||
|
package logging
|
|
@ -1,75 +1,43 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/kisom/goutils/logging"
|
"github.com/kisom/goutils/logging"
|
||||||
"github.com/kisom/testio"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logging.Init()
|
var log = logging.NewConsole()
|
||||||
var olog, _ = logging.New("subsystem #42", logging.LevelNotice)
|
var olog = logging.NewConsole()
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
exampleNewWriters()
|
log.Info("example", "Hello, world.", nil)
|
||||||
log.Notice("Hello, world.")
|
log.Warn("example", "this program is about to end", nil)
|
||||||
log.Warning("this program is about to end")
|
|
||||||
|
|
||||||
log.SetLevel(logging.LevelDebug)
|
log.Critical("example", "screaming into the void", nil)
|
||||||
log.Debug("hello world")
|
olog.Critical("other", "can anyone hear me?", nil)
|
||||||
log.SetLevel(logging.LevelNotice)
|
|
||||||
|
|
||||||
olog.Print("now online")
|
log.Warn("example", "but not for long", nil)
|
||||||
logging.Suppress("olog")
|
|
||||||
olog.Print("extraneous information")
|
|
||||||
|
|
||||||
logging.Enable("olog")
|
log.Info("example", "fare thee well", nil)
|
||||||
olog.Print("relevant now")
|
olog.Info("other", "all good journeys must come to an end",
|
||||||
|
map[string]string{"when": time.Now().String()})
|
||||||
|
|
||||||
logging.SuppressAll()
|
log.Info("example", "filelog test", nil)
|
||||||
log.Alert("screaming into the void")
|
|
||||||
olog.Critical("can anyone hear me?")
|
|
||||||
|
|
||||||
log.Enable()
|
|
||||||
log.Notice("i'm baaack")
|
|
||||||
log.Suppress()
|
|
||||||
log.Warning("but not for long")
|
|
||||||
|
|
||||||
logging.EnableAll()
|
|
||||||
log.Notice("fare thee well")
|
|
||||||
olog.Print("all good journeys must come to an end")
|
|
||||||
exampleNewFromFile()
|
exampleNewFromFile()
|
||||||
}
|
os.Remove("example.log")
|
||||||
|
os.Remove("example.err")
|
||||||
func exampleNewWriters() {
|
|
||||||
o := testio.NewBufCloser(nil)
|
|
||||||
e := testio.NewBufCloser(nil)
|
|
||||||
|
|
||||||
wlog, _ := logging.NewFromWriters("writers", logging.DefaultLevel, o, e)
|
|
||||||
wlog.Notice("hello, world")
|
|
||||||
wlog.Notice("some more things happening")
|
|
||||||
wlog.Warning("something suspicious has happened")
|
|
||||||
wlog.Alert("pick up that can, Citizen!")
|
|
||||||
|
|
||||||
fmt.Println("--- BEGIN OUT ---")
|
|
||||||
fmt.Printf("%s", o.Bytes())
|
|
||||||
fmt.Println("--- END OUT ---")
|
|
||||||
|
|
||||||
fmt.Println("--- BEGIN ERR ---")
|
|
||||||
fmt.Printf("%s", e.Bytes())
|
|
||||||
fmt.Println("--- END ERR ---")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func exampleNewFromFile() {
|
func exampleNewFromFile() {
|
||||||
flog, err := logging.NewFromFile("file logger", logging.LevelNotice,
|
flog, err := logging.NewSplitFile("example.log", "example.err", true)
|
||||||
"example.log", "example.err", true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to open logger: %v", err)
|
log.Fatal("filelog", "failed to open logger",
|
||||||
|
map[string]string{"error": err.Error()})
|
||||||
}
|
}
|
||||||
defer flog.Close()
|
|
||||||
|
|
||||||
flog.Notice("hello, world")
|
flog.Info("filelog", "hello, world", nil)
|
||||||
flog.Notice("some more things happening")
|
flog.Info("filelog", "some more things happening", nil)
|
||||||
flog.Warning("something suspicious has happened")
|
flog.Warn("filelog", "something suspicious has happened", nil)
|
||||||
flog.Alert("pick up that can, Citizen!")
|
flog.Critical("filelog", "pick up that can, Citizen!", nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,37 @@
|
||||||
package logging_test
|
package logging_test
|
||||||
|
|
||||||
import "github.com/kisom/goutils/logging"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
var log = logging.Init()
|
"github.com/kisom/goutils/logging"
|
||||||
var olog, _ = logging.New("subsystem #42", logging.LevelNotice)
|
)
|
||||||
|
|
||||||
|
var log = logging.NewConsole()
|
||||||
|
var olog = logging.NewConsole()
|
||||||
|
|
||||||
func Example() {
|
func Example() {
|
||||||
log.Notice("Hello, world.")
|
log.Info("example", "Hello, world.", nil)
|
||||||
log.Warning("this program is about to end")
|
log.Warn("example", "this program is about to end", nil)
|
||||||
|
|
||||||
olog.Print("now online")
|
log.Critical("example", "screaming into the void", nil)
|
||||||
logging.Suppress("olog")
|
olog.Critical("other", "can anyone hear me?", nil)
|
||||||
olog.Print("extraneous information")
|
|
||||||
|
|
||||||
logging.Enable("olog")
|
log.Warn("example", "but not for long", nil)
|
||||||
olog.Print("relevant now")
|
|
||||||
|
|
||||||
logging.SuppressAll()
|
log.Info("example", "fare thee well", nil)
|
||||||
log.Alert("screaming into the void")
|
olog.Info("example", "all good journeys must come to an end",
|
||||||
olog.Critical("can anyone hear me?")
|
map[string]string{"when": time.Now().String()})
|
||||||
|
|
||||||
log.Enable()
|
|
||||||
log.Notice("i'm baaack")
|
|
||||||
log.Suppress()
|
|
||||||
log.Warning("but not for long")
|
|
||||||
|
|
||||||
logging.EnableAll()
|
|
||||||
log.Notice("fare thee well")
|
|
||||||
olog.Print("all good journeys must come to an end")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleNewFromFile() {
|
func ExampleNewFromFile() {
|
||||||
log, err := logging.NewFromFile("file logger", logging.LevelNotice,
|
flog, err := logging.NewSplitFile("example.log", "example.err", true)
|
||||||
"example.log", "example.err", true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to open logger: %v", err)
|
log.Fatal("filelog", "failed to open logger",
|
||||||
|
map[string]string{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Notice("hello, world")
|
flog.Info("filelog", "hello, world", nil)
|
||||||
log.Notice("some more things happening")
|
flog.Info("filelog", "some more things happening", nil)
|
||||||
log.Warning("something suspicious has happened")
|
flog.Warn("filelog", "something suspicious has happened", nil)
|
||||||
log.Alert("pick up that can, Citizen!")
|
flog.Critical("filelog", "pick up that can, Citizen!", nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// File writes its logs to file.
|
||||||
|
type File struct {
|
||||||
|
fo, fe *os.File
|
||||||
|
*LogWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fl *File) Close() {
|
||||||
|
fl.fo.Close()
|
||||||
|
if fl.fe != nil {
|
||||||
|
fl.fe.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFile creates a new Logger that writes all logs to the file
|
||||||
|
// specified by path. If overwrite is specified, the log file will be
|
||||||
|
// truncated before writing. Otherwise, the log file will be appended
|
||||||
|
// to.
|
||||||
|
func NewFile(path string, overwrite bool) (*File, error) {
|
||||||
|
fl := new(File)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if overwrite {
|
||||||
|
fl.fo, err = os.Create(path)
|
||||||
|
} else {
|
||||||
|
fl.fo, err = os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fl.LogWriter = NewLogWriter(fl.fo, fl.fo)
|
||||||
|
return fl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSplitFile creates a new Logger that writes debug and information
|
||||||
|
// messages to the output file, and warning and higher messages to the
|
||||||
|
// error file. If overwrite is specified, the log files will be
|
||||||
|
// truncated before writing.
|
||||||
|
func NewSplitFile(outpath, errpath string, overwrite bool) (*File, error) {
|
||||||
|
fl := new(File)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if overwrite {
|
||||||
|
fl.fo, err = os.Create(outpath)
|
||||||
|
} else {
|
||||||
|
fl.fo, err = os.OpenFile(outpath, os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if overwrite {
|
||||||
|
fl.fe, err = os.Create(errpath)
|
||||||
|
} else {
|
||||||
|
fl.fe, err = os.OpenFile(errpath, os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fl.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fl.LogWriter = NewLogWriter(fl.fo, fl.fe)
|
||||||
|
return fl, nil
|
||||||
|
}
|
|
@ -1,12 +1,5 @@
|
||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Level represents a logging level.
|
// A Level represents a logging level.
|
||||||
type Level uint8
|
type Level uint8
|
||||||
|
|
||||||
|
@ -14,15 +7,11 @@ type Level uint8
|
||||||
const (
|
const (
|
||||||
// LevelDebug are debug output useful during program testing
|
// LevelDebug are debug output useful during program testing
|
||||||
// and debugging.
|
// and debugging.
|
||||||
LevelDebug = iota
|
LevelDebug = 1 << iota
|
||||||
|
|
||||||
// LevelInfo is used for informational messages.
|
// LevelInfo is used for informational messages.
|
||||||
LevelInfo
|
LevelInfo
|
||||||
|
|
||||||
// LevelNotice is for messages that are normal but
|
|
||||||
// significant.
|
|
||||||
LevelNotice
|
|
||||||
|
|
||||||
// LevelWarning is for messages that are warning conditions:
|
// LevelWarning is for messages that are warning conditions:
|
||||||
// they're not indicative of a failure, but of a situation
|
// they're not indicative of a failure, but of a situation
|
||||||
// that may lead to a failure later.
|
// that may lead to a failure later.
|
||||||
|
@ -35,15 +24,13 @@ const (
|
||||||
// LevelCritical are messages for critical conditions.
|
// LevelCritical are messages for critical conditions.
|
||||||
LevelCritical
|
LevelCritical
|
||||||
|
|
||||||
// LevelAlert are for messages indicating that action
|
|
||||||
// must be taken immediately.
|
|
||||||
LevelAlert
|
|
||||||
|
|
||||||
// LevelFatal messages are akin to syslog's LOG_EMERG: the
|
// LevelFatal messages are akin to syslog's LOG_EMERG: the
|
||||||
// system is unusable and cannot continue execution.
|
// system is unusable and cannot continue execution.
|
||||||
LevelFatal
|
LevelFatal
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultLevel = LevelInfo
|
||||||
|
|
||||||
// Cheap integer to fixed-width decimal ASCII. Give a negative width
|
// Cheap integer to fixed-width decimal ASCII. Give a negative width
|
||||||
// to avoid zero-padding. (From log/log.go in the standard library).
|
// to avoid zero-padding. (From log/log.go in the standard library).
|
||||||
func itoa(i int, wid int) string {
|
func itoa(i int, wid int) string {
|
||||||
|
@ -70,168 +57,13 @@ func writeToOut(level Level) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
var levelPrefix = [...]string{
|
var levelPrefix = [...]string{
|
||||||
LevelDebug: "[DEBUG] ",
|
LevelDebug: "DEBUG",
|
||||||
LevelInfo: "[INFO] ",
|
LevelInfo: "INFO",
|
||||||
LevelNotice: "[NOTICE] ",
|
LevelWarning: "WARNING",
|
||||||
LevelWarning: "[WARNING] ",
|
LevelError: "ERROR",
|
||||||
LevelError: "[ERROR] ",
|
LevelCritical: "CRITICAL",
|
||||||
LevelCritical: "[CRITICAL] ",
|
LevelFatal: "FATAL",
|
||||||
LevelAlert: "[ALERT] ",
|
|
||||||
LevelFatal: "[FATAL] ",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DateFormat contains the default date format string used by the logger.
|
// DateFormat contains the default date format string used by the logger.
|
||||||
var DateFormat = "2006-01-02T15:03:04-0700"
|
const DateFormat = "2006-01-02T15:03:04-0700"
|
||||||
|
|
||||||
func (l *Logger) outputf(level Level, format string, v []interface{}) {
|
|
||||||
if !l.Enabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if level >= l.level {
|
|
||||||
domain := l.domain
|
|
||||||
if level == LevelDebug {
|
|
||||||
_, file, line, ok := runtime.Caller(2)
|
|
||||||
if ok {
|
|
||||||
domain += " " + file + ":" + itoa(line, -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format = fmt.Sprintf("%s %s: %s%s\n",
|
|
||||||
time.Now().Format(DateFormat),
|
|
||||||
domain, levelPrefix[level], format)
|
|
||||||
if writeToOut(level) {
|
|
||||||
fmt.Fprintf(l.out, format, v...)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(l.err, format, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) output(level Level, v []interface{}) {
|
|
||||||
if !l.Enabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if level >= l.level {
|
|
||||||
domain := l.domain
|
|
||||||
if level == LevelDebug {
|
|
||||||
_, file, line, ok := runtime.Caller(2)
|
|
||||||
if ok {
|
|
||||||
domain += " " + file + ":" + itoa(line, -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format := fmt.Sprintf("%s %s: %s",
|
|
||||||
time.Now().Format(DateFormat),
|
|
||||||
domain, levelPrefix[level])
|
|
||||||
if writeToOut(level) {
|
|
||||||
fmt.Fprintf(l.out, format)
|
|
||||||
fmt.Fprintln(l.out, v...)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(l.err, format)
|
|
||||||
fmt.Fprintln(l.err, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalf logs a formatted message at the "fatal" level and then exits. The
|
|
||||||
// arguments are handled in the same manner as fmt.Printf.
|
|
||||||
func (l *Logger) Fatalf(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelFatal, format, v)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal logs its arguments at the "fatal" level and then exits.
|
|
||||||
func (l *Logger) Fatal(v ...interface{}) {
|
|
||||||
l.output(LevelFatal, v)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alertf logs a formatted message at the "alert" level. The
|
|
||||||
// arguments are handled in the same manner as fmt.Printf.
|
|
||||||
func (l *Logger) Alertf(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelAlert, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alert logs its arguments at the "alert" level.
|
|
||||||
func (l *Logger) Alert(v ...interface{}) {
|
|
||||||
l.output(LevelAlert, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Criticalf logs a formatted message at the "critical" level. The
|
|
||||||
// arguments are handled in the same manner as fmt.Printf.
|
|
||||||
func (l *Logger) Criticalf(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelCritical, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Critical logs its arguments at the "critical" level.
|
|
||||||
func (l *Logger) Critical(v ...interface{}) {
|
|
||||||
l.output(LevelCritical, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errorf logs a formatted message at the "error" level. The arguments
|
|
||||||
// are handled in the same manner as fmt.Printf.
|
|
||||||
func (l *Logger) Errorf(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelError, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error logs its arguments at the "error" level.
|
|
||||||
func (l *Logger) Error(v ...interface{}) {
|
|
||||||
l.output(LevelError, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warningf logs a formatted message at the "warning" level. The
|
|
||||||
// arguments are handled in the same manner as fmt.Printf.
|
|
||||||
func (l *Logger) Warningf(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelWarning, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warning logs its arguments at the "warning" level.
|
|
||||||
func (l *Logger) Warning(v ...interface{}) {
|
|
||||||
l.output(LevelWarning, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Noticef logs a formatted message at the "notice" level. The arguments
|
|
||||||
// are handled in the same manner as fmt.Printf.
|
|
||||||
func (l *Logger) Noticef(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelNotice, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notice logs its arguments at the "notice" level.
|
|
||||||
func (l *Logger) Notice(v ...interface{}) {
|
|
||||||
l.output(LevelNotice, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infof logs a formatted message at the "info" level. The arguments
|
|
||||||
// are handled in the same manner as fmt.Printf.
|
|
||||||
func (l *Logger) Infof(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelInfo, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info logs its arguments at the "info" level.
|
|
||||||
func (l *Logger) Info(v ...interface{}) {
|
|
||||||
l.output(LevelInfo, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugf logs a formatted message at the "debug" level. The arguments
|
|
||||||
// are handled in the same manner as fmt.Printf. Note that debug
|
|
||||||
// logging will print the current
|
|
||||||
func (l *Logger) Debugf(format string, v ...interface{}) {
|
|
||||||
l.outputf(LevelDebug, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logs its arguments at the "debug" level.
|
|
||||||
func (l *Logger) Debug(v ...interface{}) {
|
|
||||||
l.output(LevelDebug, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf prints a formatted message at the default level.
|
|
||||||
func (l *Logger) Printf(format string, v ...interface{}) {
|
|
||||||
l.outputf(DefaultLevel, format, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print prints its arguments at the default level.
|
|
||||||
func (l *Logger) Print(v ...interface{}) {
|
|
||||||
l.output(DefaultLevel, v)
|
|
||||||
}
|
|
||||||
|
|
463
logging/log.go
463
logging/log.go
|
@ -1,274 +1,279 @@
|
||||||
// Package logging provides domain-based logging in the same style as
|
|
||||||
// sylog. Domains are some name for which logging can be selectively
|
|
||||||
// enabled or disabled. Logging also differentiates between normal
|
|
||||||
// messages (which are sent to standard output) and errors, which are
|
|
||||||
// sent to standard error; debug messages will also include the file
|
|
||||||
// and line number.
|
|
||||||
//
|
|
||||||
// Domains are intended for identifying logging subystems. A domain
|
|
||||||
// can be suppressed with Suppress, and re-enabled with Enable. There
|
|
||||||
// are prefixed versions of these as well.
|
|
||||||
//
|
|
||||||
// Packages (e.g. those meant to be imported by programs) using the
|
|
||||||
// loggers here should observe a few etiquette guides:
|
|
||||||
//
|
|
||||||
// 1. A package should never suppress or enable other loggers except
|
|
||||||
// via an exported function that should be called by the end user.
|
|
||||||
//
|
|
||||||
// 2. A package should never call the global `SetLevel`; this is
|
|
||||||
// reserved for the end user.
|
|
||||||
//
|
|
||||||
// 3. Packages should use consistent, sane domains: preferably,
|
|
||||||
// related packages should use an unsurprising common prefix in their
|
|
||||||
// domains.
|
|
||||||
//
|
|
||||||
// This package was adapted from the CFSSL logging code.
|
|
||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"time"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/kisom/goutils/mwc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var logConfig = struct {
|
// Logger provides a standardised logging interface.
|
||||||
registered map[string]*Logger
|
type Logger interface {
|
||||||
lock *sync.Mutex
|
// SetLevel sets the minimum log level.
|
||||||
}{
|
SetLevel(Level)
|
||||||
registered: map[string]*Logger{},
|
|
||||||
lock: new(sync.Mutex),
|
// Good returns true if the Logger is healthy.
|
||||||
|
Good() bool
|
||||||
|
|
||||||
|
// Status returns an error corresponding to the logger's state;
|
||||||
|
// if it's healthy (e.g. Good() returns true), Error will
|
||||||
|
// return nil.
|
||||||
|
Status() error
|
||||||
|
|
||||||
|
// Close gives the Logger the opportunity to perform any cleanup.
|
||||||
|
Close()
|
||||||
|
|
||||||
|
// Log messages consist of four components:
|
||||||
|
//
|
||||||
|
// 1. The **level** attaches a notion of priority to the log message.
|
||||||
|
// Several log levels are available:
|
||||||
|
//
|
||||||
|
// + FATAL (32): the system is in an unsuable state, and cannot
|
||||||
|
// continue to run. Most of the logging for this will cause the
|
||||||
|
// program to exit with an error code.
|
||||||
|
// + CRITICAL (16): critical conditions. The error, if uncorrected, is
|
||||||
|
// likely to cause a fatal condition shortly. An example is running
|
||||||
|
// out of disk space. This is something that the ops team should get
|
||||||
|
// paged for.
|
||||||
|
// + ERROR (8): error conditions. A single error doesn't require an
|
||||||
|
// ops team to be paged, but repeated errors should often trigger a
|
||||||
|
// page based on threshold triggers. An example is a network
|
||||||
|
// failure: it might be a transient failure (these do happen), but
|
||||||
|
// most of the time it's self-correcting.
|
||||||
|
// + WARNING (4): warning conditions. An example of this is a bad
|
||||||
|
// request sent to a server. This isn't an error on the part of the
|
||||||
|
// program, but it may be indicative of other things. Like errors,
|
||||||
|
// the ops team shouldn't be paged for errors, but a page might be
|
||||||
|
// triggered if a certain threshold of warnings is reached (which is
|
||||||
|
// typically much higher than errors). For example, repeated
|
||||||
|
// warnings might be a sign that the system is under attack.
|
||||||
|
// + INFO (2): informational message. This is a normal log message
|
||||||
|
// that is used to deliver information, such as recording
|
||||||
|
// requests. Ops teams are never paged for informational
|
||||||
|
// messages. This is the default log level.
|
||||||
|
// + DEBUG (1): debug-level message. These are only used during
|
||||||
|
// development or if a deployed system repeatedly sees abnormal
|
||||||
|
// errors.
|
||||||
|
//
|
||||||
|
// The numeric values indicate the priority of a given level.
|
||||||
|
//
|
||||||
|
// 2. The **actor** is used to specify which component is generating
|
||||||
|
// the log message. This could be the program name, or it could be
|
||||||
|
// a specific component inside the system.
|
||||||
|
//
|
||||||
|
// 3. The **event** is a short message indicating what happened. This is
|
||||||
|
// most like the traditional log message.
|
||||||
|
//
|
||||||
|
// 4. The **attributes** are an optional set of key-value string pairs that
|
||||||
|
// provide additional information.
|
||||||
|
//
|
||||||
|
// Additionally, each log message has an associated timestamp. For the
|
||||||
|
// text-based logs, this is "%FT%T%z"; for the binary logs, this is a
|
||||||
|
// 64-bit Unix timestamp. An example text-based timestamp might look like ::
|
||||||
|
//
|
||||||
|
// [2016-03-27T20:59:27-0700] [INFO] [actor:server event:request received] client=192.168.2.5 request-size=839
|
||||||
|
//
|
||||||
|
// Note that this is organised in a manner that facilitates parsing::
|
||||||
|
//
|
||||||
|
// /\[(\d{4}-\d{3}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{4})\] \[(\w+\)]\) \[actor:(.+?) event:(.+?)\]/
|
||||||
|
//
|
||||||
|
// will cover the header:
|
||||||
|
//
|
||||||
|
// + ``$1`` contains the timestamp
|
||||||
|
// + ``$2`` contains the level
|
||||||
|
// + ``$3`` contains the actor
|
||||||
|
// + ``$4`` contains the event
|
||||||
|
Debug(actor, event string, attrs map[string]string)
|
||||||
|
Info(actor, event string, attrs map[string]string)
|
||||||
|
Warn(actor, event string, attrs map[string]string)
|
||||||
|
Error(actor, event string, attrs map[string]string)
|
||||||
|
Critical(actor, event string, attrs map[string]string)
|
||||||
|
Fatal(actor, event string, attrs map[string]string)
|
||||||
|
FatalCode(exitcode int, actor, event string, attrs map[string]string)
|
||||||
|
FatalNoDie(actor, event string, attrs map[string]string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLevel sets the logging level for all loggers.
|
// A LogWriter is a Logger that operates on an io.Writer.
|
||||||
func SetLevel(level Level) {
|
type LogWriter struct {
|
||||||
logConfig.lock.Lock()
|
wo, we io.Writer
|
||||||
defer logConfig.lock.Unlock()
|
lvl Level
|
||||||
|
state error
|
||||||
|
snl bool // suppress newline
|
||||||
|
}
|
||||||
|
|
||||||
for _, l := range logConfig.registered {
|
// NewLogWriter takes an output writer (wo) and an error writer (we),
|
||||||
l.SetLevel(level)
|
// and produces a new Logger. If the error writer is nil, error logs
|
||||||
|
// will be multiplexed onto the output writer.
|
||||||
|
func NewLogWriter(wo, we io.Writer) *LogWriter {
|
||||||
|
if we == nil {
|
||||||
|
we = wo
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LogWriter{
|
||||||
|
wo: wo,
|
||||||
|
we: we,
|
||||||
|
lvl: DefaultLevel,
|
||||||
|
state: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultLevel defaults to the notice level of logging.
|
func (lw *LogWriter) output(w io.Writer, lvl Level, actor, event string, attrs map[string]string) {
|
||||||
const DefaultLevel = LevelNotice
|
t := time.Now().Format(DateFormat)
|
||||||
|
fmt.Fprintf(w, "[%s] [%s] [actor:%s event:%s]", t, levelPrefix[lvl], actor, event)
|
||||||
// Init returns a new default logger. The domain is set to the
|
for k, v := range attrs {
|
||||||
// program's name, and the default logging level is used.
|
fmt.Fprintf(w, " %s=%s", k, v)
|
||||||
func Init() *Logger {
|
|
||||||
l, _ := New(filepath.Base(os.Args[0]), DefaultLevel)
|
|
||||||
return l
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Logger writes logs on behalf of a particular domain at a certain
|
if !lw.snl {
|
||||||
// level.
|
fmt.Fprintf(w, "\n")
|
||||||
type Logger struct {
|
|
||||||
enabled bool
|
|
||||||
lock *sync.Mutex
|
|
||||||
domain string
|
|
||||||
level Level
|
|
||||||
out io.WriteCloser
|
|
||||||
err io.WriteCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the log's writers and suppresses the logger.
|
|
||||||
func (l *Logger) Close() error {
|
|
||||||
Suppress(l.domain)
|
|
||||||
err := l.out.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return l.err.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress ignores logs from a specific domain.
|
|
||||||
func Suppress(domain string) {
|
|
||||||
logConfig.lock.Lock()
|
|
||||||
defer logConfig.lock.Unlock()
|
|
||||||
l, ok := logConfig.registered[domain]
|
|
||||||
if ok {
|
|
||||||
l.Suppress()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressPrefix suppress logs whose domain is prefixed with the
|
// Debug emits a debug-level message. These are only used during
|
||||||
// prefix.
|
// development or if a deployed system repeatedly sees abnormal
|
||||||
func SuppressPrefix(prefix string) {
|
// errors.
|
||||||
logConfig.lock.Lock()
|
//
|
||||||
defer logConfig.lock.Unlock()
|
// Actor specifies the component emitting the message; event indicates
|
||||||
for domain, l := range logConfig.registered {
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
if strings.HasPrefix(domain, prefix) {
|
// of key-value string pairs that can be used to provide additional
|
||||||
l.Suppress()
|
// information.
|
||||||
}
|
func (lw *LogWriter) Debug(actor, event string, attrs map[string]string) {
|
||||||
|
if lw.lvl > LevelDebug {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
lw.output(lw.wo, LevelDebug, actor, event, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressAll suppresses all logging output.
|
// Info emits an informational message. This is a normal log message
|
||||||
func SuppressAll() {
|
// that is used to deliver information, such as recording
|
||||||
logConfig.lock.Lock()
|
// requests. Ops teams are never paged for informational
|
||||||
defer logConfig.lock.Unlock()
|
// messages. This is the default log level.
|
||||||
for _, l := range logConfig.registered {
|
//
|
||||||
l.Suppress()
|
// Actor specifies the component emitting the message; event indicates
|
||||||
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
|
// of key-value string pairs that can be used to provide additional
|
||||||
|
// information.
|
||||||
|
func (lw *LogWriter) Info(actor, event string, attrs map[string]string) {
|
||||||
|
if lw.lvl > LevelInfo {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
lw.output(lw.wo, LevelInfo, actor, event, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable enables logs from a specific domain.
|
// Warn emits a warning message. An example of this is a bad request
|
||||||
func Enable(domain string) {
|
// sent to a server. This isn't an error on the part of the program,
|
||||||
logConfig.lock.Lock()
|
// but it may be indicative of other things. Like errors, the ops team
|
||||||
defer logConfig.lock.Unlock()
|
// shouldn't be paged for errors, but a page might be triggered if a
|
||||||
l, ok := logConfig.registered[domain]
|
// certain threshold of warnings is reached (which is typically much
|
||||||
if ok {
|
// higher than errors). For example, repeated warnings might be a sign
|
||||||
l.Enable()
|
// that the system is under attack.
|
||||||
|
//
|
||||||
|
// Actor specifies the component emitting the message; event indicates
|
||||||
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
|
// of key-value string pairs that can be used to provide additional
|
||||||
|
// information.
|
||||||
|
func (lw *LogWriter) Warn(actor, event string, attrs map[string]string) {
|
||||||
|
if lw.lvl > LevelWarning {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
lw.output(lw.we, LevelWarning, actor, event, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnablePrefix enables logs whose domain is prefixed with prefix.
|
// Error emits an error message. A single error doesn't require an ops
|
||||||
func EnablePrefix(prefix string) {
|
// team to be paged, but repeated errors should often trigger a page
|
||||||
logConfig.lock.Lock()
|
// based on threshold triggers. An example is a network failure: it
|
||||||
defer logConfig.lock.Unlock()
|
// might be a transient failure (these do happen), but most of the
|
||||||
for domain, l := range logConfig.registered {
|
// time it's self-correcting.
|
||||||
if strings.HasPrefix(domain, prefix) {
|
//
|
||||||
l.Enable()
|
// Actor specifies the component emitting the message; event indicates
|
||||||
}
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
|
// of key-value string pairs that can be used to provide additional
|
||||||
|
// information.
|
||||||
|
func (lw *LogWriter) Error(actor, event string, attrs map[string]string) {
|
||||||
|
if lw.lvl > LevelError {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
lw.output(lw.we, LevelError, actor, event, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnableAll enables all domains.
|
// Critical emits a message indicating a critical condition. The
|
||||||
func EnableAll() {
|
// error, if uncorrected, is likely to cause a fatal condition
|
||||||
logConfig.lock.Lock()
|
// shortly. An example is running out of disk space. This is
|
||||||
defer logConfig.lock.Unlock()
|
// something that the ops team should get paged for.
|
||||||
for _, l := range logConfig.registered {
|
//
|
||||||
l.Enable()
|
// Actor specifies the component emitting the message; event indicates
|
||||||
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
|
// of key-value string pairs that can be used to provide additional
|
||||||
|
// information.
|
||||||
|
func (lw *LogWriter) Critical(actor, event string, attrs map[string]string) {
|
||||||
|
if lw.lvl > LevelCritical {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
lw.output(lw.we, LevelCritical, actor, event, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new logger that writes to standard output for Notice
|
// Fatal emits a message indicating that the system is in an unsuable
|
||||||
// and below and standard error for levels above Notice. If a logger
|
// state, and cannot continue to run. The program will exit with exit
|
||||||
// with the same domain exists, the logger will set its level to level
|
// code 1.
|
||||||
// and return the logger; in this case, the registered return value
|
//
|
||||||
// will be true.
|
// Actor specifies the component emitting the message; event indicates
|
||||||
func New(domain string, level Level) (l *Logger, registered bool) {
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
logConfig.lock.Lock()
|
// of key-value string pairs that can be used to provide additional
|
||||||
defer logConfig.lock.Unlock()
|
// information.
|
||||||
|
func (lw *LogWriter) Fatal(actor, event string, attrs map[string]string) {
|
||||||
l = logConfig.registered[domain]
|
if lw.lvl > LevelFatal {
|
||||||
if l != nil {
|
return
|
||||||
l.SetLevel(level)
|
}
|
||||||
return l, true
|
lw.output(lw.we, LevelFatal, actor, event, attrs)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
l = &Logger{
|
// Fatal emits a message indicating that the system is in an unsuable
|
||||||
domain: domain,
|
// state, and cannot continue to run. The program will exit with the
|
||||||
level: level,
|
// exit code speicfied in the exitcode argument.
|
||||||
out: os.Stdout,
|
//
|
||||||
err: os.Stderr,
|
// Actor specifies the component emitting the message; event indicates
|
||||||
lock: new(sync.Mutex),
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
|
// of key-value string pairs that can be used to provide additional
|
||||||
|
// information.
|
||||||
|
func (lw *LogWriter) FatalCode(exitcode int, actor, event string, attrs map[string]string) {
|
||||||
|
if lw.lvl > LevelFatal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lw.output(lw.we, LevelFatal, actor, event, attrs)
|
||||||
|
os.Exit(exitcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Enable()
|
// Fatal emits a message indicating that the system is in an unsuable
|
||||||
logConfig.registered[domain] = l
|
// state, and cannot continue to run. The program will not exit; it is
|
||||||
return l, false
|
// assumed that the caller has some final clean up to perform.
|
||||||
|
//
|
||||||
|
// Actor specifies the component emitting the message; event indicates
|
||||||
|
// the event that caused the log message to be emitted. attrs is a map
|
||||||
|
// of key-value string pairs that can be used to provide additional
|
||||||
|
// information.
|
||||||
|
func (lw *LogWriter) FatalNoDie(actor, event string, attrs map[string]string) {
|
||||||
|
if lw.lvl > LevelFatal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lw.output(lw.we, LevelFatal, actor, event, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFromWriters returns a new logger that writes to the w io.WriteCloser
|
// Good returns true if the logger is healthy.
|
||||||
// for Notice and below and to the e io.WriteCloser for levels above
|
func (lw *LogWriter) Good() bool {
|
||||||
// Notice. If e is nil, w will be used. If a logger with the same
|
return lw.state == nil
|
||||||
// domain exists, the logger will set its level to level and return
|
|
||||||
// the logger; in this case, the registered return value will be true.
|
|
||||||
func NewFromWriters(domain string, level Level, w, e io.WriteCloser) (l *Logger, registered bool) {
|
|
||||||
logConfig.lock.Lock()
|
|
||||||
defer logConfig.lock.Unlock()
|
|
||||||
|
|
||||||
l = logConfig.registered[domain]
|
|
||||||
if l != nil {
|
|
||||||
l.SetLevel(level)
|
|
||||||
return l, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if w == nil {
|
// Status returns an error value from the logger if it isn't healthy,
|
||||||
w = os.Stdout
|
// or nil if the logger is healthy.
|
||||||
|
func (lw *LogWriter) Status() error {
|
||||||
|
return lw.state
|
||||||
}
|
}
|
||||||
|
|
||||||
if e == nil {
|
// SetLevel changes the log level.
|
||||||
e = w
|
func (lw *LogWriter) SetLevel(l Level) {
|
||||||
|
lw.lvl = l
|
||||||
}
|
}
|
||||||
|
|
||||||
l = &Logger{
|
// Close is a no-op that satisfies the Logger interface.
|
||||||
domain: domain,
|
func (lw *LogWriter) Close() {}
|
||||||
level: level,
|
|
||||||
out: w,
|
|
||||||
err: e,
|
|
||||||
lock: new(sync.Mutex),
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Enable()
|
|
||||||
logConfig.registered[domain] = l
|
|
||||||
return l, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFromFile returns a new logger that opens the files for writing. If
|
|
||||||
// multiplex is true, output will be multiplexed to standard output
|
|
||||||
// and standard error as well.
|
|
||||||
func NewFromFile(domain string, level Level, outFile, errFile string, multiplex bool, flags int) (*Logger, error) {
|
|
||||||
l := &Logger{
|
|
||||||
domain: domain,
|
|
||||||
level: level,
|
|
||||||
lock: new(sync.Mutex),
|
|
||||||
}
|
|
||||||
|
|
||||||
outf, err := os.OpenFile(outFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|flags, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
errf, err := os.OpenFile(errFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|flags, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if multiplex {
|
|
||||||
l.out = mwc.MultiWriteCloser(outf, os.Stdout)
|
|
||||||
l.err = mwc.MultiWriteCloser(errf, os.Stderr)
|
|
||||||
} else {
|
|
||||||
l.out = outf
|
|
||||||
l.err = errf
|
|
||||||
}
|
|
||||||
|
|
||||||
Enable(domain)
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable allows output from the logger.
|
|
||||||
func (l *Logger) Enable() {
|
|
||||||
l.lock.Lock()
|
|
||||||
defer l.lock.Unlock()
|
|
||||||
l.enabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled returns true if the logger is enabled.
|
|
||||||
func (l *Logger) Enabled() bool {
|
|
||||||
return l.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress ignores output from the logger.
|
|
||||||
func (l *Logger) Suppress() {
|
|
||||||
l.lock.Lock()
|
|
||||||
defer l.lock.Unlock()
|
|
||||||
l.enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain returns the domain of the logger.
|
|
||||||
func (l *Logger) Domain() string {
|
|
||||||
return l.domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLevel changes the level of the logger.
|
|
||||||
func (l *Logger) SetLevel(level Level) {
|
|
||||||
l.lock.Lock()
|
|
||||||
defer l.lock.Unlock()
|
|
||||||
l.level = level
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A list of implementations that should be tested.
|
||||||
|
var implementations []Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
lw := NewLogWriter(&bytes.Buffer{}, nil)
|
||||||
|
cw := NewConsole()
|
||||||
|
|
||||||
|
implementations = append(implementations, lw)
|
||||||
|
implementations = append(implementations, cw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSetup(t *testing.T) {
|
||||||
|
fw1, err := NewFile("fw1.log", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new file logger: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fw2, err := NewSplitFile("fw2.log", "fw2.err", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new split file logger: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
implementations = append(implementations, fw1)
|
||||||
|
implementations = append(implementations, fw2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImplementations(t *testing.T) {
|
||||||
|
for _, l := range implementations {
|
||||||
|
l.Info("TestImplementations", "Info message",
|
||||||
|
map[string]string{"type": fmt.Sprintf("%T", l)})
|
||||||
|
l.Warn("TestImplementations", "Warning message",
|
||||||
|
map[string]string{"type": fmt.Sprintf("%T", l)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseLoggers(t *testing.T) {
|
||||||
|
for _, l := range implementations {
|
||||||
|
l.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDestroyLogFiles(t *testing.T) {
|
||||||
|
os.Remove("fw1.log")
|
||||||
|
os.Remove("fw2.log")
|
||||||
|
os.Remove("fw2.err")
|
||||||
|
}
|
Loading…
Reference in New Issue