├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── logstreamer.go └── logstreamer_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | 25 | .DS_Store 26 | .vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.0 4 | - 1.1 5 | - 1.3 6 | - 1.4 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Kevin van Zonneveld 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | go test -v ./... 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logstreamer [![Build Status][BuildStatusIMGURL]][BuildStatusURL] 2 | =============== 3 | 4 | [BuildStatusIMGURL]: https://secure.travis-ci.org/kvz/logstreamer.png?branch=master 5 | [BuildStatusURL]: //travis-ci.org/kvz/logstreamer "Build Status" 6 | 7 | Prefixes streams (e.g. stdout or stderr) in Go. 8 | 9 | If you are executing a lot of (remote) commands, you may want to indent all of their 10 | output, prefix the loglines with hostnames, or mark anything that was thrown to stderr 11 | red, so you can spot errors more easily. 12 | 13 | For this purpose, Logstreamer was written. 14 | 15 | You pass 3 arguments to `NewLogstreamer()`: 16 | 17 | - Your `*log.Logger` 18 | - Your desired prefix (`"stdout"` and `"stderr"` prefixed have special meaning) 19 | - If the lines should be recorded `true` or `false`. This is useful if you want to retrieve any errors. 20 | 21 | This returns an interface that you can point `exec.Command`'s `cmd.Stderr` and `cmd.Stdout` to. 22 | All bytes that are written to it are split by newline and then prefixed to your specification. 23 | 24 | **Don't forget to call `Flush()` or `Close()` if the last line of the log 25 | might not end with a newline character!** 26 | 27 | A typical usage pattern looks like this: 28 | 29 | ```go 30 | // Create a logger (your app probably already has one) 31 | logger := log.New(os.Stdout, "--> ", log.Ldate|log.Ltime) 32 | 33 | // Setup a streamer that we'll pipe cmd.Stdout to 34 | logStreamerOut := NewLogstreamer(logger, "stdout", false) 35 | defer logStreamerOut.Close() 36 | // Setup a streamer that we'll pipe cmd.Stderr to. 37 | // We want to record/buffer anything that's written to this (3rd argument true) 38 | logStreamerErr := NewLogstreamer(logger, "stderr", true) 39 | defer logStreamerErr.Close() 40 | 41 | // Execute something that succeeds 42 | cmd := exec.Command( 43 | "ls", 44 | "-al", 45 | ) 46 | cmd.Stderr = logStreamerErr 47 | cmd.Stdout = logStreamerOut 48 | 49 | // Reset any error we recorded 50 | logStreamerErr.FlushRecord() 51 | 52 | // Execute command 53 | err := cmd.Start() 54 | ``` 55 | 56 | ## Test 57 | 58 | ```bash 59 | $ cd src/pkg/logstreamer/ 60 | $ go test 61 | ``` 62 | 63 | Here I issue two local commands, `ls -al` and `ls nonexisting`: 64 | 65 | ![screen shot 2013-07-02 at 2 48 33 pm](https://f.cloud.github.com/assets/26752/736371/16177cf0-e316-11e2-8dc6-320f52f71442.png) 66 | 67 | Over at [Transloadit](http://transloadit.com) we use it for streaming remote commands. 68 | Servers stream command output over SSH back to me, and every line is prefixed with a date, their hostname & marked red in case they 69 | wrote to stderr. 70 | 71 | ## License 72 | 73 | This project is licensed under the MIT license, see `LICENSE.txt`. 74 | -------------------------------------------------------------------------------- /logstreamer.go: -------------------------------------------------------------------------------- 1 | package logstreamer 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type Logstreamer struct { 12 | Logger *log.Logger 13 | buf *bytes.Buffer 14 | // If prefix == stdout, colors green 15 | // If prefix == stderr, colors red 16 | // Else, prefix is taken as-is, and prepended to anything 17 | // you throw at Write() 18 | prefix string 19 | // if true, saves output in memory 20 | record bool 21 | persist string 22 | 23 | // Adds color to stdout & stderr if terminal supports it 24 | colorOkay string 25 | colorFail string 26 | colorReset string 27 | } 28 | 29 | func NewLogstreamerForWriter(prefix string, writer io.Writer) *Logstreamer { 30 | logger := log.New(writer, prefix, 0) 31 | return NewLogstreamer(logger, "", false) 32 | } 33 | 34 | func NewLogstreamerForStdout(prefix string) *Logstreamer { 35 | // logger := log.New(os.Stdout, prefix, log.Ldate|log.Ltime) 36 | logger := log.New(os.Stdout, prefix, 0) 37 | return NewLogstreamer(logger, "", false) 38 | } 39 | 40 | func NewLogstreamerForStderr(prefix string) *Logstreamer { 41 | logger := log.New(os.Stderr, prefix, 0) 42 | return NewLogstreamer(logger, "", false) 43 | } 44 | 45 | func NewLogstreamer(logger *log.Logger, prefix string, record bool) *Logstreamer { 46 | streamer := &Logstreamer{ 47 | Logger: logger, 48 | buf: bytes.NewBuffer([]byte("")), 49 | prefix: prefix, 50 | record: record, 51 | persist: "", 52 | colorOkay: "", 53 | colorFail: "", 54 | colorReset: "", 55 | } 56 | 57 | if strings.HasPrefix(os.Getenv("TERM"), "xterm") { 58 | streamer.colorOkay = "\x1b[32m" 59 | streamer.colorFail = "\x1b[31m" 60 | streamer.colorReset = "\x1b[0m" 61 | } 62 | 63 | return streamer 64 | } 65 | 66 | func (l *Logstreamer) Write(p []byte) (n int, err error) { 67 | if n, err = l.buf.Write(p); err != nil { 68 | return 69 | } 70 | 71 | err = l.OutputLines() 72 | return 73 | } 74 | 75 | func (l *Logstreamer) Close() error { 76 | if err := l.Flush(); err != nil { 77 | return err 78 | } 79 | l.buf = bytes.NewBuffer([]byte("")) 80 | return nil 81 | } 82 | 83 | func (l *Logstreamer) Flush() error { 84 | p := make([]byte, l.buf.Len()) 85 | if _, err := l.buf.Read(p); err != nil { 86 | return err 87 | } 88 | 89 | l.out(string(p)) 90 | return nil 91 | } 92 | 93 | func (l *Logstreamer) OutputLines() error { 94 | for { 95 | line, err := l.buf.ReadString('\n') 96 | 97 | if len(line) > 0 { 98 | if strings.HasSuffix(line, "\n") { 99 | l.out(line) 100 | } else { 101 | // put back into buffer, it's not a complete line yet 102 | // Close() or Flush() have to be used to flush out 103 | // the last remaining line if it does not end with a newline 104 | if _, err := l.buf.WriteString(line); err != nil { 105 | return err 106 | } 107 | } 108 | } 109 | 110 | if err == io.EOF { 111 | break 112 | } 113 | 114 | if err != nil { 115 | return err 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (l *Logstreamer) FlushRecord() string { 123 | buffer := l.persist 124 | l.persist = "" 125 | return buffer 126 | } 127 | 128 | func (l *Logstreamer) out(str string) { 129 | if len(str) < 1 { 130 | return 131 | } 132 | 133 | if l.record == true { 134 | l.persist = l.persist + str 135 | } 136 | 137 | if l.prefix == "stdout" { 138 | str = l.colorOkay + l.prefix + l.colorReset + " " + str 139 | } else if l.prefix == "stderr" { 140 | str = l.colorFail + l.prefix + l.colorReset + " " + str 141 | } else { 142 | str = l.prefix + str 143 | } 144 | 145 | l.Logger.Print(str) 146 | } 147 | -------------------------------------------------------------------------------- /logstreamer_test.go: -------------------------------------------------------------------------------- 1 | package logstreamer 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestLogstreamerOk(t *testing.T) { 15 | // Create a logger (your app probably already has one) 16 | logger := log.New(os.Stdout, "--> ", log.Ldate|log.Ltime) 17 | 18 | // Setup a streamer that we'll pipe cmd.Stdout to 19 | logStreamerOut := NewLogstreamer(logger, "stdout", false) 20 | defer logStreamerOut.Close() 21 | // Setup a streamer that we'll pipe cmd.Stderr to. 22 | // We want to record/buffer anything that's written to this (3rd argument true) 23 | logStreamerErr := NewLogstreamer(logger, "stderr", true) 24 | defer logStreamerErr.Close() 25 | 26 | // Execute something that succeeds 27 | cmd := exec.Command( 28 | "ls", 29 | "-al", 30 | ) 31 | cmd.Stderr = logStreamerErr 32 | cmd.Stdout = logStreamerOut 33 | 34 | // Reset any error we recorded 35 | logStreamerErr.FlushRecord() 36 | 37 | // Execute command 38 | err := cmd.Start() 39 | 40 | // Failed to spawn? 41 | if err != nil { 42 | t.Fatal("ERROR could not spawn command.", err.Error()) 43 | } 44 | 45 | // Failed to execute? 46 | err = cmd.Wait() 47 | if err != nil { 48 | t.Fatal("ERROR command finished with error. ", err.Error(), logStreamerErr.FlushRecord()) 49 | } 50 | } 51 | 52 | func TestLogstreamerErr(t *testing.T) { 53 | // Create a logger (your app probably already has one) 54 | logger := log.New(os.Stdout, "--> ", log.Ldate|log.Ltime) 55 | 56 | // Setup a streamer that we'll pipe cmd.Stdout to 57 | logStreamerOut := NewLogstreamer(logger, "stdout", false) 58 | defer logStreamerOut.Close() 59 | // Setup a streamer that we'll pipe cmd.Stderr to. 60 | // We want to record/buffer anything that's written to this (3rd argument true) 61 | logStreamerErr := NewLogstreamer(logger, "stderr", true) 62 | defer logStreamerErr.Close() 63 | 64 | // Execute something that succeeds 65 | cmd := exec.Command( 66 | "ls", 67 | "nonexisting", 68 | ) 69 | cmd.Stderr = logStreamerErr 70 | cmd.Stdout = logStreamerOut 71 | 72 | // Reset any error we recorded 73 | logStreamerErr.FlushRecord() 74 | 75 | // Execute command 76 | err := cmd.Start() 77 | 78 | // Failed to spawn? 79 | if err != nil { 80 | logger.Print("ERROR could not spawn command. ") 81 | } 82 | 83 | // Failed to execute? 84 | err = cmd.Wait() 85 | if err != nil { 86 | fmt.Printf("Good. command finished with %s. %s. \n", err.Error(), logStreamerErr.FlushRecord()) 87 | } else { 88 | t.Fatal("This command should have failed") 89 | } 90 | } 91 | 92 | func TestLogstreamerFlush(t *testing.T) { 93 | const text = "Text without newline" 94 | 95 | var buffer bytes.Buffer 96 | byteWriter := bufio.NewWriter(&buffer) 97 | 98 | logger := log.New(byteWriter, "", 0) 99 | logStreamerOut := NewLogstreamer(logger, "", false) 100 | defer logStreamerOut.Close() 101 | 102 | logStreamerOut.Write([]byte(text)) 103 | logStreamerOut.Flush() 104 | byteWriter.Flush() 105 | 106 | s := strings.TrimSpace(string(buffer.Bytes())) 107 | 108 | if s != text { 109 | t.Fatalf("Expected '%s', got '%s'.", text, s) 110 | } 111 | } 112 | --------------------------------------------------------------------------------