Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c6a8b63
Merge pull request #25 from ASWSoftware/asw/release-0.75.0
AnthonySWest Oct 6, 2026
9d063d8
Bump version to 0.85.0-dev.1
AnthonySWest Oct 6, 2026
275e0e1
Format the default line's fields without std::format
AnthonySWest Oct 6, 2026
2a7770d
Add local-time and microsecond timestamp options
AnthonySWest Oct 7, 2026
ce4f4b3
Update ASWUnitTests to 1.2.0 and use its new checks
AnthonySWest Oct 8, 2026
3bce151
Add logger categories with per-category levels
AnthonySWest Oct 8, 2026
67319a5
Add a line pattern formatter
AnthonySWest Oct 8, 2026
d7285dc
Update ASWUnitTests to 1.2.1
AnthonySWest Oct 9, 2026
f593a73
Add a JSON Lines formatter
AnthonySWest Oct 9, 2026
9ee00a5
Add structured fields and scoped context
AnthonySWest Oct 9, 2026
b9f58db
Add optional multi-line message handling
AnthonySWest Oct 9, 2026
3f88ac2
Add a hook to filter or change entries before they are written
AnthonySWest Oct 9, 2026
86a988d
Merge pull request #26 from ASWSoftware/asw/output-format-and-context
AnthonySWest Oct 10, 2026
e779bf3
Bump version to 0.85.0-dev.2
AnthonySWest Oct 10, 2026
2f88ff1
Add TASWNullLog
AnthonySWest Oct 10, 2026
5ccedc3
Add TASWMemoryLog
AnthonySWest Oct 10, 2026
4574fac
Add TASWDebuggerLog for OutputDebugString
AnthonySWest Oct 10, 2026
763efb7
Add TASWSyslogLog sink for Linux syslog
AnthonySWest Oct 10, 2026
db1c1b1
Add TASWWindowsEventLog sink for Windows Event Log
AnthonySWest Oct 10, 2026
cf7fbde
Add rate-limited logging helpers
AnthonySWest Oct 10, 2026
6aef36f
Configure loggers from environment variables
AnthonySWest Oct 10, 2026
5865b2e
Add wide-string logging overloads
AnthonySWest Oct 10, 2026
59f1aeb
Merge pull request #27 from ASWSoftware/asw/add-sinks-and-conveniences
AnthonySWest Oct 10, 2026
660b155
Bump version to 0.85.0-dev.3
AnthonySWest Oct 10, 2026
d562284
Build CI jobs in parallel and raise their timeouts
AnthonySWest Oct 10, 2026
2efd1eb
Add CMake install and package config
AnthonySWest Oct 10, 2026
42238c3
Merge pull request #28 from ASWSoftware/asw/cmake-install
AnthonySWest Oct 10, 2026
9a3539d
Update ASWLog description and add/update header descriptions
AnthonySWest Oct 11, 2026
b693e6c
Update documentation in prep for 1.0
AnthonySWest Oct 11, 2026
f094d20
Write the ASWLog version in the startup lines
AnthonySWest Oct 11, 2026
55c9e7e
Write the application's version in the startup lines
AnthonySWest Oct 11, 2026
0a5369b
Write the startup lines at the top of each new log file
AnthonySWest Oct 11, 2026
86d2fa3
Test that DeleteOldLogs deletes only matching files
AnthonySWest Oct 11, 2026
dc95cf9
Test backup name exhaustion and open retries
AnthonySWest Oct 11, 2026
ce50957
Test TASWFileStream's stream insertion
AnthonySWest Oct 11, 2026
17c9272
Remove GetApplicationInfoString()
AnthonySWest Oct 11, 2026
c5ce37d
Test TASWMultiLog's Open() and failing loggers, and command line star…
AnthonySWest Oct 11, 2026
cc73569
Test the remaining small edge cases
AnthonySWest Oct 11, 2026
f17a469
Add a coverage job to CI
AnthonySWest Oct 11, 2026
1a0c99e
Merge pull request #29 from ASWSoftware/asw/rounding-things-out
AnthonySWest Oct 11, 2026
5d9f8ce
Bump version to 1.0.0-dev.1
AnthonySWest Oct 11, 2026
42fb41e
Bump version to 1.0.0
AnthonySWest Oct 11, 2026
c545074
Roll up the CHANGELOG for 1.0.0 and update README
AnthonySWest Oct 11, 2026
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
102 changes: 101 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ jobs:
# Safety net: a hung/looping test should fail the job quickly instead of burning the runner's full time
# allotment. --test-timeout-seconds below covers a single hung test; this covers everything else. A sanitizer
# build compiles and runs several times slower, so it gets longer.
timeout-minutes: ${{ matrix.sanitize && 10 || 5 }}
timeout-minutes: ${{ matrix.sanitize && 15 || 10 }}
env:
# cmake --build compiles up to 4 files at once (the runners have 4 cores). The Visual Studio generator only
# builds projects in parallel with it, so the MSVC job also compiles with -MP (its cxxflags).
CMAKE_BUILD_PARALLEL_LEVEL: 4
strategy:
fail-fast: false
matrix:
Expand All @@ -33,6 +37,11 @@ jobs:
generator: ""
build_args: "--config Release"
exe_path: build/bin/Release/ASWLogTests.exe
# cl takes -MP as well as /MP; Git Bash would turn "/MP" into a path when it starts CMake.
cxxflags: -MP
# A multi-config generator, so the package check builds and installs both into the same prefix (the Debug
# library is ASWLogd)
package_configs: Debug Release
sanitize: ""
- name: Windows (MinGW)
os: windows-latest
Expand Down Expand Up @@ -101,6 +110,10 @@ jobs:
export CC="${{ matrix.cc }}"
export CXX="${{ matrix.cxx }}"
fi
# CMake adds CXXFLAGS to its default flags (setting CMAKE_CXX_FLAGS instead would replace them).
if [ -n "${{ matrix.cxxflags }}" ]; then
export CXXFLAGS="${{ matrix.cxxflags }}"
fi
if [ -n "${{ matrix.generator }}" ]; then
cmake -S tests/cmake -B build -G "${{ matrix.generator }}" -DCMAKE_BUILD_TYPE=Release \
-DASWLOG_SANITIZE="${{ matrix.sanitize }}"
Expand Down Expand Up @@ -156,3 +169,90 @@ jobs:
name: test-results-${{ matrix.name }}
path: junit-results.xml
if-no-files-found: error

