-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for logging to a file #6429
Changes from all commits
234d113
fff69a5
f72febd
277a252
567ad88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package agent | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"sort" | ||
"strconv" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/hashicorp/logutils" | ||
) | ||
|
||
var ( | ||
now = time.Now | ||
) | ||
|
||
// logFile is used to setup a file based logger that also performs log rotation | ||
type logFile struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's link to https://github.com/hashicorp/consul/blob/53f3962cab68ec3590cbbe16af4e54b500b0570f/logger/logfile.go |
||
// Log level Filter to filter out logs that do not matcch LogLevel criteria | ||
logFilter *logutils.LevelFilter | ||
|
||
//Name of the log file | ||
fileName string | ||
|
||
//Path to the log file | ||
logPath string | ||
|
||
//Duration between each file rotation operation | ||
duration time.Duration | ||
|
||
//LastCreated represents the creation time of the latest log | ||
LastCreated time.Time | ||
|
||
//FileInfo is the pointer to the current file being written to | ||
FileInfo *os.File | ||
|
||
//MaxBytes is the maximum number of desired bytes for a log file | ||
MaxBytes int | ||
|
||
//BytesWritten is the number of bytes written in the current log file | ||
BytesWritten int64 | ||
|
||
// Max rotated files to keep before removing them. | ||
MaxFiles int | ||
|
||
//acquire is the mutex utilized to ensure we have no concurrency issues | ||
acquire sync.Mutex | ||
} | ||
|
||
func (l *logFile) fileNamePattern() string { | ||
// Extract the file extension | ||
fileExt := filepath.Ext(l.fileName) | ||
// If we have no file extension we append .log | ||
if fileExt == "" { | ||
fileExt = ".log" | ||
} | ||
// Remove the file extension from the filename | ||
return strings.TrimSuffix(l.fileName, fileExt) + "-%s" + fileExt | ||
} | ||
|
||
func (l *logFile) openNew() error { | ||
fileNamePattern := l.fileNamePattern() | ||
// New file name has the format : filename-timestamp.extension | ||
createTime := now() | ||
newfileName := fmt.Sprintf(fileNamePattern, strconv.FormatInt(createTime.UnixNano(), 10)) | ||
newfilePath := filepath.Join(l.logPath, newfileName) | ||
// Try creating a file. We truncate the file because we are the only authority to write the logs | ||
filePointer, err := os.OpenFile(newfilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0640) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
l.FileInfo = filePointer | ||
// New file, new bytes tracker, new creation time :) | ||
l.LastCreated = createTime | ||
l.BytesWritten = 0 | ||
return nil | ||
} | ||
|
||
func (l *logFile) rotate() error { | ||
// Get the time from the last point of contact | ||
timeElapsed := time.Since(l.LastCreated) | ||
// Rotate if we hit the byte file limit or the time limit | ||
if (l.BytesWritten >= int64(l.MaxBytes) && (l.MaxBytes > 0)) || timeElapsed >= l.duration { | ||
l.FileInfo.Close() | ||
if err := l.pruneFiles(); err != nil { | ||
return err | ||
} | ||
return l.openNew() | ||
} | ||
return nil | ||
} | ||
|
||
func (l *logFile) pruneFiles() error { | ||
if l.MaxFiles == 0 { | ||
return nil | ||
} | ||
pattern := l.fileNamePattern() | ||
//get all the files that match the log file pattern | ||
globExpression := filepath.Join(l.logPath, fmt.Sprintf(pattern, "*")) | ||
matches, err := filepath.Glob(globExpression) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Stort the strings as filepath.Glob does not publicly guarantee that files | ||
// are sorted, so here we add an extra defensive sort. | ||
sort.Strings(matches) | ||
|
||
// Prune if there are more files stored than the configured max | ||
stale := len(matches) - l.MaxFiles | ||
for i := 0; i < stale; i++ { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Glob internally guarantees (in |
||
if err := os.Remove(matches[i]); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Write is used to implement io.Writer | ||
func (l *logFile) Write(b []byte) (int, error) { | ||
// Filter out log entries that do not match log level criteria | ||
if !l.logFilter.Check(b) { | ||
return 0, nil | ||
} | ||
|
||
l.acquire.Lock() | ||
defer l.acquire.Unlock() | ||
//Create a new file if we have no file to write to | ||
if l.FileInfo == nil { | ||
if err := l.openNew(); err != nil { | ||
return 0, err | ||
} | ||
} | ||
// Check for the last contact and rotate if necessary | ||
if err := l.rotate(); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This approach will create files larger than the size limit which isn't correct but also shouldn't hurt much. Let's not worry about fixing it I guess. |
||
return 0, err | ||
} | ||
|
||
n, err := l.FileInfo.Write(b) | ||
l.BytesWritten += int64(n) | ||
return n, err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be easier to override in tests if it were on the logFile struct, but we never touch it in tests anyway so I guess it's not worth fixing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah there's a bunch of stuff I'm not a fan of in here, but the incremental benefit of adding support and improving later felt worthwhile to me. (and reduces the surface area for scary bugs bc we know this ~works)