Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions LogMonitor/LogMonitorTests/JsonProcessorTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,60 @@ namespace LogMonitorTests
Assert::IsTrue(success2);
auto src2 = std::reinterpret_pointer_cast<SourceFile>(settings2.Sources[0]);
Assert::AreEqual(300.0, src2->WaitInSeconds);
Assert::IsFalse(src2->EnableTruncationRecovery);
}

///
/// enableTruncationRecovery must parse true/false correctly and default
/// to false when the key is absent.
///
TEST_METHOD(JsonProcessor_ParsesEnableTruncationRecovery)
{
// Explicit true
auto pathTrue = WriteTempConfig(R"({
"LogConfig": {
"sources": [{
"type": "File",
"directory": "C:\\logs",
"enableTruncationRecovery": true
}]
}
})");

LoggerSettings settingsTrue;
bool successTrue = ReadConfigFile((PWCHAR)pathTrue.c_str(), settingsTrue);

Assert::IsTrue(successTrue);
Assert::AreEqual((size_t)1, settingsTrue.Sources.size());
Assert::AreEqual((int)LogSourceType::File,
(int)settingsTrue.Sources[0]->Type);
{
auto src = std::reinterpret_pointer_cast<SourceFile>(
settingsTrue.Sources[0]);
Assert::IsTrue(src->EnableTruncationRecovery);
}

// Explicit false
auto pathFalse = WriteTempConfig(R"({
"LogConfig": {
"sources": [{
"type": "File",
"directory": "C:\\logs",
"enableTruncationRecovery": false
}]
}
})");

LoggerSettings settingsFalse;
bool successFalse = ReadConfigFile((PWCHAR)pathFalse.c_str(), settingsFalse);

Assert::IsTrue(successFalse);
Assert::AreEqual((size_t)1, settingsFalse.Sources.size());
{
auto src = std::reinterpret_pointer_cast<SourceFile>(
settingsFalse.Sources[0]);
Assert::IsFalse(src->EnableTruncationRecovery);
}
}

///
Expand Down
181 changes: 173 additions & 8 deletions LogMonitor/LogMonitorTests/LogFileMonitorTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,43 @@ namespace LogMonitorTests
return (success)? 0 : GetLastError();
}

///
/// Overwrites a file from offset 0, truncating any existing content.
/// Used to simulate log-rotation-in-place for truncation recovery tests.
///
DWORD TruncateAndWriteFile(
const std::wstring& FileName,
_In_reads_bytes_opt_(BufferSize) LPCVOID Buffer,
_In_ size_t BufferSize
)
{
HANDLE hFile = CreateFile(
FileName.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH,
NULL);

if (hFile == INVALID_HANDLE_VALUE)
{
return GetLastError();
}

DWORD bytesWritten;
bool success = WriteFile(
hFile,
Buffer,
(DWORD)BufferSize,
&bytesWritten,
nullptr);

CloseHandle(hFile);

return (success)? 0 : GetLastError();
}

public:

///
Expand Down Expand Up @@ -176,7 +213,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -224,7 +261,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -293,7 +330,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -396,7 +433,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -572,7 +609,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -678,7 +715,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -798,7 +835,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -989,7 +1026,7 @@ namespace LogMonitorTests
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"");
std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories, sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
Expand Down Expand Up @@ -1062,5 +1099,133 @@ namespace LogMonitorTests
Assert::IsTrue(output.find(TO_WSTR(content)) != std::wstring::npos);
}
}

