diff --git a/gologger/http_logs.go b/gologger/http_logs.go new file mode 100644 index 0000000..06589aa --- /dev/null +++ b/gologger/http_logs.go @@ -0,0 +1,66 @@ +package gologger + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httputil" + "os" + "strings" + "time" +) + +const splitLine = "==============================================================" + +// LogHTTPRequest ... +func (l *Logger) LogHTTPRequest(req *http.Request, userID string) string { + if userID == "" { + userID = "-" + } + + var b strings.Builder + + b.WriteString("Request:\n") + // CLF-like header + fmt.Fprintf(&b, "%s %s %s\n", req.RemoteAddr, userID, time.Now().Format(dateTimeFormat)) + + body, err := httputil.DumpRequest(req, true) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + } + + const sepStr = "\r\n\r\n" + sepIndex := bytes.Index(body, []byte(sepStr)) + if sepIndex == -1 { + b.WriteString(string(body) + "\n\n") + } else { + sepIndex += len(sepStr) + payload, _ := printJSON(body[sepIndex:]) + b.WriteString(string(body[:sepIndex]) + string(payload) + "\n\n") + } + + return b.String() +} + +// LogHTTPResponse ... +func (l *Logger) LogHTTPResponse(data []byte, status int, startTime time.Time) string { + duration := time.Now().Sub(startTime) + jsonData, _ := printJSON(data) + return fmt.Sprintf("Response:\n%d %v %dB\n%s\n%s\n\n", status, duration, len(data), jsonData, splitLine) +} + +// CombineHTTPLogs ... +func (l *Logger) CombineHTTPLogs(in string, out string) { + if l.outputFile == nil { + return + } + + l.mu.Lock() + defer l.mu.Unlock() + + msg := in + out + if l.shouldSplit(len(msg)) { + l.split() + } + l.outputFile.WriteString(msg) +} diff --git a/gologger/main.go b/gologger/main.go new file mode 100644 index 0000000..5f731eb --- /dev/null +++ b/gologger/main.go @@ -0,0 +1,162 @@ +package gologger + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +const dateTimeFormat = "2006-01-02 15:04:05" + +// Block ... +const ( + MaxLogSize5MB int64 = 5 * 1024 * 1024 + MaxLogSize1MB int64 = 1 * 1024 * 1024 + MaxLogSize500KB int64 = 500 * 1024 + MaxLogSize100KB int64 = 100 * 1024 + MaxLogSize512B int64 = 512 +) + +// Logger ... +type Logger struct { + mu *sync.Mutex + outputFile *os.File + + outputFileName string + fullName string + maxFileSize int64 + + splitCount int + + directory string +} + +// New ... +func New(name, dir string, maxFileSize int64) (logger *Logger, err error) { + logger = &Logger{} + + logger.outputFileName = name + logger.mu = &sync.Mutex{} + logger.maxFileSize = maxFileSize + logger.directory = dir + + err = os.Mkdir(dir, os.ModePerm) + if err != nil { + if !os.IsExist(err) { + return nil, err + } + } + + date := strings.Replace(time.Now().Format(dateTimeFormat), ":", ".", -1) + logger.outputFileName += " " + date + logger.fullName = logger.outputFileName + ".txt" + path := filepath.Join(dir, logger.fullName) + if logger.outputFile, err = os.Create(path); err != nil { + return nil, err + } + + return logger, nil +} + +// Log ... +func (l *Logger) Log(format string, v ...interface{}) { + if l.outputFile == nil { + return + } + + l.mu.Lock() + defer l.mu.Unlock() + + msg := fmt.Sprintf(format, v...) + s := time.Now().Format(dateTimeFormat) + ": " + msg + "\n" + if l.shouldSplit(len(s)) { + l.split() + } + l.outputFile.WriteString(s) +} + +// Print ... +func (l *Logger) Print(format string, v ...interface{}) { + msg := fmt.Sprintf(format, v...) + fmt.Printf("%s: %s\n", time.Now().Format(dateTimeFormat), msg) +} + +// Trace ... +func (l *Logger) Trace(format string, v ...interface{}) string { + if l.outputFile == nil { + return "" + } + + l.mu.Lock() + defer l.mu.Unlock() + + s := getTrace(format, v...) + "\n" + + if l.shouldSplit(len(s)) { + l.split() + } + l.outputFile.WriteString(s) + + return s +} + +// PrintTrace ... +func (l *Logger) PrintTrace(format string, v ...interface{}) { + s := getTrace(format, v...) + fmt.Printf("%s\n", s) +} + +// PrintAndTrace ... +func (l *Logger) PrintAndTrace(format string, v ...interface{}) { + t := l.Trace(format, v...) + fmt.Println(t) +} + +// Close ... +func (l *Logger) Close() error { + if l.outputFile == nil { + return nil + } + + return l.outputFile.Close() +} + +func (l *Logger) split() error { + if l.outputFile == nil { + return nil + } + + // close old file + err := l.outputFile.Close() + if err != nil { + return err + } + + // open new file + l.splitCount++ + path := filepath.Join(l.directory, l.outputFileName+fmt.Sprintf("(%d)", l.splitCount)+".txt") + l.outputFile, err = os.Create(path) + + return err +} + +func (l *Logger) shouldSplit(nextEntrySize int) bool { + stats, _ := l.outputFile.Stat() + return int64(nextEntrySize) >= (l.maxFileSize - stats.Size()) +} + +func printJSON(in []byte) (out []byte, err error) { + var buf bytes.Buffer + err = json.Indent(&buf, in, "", " ") + return buf.Bytes(), err +} + +// GetOutDir ... +func (l *Logger) GetOutDir() string { + return l.directory +} diff --git a/gologger/tracing.go b/gologger/tracing.go new file mode 100644 index 0000000..28a6477 --- /dev/null +++ b/gologger/tracing.go @@ -0,0 +1,65 @@ +package gologger + +import ( + "fmt" + "math" + "runtime" + "strings" + "time" +) + +// getCallStack +func getCallStack() (stack []string) { + const ( + maxStackDepth = 10000 + skipRuntime = 2 // skip runtime and startup + skipCallers = 2 + thisPackage = "gologger" + ) + + callers := make([]uintptr, maxStackDepth) // min 1 + + stackDepth := runtime.Callers(skipCallers, callers) + frames := runtime.CallersFrames(callers) + + for i := 0; i < skipRuntime; i++ { + frames.Next() + } + + for i := 0; i < stackDepth-skipRuntime-skipCallers; i++ { + frame, next := frames.Next() + if !strings.Contains(frame.File, thisPackage) { + stack = append(stack, fmt.Sprintf("[%s %d]", frame.File, frame.Line)) + } + if !next { + break + } + } + + reverseStack(stack) + + return stack +} + +// in place +func reverseStack(stack []string) { + middle := int(math.Floor(float64(len(stack)) / 2.0)) + + lastIndex := len(stack) - 1 + for i := 0; i < middle; i++ { + stack[i], stack[lastIndex] = stack[lastIndex], stack[i] + lastIndex-- + } +} + +func getTrace(format string, v ...interface{}) (t string) { + stack := getCallStack() + + t = time.Now().Format(dateTimeFormat) + ":\n" + for _, s := range stack { + t += s + "\n" + } + t += fmt.Sprintf(format, v...) + "\n" + + return t +} diff --git a/http.go b/http.go index cfe36d4..b9aab52 100644 --- a/http.go +++ b/http.go @@ -11,6 +11,7 @@ type StatusRecorder struct { writer http.ResponseWriter status int size int + data []byte } // NewStatusRecorder ... @@ -19,6 +20,7 @@ func NewStatusRecorder(w http.ResponseWriter) *StatusRecorder { writer: w, status: 0, size: 0, + data: nil, } } @@ -31,6 +33,10 @@ func (r *StatusRecorder) WriteHeader(code int) { // Write is a wrapper for http.ResponseWriter interface func (r *StatusRecorder) Write(in []byte) (int, error) { r.size = len(in) + if r.status >= 400 { + r.data = make([]byte, len(in)) + copy(r.data, in) + } return r.writer.Write(in) } @@ -49,6 +55,11 @@ func (r *StatusRecorder) Size() int { return r.size } +// Size ... +func (r *StatusRecorder) Data() []byte { + return r.data +} + // NotFoundHandlerFunc writes HTTP error 404 to w. func NotFoundHandlerFunc(w http.ResponseWriter, req *http.Request) { SetAccessControlHeaders(w) diff --git a/middleware/middleware.go b/middleware/middleware.go index 5779f13..2fd6840 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -73,10 +73,10 @@ func CloseLogger() { } // LogHTTP ... -func LogHTTP(h http.HandlerFunc) http.HandlerFunc { +func LogHTTP(hfunc http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if httpLogger == nil { - h(w, req) + hfunc(w, req) return } @@ -87,10 +87,9 @@ func LogHTTP(h http.HandlerFunc) http.HandlerFunc { rec := web.NewStatusRecorder(w) - h(rec, req) + hfunc(rec, req) - t2 := time.Now() - out := httpLogger.LogHTTPResponse(rec.Status(), t2.Sub(t1), rec.Size()) + out := httpLogger.LogHTTPResponse(rec.Data(), rec.Status(), t1) httpLogger.CombineHTTPLogs(in, out) }