|
| 1 | +/* |
| 2 | +© Copyright IBM Corporation 2025 |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | +http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +// Package logrotation contains code to manage the logrotation and append-log logic for logs |
| 18 | +package logrotation |
| 19 | + |
| 20 | +import ( |
| 21 | + "fmt" |
| 22 | + "io" |
| 23 | + "os" |
| 24 | + "slices" |
| 25 | + "strings" |
| 26 | + "sync" |
| 27 | +) |
| 28 | + |
| 29 | +type RotatingLogger struct { |
| 30 | + basePath string |
| 31 | + maxFileSize int |
| 32 | + logFilesCount int |
| 33 | + lock sync.Mutex |
| 34 | +} |
| 35 | + |
| 36 | +// NewRotatingLogger create a new RotatingLogger, it expects three input parameters, |
| 37 | +// basePath is the log-file path prefix, |
| 38 | +// maxFileSize is the max allowed log-file size in bytes, |
| 39 | +// logFilesCount is the number of log files required to be created. |
| 40 | +func NewRotatingLogger(basePath string, maxFileSize, logFilesCount int) *RotatingLogger { |
| 41 | + return &RotatingLogger{ |
| 42 | + basePath: basePath, |
| 43 | + maxFileSize: maxFileSize, |
| 44 | + logFilesCount: logFilesCount, |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +// instanceFileName returns a log instance filename |
| 49 | +func (r *RotatingLogger) instanceFileName(instance int) string { |
| 50 | + return fmt.Sprintf("%s-%d.log", r.basePath, instance) |
| 51 | +} |
| 52 | + |
| 53 | +// Init creates log files |
| 54 | +func (r *RotatingLogger) Init() error { |
| 55 | + for i := 1; i <= r.logFilesCount; i++ { |
| 56 | + err := os.WriteFile(r.instanceFileName(i), []byte(""), 0660) |
| 57 | + if err != nil { |
| 58 | + return err |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + return nil |
| 63 | + |
| 64 | +} |
| 65 | + |
| 66 | +// Append appends the message line to the logFile and if the logFile size exceeds the maxFileSize then perform log-rotation. |
| 67 | +// messageLine is the log message that we need to append to the log-file, |
| 68 | +// if deduplicateLine is false the messageLine will always be appended, |
| 69 | +// if deduplicateLine is true messageLine will only be appended if it is different from the last line in the logfile. |
| 70 | +func (r *RotatingLogger) Append(messageLine string, deduplicateLine bool) { |
| 71 | + r.lock.Lock() |
| 72 | + defer r.lock.Unlock() |
| 73 | + |
| 74 | + // we will always log in the first instance of the log files |
| 75 | + logFilePath := r.instanceFileName(1) |
| 76 | + |
| 77 | + // open the log file in append mode |
| 78 | + // for the gosec rule Id: G302 - Expect file permissions to be 0600 or less |
| 79 | + logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) |
| 80 | + if err != nil { |
| 81 | + fmt.Printf("Failed to open log file: %v\n", err) |
| 82 | + return |
| 83 | + } |
| 84 | + |
| 85 | + defer func(f *os.File) { |
| 86 | + if err := logFile.Close(); err != nil { |
| 87 | + fmt.Printf("Error: %v, Failed to close log file: %s\n", err, logFilePath) |
| 88 | + } |
| 89 | + }(logFile) |
| 90 | + |
| 91 | + // check if the message should be appended to the file |
| 92 | + shouldBeAppended, err := r.checkIfMessageShouldBeAppended(logFilePath, messageLine, deduplicateLine) |
| 93 | + if err != nil { |
| 94 | + fmt.Printf("Failed to validate the currentLog and the lastLog line %v\n", err) |
| 95 | + } |
| 96 | + |
| 97 | + if !shouldBeAppended { |
| 98 | + return |
| 99 | + } |
| 100 | + |
| 101 | + // check if the logFileSize has exceeded the maxFileSize then perform the logrotation |
| 102 | + logFileSizeExceeded, err := r.checkIfLogFileSizeExceeded(len(messageLine), logFile) |
| 103 | + if err != nil { |
| 104 | + fmt.Printf("Failed to validate log file size: %v\n", err) |
| 105 | + return |
| 106 | + } |
| 107 | + |
| 108 | + if logFileSizeExceeded { |
| 109 | + |
| 110 | + // close the current log file |
| 111 | + err = logFile.Close() |
| 112 | + if err != nil { |
| 113 | + fmt.Printf("Error: %v, Failed to close log file: %v\n", err, logFile.Name()) |
| 114 | + } |
| 115 | + |
| 116 | + // perform log rotation |
| 117 | + err = r.performLogRotation() |
| 118 | + if err != nil { |
| 119 | + fmt.Printf("Failed to perform log-rotation: %v\n", err) |
| 120 | + } |
| 121 | + |
| 122 | + // open the newly created logFile |
| 123 | + logFile, err = os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) |
| 124 | + if err != nil { |
| 125 | + fmt.Printf("Failed to open log file: %v\n", err) |
| 126 | + return |
| 127 | + } |
| 128 | + |
| 129 | + defer func(f *os.File) { |
| 130 | + if err := logFile.Close(); err != nil { |
| 131 | + fmt.Printf("Error: %v, Failed to close log file: %s\n", err, logFilePath) |
| 132 | + } |
| 133 | + }(logFile) |
| 134 | + } |
| 135 | + |
| 136 | + // append the message to the file |
| 137 | + _, err = logFile.WriteString(messageLine) |
| 138 | + if err != nil { |
| 139 | + fmt.Printf("Failed to write to log file: %v\n", err) |
| 140 | + } |
| 141 | + |
| 142 | +} |
| 143 | + |
| 144 | +func (r *RotatingLogger) performLogRotation() error { |
| 145 | + |
| 146 | + // delete the last log file |
| 147 | + lastLogFile := r.instanceFileName(r.logFilesCount) |
| 148 | + if err := os.Remove(lastLogFile); err != nil && !os.IsNotExist(err) { |
| 149 | + return fmt.Errorf("error deleting the %d instance of log-file: %s", r.logFilesCount, r.basePath) |
| 150 | + } |
| 151 | + |
| 152 | + // rename the remaining instances of the log-files |
| 153 | + for i := r.logFilesCount; i >= 2; i-- { |
| 154 | + oldLogFileInstance := r.instanceFileName(i - 1) |
| 155 | + newLogFileInstance := r.instanceFileName(i) |
| 156 | + |
| 157 | + if err := os.Rename(oldLogFileInstance, newLogFileInstance); err != nil && !os.IsNotExist(err) { |
| 158 | + return fmt.Errorf("error renaming %d instance to %d of log-file: %s", (i - 1), i, r.basePath) |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + // create the first log-file |
| 163 | + if err := os.WriteFile(r.instanceFileName(1), []byte(""), 0660); err != nil { |
| 164 | + return fmt.Errorf("error creating the first instance of the log-file: %s", r.basePath) |
| 165 | + } |
| 166 | + |
| 167 | + return nil |
| 168 | + |
| 169 | +} |
| 170 | + |
| 171 | +func (r *RotatingLogger) checkIfLogFileSizeExceeded(messageLineLength int, logFile *os.File) (bool, error) { |
| 172 | + |
| 173 | + fileStat, err := logFile.Stat() |
| 174 | + if err != nil { |
| 175 | + return false, err |
| 176 | + } |
| 177 | + |
| 178 | + return ((fileStat.Size() + int64(messageLineLength)) >= int64(r.maxFileSize)), nil |
| 179 | + |
| 180 | +} |
| 181 | + |
| 182 | +func (r *RotatingLogger) checkIfMessageShouldBeAppended(logFilePath, currentLogLine string, deduplicateLine bool) (bool, error) { |
| 183 | + |
| 184 | + if !deduplicateLine { |
| 185 | + return true, nil |
| 186 | + } |
| 187 | + |
| 188 | + lastLogLine, err := r.getLogLastLine(logFilePath) |
| 189 | + |
| 190 | + if err != nil { |
| 191 | + return false, err |
| 192 | + } |
| 193 | + |
| 194 | + cleanedCurrentLogLine := strings.ReplaceAll(strings.TrimSpace(currentLogLine), "\n", " ") |
| 195 | + cleanedLastLogLine := strings.ReplaceAll(strings.TrimSpace(lastLogLine), "\n", " ") |
| 196 | + |
| 197 | + if cleanedCurrentLogLine != cleanedLastLogLine { |
| 198 | + return true, nil |
| 199 | + } else { |
| 200 | + return false, nil |
| 201 | + } |
| 202 | + |
| 203 | +} |
| 204 | + |
| 205 | +func (r *RotatingLogger) getLogLastLine(logFilePath string) (string, error) { |
| 206 | + |
| 207 | + logFile, err := os.Open(logFilePath) |
| 208 | + |
| 209 | + if err != nil { |
| 210 | + return "", err |
| 211 | + } |
| 212 | + |
| 213 | + defer func() { |
| 214 | + if err := logFile.Close(); err != nil { |
| 215 | + fmt.Printf("error closing logfile: %s", logFilePath) |
| 216 | + } |
| 217 | + }() |
| 218 | + |
| 219 | + lineCharsBackwards := []byte{} |
| 220 | + char := make([]byte, 1) |
| 221 | + |
| 222 | + for pos, err := logFile.Seek(0, io.SeekEnd); pos > 0; pos, err = logFile.Seek(-1, io.SeekCurrent) { |
| 223 | + |
| 224 | + if err != nil { |
| 225 | + return "", fmt.Errorf("seek failed: %w", err) |
| 226 | + } |
| 227 | + |
| 228 | + n, err := logFile.ReadAt(char, pos-1) |
| 229 | + if err != nil { |
| 230 | + return "", fmt.Errorf("read failed: %w", err) |
| 231 | + } |
| 232 | + |
| 233 | + if n < 1 { |
| 234 | + return "", fmt.Errorf("unexpectedly read 0 bytes") |
| 235 | + } |
| 236 | + |
| 237 | + if char[0] == '\n' || char[0] == '\r' { |
| 238 | + if len(lineCharsBackwards) > 0 { |
| 239 | + break |
| 240 | + } |
| 241 | + continue |
| 242 | + } |
| 243 | + |
| 244 | + lineCharsBackwards = append(lineCharsBackwards, char...) |
| 245 | + |
| 246 | + } |
| 247 | + |
| 248 | + // reverse the slice in place |
| 249 | + slices.Reverse(lineCharsBackwards) |
| 250 | + |
| 251 | + return string(lineCharsBackwards), nil |
| 252 | + |
| 253 | +} |
0 commit comments