package ulog import ( "bytes" "encoding/json" "fmt" "io" "log/syslog" "os" "path/filepath" "reflect" "regexp" "runtime" "strings" "sync" "syscall" "time" "unsafe" ) const ( TIME_NONE int = iota TIME_DATETIME TIME_MSDATETIME TIME_TIMESTAMP TIME_MSTIMESTAMP ) var ( cores int facilities = map[string]syslog.Priority{ "user": syslog.LOG_USER, "daemon": syslog.LOG_DAEMON, "local0": syslog.LOG_LOCAL0, "local1": syslog.LOG_LOCAL1, "local2": syslog.LOG_LOCAL2, "local3": syslog.LOG_LOCAL3, "local4": syslog.LOG_LOCAL4, "local5": syslog.LOG_LOCAL5, "local6": syslog.LOG_LOCAL6, "local7": syslog.LOG_LOCAL7, } severities = map[string]syslog.Priority{ "error": syslog.LOG_ERR, "warning": syslog.LOG_WARNING, "info": syslog.LOG_INFO, "debug": syslog.LOG_DEBUG, } severityLabels = map[syslog.Priority]string{ syslog.LOG_ERR: "ERRO ", syslog.LOG_WARNING: "WARN ", syslog.LOG_INFO: "INFO ", syslog.LOG_DEBUG: "DBUG ", } severityColors = map[syslog.Priority]string{ syslog.LOG_ERR: "\x1b[31m", syslog.LOG_WARNING: "\x1b[33m", syslog.LOG_INFO: "\x1b[36m", syslog.LOG_DEBUG: "\x1b[32m", } ) type FileOutput struct { handle *os.File last time.Time } type ULog struct { file, console, syslog bool fileOutputs map[string]*FileOutput filePath string fileTime int fileLast time.Time fileSeverity bool consoleHandle io.Writer consoleTime int consoleSeverity bool consoleColors bool syslogHandle *syslog.Writer syslogRemote string syslogName string syslogFacility syslog.Priority optionUTC bool level syslog.Priority sync.Mutex } func New(target string) *ULog { if cores == 0 { cores = runtime.NumCPU() } log := &ULog{ fileOutputs: map[string]*FileOutput{}, syslogHandle: nil, } return log.Load(target) } func (this *ULog) Load(target string) *ULog { this.Close() if cores > 1 { this.Lock() defer this.Unlock() } this.file = false this.filePath = "" this.fileTime = TIME_DATETIME this.fileSeverity = true this.console = false this.consoleTime = TIME_DATETIME this.consoleSeverity = true this.consoleColors = true this.consoleHandle = os.Stderr this.syslog = false this.syslogRemote = "" this.syslogName = filepath.Base(os.Args[0]) this.syslogFacility = syslog.LOG_DAEMON this.optionUTC = false this.level = syslog.LOG_INFO for _, target := range regexp.MustCompile("(file|console|syslog|option)\\s*\\(([^\\)]*)\\)").FindAllStringSubmatch(target, -1) { switch strings.ToLower(target[1]) { case "file": this.file = true for _, option := range regexp.MustCompile("([^:=,\\s]+)\\s*[:=]\\s*([^,\\s]+)").FindAllStringSubmatch(target[2], -1) { switch strings.ToLower(option[1]) { case "path": this.filePath = option[2] case "time": option[2] = strings.ToLower(option[2]) switch { case option[2] == "datetime": this.fileTime = TIME_DATETIME case option[2] == "msdatetime": this.fileTime = TIME_MSDATETIME case option[2] == "stamp" || option[2] == "timestamp": this.fileTime = TIME_TIMESTAMP case option[2] == "msstamp" || option[2] == "mstimestamp": this.fileTime = TIME_MSTIMESTAMP case option[2] != "1" && option[2] != "true" && option[2] != "on" && option[2] != "yes": this.fileTime = TIME_NONE } case "severity": option[2] = strings.ToLower(option[2]) if option[2] != "1" && option[2] != "true" && option[2] != "on" && option[2] != "yes" { this.fileSeverity = false } } } if this.filePath == "" { this.file = false } case "console": this.console = true for _, option := range regexp.MustCompile("([^:=,\\s]+)\\s*[:=]\\s*([^,\\s]+)").FindAllStringSubmatch(target[2], -1) { option[2] = strings.ToLower(option[2]) switch strings.ToLower(option[1]) { case "output": if option[2] == "stdout" { this.consoleHandle = os.Stdout } case "time": switch { case option[2] == "datetime": this.consoleTime = TIME_DATETIME case option[2] == "msdatetime": this.consoleTime = TIME_MSDATETIME case option[2] == "stamp" || option[2] == "timestamp": this.consoleTime = TIME_TIMESTAMP case option[2] == "msstamp" || option[2] == "mstimestamp": this.consoleTime = TIME_MSTIMESTAMP case option[2] != "1" && option[2] != "true" && option[2] != "on" && option[2] != "yes": this.consoleTime = TIME_NONE } case "severity": if option[2] != "1" && option[2] != "true" && option[2] != "on" && option[2] != "yes" { this.consoleSeverity = false } case "colors": if option[2] != "1" && option[2] != "true" && option[2] != "on" && option[2] != "yes" { this.consoleColors = false } } } case "syslog": this.syslog = true for _, option := range regexp.MustCompile("([^:=,\\s]+)\\s*[:=]\\s*([^,\\s]+)").FindAllStringSubmatch(target[2], -1) { switch strings.ToLower(option[1]) { case "remote": this.syslogRemote = option[2] if !regexp.MustCompile(":\\d+$").MatchString(this.syslogRemote) { this.syslogRemote += ":514" } case "name": this.syslogName = option[2] case "facility": this.syslogFacility = facilities[strings.ToLower(option[2])] } } case "option": for _, option := range regexp.MustCompile("([^:=,\\s]+)\\s*[:=]\\s*([^,\\s]+)").FindAllStringSubmatch(target[2], -1) { option[2] = strings.ToLower(option[2]) switch strings.ToLower(option[1]) { case "utc": if option[2] == "1" || option[2] == "true" || option[2] == "on" || option[2] == "yes" { this.optionUTC = true } case "level": this.level = severities[strings.ToLower(option[2])] } } } } var info syscall.Termios if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, this.consoleHandle.(*os.File).Fd(), syscall.TCGETS, uintptr(unsafe.Pointer(&info)), 0, 0, 0); err != 0 { this.consoleColors = false } return this } func (this *ULog) Close() { if cores > 1 { this.Lock() defer this.Unlock() } if this.syslogHandle != nil { this.syslogHandle.Close() this.syslogHandle = nil } for path, output := range this.fileOutputs { if output.handle != nil { output.handle.Close() } delete(this.fileOutputs, path) } } func (this *ULog) SetLevel(level string) { level = strings.ToLower(level) switch level { case "error": this.level = syslog.LOG_ERR case "warning": this.level = syslog.LOG_WARNING case "info": this.level = syslog.LOG_INFO case "debug": this.level = syslog.LOG_DEBUG } } func strftime(layout string, base time.Time) string { var output []string length := len(layout) for index := 0; index < length; index++ { switch layout[index] { case '%': if index < length-1 { switch layout[index+1] { case 'a': output = append(output, base.Format("Mon")) case 'A': output = append(output, base.Format("Monday")) case 'b': output = append(output, base.Format("Jan")) case 'B': output = append(output, base.Format("January")) case 'c': output = append(output, base.Format("Mon Jan 2 15:04:05 2006")) case 'C': output = append(output, fmt.Sprintf("%02d", base.Year()/100)) case 'd': output = append(output, fmt.Sprintf("%02d", base.Day())) case 'D': output = append(output, fmt.Sprintf("%02d/%02d/%02d", base.Month(), base.Day(), base.Year()%100)) case 'e': output = append(output, fmt.Sprintf("%2d", base.Day())) case 'f': output = append(output, fmt.Sprintf("%06d", base.Nanosecond()/1000)) case 'F': output = append(output, fmt.Sprintf("%04d-%02d-%02d", base.Year(), base.Month(), base.Day())) case 'g': year, _ := base.ISOWeek() output = append(output, fmt.Sprintf("%02d", year%100)) case 'G': year, _ := base.ISOWeek() output = append(output, fmt.Sprintf("%04d", year)) case 'h': output = append(output, base.Format("Jan")) case 'H': output = append(output, fmt.Sprintf("%02d", base.Hour())) case 'I': if base.Hour() == 0 || base.Hour() == 12 { output = append(output, "12") } else { output = append(output, fmt.Sprintf("%02d", base.Hour()%12)) } case 'j': output = append(output, fmt.Sprintf("%03d", base.YearDay())) case 'k': output = append(output, fmt.Sprintf("%2d", base.Hour())) case 'l': if base.Hour() == 0 || base.Hour() == 12 { output = append(output, "12") } else { output = append(output, fmt.Sprintf("%2d", base.Hour()%12)) } case 'm': output = append(output, fmt.Sprintf("%02d", base.Month())) case 'M': output = append(output, fmt.Sprintf("%02d", base.Minute())) case 'n': output = append(output, "\n") case 'p': if base.Hour() < 12 { output = append(output, "AM") } else { output = append(output, "PM") } case 'P': if base.Hour() < 12 { output = append(output, "am") } else { output = append(output, "pm") } case 'r': if base.Hour() == 0 || base.Hour() == 12 { output = append(output, "12") } else { output = append(output, fmt.Sprintf("%02d", base.Hour()%12)) } output = append(output, fmt.Sprintf(":%02d:%02d", base.Minute(), base.Second())) if base.Hour() < 12 { output = append(output, " AM") } else { output = append(output, " PM") } case 'R': output = append(output, fmt.Sprintf("%02d:%02d", base.Hour(), base.Minute())) case 's': output = append(output, fmt.Sprintf("%d", base.Unix())) case 'S': output = append(output, fmt.Sprintf("%02d", base.Second())) case 't': output = append(output, "\t") case 'T': output = append(output, fmt.Sprintf("%02d:%02d:%02d", base.Hour(), base.Minute(), base.Second())) case 'u': day := base.Weekday() if day == 0 { day = 7 } output = append(output, fmt.Sprintf("%d", day)) case 'U': output = append(output, fmt.Sprintf("%d", (base.YearDay()+6-int(base.Weekday()))/7)) case 'V': _, week := base.ISOWeek() output = append(output, fmt.Sprintf("%02d", week)) case 'w': output = append(output, fmt.Sprintf("%d", base.Weekday())) case 'W': day := int(base.Weekday()) if day == 0 { day = 6 } else { day -= 1 } output = append(output, fmt.Sprintf("%d", (base.YearDay()+6-day)/7)) case 'x': output = append(output, fmt.Sprintf("%02d/%02d/%02d", base.Month(), base.Day(), base.Year()%100)) case 'X': output = append(output, fmt.Sprintf("%02d:%02d:%02d", base.Hour(), base.Minute(), base.Second())) case 'y': output = append(output, fmt.Sprintf("%02d", base.Year()%100)) case 'Y': output = append(output, fmt.Sprintf("%04d", base.Year())) case 'z': output = append(output, base.Format("-0700")) case 'Z': output = append(output, base.Format("MST")) case '%': output = append(output, "%") } index++ } default: output = append(output, string(layout[index])) } } return strings.Join(output, "") } func (this *ULog) log(now time.Time, severity syslog.Priority, xlayout interface{}, a ...interface{}) { var err error if this.level < severity || (!this.syslog && !this.file && !this.console) { return } layout := "" switch reflect.TypeOf(xlayout).Kind() { case reflect.Map: var buffer bytes.Buffer encoder := json.NewEncoder(&buffer) encoder.SetEscapeHTML(false) if err := encoder.Encode(xlayout); err == nil { layout = "%s" a = []interface{}{bytes.TrimSpace(buffer.Bytes())} } case reflect.String: layout = xlayout.(string) } layout = strings.TrimSpace(layout) if this.syslog { if this.syslogHandle == nil { if cores > 1 { this.Lock() } if this.syslogHandle == nil { protocol := "" if this.syslogRemote != "" { protocol = "udp" } if this.syslogHandle, err = syslog.Dial(protocol, this.syslogRemote, this.syslogFacility, this.syslogName); err != nil { this.syslogHandle = nil } } if cores > 1 { this.Unlock() } } if this.syslogHandle != nil { switch severity { case syslog.LOG_ERR: this.syslogHandle.Err(fmt.Sprintf(layout, a...)) case syslog.LOG_WARNING: this.syslogHandle.Warning(fmt.Sprintf(layout, a...)) case syslog.LOG_INFO: this.syslogHandle.Info(fmt.Sprintf(layout, a...)) case syslog.LOG_DEBUG: this.syslogHandle.Debug(fmt.Sprintf(layout, a...)) } } } if this.optionUTC { now = now.UTC() } else { now = now.Local() } if this.file { path := strftime(this.filePath, now) if cores > 1 { this.Lock() } if this.fileOutputs[path] == nil { os.MkdirAll(filepath.Dir(path), 0755) if handle, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil { this.fileOutputs[path] = &FileOutput{handle: handle} } } if this.fileOutputs[path] != nil && this.fileOutputs[path].handle != nil { prefix := "" switch this.fileTime { case TIME_DATETIME: prefix = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d ", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) case TIME_MSDATETIME: prefix = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d.%03d ", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond()/int(time.Millisecond)) case TIME_TIMESTAMP: prefix = fmt.Sprintf("%d ", now.Unix()) case TIME_MSTIMESTAMP: prefix = fmt.Sprintf("%d ", now.UnixNano()/int64(time.Millisecond)) } if this.fileSeverity { prefix += severityLabels[severity] } this.fileOutputs[path].handle.WriteString(fmt.Sprintf(prefix+layout+"\n", a...)) this.fileOutputs[path].last = now } if now.Sub(this.fileLast) >= 5*time.Second { this.fileLast = now for path, output := range this.fileOutputs { if now.Sub(output.last) >= 5*time.Second { output.handle.Close() delete(this.fileOutputs, path) } } } if cores > 1 { this.Unlock() } } if this.console { prefix := "" switch this.consoleTime { case TIME_DATETIME: prefix = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d ", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) case TIME_MSDATETIME: prefix = fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d.%03d ", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond()/int(time.Millisecond)) case TIME_TIMESTAMP: prefix = fmt.Sprintf("%d ", now.Unix()) case TIME_MSTIMESTAMP: prefix = fmt.Sprintf("%d ", now.UnixNano()/int64(time.Millisecond)) } if this.consoleSeverity { if this.consoleColors { prefix += fmt.Sprintf("%s%s\x1b[0m", severityColors[severity], severityLabels[severity]) } else { prefix += severityLabels[severity] } } if cores > 1 { this.Lock() } fmt.Fprintf(this.consoleHandle, prefix+layout+"\n", a...) if cores > 1 { this.Unlock() } } } func (this *ULog) Error(layout interface{}, a ...interface{}) { this.log(time.Now(), syslog.LOG_ERR, layout, a...) } func (this *ULog) Warn(layout interface{}, a ...interface{}) { this.log(time.Now(), syslog.LOG_WARNING, layout, a...) } func (this *ULog) Info(layout interface{}, a ...interface{}) { this.log(time.Now(), syslog.LOG_INFO, layout, a...) } func (this *ULog) Debug(layout interface{}, a ...interface{}) { this.log(time.Now(), syslog.LOG_DEBUG, layout, a...) } func (this *ULog) ErrorTime(now time.Time, layout interface{}, a ...interface{}) { this.log(now, syslog.LOG_ERR, layout, a...) } func (this *ULog) WarnTime(now time.Time, layout interface{}, a ...interface{}) { this.log(now, syslog.LOG_WARNING, layout, a...) } func (this *ULog) InfoTime(now time.Time, layout interface{}, a ...interface{}) { this.log(now, syslog.LOG_INFO, layout, a...) } func (this *ULog) DebugTime(now time.Time, layout interface{}, a ...interface{}) { this.log(now, syslog.LOG_DEBUG, layout, a...) }