Commits · ampify
tav ·
9c9660b3634f47a03c388436ab56444505a78d09· ampify · githubRestructured amp/logging into individual files.
Changes
| src/amp/logging/Makefile | ||
| ... | ... | @@ -5,6 +5,9 @@ include $(GOROOT)/src/Make.inc |
| 5 | 5 | |
| 6 | 6 | TARG=amp/logging |
| 7 | 7 | GOFILES=\ |
| 8 | + console.go\ | |
| 9 | + file.go\ | |
| 8 | 10 | logging.go\ |
| 11 | + network.go\ | |
| 9 | 12 | |
| 10 | 13 | include $(GOROOT)/src/Make.pkg |
| src/amp/logging/console.go | ||
| ... | ... | @@ -0,0 +1,114 @@ |
| 1 | +// Public Domain (-) 2010-2011 The Ampify Authors. | |
| 2 | +// See the Ampify UNLICENSE file for details. | |
| 3 | + | |
| 4 | +package logging | |
| 5 | + | |
| 6 | +import ( | |
| 7 | + "amp/encoding" | |
| 8 | + "fmt" | |
| 9 | + "os" | |
| 10 | +) | |
| 11 | + | |
| 12 | +var ( | |
| 13 | + ConsoleFilters = make([]Filter, 0) | |
| 14 | + checker = make(chan int, 1) | |
| 15 | + waiter = make(chan int, 1) | |
| 16 | + waitable = false | |
| 17 | +) | |
| 18 | + | |
| 19 | +type ConsoleLogger struct { | |
| 20 | + receiver chan *Record | |
| 21 | +} | |
| 22 | + | |
| 23 | +func (logger *ConsoleLogger) log() { | |
| 24 | + | |
| 25 | + var record *Record | |
| 26 | + var file *os.File | |
| 27 | + var items []interface{} | |
| 28 | + var status string | |
| 29 | + var write bool | |
| 30 | + | |
| 31 | + for { | |
| 32 | + select { | |
| 33 | + case record = <-logger.receiver: | |
| 34 | + items = record.Items | |
| 35 | + write = true | |
| 36 | + for _, filter := range ConsoleFilters { | |
| 37 | + write, data := filter(record) | |
| 38 | + if !write { | |
| 39 | + break | |
| 40 | + } | |
| 41 | + if data != nil { | |
| 42 | + items = data | |
| 43 | + break | |
| 44 | + } | |
| 45 | + } | |
| 46 | + if !write { | |
| 47 | + continue | |
| 48 | + } | |
| 49 | + argLength := len(items) | |
| 50 | + if record.Error { | |
| 51 | + file = os.Stderr | |
| 52 | + } else { | |
| 53 | + file = os.Stdout | |
| 54 | + } | |
| 55 | + if record.Error { | |
| 56 | + status = "ERR" | |
| 57 | + } else { | |
| 58 | + status = "INF" | |
| 59 | + } | |
| 60 | + fmt.Fprintf(file, "%s [%s-%s-%s %s:%s:%s]", status, | |
| 61 | + encoding.PadInt64(UTC.Year, 4), encoding.PadInt(UTC.Month, 2), | |
| 62 | + encoding.PadInt(UTC.Day, 2), encoding.PadInt(UTC.Hour, 2), | |
| 63 | + encoding.PadInt(UTC.Minute, 2), encoding.PadInt(UTC.Second, 2)) | |
| 64 | + for i := 0; i < argLength; i++ { | |
| 65 | + fmt.Fprintf(file, " %v", items[i]) | |
| 66 | + } | |
| 67 | + file.Write([]byte("\n")) | |
| 68 | + case <-checker: | |
| 69 | + if len(logger.receiver) > 0 { | |
| 70 | + checker <- 1 | |
| 71 | + continue | |
| 72 | + } | |
| 73 | + waiter <- 1 | |
| 74 | + } | |
| 75 | + } | |
| 76 | + | |
| 77 | +} | |
| 78 | + | |
| 79 | +func AddConsoleLogger() { | |
| 80 | + waitable = true | |
| 81 | + console := &ConsoleLogger{ | |
| 82 | + receiver: make(chan *Record, 100), | |
| 83 | + } | |
| 84 | + go console.log() | |
| 85 | + AddReceiver(console.receiver, MixedLog) | |
| 86 | + ConsoleFilters = append(ConsoleFilters, defaultConsoleFilter) | |
| 87 | +} | |
| 88 | + | |
| 89 | +func defaultConsoleFilter(record *Record) (write bool, data []interface{}) { | |
| 90 | + if len(record.Items) > 0 { | |
| 91 | + meta := record.Items[0] | |
| 92 | + switch meta.(type) { | |
| 93 | + case string: | |
| 94 | + if meta.(string) == "m" { | |
| 95 | + return true, record.Items[1:] | |
| 96 | + } | |
| 97 | + } | |
| 98 | + } | |
| 99 | + return true, nil | |
| 100 | +} | |
| 101 | + | |
| 102 | +func AddConsoleFilter(filter Filter) { | |
| 103 | + if filter == nil { | |
| 104 | + return | |
| 105 | + } | |
| 106 | + ConsoleFilters = append(ConsoleFilters, filter) | |
| 107 | +} | |
| 108 | + | |
| 109 | +func Wait() { | |
| 110 | + if waitable { | |
| 111 | + checker <- 1 | |
| 112 | + <-waiter | |
| 113 | + } | |
| 114 | +} | |
| src/amp/logging/file.go | ||
| ... | ... | @@ -0,0 +1,149 @@ |
| 1 | +// Public Domain (-) 2010-2011 The Ampify Authors. | |
| 2 | +// See the Ampify UNLICENSE file for details. | |
| 3 | + | |
| 4 | +package logging | |
| 5 | + | |
| 6 | +import ( | |
| 7 | + "fmt" | |
| 8 | + "io/ioutil" | |
| 9 | + "os" | |
| 10 | + "path" | |
| 11 | + "strconv" | |
| 12 | + "time" | |
| 13 | +) | |
| 14 | + | |
| 15 | +const ( | |
| 16 | + endOfRecord = '\n' | |
| 17 | + terminalByte = '\xff' | |
| 18 | +) | |
| 19 | + | |
| 20 | +var endOfLogRecord = []byte{'\xff', '\n'} | |
| 21 | + | |
| 22 | +type FileLogger struct { | |
| 23 | + name string | |
| 24 | + directory string | |
| 25 | + rotate int | |
| 26 | + file *os.File | |
| 27 | + filename string | |
| 28 | + receiver chan *Record | |
| 29 | +} | |
| 30 | + | |
| 31 | +func (logger *FileLogger) log() { | |
| 32 | + | |
| 33 | + rotateSignal := make(chan string) | |
| 34 | + if logger.rotate > 0 { | |
| 35 | + go signalRotation(logger, rotateSignal) | |
| 36 | + } | |
| 37 | + | |
| 38 | + var record *Record | |
| 39 | + var filename string | |
| 40 | + | |
| 41 | + for { | |
| 42 | + select { | |
| 43 | + case filename = <-rotateSignal: | |
| 44 | + if filename != logger.filename { | |
| 45 | + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666) | |
| 46 | + if err == nil { | |
| 47 | + logger.file.Close() | |
| 48 | + logger.file = file | |
| 49 | + logger.filename = filename | |
| 50 | + } else { | |
| 51 | + fmt.Fprintf(os.Stderr, "ERROR: Couldn't rotate log: %s\n", err) | |
| 52 | + } | |
| 53 | + } | |
| 54 | + case record = <-logger.receiver: | |
| 55 | + argLength := len(record.Items) | |
| 56 | + if record.Error { | |
| 57 | + logger.file.Write([]byte{'E'}) | |
| 58 | + } else { | |
| 59 | + logger.file.Write([]byte{'I'}) | |
| 60 | + } | |
| 61 | + fmt.Fprintf(logger.file, "%v", Now) | |
| 62 | + for i := 0; i < argLength; i++ { | |
| 63 | + message := strconv.Quote(fmt.Sprint(record.Items[i])) | |
| 64 | + fmt.Fprintf(logger.file, "\xfe%s", message[0:len(message)-1]) | |
| 65 | + } | |
| 66 | + logger.file.Write(endOfLogRecord) | |
| 67 | + } | |
| 68 | + } | |
| 69 | + | |
| 70 | +} | |
| 71 | + | |
| 72 | +func (logger *FileLogger) GetFilename(timestamp *time.Time) string { | |
| 73 | + var suffix string | |
| 74 | + switch logger.rotate { | |
| 75 | + case RotateNever: | |
| 76 | + suffix = "" | |
| 77 | + case RotateDaily: | |
| 78 | + suffix = timestamp.Format(".2006-01-02") | |
| 79 | + case RotateHourly: | |
| 80 | + suffix = timestamp.Format(".2006-01-02.03") | |
| 81 | + case RotateTest: | |
| 82 | + suffix = timestamp.Format(".2006-01-02.03-04-05") | |
| 83 | + } | |
| 84 | + filename := logger.name + suffix + ".log" | |
| 85 | + return path.Join(logger.directory, filename) | |
| 86 | +} | |
| 87 | + | |
| 88 | +func FixUpLog(filename string) (pointer int) { | |
| 89 | + content, err := ioutil.ReadFile(filename) | |
| 90 | + if err != nil { | |
| 91 | + return | |
| 92 | + } | |
| 93 | + var seenTerminal bool | |
| 94 | + for idx, char := range content { | |
| 95 | + if char == terminalByte { | |
| 96 | + seenTerminal = true | |
| 97 | + } else if seenTerminal { | |
| 98 | + if char == endOfRecord { | |
| 99 | + pointer = idx + 1 | |
| 100 | + } | |
| 101 | + seenTerminal = false | |
| 102 | + } | |
| 103 | + } | |
| 104 | + os.Truncate(filename, int64(pointer)) | |
| 105 | + return pointer | |
| 106 | +} | |
| 107 | + | |
| 108 | +func signalRotation(logger *FileLogger, signalChannel chan string) { | |
| 109 | + var interval int64 | |
| 110 | + var filename string | |
| 111 | + switch logger.rotate { | |
| 112 | + case RotateDaily: | |
| 113 | + interval = 86400000000000 | |
| 114 | + case RotateHourly: | |
| 115 | + interval = 3600000000000 | |
| 116 | + case RotateTest: | |
| 117 | + interval = 3000000000 | |
| 118 | + } | |
| 119 | + for { | |
| 120 | + filename = logger.GetFilename(UTC) | |
| 121 | + if filename != logger.filename { | |
| 122 | + signalChannel <- filename | |
| 123 | + } | |
| 124 | + <-time.After(interval) | |
| 125 | + } | |
| 126 | +} | |
| 127 | + | |
| 128 | +func AddFileLogger(name string, directory string, rotate int, logType int) (logger *FileLogger, err os.Error) { | |
| 129 | + logger = &FileLogger{ | |
| 130 | + name: name, | |
| 131 | + directory: directory, | |
| 132 | + rotate: rotate, | |
| 133 | + receiver: make(chan *Record, 100), | |
| 134 | + } | |
| 135 | + filename := logger.GetFilename(UTC) | |
| 136 | + pointer := FixUpLog(filename) | |
| 137 | + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666) | |
| 138 | + if err != nil { | |
| 139 | + return logger, err | |
| 140 | + } | |
| 141 | + if pointer > 0 { | |
| 142 | + file.Seek(int64(pointer), 0) | |
| 143 | + } | |
| 144 | + logger.file = file | |
| 145 | + logger.filename = filename | |
| 146 | + go logger.log() | |
| 147 | + AddReceiver(logger.receiver, logType) | |
| 148 | + return logger, nil | |
| 149 | +} | |
| src/amp/logging/logging.go | ||
| ... | ... | @@ -4,13 +4,7 @@ |
| 4 | 4 | package logging |
| 5 | 5 | |
| 6 | 6 | import ( |
| 7 | - "amp/encoding" | |
| 8 | 7 | "fmt" |
| 9 | - "io" | |
| 10 | - "io/ioutil" | |
| 11 | - "os" | |
| 12 | - "path" | |
| 13 | - "strconv" | |
| 14 | 8 | "time" |
| 15 | 9 | ) |
| 16 | 10 | |
| ... | ... | @@ -28,173 +22,20 @@ const ( |
| 28 | 22 | MixedLog = InfoLog | ErrorLog |
| 29 | 23 | ) |
| 30 | 24 | |
| 31 | -const ( | |
| 32 | - endOfRecord = '\n' | |
| 33 | - terminalByte = '\xff' | |
| 34 | -) | |
| 35 | - | |
| 36 | -var ( | |
| 37 | - endOfLogRecord = []byte{'\xff', '\n'} | |
| 38 | -) | |
| 39 | - | |
| 40 | 25 | var ( |
| 41 | 26 | Now = time.Seconds() |
| 42 | 27 | UTC = time.UTC() |
| 43 | 28 | ErrorReceivers = make([]chan *Record, 0) |
| 44 | 29 | InfoReceivers = make([]chan *Record, 0) |
| 45 | - ConsoleFilters = make([]Filter, 0) | |
| 46 | -) | |
| 47 | - | |
| 48 | -var ( | |
| 49 | - checker = make(chan int, 1) | |
| 50 | - waiter = make(chan int, 1) | |
| 51 | - waitable = false | |
| 52 | 30 | ) |
| 53 | 31 | |
| 54 | 32 | type Filter func(record *Record) (write bool, data []interface{}) |
| 55 | 33 | |
| 56 | -type ConsoleLogger struct { | |
| 57 | - receiver chan *Record | |
| 58 | -} | |
| 59 | - | |
| 60 | -type FileLogger struct { | |
| 61 | - name string | |
| 62 | - directory string | |
| 63 | - rotate int | |
| 64 | - file *os.File | |
| 65 | - filename string | |
| 66 | - receiver chan *Record | |
| 67 | -} | |
| 68 | - | |
| 69 | -type NetworkLogger struct { | |
| 70 | - fallback *FileLogger | |
| 71 | - stream *io.Writer | |
| 72 | - receiver chan *Record | |
| 73 | -} | |
| 74 | - | |
| 75 | 34 | type Record struct { |
| 76 | 35 | Error bool |
| 77 | 36 | Items []interface{} |
| 78 | 37 | } |
| 79 | 38 | |
| 80 | -func signalRotation(logger *FileLogger, signalChannel chan string) { | |
| 81 | - var interval int64 | |
| 82 | - var filename string | |
| 83 | - switch logger.rotate { | |
| 84 | - case RotateDaily: | |
| 85 | - interval = 86400000000000 | |
| 86 | - case RotateHourly: | |
| 87 | - interval = 3600000000000 | |
| 88 | - case RotateTest: | |
| 89 | - interval = 3000000000 | |
| 90 | - } | |
| 91 | - for { | |
| 92 | - filename = logger.GetFilename(UTC) | |
| 93 | - if filename != logger.filename { | |
| 94 | - signalChannel <- filename | |
| 95 | - } | |
| 96 | - <-time.After(interval) | |
| 97 | - } | |
| 98 | -} | |
| 99 | - | |
| 100 | -func (logger *FileLogger) log() { | |
| 101 | - | |
| 102 | - rotateSignal := make(chan string) | |
| 103 | - if logger.rotate > 0 { | |
| 104 | - go signalRotation(logger, rotateSignal) | |
| 105 | - } | |
| 106 | - | |
| 107 | - var record *Record | |
| 108 | - var filename string | |
| 109 | - | |
| 110 | - for { | |
| 111 | - select { | |
| 112 | - case filename = <-rotateSignal: | |
| 113 | - if filename != logger.filename { | |
| 114 | - file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666) | |
| 115 | - if err == nil { | |
| 116 | - logger.file.Close() | |
| 117 | - logger.file = file | |
| 118 | - logger.filename = filename | |
| 119 | - } else { | |
| 120 | - fmt.Fprintf(os.Stderr, "ERROR: Couldn't rotate log: %s\n", err) | |
| 121 | - } | |
| 122 | - } | |
| 123 | - case record = <-logger.receiver: | |
| 124 | - argLength := len(record.Items) | |
| 125 | - if record.Error { | |
| 126 | - logger.file.Write([]byte{'E'}) | |
| 127 | - } else { | |
| 128 | - logger.file.Write([]byte{'I'}) | |
| 129 | - } | |
| 130 | - fmt.Fprintf(logger.file, "%v", Now) | |
| 131 | - for i := 0; i < argLength; i++ { | |
| 132 | - message := strconv.Quote(fmt.Sprint(record.Items[i])) | |
| 133 | - fmt.Fprintf(logger.file, "\xfe%s", message[0:len(message)-1]) | |
| 134 | - } | |
| 135 | - logger.file.Write(endOfLogRecord) | |
| 136 | - } | |
| 137 | - } | |
| 138 | - | |
| 139 | -} | |
| 140 | - | |
| 141 | -func (logger *ConsoleLogger) log() { | |
| 142 | - | |
| 143 | - var record *Record | |
| 144 | - var file *os.File | |
| 145 | - var items []interface{} | |
| 146 | - var status string | |
| 147 | - var write bool | |
| 148 | - | |
| 149 | - for { | |
| 150 | - select { | |
| 151 | - case record = <-logger.receiver: | |
| 152 | - items = record.Items | |
| 153 | - write = true | |
| 154 | - case <-checker: | |
| 155 | - if len(logger.receiver) > 0 { | |
| 156 | - checker <- 1 | |
| 157 | - continue | |
| 158 | - } | |
| 159 | - waiter <- 1 | |
| 160 | - continue | |
| 161 | - } | |
| 162 | - for _, filter := range ConsoleFilters { | |
| 163 | - write, data := filter(record) | |
| 164 | - if !write { | |
| 165 | - break | |
| 166 | - } | |
| 167 | - if data != nil { | |
| 168 | - items = data | |
| 169 | - break | |
| 170 | - } | |
| 171 | - } | |
| 172 | - if !write { | |
| 173 | - continue | |
| 174 | - } | |
| 175 | - argLength := len(items) | |
| 176 | - if record.Error { | |
| 177 | - file = os.Stderr | |
| 178 | - } else { | |
| 179 | - file = os.Stdout | |
| 180 | - } | |
| 181 | - if record.Error { | |
| 182 | - status = "ERR" | |
| 183 | - } else { | |
| 184 | - status = "INF" | |
| 185 | - } | |
| 186 | - fmt.Fprintf(file, "%s [%s-%s-%s %s:%s:%s]", status, | |
| 187 | - encoding.PadInt64(UTC.Year, 4), encoding.PadInt(UTC.Month, 2), | |
| 188 | - encoding.PadInt(UTC.Day, 2), encoding.PadInt(UTC.Hour, 2), | |
| 189 | - encoding.PadInt(UTC.Minute, 2), encoding.PadInt(UTC.Second, 2)) | |
| 190 | - for i := 0; i < argLength; i++ { | |
| 191 | - fmt.Fprintf(file, " %v", items[i]) | |
| 192 | - } | |
| 193 | - file.Write([]byte("\n")) | |
| 194 | - } | |
| 195 | - | |
| 196 | -} | |
| 197 | - | |
| 198 | 39 | func Info(message string, v ...interface{}) { |
| 199 | 40 | if len(v) > 0 { |
| 200 | 41 | message = fmt.Sprintf(message, v...) |
| ... | ... | @@ -229,75 +70,6 @@ func ErrorData(v ...interface{}) { |
| 229 | 70 | } |
| 230 | 71 | } |
| 231 | 72 | |
| 232 | -func (logger *FileLogger) GetFilename(timestamp *time.Time) string { | |
| 233 | - var suffix string | |
| 234 | - switch logger.rotate { | |
| 235 | - case RotateNever: | |
| 236 | - suffix = "" | |
| 237 | - case RotateDaily: | |
| 238 | - suffix = timestamp.Format(".2006-01-02") | |
| 239 | - case RotateHourly: | |
| 240 | - suffix = timestamp.Format(".2006-01-02.03") | |
| 241 | - case RotateTest: | |
| 242 | - suffix = timestamp.Format(".2006-01-02.03-04-05") | |
| 243 | - } | |
| 244 | - filename := logger.name + suffix + ".log" | |
| 245 | - return path.Join(logger.directory, filename) | |
| 246 | -} | |
| 247 | - | |
| 248 | -func FixUpLog(filename string) (pointer int) { | |
| 249 | - content, err := ioutil.ReadFile(filename) | |
| 250 | - if err != nil { | |
| 251 | - return | |
| 252 | - } | |
| 253 | - var seenTerminal bool | |
| 254 | - for idx, char := range content { | |
| 255 | - if char == terminalByte { | |
| 256 | - seenTerminal = true | |
| 257 | - } else if seenTerminal { | |
| 258 | - if char == endOfRecord { | |
| 259 | - pointer = idx + 1 | |
| 260 | - } | |
| 261 | - seenTerminal = false | |
| 262 | - } | |
| 263 | - } | |
| 264 | - os.Truncate(filename, int64(pointer)) | |
| 265 | - return pointer | |
| 266 | -} | |
| 267 | - | |
| 268 | -func AddFileLogger(name string, directory string, rotate int, logType int) (logger *FileLogger, err os.Error) { | |
| 269 | - logger = &FileLogger{ | |
| 270 | - name: name, | |
| 271 | - directory: directory, | |
| 272 | - rotate: rotate, | |
| 273 | - receiver: make(chan *Record, 100), | |
| 274 | - } | |
| 275 | - filename := logger.GetFilename(UTC) | |
| 276 | - pointer := FixUpLog(filename) | |
| 277 | - file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666) | |
| 278 | - if err != nil { | |
| 279 | - return logger, err | |
| 280 | - } | |
| 281 | - if pointer > 0 { | |
| 282 | - file.Seek(int64(pointer), 0) | |
| 283 | - } | |
| 284 | - logger.file = file | |
| 285 | - logger.filename = filename | |
| 286 | - go logger.log() | |
| 287 | - AddReceiver(logger.receiver, logType) | |
| 288 | - return logger, nil | |
| 289 | -} | |
| 290 | - | |
| 291 | -func AddConsoleLogger() { | |
| 292 | - waitable = true | |
| 293 | - console := &ConsoleLogger{ | |
| 294 | - receiver: make(chan *Record, 100), | |
| 295 | - } | |
| 296 | - go console.log() | |
| 297 | - AddReceiver(console.receiver, MixedLog) | |
| 298 | - ConsoleFilters = append(ConsoleFilters, defaultConsoleFilter) | |
| 299 | -} | |
| 300 | - | |
| 301 | 73 | func AddReceiver(receiver chan *Record, logType int) { |
| 302 | 74 | if logType&InfoLog != 0 { |
| 303 | 75 | InfoReceivers = append(InfoReceivers, receiver) |
| ... | ... | @@ -307,32 +79,7 @@ func AddReceiver(receiver chan *Record, logType int) { |
| 307 | 79 | } |
| 308 | 80 | } |
| 309 | 81 | |
| 310 | -func AddConsoleFilter(filter Filter) { | |
| 311 | - ConsoleFilters = append(ConsoleFilters, filter) | |
| 312 | -} | |
| 313 | - | |
| 314 | -func defaultConsoleFilter(record *Record) (write bool, data []interface{}) { | |
| 315 | - if len(record.Items) > 0 { | |
| 316 | - meta := record.Items[0] | |
| 317 | - switch meta.(type) { | |
| 318 | - case string: | |
| 319 | - if meta.(string) == "m" { | |
| 320 | - return true, record.Items[1:] | |
| 321 | - } | |
| 322 | - } | |
| 323 | - } | |
| 324 | - return true, nil | |
| 325 | -} | |
| 326 | - | |
| 327 | -func Wait() { | |
| 328 | - if waitable { | |
| 329 | - checker <- 1 | |
| 330 | - <-waiter | |
| 331 | - } | |
| 332 | -} | |
| 333 | - | |
| 334 | 82 | func init() { |
| 335 | - | |
| 336 | 83 | go func() { |
| 337 | 84 | for { |
| 338 | 85 | <-time.After(1000000000) |
| ... | ... | @@ -340,5 +87,4 @@ func init() { |
| 340 | 87 | UTC = time.UTC() |
| 341 | 88 | } |
| 342 | 89 | }() |
| 343 | - | |
| 344 | 90 | } |
| src/amp/logging/network.go | ||
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +// Public Domain (-) 2010-2011 The Ampify Authors. | |
| 2 | +// See the Ampify UNLICENSE file for details. | |
| 3 | + | |
| 4 | +package logging | |
| 5 | + | |
| 6 | +import ( | |
| 7 | + "io" | |
| 8 | +) | |
| 9 | + | |
| 10 | +type NetworkLogger struct { | |
| 11 | + fallback *FileLogger | |
| 12 | + stream *io.Writer | |
| 13 | + receiver chan *Record | |
| 14 | +} | |