///
/// Check that when EnableTruncationRecovery is true, a file truncated
/// in-place (size smaller than last read offset) is re-read from offset 0
/// so that new content is emitted to stdout.
///
TEST_METHOD(TestTruncationRecovery_Enabled)
{
std::wstring output;

std::wstring tempDirectory = CreateTempDirectory();
Assert::IsFalse(tempDirectory.empty());

directoriesToDeleteAtCleanup.push_back(tempDirectory);

SourceFile sourceFile;
sourceFile.Directory = tempDirectory;
sourceFile.EnableTruncationRecovery = true;

fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(
sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories,
sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
// Write initial content (200 bytes) to a new file. The monitor reads
// it, advancing NextReadOffset to 200.
//
std::wstring filename = sourceFile.Directory + L"\\rotate.log";
std::string initialContent(200, 'A');
WriteToFile(filename, initialContent.c_str(), initialContent.length());

{
int retries = 0;
do {
retries++;
Sleep(WAIT_TIME_LOGFILEMONITOR_AFTER_WRITE_SHORT);
output = RecoverOuput();
} while (output.empty() && retries < READ_OUTPUT_RETRIES);

Assert::IsFalse(output.empty());
}

//
// Truncate the file and write shorter replacement content (14 bytes).
// fileSize(14) < NextReadOffset(200), so recovery resets to offset 0
// and the new content is emitted.
//
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::string newContent = "AFTER_ROTATION";
TruncateAndWriteFile(filename, newContent.c_str(), newContent.length());

{
int retries = 0;
do {
retries++;
Sleep(WAIT_TIME_LOGFILEMONITOR_AFTER_WRITE_SHORT);
output = RecoverOuput();
} while (output.empty() && retries < READ_OUTPUT_RETRIES);

Assert::IsTrue(output.find(TO_WSTR(newContent)) != std::wstring::npos);
}
}

///
/// Check that when EnableTruncationRecovery is false, a file truncated
/// in-place is not re-read from offset 0 — the monitor attempts to read
/// from the stale offset and emits nothing.
///
TEST_METHOD(TestTruncationRecovery_Disabled)
{
std::wstring output;

std::wstring tempDirectory = CreateTempDirectory();
Assert::IsFalse(tempDirectory.empty());

directoriesToDeleteAtCleanup.push_back(tempDirectory);

SourceFile sourceFile;
sourceFile.Directory = tempDirectory;
sourceFile.EnableTruncationRecovery = false;

fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::shared_ptr<LogFileMonitor> logfileMon = std::make_shared<LogFileMonitor>(
sourceFile.Directory, sourceFile.Filter, sourceFile.IncludeSubdirectories,
sourceFile.WaitInSeconds, L"json", L"", sourceFile.EnableTruncationRecovery);
Sleep(WAIT_TIME_LOGFILEMONITOR_START);

//
// Write initial content (200 bytes). Monitor reads it, NextReadOffset = 200.
//
std::wstring filename = sourceFile.Directory + L"\\rotate.log";
std::string initialContent(200, 'B');
WriteToFile(filename, initialContent.c_str(), initialContent.length());

{
int retries = 0;
do {
retries++;
Sleep(WAIT_TIME_LOGFILEMONITOR_AFTER_WRITE_SHORT);
output = RecoverOuput();
} while (output.empty() && retries < READ_OUTPUT_RETRIES);

Assert::IsFalse(output.empty());
}

//
// Truncate and write shorter content. Without recovery, the monitor
// reads from offset 200 on a 16-byte file and gets EOF — no output.
//
fflush(stdout);
ZeroMemory(bigOutBuf, sizeof(bigOutBuf));

std::string newContent = "SKIPPED_ROTATION";
TruncateAndWriteFile(filename, newContent.c_str(), newContent.length());

Sleep(WAIT_TIME_LOGFILEMONITOR_AFTER_WRITE_LONG);

output = RecoverOuput();
Assert::AreEqual(L"", output.c_str());
}
};
}
1 change: 1 addition & 0 deletions LogMonitor/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ This will monitor any changes in log files matching a specified filter, given th
- `filter` (optional): uses [MS-DOS wildcard match type](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/indexsrv/ms-dos-and-windows-wildcard-characters) i.e.. `*, ?`. Can be set to empty, which will be default to `"*"`.
- `includeSubdirectories` (optional) : `"true|false"`, specify if sub-directories also need to be monitored. Defaults to `false`.
- `includeFileNames` (optional): `"true|false"`, specifies whether to include file names in the logline, eg. `sample.log: xxxxx`. Defaults to `false`.
- `enableTruncationRecovery` (optional): `"true|false"`, when `true` the monitor detects if a log file has been truncated in-place (its current size is smaller than the last read position) and resets the read offset to the beginning of the file so new content is not missed. This is useful when a log rotation strategy rewrites the same file rather than renaming it. Defaults to `false`.
- `waitInSeconds` (optional): specifies the duration to wait for a file or folder to be created if it does not exist. It takes integer values between 0-INFINITY. Defaults to `300` seconds, i.e, 5 minutes. It can be passed as a value or a string.

