github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/log.go (about) 1 /* 2 * Copyright (c) 2016, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package server 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "io" 26 "io/ioutil" 27 go_log "log" 28 "os" 29 "sync" 30 "sync/atomic" 31 "time" 32 33 "github.com/Psiphon-Inc/rotate-safe-writer" 34 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 35 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo" 36 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" 37 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/stacktrace" 38 "github.com/sirupsen/logrus" 39 ) 40 41 // TraceLogger adds single frame stack trace information to the underlying 42 // logging facilities. 43 type TraceLogger struct { 44 *logrus.Logger 45 } 46 47 // LogFields is an alias for the field struct in the underlying logging 48 // package. 49 type LogFields logrus.Fields 50 51 // Add copies log fields from b to a, skipping fields which already exist, 52 // regardless of value, in a. 53 func (a LogFields) Add(b LogFields) { 54 for name, value := range b { 55 _, ok := a[name] 56 if !ok { 57 a[name] = value 58 } 59 } 60 } 61 62 // WithTrace adds a "trace" field containing the caller's function name 63 // and source file line number; and "host_id" and "build_rev" fields 64 // identifying this server and build. Use this function when the log has no 65 // fields. 66 func (logger *TraceLogger) WithTrace() *logrus.Entry { 67 return logger.WithFields( 68 logrus.Fields{ 69 "trace": stacktrace.GetParentFunctionName(), 70 "host_id": logHostID, 71 "build_rev": logBuildRev, 72 }) 73 } 74 75 func renameLogFields(fields LogFields) { 76 if _, ok := fields["trace"]; ok { 77 fields["fields.trace"] = fields["trace"] 78 } 79 if _, ok := fields["host_id"]; ok { 80 fields["fields.host_id"] = fields["host_id"] 81 } 82 if _, ok := fields["build_rev"]; ok { 83 fields["fields.build_rev"] = fields["build_rev"] 84 } 85 } 86 87 // WithTraceFields adds a "trace" field containing the caller's function name 88 // and source file line number; and "host_id" and "build_rev" fields 89 // identifying this server and build. Use this function when the log has 90 // fields. Note that any existing "trace"/"host_id"/"build_rev" field will be 91 // renamed to "field.<name>". 92 func (logger *TraceLogger) WithTraceFields(fields LogFields) *logrus.Entry { 93 renameLogFields(fields) 94 fields["trace"] = stacktrace.GetParentFunctionName() 95 fields["host_id"] = logHostID 96 fields["build_rev"] = logBuildRev 97 return logger.WithFields(logrus.Fields(fields)) 98 } 99 100 // LogRawFieldsWithTimestamp directly logs the supplied fields adding only 101 // an additional "timestamp" field; and "host_id" and "build_rev" fields 102 // identifying this server and build. The stock "msg" and "level" fields are 103 // omitted. This log is emitted at the Error level. This function exists to 104 // support API logs which have neither a natural message nor severity; and 105 // omitting these values here makes it easier to ship these logs to existing 106 // API log consumers. 107 // Note that any existing "trace"/"host_id"/"build_rev" field will be renamed 108 // to "field.<name>". 109 func (logger *TraceLogger) LogRawFieldsWithTimestamp(fields LogFields) { 110 renameLogFields(fields) 111 fields["host_id"] = logHostID 112 fields["build_rev"] = logBuildRev 113 logger.WithFields(logrus.Fields(fields)).Error( 114 customJSONFormatterLogRawFieldsWithTimestamp) 115 } 116 117 // LogPanicRecover calls LogRawFieldsWithTimestamp with standard fields 118 // for logging recovered panics. 119 func (logger *TraceLogger) LogPanicRecover(recoverValue interface{}, stack []byte) { 120 log.LogRawFieldsWithTimestamp( 121 LogFields{ 122 "event_name": "panic", 123 "recover_value": recoverValue, 124 "stack": string(stack), 125 }) 126 } 127 128 type commonLogger struct { 129 traceLogger *TraceLogger 130 } 131 132 func (logger *commonLogger) WithTrace() common.LogTrace { 133 // Patch trace to be correct parent 134 return logger.traceLogger.WithTrace().WithField( 135 "trace", stacktrace.GetParentFunctionName()) 136 } 137 138 func (logger *commonLogger) WithTraceFields(fields common.LogFields) common.LogTrace { 139 // Patch trace to be correct parent 140 return logger.traceLogger.WithTraceFields(LogFields(fields)).WithField( 141 "trace", stacktrace.GetParentFunctionName()) 142 } 143 144 func (logger *commonLogger) LogMetric(metric string, fields common.LogFields) { 145 fields["event_name"] = metric 146 logger.traceLogger.LogRawFieldsWithTimestamp(LogFields(fields)) 147 } 148 149 // CommonLogger wraps a TraceLogger instance with an interface that conforms 150 // to common.Logger. This is used to make the TraceLogger available to other 151 // packages that don't import the "server" package. 152 func CommonLogger(traceLogger *TraceLogger) *commonLogger { 153 return &commonLogger{ 154 traceLogger: traceLogger, 155 } 156 } 157 158 // NewLogWriter returns an io.PipeWriter that can be used to write 159 // to the global logger. Caller must Close() the writer. 160 func NewLogWriter() *io.PipeWriter { 161 return log.Writer() 162 } 163 164 // CustomJSONFormatter is a customized version of logrus.JSONFormatter 165 type CustomJSONFormatter struct { 166 } 167 168 var ( 169 useLogCallback int32 170 logCallback atomic.Value 171 ) 172 173 // setLogCallback sets a callback that is invoked with each JSON log message. 174 // This facility is intended for use in testing only. 175 func setLogCallback(callback func([]byte)) { 176 if callback == nil { 177 atomic.StoreInt32(&useLogCallback, 0) 178 return 179 } 180 atomic.StoreInt32(&useLogCallback, 1) 181 logCallback.Store(callback) 182 } 183 184 const customJSONFormatterLogRawFieldsWithTimestamp = "CustomJSONFormatter.LogRawFieldsWithTimestamp" 185 186 // Format implements logrus.Formatter. This is a customized version 187 // of the standard logrus.JSONFormatter adapted from: 188 // https://github.com/Sirupsen/logrus/blob/f1addc29722ba9f7651bc42b4198d0944b66e7c4/json_formatter.go 189 // 190 // The changes are: 191 // - "time" is renamed to "timestamp" 192 // - there's an option to omit the standard "msg" and "level" fields 193 // 194 func (f *CustomJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) { 195 data := make(logrus.Fields, len(entry.Data)+3) 196 for k, v := range entry.Data { 197 switch v := v.(type) { 198 case error: 199 // Otherwise errors are ignored by `encoding/json` 200 // https://github.com/Sirupsen/logrus/issues/137 201 data[k] = v.Error() 202 default: 203 data[k] = v 204 } 205 } 206 207 if t, ok := data["timestamp"]; ok { 208 data["fields.timestamp"] = t 209 } 210 211 data["timestamp"] = entry.Time.Format(time.RFC3339) 212 213 if entry.Message != customJSONFormatterLogRawFieldsWithTimestamp { 214 215 if m, ok := data["msg"]; ok { 216 data["fields.msg"] = m 217 } 218 219 if l, ok := data["level"]; ok { 220 data["fields.level"] = l 221 } 222 223 data["msg"] = entry.Message 224 data["level"] = entry.Level.String() 225 } 226 227 serialized, err := json.Marshal(data) 228 if err != nil { 229 return nil, fmt.Errorf("failed to marshal fields to JSON, %v", err) 230 } 231 232 if atomic.LoadInt32(&useLogCallback) == 1 { 233 logCallback.Load().(func([]byte))(serialized) 234 } 235 236 return append(serialized, '\n'), nil 237 } 238 239 var log *TraceLogger 240 var logHostID, logBuildRev string 241 var initLogging sync.Once 242 243 // InitLogging configures a logger according to the specified 244 // config params. If not called, the default logger set by the 245 // package init() is used. 246 // Concurrency notes: this should only be called from the main 247 // goroutine; InitLogging only has effect on the first call, as 248 // the logging facilities it initializes may be in use by other 249 // goroutines after that point. 250 func InitLogging(config *Config) (retErr error) { 251 252 initLogging.Do(func() { 253 254 logHostID = config.HostID 255 logBuildRev = buildinfo.GetBuildInfo().BuildRev 256 257 level, err := logrus.ParseLevel(config.LogLevel) 258 if err != nil { 259 retErr = errors.Trace(err) 260 return 261 } 262 263 var logWriter io.Writer 264 265 if config.LogFilename != "" { 266 267 retries, create, mode := config.GetLogFileReopenConfig() 268 logWriter, err = rotate.NewRotatableFileWriter( 269 config.LogFilename, retries, create, mode) 270 if err != nil { 271 retErr = errors.Trace(err) 272 return 273 } 274 275 if !config.SkipPanickingLogWriter { 276 277 // Use PanickingLogWriter, which will intentionally 278 // panic when a Write fails. Set SkipPanickingLogWriter 279 // if this behavior is not desired. 280 // 281 // Note that NewRotatableFileWriter will first attempt 282 // a retry when a Write fails. 283 // 284 // It is assumed that continuing operation while unable 285 // to log is unacceptable; and that the psiphond service 286 // is managed and will restart when it terminates. 287 // 288 // It is further assumed that panicking will result in 289 // an error that is externally logged and reported to a 290 // monitoring system. 291 // 292 // TODO: An orderly shutdown may be preferred, as some 293 // data will be lost in a panic (e.g., server_tunnel logs). 294 // It may be possible to perform an orderly shutdown first 295 // and then panic, or perform an orderly shutdown and 296 // simulate a panic message that will be reported. 297 298 logWriter = NewPanickingLogWriter(config.LogFilename, logWriter) 299 } 300 301 } else { 302 logWriter = os.Stderr 303 } 304 305 log = &TraceLogger{ 306 &logrus.Logger{ 307 Out: logWriter, 308 Formatter: &CustomJSONFormatter{}, 309 Level: level, 310 }, 311 } 312 }) 313 314 return retErr 315 } 316 317 func IsLogLevelDebug() bool { 318 return log.Logger.Level == logrus.DebugLevel 319 } 320 321 func init() { 322 323 // Suppress standard "log" package logging performed by other packages. 324 // For example, "net/http" logs messages such as: 325 // "http: TLS handshake error from <client-ip-addr>:<port>: [...]: i/o timeout" 326 go_log.SetOutput(ioutil.Discard) 327 328 log = &TraceLogger{ 329 &logrus.Logger{ 330 Out: os.Stderr, 331 Formatter: &CustomJSONFormatter{}, 332 Hooks: make(logrus.LevelHooks), 333 Level: logrus.DebugLevel, 334 }, 335 } 336 }