Skip to content
Merged
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,57 @@ jobs:
cmake --build build-package --config "$config"
ctest --test-dir build-package -C "$config" --output-on-failure
done

coverage:
# Which lines of the library (ASWLog/) the tests run, on Linux: a table on this job's summary page, and an HTML
# report (each source line marked) as an artifact. Informational: continue-on-error keeps it from failing the run;
# the jobs above are the ones that must pass.
name: Linux (GCC, coverage)
runs-on: ubuntu-latest
timeout-minutes: 15
continue-on-error: true
env:
CMAKE_BUILD_PARALLEL_LEVEL: 4
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
submodules: true

- name: Configure
# Debug (no optimization), so each line is counted as written. -fprofile-update=atomic keeps the counts right
# in the tests that log from several threads at once; without it the threads' updates race and get lost.
env:
CC: gcc
CXX: g++
CXXFLAGS: "--coverage -fprofile-update=atomic"
run: cmake -S tests/cmake -B build -DCMAKE_BUILD_TYPE=Debug

- name: Build
run: cmake --build build

- name: Run unit tests
# A failing test fails the jobs above; the coverage of this run is still worth reporting
continue-on-error: true
run: ./build/bin/ASWLogTests --test-timeout-seconds 120 --catch-crashes

- name: Install gcovr
# 8.4 added the Markdown report. pipx installs it in an environment of its own (the runner's system Python
# refuses pip installs).
run: pipx install "gcovr>=8.4"

- name: Coverage report
# The crash handlers' tests crash child processes, which end before they can write their counts, so the crash
# paths show as not run.
run: |
mkdir -p coverage-html
gcovr --root . --filter 'ASWLog/' --print-summary --markdown coverage.md \
--html-details coverage-html/index.html
cat coverage.md >> "$GITHUB_STEP_SUMMARY"

