Skip to content
Browse files

Merge pull request #4 from larzconwell/master

JSON config file support
  • Loading branch information...
2 parents 042afd2 + 4566241 commit 931a9ef6bf06dddb61c050d2bfafb0dab02b694d @smw1218 smw1218 committed Sep 25, 2012
Showing with 400 additions and 159 deletions.
  1. +3 −3 README.md
  2. +25 −0 config.go
  3. +123 −0 config_json.go
  4. +125 −0 config_xml.go
  5. +39 −16 timber.go
  6. +67 −0 timber.json
  7. +18 −18 timber.xml
  8. +0 −122 xml_config.go
View
6 README.md
@@ -6,7 +6,7 @@ Timber is a logger for go with a similar interface as log4go and also can be use
Features
--------
* Log levels: Finest, Fine, Debug, Trace, Info, Warn, Error, Critical
-* External configuration via XML
+* External configuration via XML and JSON
* Multiple log destinations (console, file, socket)
* Configurable format per destination
* Extensible and pluggable design (if you configure via code rather than XML)
@@ -26,12 +26,12 @@ The easiest way to use Timber is to use configure the built-in singleton:
)
func main() {
- // load xml config
+ // load xml config, json also supported
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.
+An example timber.xml and timber.json are 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.
View
25 config.go
@@ -0,0 +1,25 @@
+package timber
+
+import (
+ "log"
+ "path"
+)
+
+func (t *Timber) LoadConfig(filename string) {
+ if len(filename) <= 0 {
+ return
+ }
+ ext := path.Ext(filename)
+ ext = ext[1:]
+
+ switch ext {
+ case "xml":
+ t.LoadXMLConfig(filename)
+ break
+ case "json":
+ t.LoadJSONConfig(filename)
+ break
+ default:
+ log.Printf("TIMBER! Unknown config file type %v, only XML and JSON are supported types\n", ext)
+ }
+}
View
123 config_json.go
@@ -0,0 +1,123 @@
+package timber
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "reflect"
+)
+
+type JSONProperty struct {
+ Name string
+ Value string
+}
+
+type JSONFilter struct {
+ Enabled bool
+ Tag string
+ Type string
+ Level string
+ Format JSONProperty
+ Properties []JSONProperty
+}
+
+type JSONConfig struct {
+ Filters []JSONFilter
+}
+
+// Loads the configuration from an JSON file (as you were probably expecting)
+func (t *Timber) LoadJSONConfig(filename string) {
+ if len(filename) <= 0 {
+ return
+ }
+
+ file, err := os.Open(filename)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't load json config file: %s %v", filename, err))
+ }
+ defer file.Close()
+
+ config := JSONConfig{}
+ err = json.NewDecoder(file).Decode(&config)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't parse json config file: %s %v", filename, err))
+ }
+
+ for _, filter := range config.Filters {
+ if !filter.Enabled {
+ continue
+ }
+ level := getLevel(filter.Level)
+ formatter := getJSONFormatter(filter)
+ configLogger := ConfigLogger{Level: level, Formatter: formatter}
+
+ switch filter.Type {
+ case "console":
+ configLogger.LogWriter = new(ConsoleWriter)
+ case "socket":
+ configLogger.LogWriter = getJSONSocketWriter(filter)
+ case "file":
+ configLogger.LogWriter = getJSONFileWriter(filter)
+ default:
+ log.Printf("TIMBER! Warning unrecognized filter in config file: %v\n", filter.Tag)
+ continue
+ }
+
+ t.AddLogger(configLogger)
+ }
+}
+
+func getJSONFormatter(filter JSONFilter) LogFormatter {
+ format := ""
+ property := JSONProperty{}
+
+ // If format field is set then use it's value, otherwise
+ // attempt to get the format field from the filters properties
+ if !reflect.DeepEqual(filter.Format, property) {
+ format = filter.Format.Value
+ } else {
+ for _, prop := range filter.Properties {
+ if prop.Name == "format" {
+ format = prop.Value
+ }
+ }
+ }
+
+ // If empty format set the default as just the message
+ if format == "" {
+ format = "%M"
+ }
+ return NewPatFormatter(format)
+}
+
+func getJSONSocketWriter(filter JSONFilter) LogWriter {
+ var protocol, endpoint string
+
+ for _, property := range filter.Properties {
+ if property.Name == "protocol" {
+ protocol = property.Value
+ } else if property.Name == "endpoint" {
+ endpoint = property.Value
+ }
+ }
+
+ if protocol == "" || endpoint == "" {
+ panic("TIMBER! Missing protocol or endpoint for socket log writer")
+ }
+ return NewSocketWriter(protocol, endpoint)
+}
+
+func getJSONFileWriter(filter JSONFilter) LogWriter {
+ filename := ""
+
+ for _, property := range filter.Properties {
+ if property.Name == "filename" {
+ filename = property.Value
+ }
+ }
+ if filename == "" {
+ panic("TIMBER! Missing filename for file log writer")
+ }
+ return NewFileWriter(filename)
+}
View
125 config_xml.go
@@ -0,0 +1,125 @@
+package timber
+
+import (
+ "encoding/xml"
+ "fmt"
+ "log"
+ "os"
+ "reflect"
+)
+
+// match the log4go structure so i don't have to change my configs
+type XMLProperty struct {
+ Name string `xml:"name,attr"`
+ Value string `xml:",chardata"`
+}
+type XMLFilter struct {
+ XMLName xml.Name `xml:"filter"`
+ Enabled bool `xml:"enabled,attr"`
+ Tag string `xml:"tag"`
+ Type string `xml:"type"`
+ Level string `xml:"level"`
+ Format XMLProperty `xml:"format"`
+ Properties []XMLProperty `xml:"property"`
+}
+
+type XMLConfig struct {
+ XMLName xml.Name `xml:"logging"`
+ Filters []XMLFilter `xml:"filter"`
+}
+
+// Loads the configuration from an XML file (as you were probably expecting)
+func (t *Timber) LoadXMLConfig(filename string) {
+ if len(filename) <= 0 {
+ return
+ }
+
+ file, err := os.Open(filename)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't load xml config file: %s %v", filename, err))
+ }
+ defer file.Close()
+
+ config := XMLConfig{}
+ err = xml.NewDecoder(file).Decode(&config)
+ if err != nil {
+ panic(fmt.Sprintf("TIMBER! Can't parse xml config file: %s %v", filename, err))
+ }
+
+ for _, filter := range config.Filters {
+ if !filter.Enabled {
+ continue
+ }
+ level := getLevel(filter.Level)
+ formatter := getXMLFormatter(filter)
+ configLogger := ConfigLogger{Level: level, Formatter: formatter}
+
+ switch filter.Type {
+ case "console":
+ configLogger.LogWriter = new(ConsoleWriter)
+ case "socket":
+ configLogger.LogWriter = getXMLSocketWriter(filter)
+ case "file":
+ configLogger.LogWriter = getXMLFileWriter(filter)
+ default:
+ log.Printf("TIMBER! Warning unrecognized filter in config file: %v\n", filter.Tag)
+ continue
+ }
+
+ t.AddLogger(configLogger)
+ }
+}
+
+func getXMLFormatter(filter XMLFilter) LogFormatter {
+ format := ""
+ property := XMLProperty{}
+
+ // If format field is set then use it's value, otherwise
+ // attempt to get the format field from the filters properties
+ if !reflect.DeepEqual(filter.Format, property) {
+ format = filter.Format.Value
+ } else {
+ for _, prop := range filter.Properties {
+ if prop.Name == "format" {
+ format = prop.Value
+ }
+ }
+ }
+
+ // If empty format set the default as just the message
+ if format == "" {
+ format = "%M"
+ }
+ return NewPatFormatter(format)
+}
+
+func getXMLSocketWriter(filter XMLFilter) LogWriter {
+ var protocol, endpoint string
+
+ for _, property := range filter.Properties {
+ if property.Name == "protocol" {
+ protocol = property.Value
+ } else if property.Name == "endpoint" {
+ endpoint = property.Value
+ }
+ }
+
+ if protocol == "" || endpoint == "" {
+ panic("TIMBER! Missing protocol or endpoint for socket log writer")
+ }
+ return NewSocketWriter(protocol, endpoint)
+}
+
+func getXMLFileWriter(filter XMLFilter) LogWriter {
+ filename := ""
+
+ for _, property := range filter.Properties {
+ if property.Name == "filename" {
+ filename = property.Value
+ }
+ }
+ if filename == "" {
+ panic("TIMBER! Missing filename for file log writer")
+ }
+ return NewFileWriter(filename)
+}
View
55 timber.go
@@ -1,6 +1,6 @@
// 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")
@@ -27,10 +27,10 @@
// <level>FINEST</level>
// <property name="protocol">unixgram</property>
// <property name="endpoint">/dev/log</property>
-// <format name="pattern">%L %M</property>
+// <format name="pattern">%L %M</property>
// </filter>
// </logging>
-// The <tag> is ignored.
+// The <tag> is ignored.
//
// To configure the pattern formatter all filters accept:
// <format name="pattern">[%D %T] %L %M</format>
@@ -51,20 +51,20 @@
// 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:
+// 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
+// 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
@@ -92,12 +92,35 @@ const (
CRITICAL
)
-// Default level passed to runtime.Caller by Timber, add to this if you wrap Timber in your own logging code
+// 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"}
+// Full level names
+var LongLevelStrings = []string{
+ "NONE",
+ "FINEST",
+ "FINE",
+ "DEBUG",
+ "TRACE",
+ "INFO",
+ "WARNING",
+ "ERROR",
+ "CRITICAL",
+}
+
+// Return a given level string as the actual Level value
+func getLevel(lvlString string) Level {
+ for idx, str := range LongLevelStrings {
+ if str == lvlString {
+ return Level(idx)
+ }
+ }
+ return Level(0)
+}
+
// This explicitly defines the contract for a logger
// Not really useful except for documentation for
// writing an separate implementation
@@ -196,7 +219,7 @@ 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
+ // 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
}
@@ -275,7 +298,7 @@ func closeAllWriters(cls []ConfigLogger) {
}
}
-// MultiLogger interface
+// MultiLogger interface
func (t *Timber) AddLogger(logger ConfigLogger) int {
tcChan := make(chan int, 1) // buffered
tc := timberConfig{Action: actionAdd, Cfg: logger, Ret: tcChan}
@@ -342,7 +365,7 @@ 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
+// 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)
@@ -351,7 +374,7 @@ 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
+// 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)
@@ -422,6 +445,6 @@ func Fatalln(v ...interface{}) { Global.Fatalln(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) }
+func LoadConfiguration(filename string) { Global.LoadConfig(filename) }
+func LoadXMLConfiguration(filename string) { Global.LoadXMLConfig(filename) }
+func LoadJSONConfiguration(filename string) { Global.LoadJSONConfig(filename) }
View
67 timber.json
@@ -0,0 +1,67 @@
+// Note: Comments must be removed for "encoding/json" to properly parse JSON files
+{
+ "filters": [
+ {
+ "enabled": true,
+ "tag": "stderr",
+ "type": "console",
+ // Levels are FINEST|FINE|DEBUG|TRACE|INFO|WARNING|ERROR
+ "level": "DEBUG",
+ // 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
+ // Setting formats can be either through filter.format or through a filter.properties item,
+ // but only support the above formats(Example included below)
+ "format": {
+ "name": "pattern",
+ "value": "[%D %T] %L %M"
+ }
+ },
+ {
+ "enabled": true,
+ "tag": "file",
+ "type": "file",
+ "level": "FINEST",
+ "properties": [
+ {
+ "name": "filename",
+ "value": "timber_test.log"
+ },
+ {
+ "name": "format",
+ "value": "[%D %T] [%L] %M"
+ }
+ ]
+ },
+ {
+ "enabled": true,
+ "tag": "syslog",
+ "type": "socket",
+ "level": "FINEST",
+ "properties": [
+ {
+ "name": "protocol",
+ "value": "udp"
+ },
+ {
+ "name": "endpoint",
+ "value": "localhost:9500"
+ },
+ {
+ "name": "format",
+ "value": "%L %M"
+ }
+ ]
+ }
+ ]
+}
View
36 timber.xml
@@ -2,24 +2,24 @@
<filter enabled="true">
<tag>stderr</tag>
<type>console</type>
- <!-- level is (:?FINEST|FINE|DEBUG|TRACE|INFO|WARNING|ERROR) -->
+ <!-- Levels are 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 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>
@@ -34,9 +34,9 @@
<tag>syslog</tag>
<type>socket</type>
<level>FINEST</level>
- <property name="protocol">udp</property> <!-- tcp or udp -->
+ <property name="protocol">udp</property> <!-- tcp or udp -->
<property name="endpoint">localhost:9500</property> <!-- recommend UDP broadcast -->
- <property name="format">%L %M</property>
+ <property name="format">%L %M</property>
</filter>
</logging>
View
122 xml_config.go
@@ -1,122 +0,0 @@
-package timber
-
-import (
- "encoding/xml"
- "fmt"
- "log"
- "os"
- "reflect"
-)
-
-// 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:"name,attr"`
- Value string `xml:",chardata"`
-}
-type xmlFilter struct {
- XMLName xml.Name `xml:"filter"`
- Tag string `xml:"tag"`
- Enabled bool `xml:"enabled,attr"`
- Type string `xml:"type"`
- Level string `xml:"level"`
- Format xmlProperty `xml:"format"`
- Property []xmlProperty `xml:"property"`
-}
-
-type xmlConfig struct {
- XMLName xml.Name `xml:"logging"`
- Filter []xmlFilter `xml:"filter"`
-}
-
-// 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.NewDecoder(file).Decode(&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)
-}

0 comments on commit 931a9ef

Please sign in to comment.
Something went wrong with that request. Please try again.