agent/logging/agent_logging.go (202 lines of code) (raw):
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
package logging
import (
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/aws/aws-app-mesh-agent/agent/config"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
)
type AgentLogWriter interface {
io.Writer
GetOutputFileDescriptors() []uintptr
}
type AgentLogWriterImpl struct {
fileHandles []*os.File
defaultSink *os.File
lock sync.Mutex
}
func (a *AgentLogWriterImpl) GetOutputFileDescriptors() []uintptr {
fds := []uintptr{
a.fileHandles[syscall.Stdin].Fd(),
a.fileHandles[syscall.Stdout].Fd(),
a.fileHandles[syscall.Stderr].Fd(),
}
log.Tracef("Returning file descriptors: %v", fds)
return fds
}
func (a *AgentLogWriterImpl) Write(p []byte) (int, error) {
var bytesWritten int = -1
var err error = nil
a.lock.Lock()
defer a.lock.Unlock()
if a.defaultSink != nil {
bytesWritten, err = a.defaultSink.Write(p)
}
return bytesWritten, err
}
func copyLogFile(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
return -1, err
}
if !sourceFileStat.Mode().IsRegular() {
return -1, fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return -1, err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return -1, err
}
defer destination.Close()
return io.Copy(destination, source)
}
func CleanupLogFiles(logFile string, maxFilesToKeep int) {
// if fileCount is zero clean up all the log files matching the logFile
// pattern
var logfilePattern string = fmt.Sprintf("%s.*", logFile)
matches, err := filepath.Glob(logfilePattern)
if err != nil {
log.Warnf("Errors when attempting to match glob pattern: %s: [error %v]", logfilePattern, err)
return
}
if len(matches) < maxFilesToKeep {
return
}
// Sort the files in reverse chronological order (newest first)
sort.Slice(matches, func(i, j int) bool {
a_parts := strings.Split(matches[i], ".")
b_parts := strings.Split(matches[j], ".")
// If the filename does not end in an epoch bubble it to the end
a_time, err := strconv.ParseInt(a_parts[len(a_parts)-1], 10, 64)
if err != nil {
return false
}
b_time, err := strconv.ParseInt(b_parts[len(b_parts)-1], 10, 64)
if err != nil {
return false
}
return a_time > b_time
})
for i, f := range matches {
if i < maxFilesToKeep {
continue
}
os.Remove(f)
}
}
func (a *AgentLogWriterImpl) monitorAndRotateLog(agentConfig config.AgentConfig, watcher *fsnotify.Watcher) {
logPath := path.Join(
agentConfig.EnvoyLoggingDestination,
agentConfig.EnvoyLogFileName,
)
watcher.Add(logPath)
// We cannot log in this function. This loop operates on inotify events
// so logging here ends up begin recursive.
var maxSize int64 = int64(agentConfig.MaxLogFileSizeMB * 1_048_576)
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
fileInfo, err := os.Stat(logPath)
if err != nil {
continue
}
if fileInfo.Size() > maxSize {
// Append the epoch to the filename. This represents the end of the log's
// contents
var dstFileName string = fmt.Sprintf("%s.%d", logPath, time.Now().Unix())
copyLogFile(logPath, dstFileName)
// Truncate the existing log file
a.defaultSink.Truncate(0)
a.defaultSink.Seek(0, 0)
a.defaultSink.Sync()
// Purge old files, and preserve the configured file count
CleanupLogFiles(logPath, agentConfig.MaxLogCount)
}
}
}
}
}
func openDiskLogFile(agentConfig config.AgentConfig) (*os.File, error) {
logFullPath := path.Join(agentConfig.EnvoyLoggingDestination, agentConfig.EnvoyLogFileName)
log.Debugf("Writing output to %s\n", logFullPath)
logFile, err := os.Create(logFullPath)
if err != nil {
log.Errorf("Unable to open the full output log path [%s]: %v", logFullPath, err)
return nil, err
}
return logFile, nil
}
func getOutputRedirection(agentconfig config.AgentConfig) ([]*os.File, error) {
var outputFds []*os.File = make([]*os.File, 3)
// Set the default file descriptors unless we are configured differently
outputFds[syscall.Stdin] = os.Stdin
outputFds[syscall.Stdout] = os.Stdout
outputFds[syscall.Stderr] = os.Stderr
// We are configured to log to a disk location
if agentconfig.EnvoyLoggingDestination != config.ENVOY_LOG_DESTINATION_DEFAULT {
fileInfo, err := os.Stat(agentconfig.EnvoyLoggingDestination)
if err != nil {
log.Errorf("Unable to determine state of log destination %s", agentconfig.EnvoyLoggingDestination)
return outputFds, err
}
if !fileInfo.IsDir() {
log.Errorf("Log destination %s, is not a directory", agentconfig.EnvoyLoggingDestination)
return outputFds, errors.New("log destination is not a directory")
}
start := time.Now()
// Create a file and use the copy/truncate method to rotate it.
redirectedLog, err := openDiskLogFile(agentconfig)
if err != nil {
log.Warnf("Unable to redirect output to the configured log file: %v", err)
return outputFds, err
}
// Reset the agent stdout/stderr to the output Fds
outputFds[syscall.Stdout] = redirectedLog
outputFds[syscall.Stderr] = redirectedLog
log.Debugf("Log rotation took [%d us]\n", time.Since(start).Microseconds())
}
return outputFds, nil
}
func newAgentLogWriter(agentConfig config.AgentConfig) AgentLogWriter {
var logger = new(AgentLogWriterImpl)
fileHandles, err := getOutputRedirection(agentConfig)
if err != nil {
log.Warn("Unable to setup log output redirection to file.")
}
// If redirection fails we still use the default file descriptors. Logging is still
// sent to stderr by default
logger.fileHandles = fileHandles
logger.defaultSink = logger.fileHandles[syscall.Stderr]
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Error("Unable to create inotify watcher for log rotation")
return logger
}
go logger.monitorAndRotateLog(agentConfig, watcher)
return logger
}
// initialize the logger with a specified log format
func SetupLogger(agentConfig *config.AgentConfig) {
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
DisableLevelTruncation: true,
})
if agentConfig != nil {
agentWriter := newAgentLogWriter(*agentConfig)
// We need file descriptors for the forked process output. If we are
// redirecting to a file, these file descriptors must also point to this
// same logfile. Not certain that we can update the file descriptors
// once they are in use, so this constrains us to a copy/truncate log
// rotation mechanism
agentConfig.OutputFileDescriptors = agentWriter.GetOutputFileDescriptors()
log.SetOutput(agentWriter)
}
var logLevel log.Level = log.InfoLevel
switch agentConfig.EnvoyLogLevel {
case "debug":
logLevel = log.DebugLevel
case "warn":
logLevel = log.WarnLevel
case "error":
logLevel = log.ErrorLevel
case "trace":
logLevel = log.TraceLevel
default:
logLevel = log.InfoLevel
}
log.SetLevel(logLevel)
log.SetFormatter(NewAgentLogFormatter())
}