- name: Upload coverage report
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: coverage-html/
if-no-files-found: error
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ These instructions apply to the entire repository unless a more specific `AGENTS

## Repository Overview

ASWLog is a thread-safe light-weight C++ logging tool for Windows and Linux projects.
ASWLog is a thread-safe, dependency-free C++20 logging library for Windows and Linux.

- `ASWLog/` contains the framework implementation.
- `tests/` contains the unit tests: test modules (`Test_ASWLog_<Unit>`, the tests of `ASWLog_<Unit>`) and shared test helpers (`UT_Helper_<Topic>`, e.g. `UT_Helper_DateTime`) in `tests/tests/`, their CMake project in `tests/cmake/`, their RAD Studio console project in `tests/rad370/`, and their RAD Studio VCL GUI runner project in `tests/vcl/gui/rad370/` (built from the submodule's `vcl/gui/src`). `tests/compile_checks/` holds "must not compile" checks, which `tests/cmake/CMakeLists.txt` runs with `try_compile` when configuring (a failed check stops the configure). `tests/package/` is a project that builds against an installed ASWLog with `find_package` (CI installs the library and runs it).
- `CMakeLists.txt` (the root) defines the `ASWLog::ASWLog` library target and its install rules; `cmake/ASWLogConfig.cmake.in` is the template of the installed package file. The project version is read from `ASWLog/ASWLog_Version.h`.
- `third_party/asw-unit-tests/` is the ASWUnitTests framework, a git submodule pinned to a release tag. Never edit files in it; test modules self-register with `ASW_REGISTER_TEST_GROUP`, so adding a test only touches `tests/` and its build files.
- `.github/workflows/ci.yml` builds and runs the unit tests on Windows (MSVC, MinGW) and Linux (GCC, Clang), plus Linux Clang builds with ThreadSanitizer and with AddressSanitizer + UBSan (`ASWLOG_SANITIZE`); the non-sanitizer jobs also check the installed CMake package (`tests/package/`).
- `.github/workflows/ci.yml` builds and runs the unit tests on Windows (MSVC, MinGW) and Linux (GCC, Clang), plus Linux Clang builds with ThreadSanitizer and with AddressSanitizer + UBSan (`ASWLOG_SANITIZE`); the non-sanitizer jobs also check the installed CMake package (`tests/package/`). A Linux GCC coverage job (informational, it never fails the run) shows the library's line coverage on its summary page and uploads an HTML report.
- `example/` contains example app that uses the logger.
- `example/rad370/` contains the RAD Studio 13.1 project and Windows build scripts.
- `README.md` contains repo details.
- `README.md` contains repo details: features, quick start, project setup, API overview, versions and tests.
- `docs/` contains the reference documentation: `Configuration.md` (every `TASWLogConfig` setting, its default and which loggers use it, and the environment variables), `Guarantees.md` (thread safety, order, callback threads, lifetimes, crash durability) and `CustomLoggers.md` (custom formatters and loggers).
- `CHANGELOG.md` records notable changes per release (see Changelog below).
- `CONTRIBUTING.md` contains the guidelines for contributors (issues, branches, commit history, pull requests, and bug reports); it defers to this file for coding rules, so keep the two consistent.
- `.uncrustify.cfg` and `.githooks/` define the repository formatting workflow.
Expand Down Expand Up @@ -54,6 +55,7 @@ Prefer standard C++ and portable library facilities. Avoid compiler-, IDE-, or o
- Follow the surrounding naming, indentation, brace, include, and comment style. Avoid unrelated formatting changes.
- Keep declarations and definitions consistent, and update all relevant project or build files when adding source files.
- Add or update focused tests for behavior changes, including error and platform-specific cases where applicable.
- Keep the documentation in step: a new or changed `TASWLogConfig` setting or environment key goes in `docs/Configuration.md`, a new logger in the README's API overview, and a change to threading, lifetimes or durability in `docs/Guarantees.md`. Add a new docs file to `Deploy.bat`.

## Building and Testing

Expand Down
6 changes: 5 additions & 1 deletion ASWLog/ASWLog_Base.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
ASWLog_Base.h
Author: Anthony S. West - ASW Software

A light-weight logging tool.
A logging library.

TASWLogBase, the base of the built-in loggers: the minimum level, the
enabled flag, the backtrace, the config and the error reporting that every
logger shares.

Requires C++ 20 or higher.

Expand Down
5 changes: 4 additions & 1 deletion ASWLog/ASWLog_CategoryLog.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
ASWLog_CategoryLog.h
Author: Anthony S. West - ASW Software

A light-weight logging tool.
A logging library.

TASWCategoryLog, a named category of entries (e.g. "Net") with a minimum
level of its own, which wraps another logger.

Requires C++ 20 or higher.

Expand Down
19 changes: 17 additions & 2 deletions ASWLog/ASWLog_Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
ASWLog_Config.h
Author: Anthony S. West - ASW Software

A light-weight logging tool.
A logging library.

Source for the ASWLog config options.
TASWLogConfig, the settings of a logger, and the structs that group them
(line layout, file, asynchronous writing, backtrace, startup and shutdown
lines, and the settings of the system loggers).

Requires C++ 20 or higher.

Expand Down Expand Up @@ -175,6 +177,13 @@ struct TASWFileConfig
// Rolls the file over at midnight, and at Initialize() if the file was last written on an earlier day, both in
// Line.TimestampZone (UTC by default). The backup is named for the day it holds.
bool EnableDailyRolling = false;
// Starts each file the logger begins after Initialize() (by rotation, daily rolling or Reconfigure() to a new path)
// with a line saying where the log continues from, e.g. "Log continued from aswlog.size.2026-10-10_143005_123.bak
// (size)" or "Log continued from logs\old.txt (reconfigure)", followed by the startup lines as Initialize() writes
// them (see TASWStartupConfig, whose settings still apply), so each file says what wrote it. They are Info lines,
// written before the entry that started the file. Not after a failed rotation, and with AutoOpenClosePerWrite, only
// by the process that rotated the shared log.
bool WriteStartupLinesInNewFiles = true;

// --- Backup Event ---
// Called once for each backup that rotation makes, e.g. to compress it, upload it or move it elsewhere. Called
Expand Down Expand Up @@ -299,10 +308,16 @@ struct TASWShutdownConfig
/////////////////////////////////////////////////////////////////////////////
struct TASWStartupConfig
{
// The application's version, written on the "App:" line as app_version='...' (see WriteApplicationInfo). Empty: on
// Windows, the executable's file version from its version resource, if it has one (see GetFileVersionString()).
std::string ApplicationVersion;
std::string Banner; // Written first, if set
bool WriteApplicationInfo = true;
bool WriteCommandLine = false; // Added to the application info line
bool WriteDriveInfo = true;
// "ASWLog: version=<ASWLOG_VERSION_STRING>, logger=<class>", the first line after the banner, so a log says which
// ASWLog wrote it
bool WriteLoggerVersion = true;
bool WriteMemoryUsage = true;
bool WriteOSInfo = true;
bool WriteSystemMemoryInfo = true;
Expand Down
5 changes: 4 additions & 1 deletion ASWLog/ASWLog_ConsoleLog.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
ASWLog_ConsoleLog.h
Author: Anthony S. West - ASW Software

A light-weight logging tool.
A logging library.

TASWConsoleLog, a text logger that writes to the console (stdout, and
stderr for warnings and errors), optionally in color.

Requires C++ 20 or higher.

Expand Down
5 changes: 4 additions & 1 deletion ASWLog/ASWLog_CrashHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
ASWLog_CrashHandler.h
Author: Anthony S. West - ASW Software

A light-weight logging tool.
A logging library.

Crash handlers that flush every text logger and write a "Crash: <reason>"
line when the application crashes (InstallCrashHandlers(), HandleCrash()).

Requires C++ 20 or higher.

Expand Down
5 changes: 4 additions & 1 deletion ASWLog/ASWLog_DebuggerLog.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
ASWLog_DebuggerLog.h
Author: Anthony S. West - ASW Software

A light-weight logging tool.
A logging library.

TASWDebuggerLog, a text logger that writes to the debugger's output on
Windows (OutputDebugStringW); it writes nothing on other platforms.

Requires C++ 20 or higher.

Expand Down
5 changes: 3 additions & 2 deletions ASWLog/ASWLog_Fields.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
ASWLog_Fields.h
Author: Anthony S. West - ASW Software

A light-weight logging tool.
A logging library.

Source for the ASWLog structured fields and scoped context.
Structured fields: TASWLogField, a named value attached to one entry, and
TASWLogScope, context that every entry logged on the thread carries.

Requires C++ 20 or higher.

Expand Down
79 changes: 67 additions & 12 deletions ASWLog/ASWLog_FileLog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,8 @@ TASWFileLog::~TASWFileLog()
//---------------------------------------------------------------------------
void TASWFileLog::AfterEntryUnlocked()
{
// In this mode the file is only open while an entry (or Initialize()'s startup lines) is written. Unlike Close(),
// this leaves the logger initialized.
// In this mode the file is only open while an entry (or the startup lines) is written. Unlike Close(), this leaves
// the logger initialized.
if (GetConfigUnlocked().File.AutoOpenClosePerWrite)
CloseFileUnlocked();
}
Expand Down Expand Up @@ -704,6 +704,9 @@ bool TASWFileLog::InitializeUnlocked()
if (GetConfigUnlocked().File.EnableDailyRolling)
RotateDailyLogFromEarlierDayUnlocked();

// Initialize() writes the startup lines itself, and Reconfigure() its own continuation line
m_PendingContinuationLine.clear();

if (!OpenUnlocked())
{
return false;
Expand Down Expand Up @@ -792,15 +795,19 @@ bool TASWFileLog::OpenUnlocked()

m_IsOpen.store(true, std::memory_order_release);
m_IsInitialized.store(true, std::memory_order_release);

WriteNewFileStartupLinesUnlocked();

return true;
}

//---------------------------------------------------------------------------
/*
TASWFileLog::PrepareWriteUnlocked

Rotates the log file if the day has changed or the file has reached its maximum size, as configured. Returns false
if the file isn't open (e.g. it couldn't be reopened after a rotation), which drops the line.
Rotates the log file if the day has changed or the file has reached its maximum size, as configured, but not while
the startup lines are written (the new file's own, or Initialize()'s): the next entry rotates it. Returns false if
the file isn't open (e.g. it couldn't be reopened after a rotation), which drops the line.
*/
bool TASWFileLog::PrepareWriteUnlocked(std::chrono::system_clock::time_point now)
{
Expand All @@ -809,7 +816,7 @@ bool TASWFileLog::PrepareWriteUnlocked(std::chrono::system_clock::time_point now
// Only rolls forward (ISO dates compare as text): an entry stamped just before midnight can get the lock after
// another thread's entry has rolled the log over, and then goes into the new day's log
const auto currentDateStr = Time::ToDateString(now, GetConfigUnlocked().Line.TimestampZone);
if (currentDateStr > m_LastLogDateStr)
if (currentDateStr > m_LastLogDateStr && !IsWritingStartupLinesUnlocked())
{
// Name the backup for the day its content is from, not the day that just started. A log shared with other
// processes may already have been rolled over by one of them, so there the file's last write decides.
Expand All @@ -825,7 +832,8 @@ bool TASWFileLog::PrepareWriteUnlocked(std::chrono::system_clock::time_point now
// Uses the size tracked by the stream, since on Windows the size read through the path isn't current while open.
// After a failed rotation (e.g. another program holds the file), waits RotationRetryDelay before trying again.
if (GetConfigUnlocked().File.EnableRotation && m_FileStream.IsOpen() && m_FileStream.GetSize() >= GetConfigUnlocked().File.MaxFileSizeBytes &&
IsRetryDue(m_LastRotationFailure, now, GetConfigUnlocked().File.RotationRetryDelay))
IsRetryDue(m_LastRotationFailure, now, GetConfigUnlocked().File.RotationRetryDelay) &&
!IsWritingStartupLinesUnlocked())
{
RotateLogFilesUnlocked("size", ToBackupTimeLabel(now, GetConfigUnlocked().Line.TimestampZone));
}
Expand All @@ -838,9 +846,11 @@ bool TASWFileLog::PrepareWriteUnlocked(std::chrono::system_clock::time_point now
TASWFileLog::ReconfigureUnlocked

If the file path or AutoOpenClosePerWrite changed, closes the old file and starts on the new one as Initialize()
does (rolling over a file from an earlier day, if daily rolling is on, then opening it). With AutoOpenClosePerWrite
the new file is closed again until the next entry, but opening it here reports a file that can't be opened. A
failed open keeps the logger initialized, so later entries try again (see EnsureReadyUnlocked()).
does (rolling over a file from an earlier day, if daily rolling is on, then opening it). A new path starts with
the startup lines (see WriteNewFileStartupLinesUnlocked()). With AutoOpenClosePerWrite the new file is closed again
until the next entry, but opening it here reports a file that can't be opened. A failed open keeps the logger
initialized, so later entries try again (see EnsureReadyUnlocked()), and the first one that opens the file writes
the startup lines.

Otherwise the file stays open: flushes what the previous flush mode may have buffered, and the other settings take
effect with the next entry (a FlushMode::Periodic thread is started, stopped or given its new interval after the
Expand All @@ -853,8 +863,9 @@ bool TASWFileLog::ReconfigureUnlocked(const TASWLogConfig& previous)
const auto& file = config.File;
m_SyncsCrashLine.store(Level::Critical >= file.SyncToDiskAtLevel, std::memory_order_relaxed);

if (file.ResolvePath() == previous.File.ResolvePath() &&
file.AutoOpenClosePerWrite == previous.File.AutoOpenClosePerWrite)
const auto previousPath = previous.File.ResolvePath();
const bool isNewPath = file.ResolvePath() != previousPath;
if (!isNewPath && file.AutoOpenClosePerWrite == previous.File.AutoOpenClosePerWrite)
{
if (config.Line.TimestampZone != previous.Line.TimestampZone)
m_LastLogDateStr = Time::ToDateString(NowUTC(), config.Line.TimestampZone);
Expand All @@ -867,6 +878,13 @@ bool TASWFileLog::ReconfigureUnlocked(const TASWLogConfig& previous)

CloseFileUnlocked();
const bool isOpen = InitializeUnlocked();

if (isNewPath && file.WriteStartupLinesInNewFiles)
{
m_PendingContinuationLine = std::format("Log continued from {} (reconfigure)", PathToUTF8String(previousPath));
WriteNewFileStartupLinesUnlocked();
}

AfterEntryUnlocked();

return isOpen;
Expand Down Expand Up @@ -913,7 +931,18 @@ bool TASWFileLog::RotateLogFiles(std::string_view reasonTag)
{
TDeferredWorkRunner deferredWorkRunner(*this);
std::lock_guard<std::mutex> lock(m_Mutex);
return RotateLogFilesUnlocked(reasonTag, ToBackupTimeLabel(NowUTC(), GetConfigUnlocked().Line.TimestampZone));
const auto timeLabel = ToBackupTimeLabel(NowUTC(), GetConfigUnlocked().Line.TimestampZone);
const bool isRotated = RotateLogFilesUnlocked(reasonTag, timeLabel);

// A file that is closed between entries (AutoOpenClosePerWrite) is opened for its startup lines now, so they come
// first even if another process writes to it next
if (isRotated && !m_PendingContinuationLine.empty() && m_IsInitialized.load(std::memory_order_acquire) &&
EnsureReadyUnlocked())
{
AfterEntryUnlocked();
}

return isRotated;
}

//---------------------------------------------------------------------------
Expand Down Expand Up @@ -994,6 +1023,13 @@ bool TASWFileLog::RotateLogFilesUnlocked(std::string_view reasonTag, std::string
return false;
}

// The next file starts with the startup lines, written once it is open (see WriteNewFileStartupLinesUnlocked())
if (!backupPath.empty() && GetConfigUnlocked().File.WriteStartupLinesInNewFiles)
{
m_PendingContinuationLine = std::format("Log continued from {} ({})", PathToUTF8String(backupPath.filename()),
reasonTag);
}

const bool isOpen = wasOpen ? OpenUnlocked() : true;

// OnBackupCreated calls into the application and the cleanup scans the folder, so both run after the lock is
Expand Down Expand Up @@ -1060,6 +1096,25 @@ void TASWFileLog::WriteLineUnlocked(Level level, std::string_view line, bool end
MaybeFlush(level, endsLine);
}

//---------------------------------------------------------------------------
/*
TASWFileLog::WriteNewFileStartupLinesUnlocked

Starts a file the logger began after Initialize() (see TASWFileConfig::WriteStartupLinesInNewFiles), once it is
open: writes the line saying where the log continues from, then the startup lines. Called by OpenUnlocked(), so a
rotation writes them before the entry that triggered it, and a file that couldn't be opened right away gets them
from the open that succeeds. Does nothing if no new file is waiting for them.
*/
void TASWFileLog::WriteNewFileStartupLinesUnlocked()
{
if (m_PendingContinuationLine.empty() || !m_FileStream.IsOpen())
return;

// Taken first, so that the lines are written once even if they fail
const auto continuationLine = std::exchange(m_PendingContinuationLine, std::string());
WriteStartupLinesUnlocked(continuationLine);
}

//---------------------------------------------------------------------------

} // namespace ASWLog
Loading
Loading