2025-01-12 18:08:57 +01:00
|
|
|
#include "util/LogRotate.h"
|
|
|
|
|
#include "util/ILog.h"
|
2024-12-31 22:00:20 +01:00
|
|
|
#include <ctime>
|
|
|
|
|
|
|
|
|
|
#define FILE_PREFIX "log_"
|
|
|
|
|
|
|
|
|
|
LogRotate::LogRotate(fs::FS &fs, const char *logDir, uint32_t maxLen, uint32_t maxSize, uint32_t maxFiles, uint32_t maxFileSize)
|
|
|
|
|
: c_maxLen(maxLen), c_maxSize(maxSize), c_maxFiles(maxFiles), c_maxFileSize(maxFileSize), _fs(fs), rootDirName(logDir),
|
|
|
|
|
numFiles(0), minLogNum(0), maxLogNum(0), currentLogRead(0), currentLogWrite(0), currentSize(0), totalSize(0)
|
2025-01-05 22:36:45 +01:00
|
|
|
|
2024-12-31 22:00:20 +01:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LogRotate::init(void)
|
|
|
|
|
{
|
|
|
|
|
if (!_fs.exists(rootDirName)) {
|
|
|
|
|
_fs.mkdir(rootDirName);
|
|
|
|
|
ILOG_INFO("LogRotate: no log files found.");
|
|
|
|
|
} else {
|
|
|
|
|
scanLogDir(numFiles, minLogNum, maxLogNum, currentSize, totalSize);
|
|
|
|
|
currentLogRead = minLogNum;
|
|
|
|
|
currentLogWrite = maxLogNum;
|
|
|
|
|
ILOG_INFO("LogRotate: found %d log files using %d bytes (%d%%).", numFiles, totalSize, (totalSize * 100) / c_maxSize);
|
|
|
|
|
if (currentSize > c_maxFileSize - c_maxLen) {
|
|
|
|
|
ILOG_DEBUG("currentSize(%d) > c_maxFileSize(%d) - c_maxLen(%d)", currentSize, c_maxFileSize, c_maxLen);
|
|
|
|
|
ILOG_DEBUG("numFiles(%d) > c_maxFiles(%d) || totalSize(%d) >= c_maxSize(%d)", numFiles, c_maxFiles, totalSize,
|
|
|
|
|
c_maxSize);
|
|
|
|
|
while ((numFiles > c_maxFiles || totalSize >= c_maxSize) && removeLog())
|
|
|
|
|
;
|
|
|
|
|
numFiles++;
|
|
|
|
|
currentLogWrite++;
|
|
|
|
|
currentSize = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (minLogNum == 0) {
|
|
|
|
|
numFiles = 1;
|
|
|
|
|
minLogNum = 1;
|
|
|
|
|
currentLogRead = 0;
|
|
|
|
|
currentLogWrite = 1;
|
|
|
|
|
}
|
|
|
|
|
currentLogName = logFileName(currentLogWrite);
|
|
|
|
|
ILOG_INFO("Logging to %s", currentLogName.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* retrieve next entry from list of logFiles
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
bool LogRotate::readNext(ILogEntry &entry)
|
|
|
|
|
{
|
|
|
|
|
if (!rootDir) {
|
|
|
|
|
rootDir = _fs.open(rootDirName);
|
|
|
|
|
if (!rootDir)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!currentFile) {
|
|
|
|
|
if (currentLogRead == 0 || currentLogRead > maxLogNum)
|
|
|
|
|
return false;
|
|
|
|
|
do {
|
|
|
|
|
currentFile = _fs.open(logFileName(currentLogRead), FILE_READ);
|
|
|
|
|
if (currentFile && currentFile.available()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
currentFile.close();
|
|
|
|
|
currentLogRead++;
|
|
|
|
|
} while (currentLogRead <= maxLogNum);
|
|
|
|
|
if (!currentFile || !currentFile.available() || currentLogRead > maxLogNum) {
|
|
|
|
|
rootDir.close();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
ILOG_DEBUG("-> reading %s (%d bytes)", currentFile.name(), currentFile.size());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// elegant way to let the logentry do its work it knows best and pass just a temporary function for reading
|
|
|
|
|
if (!entry.deserialize([this](uint8_t *buf, size_t size) { return this->currentFile.read(buf, size); })) {
|
|
|
|
|
currentFile.close();
|
|
|
|
|
currentLogRead++;
|
|
|
|
|
return readNext(entry);
|
|
|
|
|
} else
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool LogRotate::write(const ILogEntry &entry)
|
|
|
|
|
{
|
|
|
|
|
time_t start = millis();
|
|
|
|
|
if (currentSize + entry.size() >= c_maxFileSize || totalSize + entry.size() >= c_maxSize) {
|
|
|
|
|
// log rotation
|
|
|
|
|
ILOG_DEBUG("LogRotation: %d >= %d || %d >= %d", currentSize + entry.size(), c_maxFileSize, totalSize + entry.size(),
|
|
|
|
|
c_maxSize);
|
|
|
|
|
numFiles++;
|
|
|
|
|
currentSize = 0;
|
|
|
|
|
currentLogWrite++;
|
|
|
|
|
currentLogName = logFileName(currentLogWrite);
|
|
|
|
|
while ((numFiles >= c_maxFiles || totalSize + entry.size() > c_maxSize) && removeLog())
|
|
|
|
|
;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// elegant way to let the logentry do its work it knows best and pass just a temporary function for writing
|
|
|
|
|
File file = _fs.open(currentLogName, FILE_APPEND);
|
|
|
|
|
entry.serialize([&file](const uint8_t *buf, size_t size) { return file.write(buf, size); });
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
|
|
currentSize += entry.size();
|
|
|
|
|
totalSize += entry.size();
|
|
|
|
|
|
2025-08-07 23:34:09 +02:00
|
|
|
// ILOG_DEBUG("LogRotate: %d bytes written in %d ms to %s (%d/%d bytes, total: %d)", entry.size(), millis() - start,
|
|
|
|
|
// currentLogName.c_str(), currentSize, c_maxFileSize, totalSize);
|
2024-12-31 22:00:20 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* remove all log files
|
|
|
|
|
*/
|
|
|
|
|
bool LogRotate::clear(void)
|
|
|
|
|
{
|
|
|
|
|
time_t start = millis();
|
|
|
|
|
File root = _fs.open(rootDirName);
|
|
|
|
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
bool error = false;
|
|
|
|
|
File file = root.openNextFile();
|
|
|
|
|
while (file) {
|
2024-12-31 23:50:20 +01:00
|
|
|
String name;
|
|
|
|
|
if (file.name()[0] != '/')
|
|
|
|
|
name = rootDirName + '/' + file.name();
|
|
|
|
|
else
|
|
|
|
|
name = file.name();
|
2024-12-31 22:00:20 +01:00
|
|
|
if (!file.isDirectory()) {
|
|
|
|
|
file.close();
|
|
|
|
|
if (_fs.remove(name)) {
|
|
|
|
|
count++;
|
|
|
|
|
ILOG_DEBUG("removed %s", name.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
ILOG_ERROR("failed to remove %s!", name.c_str());
|
|
|
|
|
error = true;
|
|
|
|
|
}
|
2025-01-05 22:36:45 +01:00
|
|
|
} else {
|
2024-12-31 22:00:20 +01:00
|
|
|
file.close();
|
|
|
|
|
}
|
|
|
|
|
file = root.openNextFile();
|
|
|
|
|
}
|
|
|
|
|
ILOG_DEBUG("removed %d logs in %d ms", count, millis() - start);
|
|
|
|
|
|
|
|
|
|
numFiles = 1;
|
|
|
|
|
minLogNum = 1;
|
|
|
|
|
currentLogWrite = 1;
|
|
|
|
|
currentSize = 0;
|
|
|
|
|
totalSize = 0;
|
|
|
|
|
currentLogName = logFileName(currentLogWrite);
|
|
|
|
|
return error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return number of log files
|
|
|
|
|
*/
|
2025-02-01 20:12:43 +01:00
|
|
|
uint32_t LogRotate::size(void) const
|
|
|
|
|
{
|
|
|
|
|
return totalSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return number of log files
|
|
|
|
|
*/
|
|
|
|
|
uint32_t LogRotate::count(void) const
|
2024-12-31 22:00:20 +01:00
|
|
|
{
|
|
|
|
|
return numFiles;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return current log number that is processed for reading
|
|
|
|
|
*/
|
2025-02-01 20:12:43 +01:00
|
|
|
uint32_t LogRotate::current(void) const
|
2024-12-31 22:00:20 +01:00
|
|
|
{
|
|
|
|
|
return currentLogRead;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate a log file name based on num
|
|
|
|
|
*/
|
|
|
|
|
String LogRotate::logFileName(uint32_t num)
|
|
|
|
|
{
|
|
|
|
|
char filename[40];
|
|
|
|
|
sprintf(filename, "%s/" FILE_PREFIX "%06d.log", rootDirName.c_str(), num);
|
|
|
|
|
return filename;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* remove the oldest log
|
|
|
|
|
*/
|
|
|
|
|
size_t LogRotate::removeLog(void)
|
|
|
|
|
{
|
|
|
|
|
ILOG_DEBUG("removeLog minLogNum=%d, numFiles=%d, totalSize=%d", minLogNum, numFiles, totalSize);
|
|
|
|
|
size_t size = 0;
|
|
|
|
|
if (minLogNum > 0) {
|
|
|
|
|
String log = logFileName(minLogNum);
|
|
|
|
|
File file = _fs.open(log, FILE_READ);
|
|
|
|
|
size = file.size();
|
|
|
|
|
file.close();
|
|
|
|
|
if (_fs.remove(log)) {
|
|
|
|
|
ILOG_DEBUG("removed %s, freeing %d bytes", log.c_str(), size);
|
|
|
|
|
totalSize -= size;
|
|
|
|
|
numFiles--;
|
|
|
|
|
} else {
|
|
|
|
|
ILOG_ERROR("failed to remove %s", log.c_str());
|
|
|
|
|
size = 0;
|
|
|
|
|
}
|
|
|
|
|
minLogNum++;
|
|
|
|
|
}
|
|
|
|
|
return size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Scan the log directory (rootDirName) for files and return the following
|
|
|
|
|
* @param num number of logs
|
|
|
|
|
* @param minLog log starting number
|
|
|
|
|
* @param maxLog log ending number
|
|
|
|
|
* @param logSize size of last log
|
|
|
|
|
* @param total total size of all logs
|
|
|
|
|
*/
|
|
|
|
|
void LogRotate::scanLogDir(uint32_t &num, uint32_t &minLog, uint32_t &maxLog, uint32_t &logSize, uint32_t &total)
|
|
|
|
|
{
|
|
|
|
|
num = maxLog = logSize = 0;
|
|
|
|
|
minLog = UINT32_MAX;
|
|
|
|
|
|
|
|
|
|
ILOG_DEBUG("scanning log folder %s", rootDirName.c_str());
|
|
|
|
|
if (!rootDir)
|
|
|
|
|
rootDir = _fs.open(rootDirName);
|
|
|
|
|
File file = rootDir.openNextFile();
|
|
|
|
|
while (file) {
|
|
|
|
|
if (!file.isDirectory()) {
|
|
|
|
|
num++;
|
|
|
|
|
size_t size = file.size();
|
|
|
|
|
total += size;
|
|
|
|
|
ILOG_DEBUG(" %s(%d bytes)", file.name(), size);
|
|
|
|
|
|
|
|
|
|
String format;
|
|
|
|
|
if (file.name()[0] == '/') {
|
|
|
|
|
format = rootDirName + '/' + String(FILE_PREFIX) + String("%u.log");
|
2025-01-05 22:36:45 +01:00
|
|
|
} else {
|
2024-12-31 22:00:20 +01:00
|
|
|
format = String(FILE_PREFIX) + String("%u.log");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t logNum = 0;
|
|
|
|
|
if (sscanf(file.name(), format.c_str(), &logNum) > 0 && logNum > 0) {
|
|
|
|
|
if (logNum < minLog) {
|
|
|
|
|
minLog = logNum;
|
|
|
|
|
}
|
|
|
|
|
if (logNum > maxLog) {
|
|
|
|
|
maxLog = logNum;
|
|
|
|
|
logSize = size;
|
|
|
|
|
}
|
2025-01-05 22:36:45 +01:00
|
|
|
} else {
|
2024-12-31 22:00:20 +01:00
|
|
|
ILOG_ERROR("sscanf() failed for %s", file.name());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
file = rootDir.openNextFile();
|
|
|
|
|
}
|
|
|
|
|
rootDir.close();
|
|
|
|
|
|
|
|
|
|
if (minLog == UINT32_MAX)
|
|
|
|
|
minLog = 0;
|
|
|
|
|
}
|