diff --git a/client/ng-ss.js b/client/ng-ss.js new file mode 100644 index 0000000..2202eb4 --- /dev/null +++ b/client/ng-ss.js @@ -0,0 +1,63 @@ +(function(window){ 'use strict'; + if(!window.angular){ + throw 'angular not loaded'; + return; + } + + if(!window.SS){ + throw 'sacrificial socket not loaded'; + return; + } + + window.angular.module('sacrificial-socket', []) + .factory('ss', ['$window', '$rootScope', '$log', function($window, $rootScope, $log){ + function SSNG(url, opts){ + var self = this, + socket = new $window.SS(url, opts); + + self.onConnect = function(callback){ + callback = callback || socket.noop; + socket.onConnect(function(){ + var args = arguments; + $rootScope.$apply(function(){ + callback.apply(self, args); + }) + }); + }; + + self.onDisconnect = function(callback){ + callback = callback || socket.noop; + socket.onDisconnect(function(){ + var args = arguments; + $rootScope.$apply(function(){ + callback.apply(self, args); + }); + }); + }; + + self.on = function(eventName, callback){ + callback = callback || socket.noop; + socket.on(eventName, function(){ + var args = arguments; + $rootScope.$apply(function(){ + callback.apply(self, args); + }); + }); + }; + + self.off = function(eventName){ + return socket.off(eventName); + }; + + self.emit = function(eventName, data){ + return socket.emit(eventName, data); + }; + + self.close = function(){ + return socket.close(); + }; + } + + return function(url, opts){ return new SSNG(url, opts); }; + }]); +})(window); \ No newline at end of file diff --git a/client/sacrificial-socket.js b/client/sacrificial-socket.js index 3658587..fcc4e12 100644 --- a/client/sacrificial-socket.js +++ b/client/sacrificial-socket.js @@ -5,15 +5,40 @@ * @class SS * @constructor * @param {String} url - The url to the sacrificial-socket server endpoint. The url must conform to the websocket URI Scheme ("ws" or "wss") + * @param {Object} opts - connection options + * + * Default opts = { + * reconnectOpts: { + * enabled: true, + * replayOnConnect: true, + * intervalMS: 5000 + * } + * } + * */ - var SS = function(url){ + var SS = function(url, opts){ + opts = opts || {}; + var self = this, - ws = new WebSocket(url, 'sac-sock'), events = {}, + reconnectOpts = {enabled: true, replayOnConnect: true, intervalMS: 5000}, + reconnecting = false, + connectedOnce = false, headerStartCharCode = 1, headerStartChar = String.fromCharCode(headerStartCharCode), dataStartCharCode = 2, - dataStartChar = String.fromCharCode(dataStartCharCode); + dataStartChar = String.fromCharCode(dataStartCharCode), + ws = new WebSocket(url, 'sac-sock'); + + //we really only support reconnect options for now + if(typeof opts.reconnectOpts == 'object'){ + for(var i in opts.reconnectOpts){ + if(!opts.reconnectOpts.hasOwnProperty(i)) continue; + reconnectOpts[i] = opts.reconnectOpts[i]; + } + } + + self.noop = function(){ }; //sorry, only supporting arraybuffer at this time //maybe if there is demand for it, I'll add Blob support @@ -68,29 +93,74 @@ if(eventName.length === 0) return; //no event to dispatch if(typeof events[eventName] === 'undefined') return; - events[eventName]((headers.J) ? JSON.parse(data) : data); + events[eventName].call(self, (headers.J) ? JSON.parse(data) : data); }; + /** + * startReconnect is an internal function for reconnecting after an unexpected disconnect + * + * @function startReconnect + * + */ + function startReconnect(){ + setTimeout(function(){ + console.log('attempting reconnect'); + var newWS = new WebSocket(url, 'sac-sock'); + newWS.onmessage = ws.onmessage; + newWS.onclose = ws.onclose; + newWS.binaryType = ws.binaryType; + + //we need to run the initially set onConnect function on first successful connect, + //even if replayOnConnect is disabled. The server might not be available on first + //connection attempt. + if(reconnectOpts.replayOnConnect || !connectedOnce){ + newWS.onopen = ws.onopen; + } + ws = newWS; + if(!reconnectOpts.replayOnConnect && connectedOnce){ + self.onConnect(self.noop); + } + }, reconnectOpts.intervalMS); + } + /** * onConnect registers a callback to be run when the websocket connection is open. * * @method onConnect - * @param {Function} callback(SS) - The callback that will be executed when the websocket connection opens. + * @param {Function} callback(event) - The callback that will be executed when the websocket connection opens. * */ self.onConnect = function(callback){ - ws.onopen = function(){ callback(self); }; + ws.onopen = function(){ + connectedOnce = true; + var args = arguments; + callback.apply(self, args); + if(reconnecting){ + reconnecting = false; + } + }; }; + self.onConnect(self.noop); /** * onDisconnect registers a callback to be run when the websocket connection is closed. * * @method onDisconnect - * @param {Function} callback(SS) - The callback that will be executed when the websocket connection is closed. + * @param {Function} callback(event) - The callback that will be executed when the websocket connection is closed. */ self.onDisconnect = function(callback){ - ws.onclose = function(){ callback(self); }; + ws.onclose = function(){ + var args = arguments; + if(!reconnecting && connectedOnce){ + callback.apply(self, args); + } + if(reconnectOpts.enabled){ + reconnecting = true; + startReconnect(); + } + }; }; + self.onDisconnect(self.noop); /** * on registers an event to be called when the client receives an emit from the server for @@ -162,6 +232,7 @@ * @method close */ self.close = function(){ + reconnectOpts.enabled = false; //don't reconnect if close is called return ws.close(); }; }; diff --git a/examples/not-so-simple-examples/grpc-multihome/webroot/js/sacrificial-socket.js b/examples/not-so-simple-examples/grpc-multihome/webroot/js/sacrificial-socket.js index 3658587..fcc4e12 100644 --- a/examples/not-so-simple-examples/grpc-multihome/webroot/js/sacrificial-socket.js +++ b/examples/not-so-simple-examples/grpc-multihome/webroot/js/sacrificial-socket.js @@ -5,15 +5,40 @@ * @class SS * @constructor * @param {String} url - The url to the sacrificial-socket server endpoint. The url must conform to the websocket URI Scheme ("ws" or "wss") + * @param {Object} opts - connection options + * + * Default opts = { + * reconnectOpts: { + * enabled: true, + * replayOnConnect: true, + * intervalMS: 5000 + * } + * } + * */ - var SS = function(url){ + var SS = function(url, opts){ + opts = opts || {}; + var self = this, - ws = new WebSocket(url, 'sac-sock'), events = {}, + reconnectOpts = {enabled: true, replayOnConnect: true, intervalMS: 5000}, + reconnecting = false, + connectedOnce = false, headerStartCharCode = 1, headerStartChar = String.fromCharCode(headerStartCharCode), dataStartCharCode = 2, - dataStartChar = String.fromCharCode(dataStartCharCode); + dataStartChar = String.fromCharCode(dataStartCharCode), + ws = new WebSocket(url, 'sac-sock'); + + //we really only support reconnect options for now + if(typeof opts.reconnectOpts == 'object'){ + for(var i in opts.reconnectOpts){ + if(!opts.reconnectOpts.hasOwnProperty(i)) continue; + reconnectOpts[i] = opts.reconnectOpts[i]; + } + } + + self.noop = function(){ }; //sorry, only supporting arraybuffer at this time //maybe if there is demand for it, I'll add Blob support @@ -68,29 +93,74 @@ if(eventName.length === 0) return; //no event to dispatch if(typeof events[eventName] === 'undefined') return; - events[eventName]((headers.J) ? JSON.parse(data) : data); + events[eventName].call(self, (headers.J) ? JSON.parse(data) : data); }; + /** + * startReconnect is an internal function for reconnecting after an unexpected disconnect + * + * @function startReconnect + * + */ + function startReconnect(){ + setTimeout(function(){ + console.log('attempting reconnect'); + var newWS = new WebSocket(url, 'sac-sock'); + newWS.onmessage = ws.onmessage; + newWS.onclose = ws.onclose; + newWS.binaryType = ws.binaryType; + + //we need to run the initially set onConnect function on first successful connect, + //even if replayOnConnect is disabled. The server might not be available on first + //connection attempt. + if(reconnectOpts.replayOnConnect || !connectedOnce){ + newWS.onopen = ws.onopen; + } + ws = newWS; + if(!reconnectOpts.replayOnConnect && connectedOnce){ + self.onConnect(self.noop); + } + }, reconnectOpts.intervalMS); + } + /** * onConnect registers a callback to be run when the websocket connection is open. * * @method onConnect - * @param {Function} callback(SS) - The callback that will be executed when the websocket connection opens. + * @param {Function} callback(event) - The callback that will be executed when the websocket connection opens. * */ self.onConnect = function(callback){ - ws.onopen = function(){ callback(self); }; + ws.onopen = function(){ + connectedOnce = true; + var args = arguments; + callback.apply(self, args); + if(reconnecting){ + reconnecting = false; + } + }; }; + self.onConnect(self.noop); /** * onDisconnect registers a callback to be run when the websocket connection is closed. * * @method onDisconnect - * @param {Function} callback(SS) - The callback that will be executed when the websocket connection is closed. + * @param {Function} callback(event) - The callback that will be executed when the websocket connection is closed. */ self.onDisconnect = function(callback){ - ws.onclose = function(){ callback(self); }; + ws.onclose = function(){ + var args = arguments; + if(!reconnecting && connectedOnce){ + callback.apply(self, args); + } + if(reconnectOpts.enabled){ + reconnecting = true; + startReconnect(); + } + }; }; + self.onDisconnect(self.noop); /** * on registers an event to be called when the client receives an emit from the server for @@ -162,6 +232,7 @@ * @method close */ self.close = function(){ + reconnectOpts.enabled = false; //don't reconnect if close is called return ws.close(); }; }; diff --git a/examples/simple-examples/chat/webroot/js/sacrificial-socket.js b/examples/simple-examples/chat/webroot/js/sacrificial-socket.js index 3658587..fcc4e12 100644 --- a/examples/simple-examples/chat/webroot/js/sacrificial-socket.js +++ b/examples/simple-examples/chat/webroot/js/sacrificial-socket.js @@ -5,15 +5,40 @@ * @class SS * @constructor * @param {String} url - The url to the sacrificial-socket server endpoint. The url must conform to the websocket URI Scheme ("ws" or "wss") + * @param {Object} opts - connection options + * + * Default opts = { + * reconnectOpts: { + * enabled: true, + * replayOnConnect: true, + * intervalMS: 5000 + * } + * } + * */ - var SS = function(url){ + var SS = function(url, opts){ + opts = opts || {}; + var self = this, - ws = new WebSocket(url, 'sac-sock'), events = {}, + reconnectOpts = {enabled: true, replayOnConnect: true, intervalMS: 5000}, + reconnecting = false, + connectedOnce = false, headerStartCharCode = 1, headerStartChar = String.fromCharCode(headerStartCharCode), dataStartCharCode = 2, - dataStartChar = String.fromCharCode(dataStartCharCode); + dataStartChar = String.fromCharCode(dataStartCharCode), + ws = new WebSocket(url, 'sac-sock'); + + //we really only support reconnect options for now + if(typeof opts.reconnectOpts == 'object'){ + for(var i in opts.reconnectOpts){ + if(!opts.reconnectOpts.hasOwnProperty(i)) continue; + reconnectOpts[i] = opts.reconnectOpts[i]; + } + } + + self.noop = function(){ }; //sorry, only supporting arraybuffer at this time //maybe if there is demand for it, I'll add Blob support @@ -68,29 +93,74 @@ if(eventName.length === 0) return; //no event to dispatch if(typeof events[eventName] === 'undefined') return; - events[eventName]((headers.J) ? JSON.parse(data) : data); + events[eventName].call(self, (headers.J) ? JSON.parse(data) : data); }; + /** + * startReconnect is an internal function for reconnecting after an unexpected disconnect + * + * @function startReconnect + * + */ + function startReconnect(){ + setTimeout(function(){ + console.log('attempting reconnect'); + var newWS = new WebSocket(url, 'sac-sock'); + newWS.onmessage = ws.onmessage; + newWS.onclose = ws.onclose; + newWS.binaryType = ws.binaryType; + + //we need to run the initially set onConnect function on first successful connect, + //even if replayOnConnect is disabled. The server might not be available on first + //connection attempt. + if(reconnectOpts.replayOnConnect || !connectedOnce){ + newWS.onopen = ws.onopen; + } + ws = newWS; + if(!reconnectOpts.replayOnConnect && connectedOnce){ + self.onConnect(self.noop); + } + }, reconnectOpts.intervalMS); + } + /** * onConnect registers a callback to be run when the websocket connection is open. * * @method onConnect - * @param {Function} callback(SS) - The callback that will be executed when the websocket connection opens. + * @param {Function} callback(event) - The callback that will be executed when the websocket connection opens. * */ self.onConnect = function(callback){ - ws.onopen = function(){ callback(self); }; + ws.onopen = function(){ + connectedOnce = true; + var args = arguments; + callback.apply(self, args); + if(reconnecting){ + reconnecting = false; + } + }; }; + self.onConnect(self.noop); /** * onDisconnect registers a callback to be run when the websocket connection is closed. * * @method onDisconnect - * @param {Function} callback(SS) - The callback that will be executed when the websocket connection is closed. + * @param {Function} callback(event) - The callback that will be executed when the websocket connection is closed. */ self.onDisconnect = function(callback){ - ws.onclose = function(){ callback(self); }; + ws.onclose = function(){ + var args = arguments; + if(!reconnecting && connectedOnce){ + callback.apply(self, args); + } + if(reconnectOpts.enabled){ + reconnecting = true; + startReconnect(); + } + }; }; + self.onDisconnect(self.noop); /** * on registers an event to be called when the client receives an emit from the server for @@ -162,6 +232,7 @@ * @method close */ self.close = function(){ + reconnectOpts.enabled = false; //don't reconnect if close is called return ws.close(); }; }; diff --git a/log/json.go b/log/json.go new file mode 100644 index 0000000..e559fc4 --- /dev/null +++ b/log/json.go @@ -0,0 +1,152 @@ +package log + +import ( + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "strings" + "time" +) + +type jLog struct { + Timestamp time.Time `json:"ts"` + File string `json:"file"` + Line int `json:"line"` + Level string `json:"level"` + Fatal bool `json:"fatal"` + Msg string `json:"msg"` +} + +func (jl *jLog) Marshal() []byte { + data, err := json.Marshal(jl) + if err != nil { + return []byte(fmt.Sprintf(`{"Error":"%s", "Line": %d, "File":"%s"}`, err, jl.Line, jl.File)) + } + return data +} + +type jsonLogFmt struct { + out io.Writer + logLevel int + logLevelAllowed int +} + +func (j *jsonLogFmt) canLog() bool { + return (j.logLevel & j.logLevelAllowed) == j.logLevelAllowed +} + +func (j *jsonLogFmt) writeJLog(msg string, fatal bool) { + jl := newJLog(msg, LogLevelMap[j.logLevelAllowed], fatal, 3) + data := jl.Marshal() + j.out.Write(append(data, '\n')) +} + +func newJLog(msg, level string, fatal bool, callDepth int) *jLog { + jl := &jLog{ + Timestamp: time.Now(), + Level: level, + Fatal: fatal, + Msg: msg, + } + _, file, line, ok := runtime.Caller(callDepth) + + jl.File = file + jl.Line = line + + if !ok { + jl.File = "???" + jl.Line = -1 + } + + return jl +} + +func (j *jsonLogFmt) Print(v ...interface{}) { + if !j.canLog() { + return + } + msg, fatal := fmt.Sprint(v...), false + msg = strings.TrimRight(msg, "\n") + j.writeJLog(msg, fatal) +} + +func (j *jsonLogFmt) Printf(format string, v ...interface{}) { + if !j.canLog() { + return + } + + msg, fatal := fmt.Sprintf(format, v...), false + msg = strings.TrimRight(msg, "\n") + + j.writeJLog(msg, fatal) +} + +func (j *jsonLogFmt) Println(v ...interface{}) { + if !j.canLog() { + return + } + + msg, fatal := fmt.Sprintln(v...), false + msg = strings.TrimRight(msg, "\n") + + j.writeJLog(msg, fatal) +} + +func (j *jsonLogFmt) Fatal(v ...interface{}) { + msg, fatal := fmt.Sprint(v...), true + msg = strings.TrimRight(msg, "\n") + + j.writeJLog(msg, fatal) + os.Exit(1) +} + +func (j *jsonLogFmt) Fatalf(format string, v ...interface{}) { + msg, fatal := fmt.Sprintf(format, v...), true + msg = strings.TrimRight(msg, "\n") + + j.writeJLog(msg, fatal) + os.Exit(1) +} + +func (j *jsonLogFmt) Fatalln(v ...interface{}) { + msg, fatal := fmt.Sprintln(v...), true + msg = strings.TrimRight(msg, "\n") + + j.writeJLog(msg, fatal) + os.Exit(1) +} + +func NewJSONLogger(out io.Writer, logLevel int) *Logger { + info := &jsonLogFmt{ + out: out, + logLevel: logLevel, + logLevelAllowed: LogLevelINFO, + } + + warn := &jsonLogFmt{ + out: out, + logLevel: logLevel, + logLevelAllowed: LogLevelWARN, + } + + err := &jsonLogFmt{ + out: out, + logLevel: logLevel, + logLevelAllowed: LogLevelERR, + } + + debug := &jsonLogFmt{ + out: out, + logLevel: logLevel, + logLevelAllowed: LogLevelDEBUG, + } + + return &Logger{ + Info: info, + Warn: warn, + Err: err, + Debug: debug, + } +} diff --git a/log/log.go b/log/log.go index a967b2b..cfa28bb 100644 --- a/log/log.go +++ b/log/log.go @@ -1,19 +1,62 @@ -/* -Package log is used all throughout Sacrificial Socket for logging info and error messages -*/ package log import ( - l "log" "os" ) -var Err = l.New(os.Stderr, "ERROR: ", l.Ldate|l.Ltime|l.Lshortfile) -var Info = l.New(os.Stdout, "INFO: ", l.Ldate|l.Ltime) +const ( + LogLevelINFO = 1 << iota + LogLevelWARN + LogLevelERR + LogLevelDEBUG -func CheckFatal(err error) { - if err != nil { - Err.Output(2, err.Error()) - os.Exit(1) + LogLevelNone = 0 + LogLevelStd = LogLevelINFO | LogLevelWARN | LogLevelERR + LogLevelWarn = LogLevelWARN | LogLevelERR + LogLevelErr = LogLevelERR + LogLevelDbg = LogLevelStd | LogLevelDEBUG +) + +var ( + defaultLogger = NewColorLogger(os.Stdout, LogLevelDbg) + + Info = defaultLogger.Info + Warn = defaultLogger.Warn + Err = defaultLogger.Err + Debug = defaultLogger.Debug + + LogLevelMap = map[int]string{ + LogLevelINFO: "INFO", + LogLevelWARN: "WARN", + LogLevelERR: "ERROR", + LogLevelDEBUG: "DEBUG", } +) + +//this is pretty much the only thing that isn't +//safe to run in multiple go routines, you should +//call SetDefaultLogger at the beginning of your main function +func SetDefaultLogger(l *Logger) { + defaultLogger = l + Info = defaultLogger.Info + Warn = defaultLogger.Warn + Err = defaultLogger.Err + Debug = defaultLogger.Debug +} + +type LogFormatter interface { + Fatal(v ...interface{}) + Fatalf(format string, v ...interface{}) + Fatalln(v ...interface{}) + + Print(v ...interface{}) + Printf(format string, v ...interface{}) + Println(v ...interface{}) +} + +type Logger struct { + Info LogFormatter + Warn LogFormatter + Err LogFormatter + Debug LogFormatter } diff --git a/log/std-out.go b/log/std-out.go new file mode 100644 index 0000000..e6fb78c --- /dev/null +++ b/log/std-out.go @@ -0,0 +1,177 @@ +package log + +import ( + "bytes" + "fmt" + "io" + goLog "log" + "os" +) + +const ( + colorLogDepth = 4 +) + +var ( + ansiColorPallet = map[string][]byte{ + "none": []byte("\x1b[0m"), + "black": []byte("\x1b[0;30m"), + "red": []byte("\x1b[0;31m"), + "green": []byte("\x1b[0;32m"), + "orange": []byte("\x1b[0;33m"), + "blue": []byte("\x1b[0;34m"), + "purple": []byte("\x1b[0;35m"), + "cyan": []byte("\x1b[0;36m"), + "light-gray": []byte("\x1b[0;37m"), + "dark-gray": []byte("\x1b[1;30m"), + "light-red": []byte("\x1b[1;31m"), + "light-green": []byte("\x1b[1;32m"), + "yellow": []byte("\x1b[1;33m"), + "light-blue": []byte("\x1b[1;34m"), + "light-purple": []byte("\x1b[1;35m"), + "light-cyan": []byte("\x1b[1;36m"), + "white": []byte("\x1b[1;37m"), + } + + logColors = map[int][]byte{ + LogLevelINFO: ansiColorPallet["white"], + LogLevelWARN: ansiColorPallet["orange"], + LogLevelERR: ansiColorPallet["red"], + LogLevelDEBUG: ansiColorPallet["light-blue"], + } +) + +func NewColorLogger(out io.Writer, logLevel int) *Logger { + return newLogger(out, logLevel, true) +} + +func NewLogger(out io.Writer, logLevel int) *Logger { + return newLogger(out, logLevel, false) +} + +func newLogger(out io.Writer, logLevel int, color bool) *Logger { + logger := &Logger{ + Info: newColorLogFmt(out, logLevel, LogLevelINFO, color), + Warn: newColorLogFmt(out, logLevel, LogLevelWARN, color), + Err: newColorLogFmt(out, logLevel, LogLevelERR, color), + Debug: newColorLogFmt(out, logLevel, LogLevelDEBUG, color), + } + + return logger +} + +type colorLogFmt struct { + out io.Writer + logger *goLog.Logger + logLevel int + fmtLevel int + color bool +} + +func newColorLogFmt(out io.Writer, logLevel, fmtLevel int, color bool) *colorLogFmt { + clf := &colorLogFmt{ + logLevel: logLevel, + fmtLevel: fmtLevel, + out: out, + color: color, + } + lFlags := goLog.LstdFlags + + if fmtLevel != LogLevelINFO { + lFlags |= goLog.Lshortfile + } + clf.logger = goLog.New(out, padStr(LogLevelMap[fmtLevel], 6), lFlags) + + return clf +} + +func (lf *colorLogFmt) canLog() bool { + return (lf.logLevel & lf.fmtLevel) == lf.fmtLevel +} + +func (lf *colorLogFmt) doOutput(pType, format string, v ...interface{}) { + switch pType { + case "Print", "Fatal": + lf.logger.Output(colorLogDepth, fmt.Sprint(v...)) + case "Printf", "Fatalf": + lf.logger.Output(colorLogDepth, fmt.Sprintf(format, v...)) + case "Println", "Fatalln": + lf.logger.Output(colorLogDepth, fmt.Sprintln(v...)) + default: + lf.logger.Output(colorLogDepth, fmt.Sprint(v...)) + } +} + +func (lf *colorLogFmt) doPrint(pType, format string, v ...interface{}) { + switch pType { + case "Fatal", "Fatalf", "Fatalln": + lf.setErrColor() + lf.doOutput(pType, format, v...) + lf.dropColor() + os.Exit(1) + case "Print", "Printf", "Println": + if !lf.canLog() { + return + } + lf.setFmtColor() + lf.doOutput(pType, format, v...) + lf.dropColor() + } +} + +//Fatal logs don't care what the logLevel is. They print +//and exit with a satus of 1 +func (lf *colorLogFmt) Fatal(v ...interface{}) { + lf.doPrint("Fatal", "", v...) +} + +func (lf *colorLogFmt) Fatalf(format string, v ...interface{}) { + lf.doPrint("Fatalf", format, v...) +} + +func (lf *colorLogFmt) Fatalln(v ...interface{}) { + lf.doPrint("Fatalln", "", v...) +} + +func (lf *colorLogFmt) Print(v ...interface{}) { + lf.doPrint("Print", "", v...) +} + +func (lf *colorLogFmt) Printf(format string, v ...interface{}) { + lf.doPrint("Printf", format, v...) +} + +func (lf *colorLogFmt) Println(v ...interface{}) { + lf.doPrint("Println", "", v...) +} + +func (lf *colorLogFmt) setColor(color []byte) { + if !lf.color { + return + } + + lf.out.Write(color) +} + +func (lf *colorLogFmt) setErrColor() { + lf.setColor(logColors[LogLevelERR]) +} + +func (lf *colorLogFmt) setFmtColor() { + lf.setColor(logColors[lf.fmtLevel]) +} + +func (lf *colorLogFmt) dropColor() { + lf.setColor(ansiColorPallet["none"]) +} + +func padStr(s string, length int) string { + b := bytes.NewBuffer(nil) + b.WriteString(s) + for { + if b.Len() >= length { + return b.String() + } + b.WriteString(" ") + } +}