- `waitInSeconds = 0`
Expand Down
11 changes: 10 additions & 1 deletion LogMonitor/src/LogMonitor/JsonProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,14 @@ bool handleFileLog(
);
}

if (source.contains("enableTruncationRecovery") &&
source["enableTruncationRecovery"].is_boolean()) {
Attributes[JSON_TAG_ENABLE_TRUNCATION_RECOVERY] = reinterpret_cast<void*>(
std::make_unique<bool>(
source["enableTruncationRecovery"].get<bool>()).release()
);
}

auto sourceFile = std::make_shared<SourceFile>();
if (!SourceFile::Unwrap(Attributes, *sourceFile)) {
logWriter.TraceError(L"Error parsing configuration file. Invalid File source");
Expand Down Expand Up @@ -527,7 +535,8 @@ void cleanupAttributes(_In_ AttributesMap& Attributes) {

if (key == JSON_TAG_START_AT_OLDEST_RECORD ||
key == JSON_TAG_FORMAT_MULTILINE ||
key == JSON_TAG_INCLUDE_SUBDIRECTORIES) {
key == JSON_TAG_INCLUDE_SUBDIRECTORIES ||
key == JSON_TAG_ENABLE_TRUNCATION_RECOVERY) {
delete static_cast<bool*>(attributePair.second);
} else if (key == JSON_TAG_CUSTOM_LOG_FORMAT ||
key == JSON_TAG_DIRECTORY ||
Expand Down
29 changes: 23 additions & 6 deletions LogMonitor/src/LogMonitor/LogFileMonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,27 @@ using namespace std;
/// that thread registers for directory change notifications. This ensures that no
/// changes to log files are missed once the LogFileMonitor object is created.
///
/// \param LogDirectory: The log directory to be monitored
/// \param Filter: The filter to apply when looking fr log files
/// \param IncludeSubfolders: TRUE if subdirectories also needs to be monitored
/// \param WaitInSeconds: Waiting time in seconds to retry if folder/file to be monitored does not exist
/// \param LogDirectory: The log directory to be monitored
/// \param Filter: The filter to apply when looking fr log files
/// \param IncludeSubfolders: TRUE if subdirectories also needs to be monitored
/// \param WaitInSeconds: Waiting time in seconds to retry if folder/file to be monitored does not exist
/// \param EnableTruncationRecovery: TRUE if detection and handling of truncated files is desired
///
LogFileMonitor::LogFileMonitor(_In_ const std::wstring& LogDirectory,
_In_ const std::wstring& Filter,
_In_ bool IncludeSubfolders,
_In_ const std::double_t& WaitInSeconds,
_In_ std::wstring LogFormat,
_In_ std::wstring CustomLogFormat = L""
_In_ std::wstring CustomLogFormat = L"",
_In_ bool EnableTruncationRecovery = false
) :
m_logDirectory(LogDirectory),
m_filter(Filter),
m_includeSubfolders(IncludeSubfolders),
m_waitInSeconds(WaitInSeconds),
m_logFormat(LogFormat),
m_customLogFormat(CustomLogFormat)
m_customLogFormat(CustomLogFormat),
m_enableTruncationRecovery(EnableTruncationRecovery)
{
m_stopEvent = NULL;
m_overlappedEvent = NULL;
Expand Down Expand Up @@ -1451,6 +1454,20 @@ LogFileMonitor::ReadLogFile(
return status;
}

if (m_enableTruncationRecovery)
{
LARGE_INTEGER fileSize = {};
if (GetFileSizeEx(logFile, &fileSize))
{
if (fileSize.QuadPart < LogFileInfo->NextReadOffset)
{
// File was truncated and rewritten in-place; read from beginning.
LogFileInfo->NextReadOffset = 0;
LogFileInfo->LastReadTimestamp = 0;
}
}
}

DWORD dwPtr = SetFilePointer(logFile, 0L, NULL, FILE_BEGIN);
if (dwPtr == INVALID_SET_FILE_POINTER)
{
Expand Down
Loading