Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first public commit

  • Loading branch information...
commit 498e4b5c49c4f10959996b2977bf1099f25910ef 0 parents
Scott White authored
3  .gitignore
@@ -0,0 +1,3 @@
+*.out
+_*
+*.log
8 LICENSE.md
@@ -0,0 +1,8 @@
+Copyright (c) 2012 ngmoco:) inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
14 Makefile
@@ -0,0 +1,14 @@
+include $(GOROOT)/src/Make.inc
+
+TARG=timber
+GOFILES=\
+ timber.go\
+ pattern_formatter.go\
+ console_writer.go\
+ buffered_writer.go\
+ socket_writer.go\
+ file_writer.go\
+ xml_config.go\
+
+include $(GOROOT)/src/Make.pkg
+
62 README.md
@@ -0,0 +1,62 @@
+Timber!
+=======
+
+Timber is a logger for go with a similar interface as log4go and also can be used as a drop-in replacement for the go standard logger.
+
+Features
+--------
+* Log levels: Finest, Fine, Debug, Trace, Info, Warn, Error, Critical
+* External configuration via XML
+* Multiple log destinations (console, file, socket)
+* Configurable format per destination
+* Extensible and pluggable design (if you configure via code rather than XML)
+
+Motivation
+----------
+I like the log4go design with multiple configurable loggers, but it did not support adding formatters to all of the loggers (it is only supported for files). I thought about trying to contribute patches to log4go but some of the features I wanted would break complete backwards compatibility so I decided to do a rewrite from scratch.
+
+I try to expose everything possible that I think might be useful for someone to replace or extend.
+
+Usage
+-----
+The easiest way to use Timber is to use configure the built-in singleton:
+
+ import (
+ log "timber"
+ )
+
+ func main() {
+ // load xml config
+ log.LoadConfiguration("timber.xml")
+ log.Info("Timber!!!")
+ }
+
+An example timber.xml file is included in the package. Timber does implement the interface of the go log package so replacing the log with Timber will work ok.
+
+`log.Close()` should be called before your program exits to make sure all the buffers are drained and all messages are printed.
+
+
+Design
+------
+
+`Logger` is the interface that is used for logging itself with methods like Warn, Critical, Error, etc. All of these functions expect a Printf-like arguments and syntax for the message.
+
+`LogFormatter` is a generic interface for taking a `LogRecord` and formatting into a string to be logged. `PatFormatter` is the only included implementation of this interface.
+
+`LogWriter` interface wraps an underlying `Writer` but doesn't allow errors to propagate. There are implementations for writing to files, sockets and the console.
+
+`Timber` is a `MultiLogger` which just means that it implements the `Logger` interface but can log messages to multiple destinations. Each destination has a `LogWriter`, `level` and `LogFormatter`.
+
+`Global` is the default unconfigured instance of `Timber` which may be configured and used or, less commonly, replaced with your own instance (be sure to call `Global.Close()` before replacing for proper cleanup).
+
+Are you planning to wrap Timber in your own logger? Ever notice that if you wrap the go log package or log4go the source file that gets printed is always your wrapper? `Timber.FileDepth` sets how far up the stack to go to find the file you actually want. It's set to `DefaultFileDepth` so add your wrapper stack depth to that.
+
+Completeness
+------------
+* Some of the runtime configuration changes have not been implemented, such as `MultiLogger.SetLevel` and `MultiLogger.SetFormatter` which change the Level or `LogFormatter` on-the-fly. Loggers may be added at any time with `AddLogger` but there is no way to delete loggers right now.
+
+Compatibility
+-------------
+* I don't support the log4go special handling of the first parameter and probably never will. Right now, all of the `Logger` methods just expect a Printf-like syntax. If there is demand, I may get the proc syntax in for delayed evaluation.
+* `PatFormatter` format codes are not the same as log4go
+* `PatFormatter` always adds a newline at the end of the string so if there's already one there, then you'll get 2 so using Timber to replace the go log package may look a bit messy depending on how you formatted your logging.
33 buffered_writer.go
@@ -0,0 +1,33 @@
+package timber
+
+import (
+ "bufio"
+ "io"
+ "log"
+)
+
+// Use this of you need some buffering, or not
+type BufferedWriter struct {
+ buf *bufio.Writer
+ writer io.WriteCloser
+}
+
+func NewBufferedWriter(writer io.WriteCloser) *BufferedWriter {
+ bw := new(BufferedWriter)
+ bw.writer = writer
+ bw.buf = bufio.NewWriter(writer)
+ return bw
+}
+
+func (bw *BufferedWriter) LogWrite(msg string) {
+ _, err := bw.buf.Write([]byte(msg))
+ if err != nil {
+ // uh-oh... what do i do if logging fails; punt!
+ log.Printf("TIMBER! epic fail: %v", err)
+ }
+}
+
+func (bw *BufferedWriter) Close() {
+ bw.buf.Flush()
+ bw.writer.Close()
+}
20 console_writer.go
@@ -0,0 +1,20 @@
+package timber
+
+import (
+ "log"
+)
+
+func init() {
+ log.SetFlags(0)
+}
+
+// This uses the standard go logger to write the messages
+type ConsoleWriter func(string)
+
+func (c ConsoleWriter) LogWrite(msg string) {
+ log.Print(msg)
+}
+
+func (c ConsoleWriter) Close() {
+ // Nothing
+}
38 file_writer.go
@@ -0,0 +1,38 @@
+package timber
+
+import (
+ "os"
+ "fmt"
+)
+/* unbuffered impl
+type FileWriter struct {
+ file io.WriteCloser
+}
+func NewFileWriter(name string) (*FileWriter) {
+ fw := new(FileWriter)
+ file, err := os.OpenFile(name, os.O_WRONLY | os.O_APPEND | os.O_CREATE, 0666)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't open: %v", name))
+ }
+ fw.file = file
+ return fw
+}
+
+func (fw *FileWriter) LogWrite(msg string) {
+ fw.file.Write([]byte(msg))
+}
+
+func (fw *FileWriter) Close() {
+ fw.file.Close()
+}
+*/
+
+// This writer has a buffer that I don't ever bother to flush, so it may take a while
+// to see messages
+func NewFileWriter(name string) LogWriter {
+ file, err := os.OpenFile(name, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't open: %v", name))
+ }
+ return NewBufferedWriter(file)
+}
216 pattern_formatter.go
@@ -0,0 +1,216 @@
+package timber
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+ "regexp"
+ "strings"
+)
+
+var prefixRegexp = regexp.MustCompile(`^[\-+]?[0-9]+`)
+
+type PatFormatter struct {
+ format string
+ formatCompile string
+ formatDynamic []byte
+}
+
+// Format codes:
+// %T - Time: 17:24:05.333 HH:MM:SS.ms
+// %t - Time: 17:24:05 HH:MM:SS
+// %D - Date: 2011-12-25 yyyy-mm-dd
+// %d - Date: 2011/12/25
+// %L - Level (FNST, FINE, DEBG, TRAC, WARN, EROR, CRIT)
+// %S - Source: full runtime.Caller line
+// %s - Short Source: just file and line number
+// %x - Extra Short Source: just file without .go suffix
+// %M - Message
+// %% - Percent sign
+// the string number prefixes are allowed e.g.: %10s will pad the source field to 10 spaces
+func NewPatFormatter(format string) *PatFormatter {
+ pf := new(PatFormatter)
+ pf.format = format
+ pf.formatDynamic = make([]byte, 0, 9) // there are only 9 format codes so this is probably enough
+ pf.formatCompile = string(pf.compileForLevel(0)) // TODO figure out if I really want to cache each level
+ return pf
+}
+
+func (pf *PatFormatter) precompileLevels() {
+
+ for lvl := 0; lvl <= int(CRITICAL); lvl++ {
+ //pf.formatCompile[lvl] = string(pf.compileForLevel(pf.format, lvl))
+ //fmt.Printf("PreComp: %s", pf.formatCompile[lvl])
+ }
+}
+
+// this precompiles a sprintf format string for later use
+// it looks nasty but it should only be run once at config time
+func (pf *PatFormatter) compileForLevel(level int) []byte {
+ parts := bytes.Split([]byte(pf.format), []byte{'%'})
+ // check for a number formatter
+ var sprintfFmt []byte
+ for i, part := range parts {
+ if i == 0 {
+ sprintfFmt = append(sprintfFmt, part...)
+ continue
+ }
+ fmt_str := part
+ var num []byte
+ if num = prefixRegexp.Find(part); num != nil {
+ fmt_str = part[len(num):]
+ }
+
+ //fmt.Printf("%d A:<%s> N:<%s> P:<%s>\n", i, string(fmt_str), string(num), string(part))
+ switch fmt_str[0] {
+ case 'T':
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, '%')
+ sprintfFmt = append(sprintfFmt, num...)
+ sprintfFmt = append(sprintfFmt, 's')
+ pf.formatDynamic = append(pf.formatDynamic, 'e')
+ }
+ sprintfFmt = append(sprintfFmt, []byte("%02d:%02d:%02d.%03d")...)
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 'T')
+ case 't':
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, '%')
+ sprintfFmt = append(sprintfFmt, num...)
+ sprintfFmt = append(sprintfFmt, 's')
+ pf.formatDynamic = append(pf.formatDynamic, 'e')
+ }
+ sprintfFmt = append(sprintfFmt, []byte("%02d:%02d:%02d")...)
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 't')
+ case 'D':
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, '%')
+ sprintfFmt = append(sprintfFmt, num...)
+ sprintfFmt = append(sprintfFmt, 's')
+ pf.formatDynamic = append(pf.formatDynamic, 'e')
+ }
+ sprintfFmt = append(sprintfFmt, []byte("%d-%02d-%02d")...)
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 'D')
+ case 'd':
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, '%')
+ sprintfFmt = append(sprintfFmt, num...)
+ sprintfFmt = append(sprintfFmt, 's')
+ pf.formatDynamic = append(pf.formatDynamic, 'e')
+ }
+ sprintfFmt = append(sprintfFmt, []byte("%d/%02d/%02d")...)
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 'd')
+ case 'L':
+ sprintfFmt = append(sprintfFmt, '%')
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, num...)
+ }
+ sprintfFmt = append(sprintfFmt, 's')
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 'L')
+ case 'S':
+ sprintfFmt = append(sprintfFmt, '%')
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, num...)
+ }
+ sprintfFmt = append(sprintfFmt, 's')
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 'S')
+ case 's':
+ sprintfFmt = append(sprintfFmt, '%')
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, num...)
+ }
+ sprintfFmt = append(sprintfFmt, 's')
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 's')
+ case 'x':
+ sprintfFmt = append(sprintfFmt, '%')
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, num...)
+ }
+ sprintfFmt = append(sprintfFmt, 's')
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 'x')
+ case 'M':
+ sprintfFmt = append(sprintfFmt, '%')
+ if num != nil {
+ sprintfFmt = append(sprintfFmt, num...)
+ }
+ sprintfFmt = append(sprintfFmt, 's')
+ sprintfFmt = append(sprintfFmt, fmt_str[1:]...)
+ pf.formatDynamic = append(pf.formatDynamic, 'M')
+ case '%':
+ sprintfFmt = append(sprintfFmt, '%')
+ sprintfFmt = append(sprintfFmt, fmt_str...)
+ default:
+ sprintfFmt = append(sprintfFmt, fmt_str...)
+ } // end switch
+
+ } // end for
+ sprintfFmt = append(sprintfFmt, '\n')
+ //fmt.Printf("%s", string(sprintfFmt))
+ return sprintfFmt
+}
+
+// LogFormatter interface
+func (pf *PatFormatter) Format(rec LogRecord) string {
+ data := pf.getDynamic(rec)
+ //fmt.Printf("%v", data)
+ return fmt.Sprintf(pf.formatCompile, data...)
+}
+
+func (pf *PatFormatter) getDynamic(rec LogRecord) []interface{} {
+ tm := time.NanosecondsToLocalTime(rec.Timestamp)
+ ret := make([]interface{}, 0, 10)
+ for _, dyn := range pf.formatDynamic {
+ switch dyn {
+ case 'e':
+ ret = append(ret, "")
+ case 'T':
+ ret = append(ret, parseTimeMs(tm)...)
+ case 't':
+ ret = append(ret, parseTime(tm)...)
+ case 'D', 'd':
+ ret = append(ret, parseDate(tm)...)
+ case 'L':
+ ret = append(ret, LevelStrings[rec.Level])
+ case 'S':
+ ret = append(ret, parseSourceLong(rec.SourceFile, rec.SourceLine))
+ case 's':
+ ret = append(ret, parseSourceShort(rec.SourceFile, rec.SourceLine))
+ case 'x':
+ ret = append(ret, parseSourceXShort(rec.SourceFile))
+ case 'M':
+ ret = append(ret, rec.Message)
+ }
+ }
+ return ret
+}
+
+func parseSourceLong(file string, line int) string {
+ return fmt.Sprintf("%s:%d", file, line)
+}
+
+func parseSourceShort(file string, line int) string {
+ just_file := file[strings.LastIndex(file, "/")+1:]
+ return fmt.Sprintf("%s:%d", just_file, line)
+}
+func parseSourceXShort(file string) string {
+ return file[strings.LastIndex(file, "/")+1 : (len(file) - 3)]
+}
+
+func parseDate(t *time.Time) []interface{} {
+ return []interface{}{t.Year, t.Month, t.Day}
+}
+
+func parseTime(t *time.Time) []interface{} {
+ return []interface{}{t.Hour, t.Minute, t.Second}
+}
+
+func parseTimeMs(t *time.Time) []interface{} {
+ return []interface{}{t.Hour, t.Minute, t.Second, t.Nanosecond / 1e6}
+}
74 pattern_formatter_test.go
@@ -0,0 +1,74 @@
+package timber
+
+import (
+ "testing"
+ "fmt"
+)
+
+func TestWorstPatternFormat(t *testing.T) {
+ pf := NewPatFormatter("short:[%d %t] good:[%D %T] levelPadded:[%-10L] long:%S short:%s xs:%10x Msg:%M")
+ var now int64 = 1319150347383485000
+ lr := LogRecord{
+ Level: INFO,
+ Timestamp: now,
+ SourceFile: "/blah/der/some_file.go",
+ SourceLine: 7,
+ Message: "hellooooo nurse!"}
+ msg := pf.Format(lr)
+ pass := "short:[2011/10/20 15:39:07] good:[2011-10-20 15:39:07.383] levelPadded:[INFO ] long:/blah/der/some_file.go:7 short:some_file.go:7 xs: some_file Msg:hellooooo nurse!\n"
+ if msg != pass {
+ t.Errorf("Expect:%s != \nResult:%s", pass, msg)
+ }
+}
+func TestRealPatternFormat(t *testing.T) {
+ pf := NewPatFormatter("[%D %T] [%L] %-10x %M")
+ var now int64 = 1319150347383485000
+ lr := LogRecord{
+ Level: INFO,
+ Timestamp: now,
+ SourceFile: "/blah/der/some_file.go",
+ SourceLine: 7,
+ Message: "hellooooo nurse!"}
+ msg := pf.Format(lr)
+ pass := "[2011-10-20 15:39:07.383] [INFO] some_file hellooooo nurse!\n"
+ if msg != pass {
+ t.Errorf("Expect:%s != \nResult:%s", pass, msg)
+ }
+}
+
+func BenchmarkWorstPatternFormat(b *testing.B) {
+ pf := NewPatFormatter("short:[%d %t] good:[%D %T] levelPadded:[%-10L] long:%S short:%s xs:%10x Msg:%M")
+ var now int64 = 1319150347383485000
+ lr := LogRecord{
+ Level: INFO,
+ Timestamp: now,
+ SourceFile: "/blah/der/some_file.go",
+ SourceLine: 7,
+ Message: "hellooooo nurse!"}
+ for i := 0; i < b.N; i++ {
+ pf.Format(lr)
+ }
+}
+func BenchmarkWorstJustSprintf(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ fmt.Sprintf("short:[%d/%02d/%02d %02d:%02d:%02d] good:[%d-%02d-%02d %02d:%02d:%02d.%03d] levelPadded:[%-10s] long:%s short:%s xs:%10s Msg:%s\n", 2011, 10, 20, 15, 39, 7, 2011, 10, 20, 15, 39, 7, 383, "INFO", "/blah/der/some_file.go:7", "some_file.go:7", "some_file", "hellooooo nurse!")
+ }
+}
+func BenchmarkRealPatternFormat(b *testing.B) {
+ pf := NewPatFormatter("[%D %T] [%L] %-10x %M")
+ var now int64 = 1319150347383485000
+ lr := LogRecord{
+ Level: INFO,
+ Timestamp: now,
+ SourceFile: "/blah/der/some_file.go",
+ SourceLine: 7,
+ Message: "hellooooo nurse!"}
+ for i := 0; i < b.N; i++ {
+ pf.Format(lr)
+ }
+}
+func BenchmarkReallJustSprintf(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d.%03d] [%s] %-10s %s\n", 2011, 10, 20, 15, 39, 7, 383, "INFO", "some_file", "hellooooo nurse!")
+ }
+}
39 socket_writer.go
@@ -0,0 +1,39 @@
+package timber
+
+import (
+ "net"
+ "fmt"
+)
+
+// This should write to anything that you can write to with net.Dial
+type SocketWriter struct {
+ conn net.Conn
+}
+
+func NewSocketWriter(network, addr string) *SocketWriter {
+ sw := new(SocketWriter)
+ conn, err := net.Dial(network, addr)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't connect to socket: %v %v", network, addr))
+ }
+ sw.conn = conn
+ return sw
+}
+
+func (sw *SocketWriter) LogWrite(msg string) {
+ sw.conn.Write([]byte(msg))
+}
+
+func (sw *SocketWriter) Close() {
+ sw.conn.Close()
+}
+//************/
+/*************
+func NewSocketWriter(network, addr string) (LogWriter) {
+ conn, err := net.Dial(network, addr)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't connect to socket: %v %v", network, addr))
+ }
+ return NewBufferedWriter(conn)
+}
+**************/
422 timber.go
@@ -0,0 +1,422 @@
+// This is a brand new logger implementation that matches the log4go interface and also may be
+// used as a drop-in replacement for the standard logger
+//
+// Basic use:
+// import log "timber"
+// log.LoadConfiguration("timber.xml")
+// log.Debug("Debug message!")
+//
+// XML Config file:
+// <logging>
+// <filter enabled="true">
+// <tag>stdout</tag>
+// <type>console</type>
+// <!-- level is (:?FINEST|FINE|DEBUG|TRACE|INFO|WARNING|ERROR) -->
+// <level>DEBUG</level>
+// </filter>
+// <filter enabled="true">
+// <tag>file</tag>
+// <type>file</type>
+// <level>FINEST</level>
+// <property name="filename">log/server.log</property>
+// <property name="format">server [%D %T] [%L] %M</property>
+// </filter>
+// <filter enabled="false">
+// <tag>syslog</tag>
+// <type>socket</type>
+// <level>FINEST</level>
+// <property name="protocol">unixgram</property>
+// <property name="endpoint">/dev/log</property>
+// <format name="pattern">%L %M</property>
+// </filter>
+// </logging>
+// The <tag> is ignored.
+//
+// To configure the pattern formatter all filters accept:
+// <format name="pattern">[%D %T] %L %M</format>
+// Pattern format specifiers (not the same as log4go!):
+// %T - Time: 17:24:05.333 HH:MM:SS.ms
+// %t - Time: 17:24:05 HH:MM:SS
+// %D - Date: 2011-12-25 yyyy-mm-dd
+// %d - Date: 2011/12/25 yyyy/mm/dd
+// %L - Level (FNST, FINE, DEBG, TRAC, WARN, EROR, CRIT)
+// %S - Source: full runtime.Caller line and line number
+// %s - Short Source: just file and line number
+// %x - Extra Short Source: just file without .go suffix
+// %M - Message
+// %% - Percent sign
+// the string number prefixes are allowed e.g.: %10s will pad the source field to 10 spaces
+// pattern defaults to %M
+// Both log4go synatax of <property name="format"> and new <format name=type> are supported
+// the property syntax will only ever support the pattern formatter
+//
+// Code Architecture:
+// A MultiLogger <logging> which consists of many ConfigLoggers <filter>. ConfigLoggers have three properties:
+// LogWriter <type>, Level (as a threshold) <level> and LogFormatter <format>.
+//
+// In practice, this means that you define ConfigLoggers with a LogWriter (where the log prints to
+// eg. socket, file, stdio etc), the Level threshold, and a LogFormatter which formats the message
+// before writing. Because the LogFormatters and LogWriters are simple interfaces, it is easy to
+// write your own custom implementations.
+//
+// Once configured, you only deal with the "Logger" interface and use the log methods in your code
+//
+// The motivation for this package grew from a need to make some changes to the functionality of
+// log4go (which had already been integrated into a larger project). I tried to maintain compatiblity
+// with log4go for the interface and configuration. The main issue I had with log4go was that each of
+// logger types had incisistent and incompatible configuration. I looked at contributing changes to
+// log4go, but I would have needed to break existing use cases so I decided to do a rewrite from scratch.
+//
+package timber
+
+import (
+ "os"
+ "time"
+ "runtime"
+ "fmt"
+)
+
+type level int
+// Log levels
+const (
+ NONE level = iota // NONE to be used for standard go log impl's
+ FINEST
+ FINE
+ DEBUG
+ TRACE
+ INFO
+ WARNING
+ ERROR
+ CRITICAL
+)
+// Default level passed to runtime.Caller by Timber, add to this if you wrap Timber in your own logging code
+const DefaultFileDepth int = 3
+
+// What gets printed for each Log level
+var LevelStrings = [...]string{"", "FNST", "FINE", "DEBG", "TRAC", "INFO", "WARN", "EROR", "CRIT"}
+
+// This explicitly defines the contract for a logger
+// Not really useful except for documentation for
+// writing an separate implementation
+type Logger interface {
+ // match log4go interface to drop-in replace
+ Finest(arg0 interface{}, args ...interface{})
+ Fine(arg0 interface{}, args ...interface{})
+ Debug(arg0 interface{}, args ...interface{})
+ Trace(arg0 interface{}, args ...interface{})
+ Info(arg0 interface{}, args ...interface{})
+ Warn(arg0 interface{}, args ...interface{}) os.Error
+ Error(arg0 interface{}, args ...interface{}) os.Error
+ Critical(arg0 interface{}, args ...interface{}) os.Error
+ Log(lvl level, arg0 interface{}, args ...interface{})
+
+ // support standard log too
+ Print(v ...interface{})
+ Printf(format string, v ...interface{})
+ Println(v ...interface{})
+ Panic(v ...interface{})
+ Panicf(format string, v ...interface{})
+ Panicln(v ...interface{})
+ Fatal(v ...interface{})
+ Fatalf(format string, v ...interface{})
+ Fatalln(v ...interface{})
+}
+
+// Not used
+type LoggerConfig interface {
+ // When set, messages with level < lvl will be ignored. It's up to the implementor to keep the contract or not
+ SetLevel(lvl level)
+ // Set the formatter for the log
+ SetFormatter(formatter LogFormatter)
+}
+
+// Interface required for a log writer endpoint. It's more or less a
+// io.WriteCloser with no errors allowed to be returned and string
+// instead of []byte.
+//
+// TODO: Maybe this should just be a standard io.WriteCloser?
+type LogWriter interface {
+ LogWrite(msg string)
+ Close()
+}
+
+// This packs up all the message data and metadata. This structure
+// will be passed to the LogFormatter
+type LogRecord struct {
+ Level level
+ Timestamp int64
+ SourceFile string
+ SourceLine int
+ Message string
+}
+
+// Format a log message before writing
+type LogFormatter interface {
+ Format(rec LogRecord) string
+}
+
+// Container a single log format/destination
+type ConfigLogger struct {
+ LogWriter LogWriter
+ // Messages with level < Level will be ignored. It's up to the implementor to keep the contract or not
+ Level level
+ Formatter LogFormatter
+}
+
+// Allow logging to multiple places
+type MultiLogger interface {
+ // returns an int that identifies the logger for future calls to SetLevel and SetFormatter
+ AddLogger(logger ConfigLogger) int
+ // dynamically change level or format
+ SetLevel(index int, lvl level)
+ SetFormatter(index int, formatter LogFormatter)
+ Close()
+}
+
+//
+//
+//
+// Implementation
+//
+//
+//
+
+// The Timber instance is the concrete implementation of the logger interfaces.
+// New instances may be created, but usually you'll just want to use the default
+// instance in Global
+//
+// NOTE: I don't supporting the log4go special handling of the first parameter based on type
+// mainly cuz I don't think it's particularly useful (I kept passing a data string as the first
+// param and expecting a Println-like output but that would always break expecting a format string)
+// I also don't support the passing of the closure stuff
+type Timber struct {
+ writerConfigChan chan timberConfig
+ recordChan chan LogRecord
+ hasLogger bool
+ // This value is passed to runtime.Caller to get the file name/line and may require
+ // tweaking if you want to wrap the logger
+ FileDepth int
+}
+
+type timberAction int
+
+const (
+ actionAdd timberAction = iota
+ actionModify
+ actionQuit
+)
+
+type timberConfig struct {
+ Action timberAction // type of config action
+ Index int // only for modify
+ Cfg ConfigLogger // used for modify or add
+ Ret chan int // only used for add
+}
+
+// Creates a new Timber logger that is ready to be configured
+// With no subsequent configuration, nothing will be logged
+//
+func NewTimber() *Timber {
+ t := new(Timber)
+ t.writerConfigChan = make(chan timberConfig)
+ t.recordChan = make(chan LogRecord, 300)
+ t.FileDepth = DefaultFileDepth
+ go t.asyncLumberJack()
+ return t
+}
+
+func (t *Timber) asyncLumberJack() {
+ var loggers []ConfigLogger = make([]ConfigLogger, 0, 2)
+ loopIt := true
+ for loopIt {
+ select {
+ case rec := <-t.recordChan:
+ sendToLoggers(loggers, rec)
+ case cfg := <-t.writerConfigChan:
+ switch cfg.Action {
+ case actionAdd:
+ loggers = append(loggers, cfg.Cfg)
+ cfg.Ret <- (len(loggers) - 1)
+ case actionModify:
+ case actionQuit:
+ close(t.recordChan)
+ loopIt = false
+ defer func() {
+ cfg.Ret <- 0
+ }()
+ }
+ } // select
+ } // for
+ // drain the log channel before closing
+ for rec := range t.recordChan {
+ sendToLoggers(loggers, rec)
+ }
+ closeAllWriters(loggers)
+}
+
+func sendToLoggers(loggers []ConfigLogger, rec LogRecord) {
+ formatted := ""
+ for _, cLog := range loggers {
+ if rec.Level >= cLog.Level || rec.Level == 0 {
+ if formatted == "" {
+ formatted = cLog.Formatter.Format(rec)
+ }
+ cLog.LogWriter.LogWrite(formatted)
+ }
+ }
+}
+
+func closeAllWriters(cls []ConfigLogger) {
+ for _, cLog := range cls {
+ cLog.LogWriter.Close()
+ }
+}
+
+// MultiLogger interface
+func (t *Timber) AddLogger(logger ConfigLogger) int {
+ tcChan := make(chan int, 1) // buffered
+ tc := timberConfig{Action: actionAdd, Cfg: logger, Ret: tcChan}
+ t.writerConfigChan <- tc
+ return <-tcChan
+}
+
+// MultiLogger interface
+func (t *Timber) Close() {
+ tcChan := make(chan int)
+ tc := timberConfig{Action: actionQuit, Ret: tcChan}
+ t.writerConfigChan <- tc
+ <-tcChan // block for closing
+}
+
+// Not yet implemented
+func (t *Timber) SetLevel(index int, lvl level) {
+ // TODO
+}
+
+// Not yet implemented
+func (t *Timber) SetFormatter(index int, formatter LogFormatter) {
+ // TODO
+}
+
+// Logger interface
+func (t *Timber) prepareAndSend(lvl level, msg string, depth int) {
+ now := time.Nanoseconds()
+ _, file, line, _ := runtime.Caller(depth)
+ t.recordChan <- LogRecord{Level: lvl, Timestamp: now, SourceFile: file, SourceLine: line, Message: msg}
+}
+
+func (t *Timber) Finest(arg0 interface{}, args ...interface{}) {
+ t.prepareAndSend(FINEST, fmt.Sprintf(arg0.(string), args...), t.FileDepth)
+}
+func (t *Timber) Fine(arg0 interface{}, args ...interface{}) {
+ t.prepareAndSend(FINE, fmt.Sprintf(arg0.(string), args...), t.FileDepth)
+}
+func (t *Timber) Debug(arg0 interface{}, args ...interface{}) {
+ t.prepareAndSend(DEBUG, fmt.Sprintf(arg0.(string), args...), t.FileDepth)
+}
+func (t *Timber) Trace(arg0 interface{}, args ...interface{}) {
+ t.prepareAndSend(TRACE, fmt.Sprintf(arg0.(string), args...), t.FileDepth)
+}
+func (t *Timber) Info(arg0 interface{}, args ...interface{}) {
+ t.prepareAndSend(INFO, fmt.Sprintf(arg0.(string), args...), t.FileDepth)
+}
+func (t *Timber) Warn(arg0 interface{}, args ...interface{}) os.Error {
+ msg := fmt.Sprintf(arg0.(string), args...)
+ t.prepareAndSend(WARNING, msg, t.FileDepth)
+ return os.NewError(msg)
+}
+func (t *Timber) Error(arg0 interface{}, args ...interface{}) os.Error {
+ msg := fmt.Sprintf(arg0.(string), args...)
+ t.prepareAndSend(ERROR, msg, t.FileDepth)
+ return os.NewError(msg)
+}
+func (t *Timber) Critical(arg0 interface{}, args ...interface{}) os.Error {
+ msg := fmt.Sprintf(arg0.(string), args...)
+ t.prepareAndSend(CRITICAL, msg, t.FileDepth)
+ return os.NewError(msg)
+}
+func (t *Timber) Log(lvl level, arg0 interface{}, args ...interface{}) {
+ t.prepareAndSend(lvl, fmt.Sprintf(arg0.(string), args...), t.FileDepth)
+}
+
+// Print won't work well with a pattern_logger because it explicitly adds
+// its own \n; so you'd have to write your own formatter to remove it
+func (t *Timber) Print(v ...interface{}) {
+ t.prepareAndSend(NONE, fmt.Sprint(v...), t.FileDepth)
+}
+func (t *Timber) Printf(format string, v ...interface{}) {
+ t.prepareAndSend(NONE, fmt.Sprintf(format, v...), t.FileDepth)
+}
+// Println won't work well either with a pattern_logger because it explicitly adds
+// its own \n; so you'd have to write your own formatter to not have 2 \n's
+func (t *Timber) Println(v ...interface{}) {
+ t.prepareAndSend(NONE, fmt.Sprintln(v...), t.FileDepth)
+}
+func (t *Timber) Panic(v ...interface{}) {
+ msg := fmt.Sprint(v...)
+ t.prepareAndSend(NONE, msg, t.FileDepth)
+ panic(msg)
+}
+func (t *Timber) Panicf(format string, v ...interface{}) {
+ msg := fmt.Sprintf(format, v...)
+ t.prepareAndSend(NONE, msg, t.FileDepth)
+ panic(msg)
+}
+func (t *Timber) Panicln(v ...interface{}) {
+ msg := fmt.Sprintln(v...)
+ t.prepareAndSend(NONE, msg, t.FileDepth)
+ panic(msg)
+}
+func (t *Timber) Fatal(v ...interface{}) {
+ msg := fmt.Sprint(v...)
+ t.prepareAndSend(NONE, msg, t.FileDepth)
+ t.Close()
+ os.Exit(1)
+}
+func (t *Timber) Fatalf(format string, v ...interface{}) {
+ msg := fmt.Sprintf(format, v...)
+ t.prepareAndSend(NONE, msg, t.FileDepth)
+ t.Close()
+ os.Exit(1)
+}
+func (t *Timber) Fatalln(v ...interface{}) {
+ msg := fmt.Sprintln(v...)
+ t.prepareAndSend(NONE, msg, t.FileDepth)
+ t.Close()
+ os.Exit(1)
+}
+
+//
+//
+// Default Instance
+//
+//
+
+// Default Timber Instance (used for all the package level function calls)
+var Global = NewTimber()
+
+// Simple wrappers for Logger interface
+func Finest(arg0 interface{}, args ...interface{}) { Global.Finest(arg0, args...) }
+func Fine(arg0 interface{}, args ...interface{}) { Global.Fine(arg0, args...) }
+func Debug(arg0 interface{}, args ...interface{}) { Global.Debug(arg0, args...) }
+func Trace(arg0 interface{}, args ...interface{}) { Global.Trace(arg0, args...) }
+func Info(arg0 interface{}, args ...interface{}) { Global.Info(arg0, args...) }
+func Warn(arg0 interface{}, args ...interface{}) os.Error { return Global.Warn(arg0, args...) }
+func Error(arg0 interface{}, args ...interface{}) os.Error { return Global.Error(arg0, args...) }
+func Critical(arg0 interface{}, args ...interface{}) os.Error { return Global.Critical(arg0, args...) }
+func Log(lvl level, arg0 interface{}, args ...interface{}) { Global.Log(lvl, arg0, args...) }
+func Print(v ...interface{}) { Global.Print(v...) }
+func Printf(format string, v ...interface{}) { Global.Printf(format, v...) }
+func Println(v ...interface{}) { Global.Println(v...) }
+func Panic(v ...interface{}) { Global.Panic(v...) }
+func Panicf(format string, v ...interface{}) { Global.Panicf(format, v...) }
+func Panicln(v ...interface{}) { Global.Panicln(v...) }
+func Fatal(v ...interface{}) { Global.Print(v...) }
+func Fatalf(format string, v ...interface{}) { Global.Printf(format, v...) }
+func Fatalln(v ...interface{}) { Global.Println(v...) }
+
+func AddLogger(logger ConfigLogger) int { return Global.AddLogger(logger) }
+func Close() { Global.Close() }
+// Match log4go to load configuration. This could probably also be made extensible
+// but it's not worth it right now
+func LoadConfiguration(filename string) { Global.LoadXMLConfig(filename) }
42 timber.xml
@@ -0,0 +1,42 @@
+<logging>
+ <filter enabled="true">
+ <tag>stderr</tag>
+ <type>console</type>
+ <!-- level is (:?FINEST|FINE|DEBUG|TRACE|INFO|WARNING|ERROR) -->
+ <level>DEBUG</level>
+ <!--
+ // Format codes:
+ // %T - Time: 17:24:05.333 HH:MM:SS.ms
+ // %t - Time: 17:24:05 HH:MM:SS
+ // %D - Date: 2011-12-25 yyyy-mm-dd
+ // %d - Date: 2011/12/25
+ // %L - Level (FNST, FINE, DEBG, TRAC, WARN, EROR, CRIT)
+ // %S - Source: full runtime.Caller line
+ // %s - Short Source: just file and line number
+ // %x - Extra Short Source: just file without .go suffix
+ // %M - Message
+ // %% - Percent sign
+ // the string number prefixes are allowed e.g.: %10s will pad the source field to 10 spaces
+ // pattern defaults to %M
+ // both log4go synatax of <property name="format"> and new <format name=type> are supported
+ // the property syntax will only ever support the pattern formatter
+ -->
+ <format name="pattern">[%D %T] %L %M</format>
+ </filter>
+ <filter enabled="true">
+ <tag>file</tag>
+ <type>file</type>
+ <level>FINEST</level>
+ <property name="filename">timber_test.log</property>
+ <property name="format">[%D %T] [%L] %M</property>
+ </filter>
+ <filter enabled="true">
+ <tag>syslog</tag>
+ <type>socket</type>
+ <level>FINEST</level>
+ <property name="protocol">udp</property> <!-- tcp or udp -->
+ <property name="endpoint">localhost:9500</property> <!-- recommend UDP broadcast -->
+ <property name="format">%L %M</property>
+ </filter>
+</logging>
+
47 timber_test.go
@@ -0,0 +1,47 @@
+package timber
+
+import (
+ "testing"
+)
+
+func TestConsole(t *testing.T) {
+ log := NewTimber()
+ console := new(ConsoleWriter)
+ formatter := NewPatFormatter("[%D %T] [%L] %-10x %M")
+ idx := log.AddLogger(ConfigLogger{LogWriter: console,
+ Level: DEBUG,
+ Formatter: formatter})
+ log.Error("what error? %v", idx)
+ log.Close()
+}
+
+func TestFile(t *testing.T) {
+ log := NewTimber()
+ writer := NewFileWriter("test.log")
+ formatter := NewPatFormatter("[%D %T] [%L] %-10x %M")
+ idx := log.AddLogger(ConfigLogger{LogWriter: writer,
+ Level: FINEST,
+ Formatter: formatter})
+ log.Error("what error? %v", idx)
+ log.Warn("I'm waringing you!")
+ log.Info("FYI")
+ log.Fine("you soo fine!")
+ log.Close()
+}
+
+func TestXmlConfig(t *testing.T) {
+ log := NewTimber()
+ log.LoadXMLConfig("timber.xml")
+ log.Info("Message to XML loggers")
+ log.Close()
+}
+
+func TestDefaultLogger(t *testing.T) {
+ console := new(ConsoleWriter)
+ formatter := NewPatFormatter("%DT%T %L %-10x %M")
+ AddLogger(ConfigLogger{LogWriter: console,
+ Level: DEBUG,
+ Formatter: formatter})
+ Warn("Some sweet default logging")
+ Close()
+}
122 xml_config.go
@@ -0,0 +1,122 @@
+package timber
+
+import (
+ "xml"
+ "fmt"
+ "os"
+ "reflect"
+ "log"
+)
+
+// These levels match log4go configuration
+var LongLevelStrings = []string{"NONE", "FINEST", "FINE", "DEBUG", "TRACE", "INFO", "WARNING", "ERROR", "CRITICAL"}
+
+// match the log4go structure so i don't have to change my configs
+type xmlProperty struct {
+ Name string `xml:"attr"`
+ Value string `xml:"chardata"`
+}
+type xmlFilter struct {
+ XMLName xml.Name `xml:"filter"`
+ Tag string
+ Enabled bool `xml:"attr"`
+ Type string
+ Level string
+ Format xmlProperty
+ Property []xmlProperty
+}
+
+type xmlConfig struct {
+ XMLName xml.Name `xml:"logging"`
+ Filter []xmlFilter
+}
+
+// Loads the configuration from an XML file (as you were probably expecting)
+func (tim *Timber) LoadXMLConfig(fileName string) {
+ file, err := os.Open(fileName)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't load xml config file: %s %v", fileName, err))
+ }
+
+ val := xmlConfig{}
+ err = xml.Unmarshal(file, &val)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't parse xml config file: %s %v", fileName, err))
+ }
+
+ for _, filter := range val.Filter {
+ if filter.Enabled {
+ level := getLevel(filter.Level)
+ formatter := getFormatter(filter)
+
+ switch filter.Type {
+ case "console":
+ tim.AddLogger(ConfigLogger{LogWriter: new(ConsoleWriter), Level: level, Formatter: formatter})
+ case "socket":
+ tim.AddLogger(ConfigLogger{LogWriter: getSocketWriter(filter), Level: level, Formatter: formatter})
+ case "file":
+ tim.AddLogger(ConfigLogger{LogWriter: getFileWriter(filter), Level: level, Formatter: formatter})
+ default:
+ log.Printf("TIMBER! Warning unrecognized filter in config file: %v\n", filter)
+ }
+ }
+ }
+
+}
+
+func getLevel(lvlString string) level {
+ for idx, str := range LongLevelStrings {
+ if str == lvlString {
+ return level(idx)
+ }
+ }
+ return level(0)
+}
+
+func getFormatter(filter xmlFilter) LogFormatter {
+ var format string
+ // try to get the new format tag first, then fall back to the generic property one
+ val := xmlProperty{}
+ if !reflect.DeepEqual(filter.Format, val) { // not equal to the empty value
+ format = filter.Format.Value
+ } else {
+ props := filter.Property
+ for _, prop := range props {
+ if prop.Name == `format` {
+ format = prop.Value
+ }
+ }
+ }
+ if format == "" {
+ format = "%M" // just the message by default
+ }
+ return NewPatFormatter(format)
+}
+
+func getSocketWriter(filter xmlFilter) LogWriter {
+ var protocol, endpoint string
+ for _, prop := range filter.Property {
+ if prop.Name == "protocol" {
+ protocol = prop.Value
+ } else if prop.Name == "endpoint" {
+ endpoint = prop.Value
+ }
+ }
+ if protocol == "" || endpoint == "" {
+ panic("TIMBER! Missing protocol or endpoint for socket log writer")
+ }
+ return NewSocketWriter(protocol, endpoint)
+}
+
+func getFileWriter(filter xmlFilter) LogWriter {
+ var filename string
+ for _, prop := range filter.Property {
+ if prop.Name == "filename" {
+ filename = prop.Value
+ }
+ }
+ if filename == "" {
+ panic("TIMBER! Missing filename for file log writer")
+ }
+ return NewFileWriter(filename)
+}
Please sign in to comment.
Something went wrong with that request. Please try again.