- name: Check the installed CMake package
# Builds and installs the library (the root project) to a temporary prefix, then builds tests/package against
# it with find_package() and runs it, which checks the install rules and the package files. Not in the
# sanitizer jobs: they build the same CMake files, and the package would pass their flags on to the consumer.
if: ${{ !matrix.sanitize }}
shell: bash
run: |
if [ -n "${{ matrix.cc }}" ]; then
export CC="${{ matrix.cc }}"
export CXX="${{ matrix.cxx }}"
fi
if [ -n "${{ matrix.cxxflags }}" ]; then
export CXXFLAGS="${{ matrix.cxxflags }}"
fi
generatorArgs=()
if [ -n "${{ matrix.generator }}" ]; then
generatorArgs=(-G "${{ matrix.generator }}")
fi
# Forward slashes, which CMake takes on Windows too (RUNNER_TEMP has backslashes there)
prefix="${RUNNER_TEMP//\\//}/aswlog"
configs="${{ matrix.package_configs || 'Release' }}"
cmake -S . -B build-install "${generatorArgs[@]}" -DCMAKE_BUILD_TYPE=Release
for config in $configs; do
cmake --build build-install --config "$config"
cmake --install build-install --config "$config" --prefix "$prefix"
done
cmake -S tests/package -B build-package "${generatorArgs[@]}" -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$prefix"
for config in $configs; do
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
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +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 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/` 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`).
- `.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 @@ -53,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 All @@ -67,7 +70,7 @@ cmake --build tests/cmake/build --config Release

The VCL GUI runner builds with `tests/vcl/gui/rad370/Build_Win64x_Debug.bat` or `Build_Win64x_Release.bat` into the same output folder. To run every test in it without interaction, use `start /wait ASWLogTests_VCL_GUI.exe --run --exit --layout-ignore --report-junit results.xml`; its exit code matches the console runner's.

When adding a test module, add its `.cpp`/`.h` to `tests/cmake/CMakeLists.txt`, `tests/rad370/ASWLogTests.cbproj`, and `tests/vcl/gui/rad370/ASWLogTests_VCL_GUI.cbproj`, and keep their include paths in sync. Never open the GUI's main form (`ASWUnitTests_GUI_MainForm`) in the RAD Studio designer and save it, since that writes into the submodule.
When adding a test module or a test helper, add its `.cpp`/`.h` to `tests/cmake/CMakeLists.txt`, `tests/rad370/ASWLogTests.cbproj`, and `tests/vcl/gui/rad370/ASWLogTests_VCL_GUI.cbproj`, and keep their include paths in sync. Never open the GUI's main form (`ASWUnitTests_GUI_MainForm`) in the RAD Studio designer and save it, since that writes into the submodule.

For other environments, use the repository's CMake configuration when present or the IDE's native project configuration. Tests should be run outside the debugger unless debugging an expected exception is intentional. Before considering a change complete:

Expand Down
146 changes: 127 additions & 19 deletions ASWLog/ASWLog_Base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,47 @@ class TReportingErrorScope
TReportingErrorScope& operator=(const TReportingErrorScope&) = delete;
};

// True while this thread is in a logger's OnBeforeWrite hook (see TASWLogBase::WriteThroughHook()). Read only by a
// logger that has a hook, since a thread_local read is a library call with some compilers (e.g. MinGW's GCC).
thread_local bool IsRunningBeforeWrite = false;

// Sets IsRunningBeforeWrite while alive (hooks don't nest: an entry logged inside one skips them)
class TRunningBeforeWriteScope
{
public:
TRunningBeforeWriteScope() noexcept
{
IsRunningBeforeWrite = true;
}

~TRunningBeforeWriteScope()
{
IsRunningBeforeWrite = false;
}

TRunningBeforeWriteScope(const TRunningBeforeWriteScope&) = delete;
TRunningBeforeWriteScope& operator=(const TRunningBeforeWriteScope&) = delete;
};

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

void ForgetCopiedViews(TASWLogRecord& record) noexcept;

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

/*
ForgetCopiedViews

Empties the views of a kept record whose texts and fields are held in copies beside it (see TBacktraceEntry)
*/
void ForgetCopiedViews(TASWLogRecord& record) noexcept
{
record.Message = {};
record.Category = {};
record.Fields = nullptr;
record.Scope = nullptr;
}

} // namespace

//---------------------------------------------------------------------------
Expand Down Expand Up @@ -171,20 +212,28 @@ void TASWLogBase::KeepInBacktrace(const TASWLogRecord& record)
if (m_BacktraceCapacity == 0)
return; // Switched off since the gate let the entry in

// The fields are copied first, so a failed copy changes nothing
Detail::TOwnedFields fields;
fields.AssignMerged(record.Scope, record.GetOwnFields());

if (m_Backtrace.size() < m_BacktraceCapacity)
{
// The message is copied first, so a failed copy leaves no empty entry behind
TBacktraceEntry entry{ record, std::string(record.Message) };
entry.Record.Message = {};
// The texts are copied first, so a failed copy leaves no empty entry behind
TBacktraceEntry entry{ record, std::string(record.Message), std::string(record.Category), std::move(fields) };
ForgetCopiedViews(entry.Record);
m_Backtrace.push_back(std::move(entry));
}
else
{
// Replaces the oldest, reusing its message's memory
// Replaces the oldest, reusing its message's memory. The category is copied first and swapped in last, so a
// failed copy leaves the oldest entry as it was.
auto& entry = m_Backtrace[m_BacktraceOldest];
std::string category(record.Category);
entry.Message.assign(record.Message);
entry.Category.swap(category);
entry.Fields = std::move(fields);
entry.Record = record;
entry.Record.Message = {};
ForgetCopiedViews(entry.Record);
m_BacktraceOldest = (m_BacktraceOldest + 1) % m_BacktraceCapacity;
}

Expand Down Expand Up @@ -292,6 +341,8 @@ std::shared_ptr<const TASWLogConfig> TASWLogBase::SetConfig(const TASWLogConfig&
m_Config.swap(snapshot);
}

m_HasBeforeWrite.store(config.OnBeforeWrite != nullptr, std::memory_order_relaxed);

if (KeepsBacktrace())
ApplyBacktraceConfig(config.Backtrace);

Expand Down Expand Up @@ -333,30 +384,25 @@ void TASWLogBase::Write(const TASWLogRecord& record) noexcept
{
// Off isn't a severity and a disabled logger writes nothing, even when forced; a forced entry ignores only the
// minimum level. Checked before stamping, so a filtered entry costs no clock or thread id read.
const bool isAccepted = record.Forced ? record.LogLevel != Level::Off && IsEnabled() : PassesLevelGate(record.LogLevel);
if (!isAccepted)
if (!PassesLevelGate(record))
return;

// Stamped now, on the calling thread and before any lock, so the time is the moment of the call
TASWLogRecord stampedRecord = record;
StampRecord(stampedRecord);

if (stampedRecord.Scope == nullptr)
stampedRecord.Scope = TASWLogScope::GetCurrent();

// A derived logger's WriteRecord() may throw (e.g. out of memory, or a custom logger's own error); logging must
// never throw into the application, so the entry is dropped instead
try
{
// Let in only for the backtrace. Read again: if the minimum level changed since the gate, the entry is kept or
// written as if it had been logged just before or after the change.
if (!record.Forced && record.LogLevel < GetMinimumLevel())
{
KeepInBacktrace(stampedRecord);
return;
}

if (m_HasBacktrace.load(std::memory_order_relaxed) && record.LogLevel >= m_BacktraceDumpLevel.load(std::memory_order_relaxed))
WriteBacktrace();

WriteRecord(stampedRecord);
// An entry logged from inside a hook is written as it is, so a hook that logs doesn't recurse
if (m_HasBeforeWrite.load(std::memory_order_relaxed) && !IsRunningBeforeWrite)
WriteThroughHook(stampedRecord);
else
WriteOrKeep(stampedRecord);
}
catch (...)
{
Expand Down Expand Up @@ -394,15 +440,77 @@ void TASWLogBase::WriteBacktrace()

for (const auto& entry : entries)
{
const auto fields = entry.Fields.Get();

auto record = entry.Record;
record.Message = entry.Message;
record.Category = entry.Category;
record.Fields = &fields;
record.Forced = true;

WriteRecord(record);
}

writeMarker(Detail::BacktraceEndText);
}

//---------------------------------------------------------------------------
/*
TASWLogBase::WriteOrKeep

Write()'s last step, for a stamped entry that passed the gate (and the hook, if any): keeps it in the backtrace if
only the backtrace let it in, else writes the backtrace first if the entry's level dumps it, then the entry.
*/
void TASWLogBase::WriteOrKeep(const TASWLogRecord& record)
{
// Read again: if the minimum level changed since the gate, the entry is kept or written as if it had been logged
// just before or after the change
if (!record.Forced && record.LogLevel < GetMinimumLevelFor(record))
{
KeepInBacktrace(record);
return;
}

if (m_HasBacktrace.load(std::memory_order_relaxed) && record.LogLevel >= m_BacktraceDumpLevel.load(std::memory_order_relaxed))
WriteBacktrace();

WriteRecord(record);
}

//---------------------------------------------------------------------------
/*
TASWLogBase::WriteThroughHook

Passes a stamped entry to the config's OnBeforeWrite hook, then on to WriteOrKeep() as the hook changed it, unless
the hook drops it. A hook that throws drops the entry, which is reported.
*/
void TASWLogBase::WriteThroughHook(const TASWLogRecord& record)
{
const auto config = GetConfig();
if (config->OnBeforeWrite == nullptr)
{
WriteOrKeep(record); // Reconfigured without a hook since Write() checked
return;
}

TASWPendingEntry entry(record);
bool isKept = false;

try
{
TRunningBeforeWriteScope runningScope;
isKept = config->OnBeforeWrite(entry);
}
catch (...)
{
ReportError(*config, MakeExceptionError("Dropped an entry, OnBeforeWrite threw"));
return;
}

if (isKept)
WriteOrKeep(entry.GetRecord());
}

//---------------------------------------------------------------------------
/*
TASWLogBase::WriteErrorToStdErr
Expand Down
Loading
Loading