diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44a97dd..d59c7d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 6d21c27..e219277 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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_`, the tests of `ASWLog_`) and shared test helpers (`UT_Helper_`, 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. @@ -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 diff --git a/ASWLog/ASWLog_Base.h b/ASWLog/ASWLog_Base.h index 685181a..1a6cf3f 100644 --- a/ASWLog/ASWLog_Base.h +++ b/ASWLog/ASWLog_Base.h @@ -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. diff --git a/ASWLog/ASWLog_CategoryLog.h b/ASWLog/ASWLog_CategoryLog.h index f674423..58c20f8 100644 --- a/ASWLog/ASWLog_CategoryLog.h +++ b/ASWLog/ASWLog_CategoryLog.h @@ -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. diff --git a/ASWLog/ASWLog_Config.h b/ASWLog/ASWLog_Config.h index 8920a8b..29d7e2e 100644 --- a/ASWLog/ASWLog_Config.h +++ b/ASWLog/ASWLog_Config.h @@ -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. @@ -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 @@ -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=, logger=", 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; diff --git a/ASWLog/ASWLog_ConsoleLog.h b/ASWLog/ASWLog_ConsoleLog.h index 3ea3338..11e5fd1 100644 --- a/ASWLog/ASWLog_ConsoleLog.h +++ b/ASWLog/ASWLog_ConsoleLog.h @@ -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. diff --git a/ASWLog/ASWLog_CrashHandler.h b/ASWLog/ASWLog_CrashHandler.h index cc27630..b176262 100644 --- a/ASWLog/ASWLog_CrashHandler.h +++ b/ASWLog/ASWLog_CrashHandler.h @@ -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: " +line when the application crashes (InstallCrashHandlers(), HandleCrash()). Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_DebuggerLog.h b/ASWLog/ASWLog_DebuggerLog.h index 5c5c6e6..64ab5aa 100644 --- a/ASWLog/ASWLog_DebuggerLog.h +++ b/ASWLog/ASWLog_DebuggerLog.h @@ -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. diff --git a/ASWLog/ASWLog_Fields.h b/ASWLog/ASWLog_Fields.h index ed8eb86..3bf8c20 100644 --- a/ASWLog/ASWLog_Fields.h +++ b/ASWLog/ASWLog_Fields.h @@ -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. diff --git a/ASWLog/ASWLog_FileLog.cpp b/ASWLog/ASWLog_FileLog.cpp index aa00487..d3a6103 100644 --- a/ASWLog/ASWLog_FileLog.cpp +++ b/ASWLog/ASWLog_FileLog.cpp @@ -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(); } @@ -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; @@ -792,6 +795,9 @@ bool TASWFileLog::OpenUnlocked() m_IsOpen.store(true, std::memory_order_release); m_IsInitialized.store(true, std::memory_order_release); + + WriteNewFileStartupLinesUnlocked(); + return true; } @@ -799,8 +805,9 @@ bool TASWFileLog::OpenUnlocked() /* 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) { @@ -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. @@ -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)); } @@ -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 @@ -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); @@ -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; @@ -913,7 +931,18 @@ bool TASWFileLog::RotateLogFiles(std::string_view reasonTag) { TDeferredWorkRunner deferredWorkRunner(*this); std::lock_guard 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; } //--------------------------------------------------------------------------- @@ -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 @@ -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 diff --git a/ASWLog/ASWLog_FileLog.h b/ASWLog/ASWLog_FileLog.h index 80e08ba..f2c0939 100644 --- a/ASWLog/ASWLog_FileLog.h +++ b/ASWLog/ASWLog_FileLog.h @@ -2,7 +2,10 @@ ASWLog_FileLog.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. + +TASWFileLog, a text logger that writes to a file, with rotation by size and +by day, backup cleanup, and flush and sync settings for crash durability. Requires C++ 20 or higher. @@ -125,6 +128,9 @@ class TASWFileLog : public TASWTextLogBase std::string m_LastLogDateStr; // Stores YYYY-MM-DD state to detect structural calendar shifts std::chrono::system_clock::time_point m_LastOpenFailure{}; // NowUTC() when opening the file last failed std::chrono::system_clock::time_point m_LastRotationFailure{}; // NowUTC() when a rotation last failed + // The first line of a new file the logger started (see TASWFileConfig::WriteStartupLinesInNewFiles), waiting for + // the file to be open; empty if none + std::string m_PendingContinuationLine; // File.SyncToDiskAtLevel is Critical or lower, for WriteCrashLineDirect(), which may run without m_Mutex std::atomic m_SyncsCrashLine{ false }; @@ -139,6 +145,7 @@ class TASWFileLog : public TASWTextLogBase void RotateDailyLogFromEarlierDayUnlocked(); bool RotateLogFilesUnlocked(std::string_view reasonTag, std::string_view timeLabel); void SyncToDiskUnlocked(); + void WriteNewFileStartupLinesUnlocked(); protected: // TASWTextLogBase hooks void AfterEntryUnlocked() override; @@ -183,8 +190,9 @@ class TASWFileLog : public TASWTextLogBase // Renames the log file to "...bak" (the time in Line.TimestampZone, UTC by // default), adding "_1", "_2", ... to the time if that name is taken, so an existing backup is never replaced. Then - // reopens the log if it was open. A 'reasonTag' that contains '.' makes a backup that TASWFileConfig's backup - // cleanup doesn't recognize, so it is kept. + // reopens the log if it was open, and starts the new file with the startup lines (see + // TASWFileConfig::WriteStartupLinesInNewFiles; with AutoOpenClosePerWrite it is opened for them). A 'reasonTag' + // that contains '.' makes a backup that TASWFileConfig's backup cleanup doesn't recognize, so it is kept. bool RotateLogFiles(std::string_view reasonTag = "manual"); }; diff --git a/ASWLog/ASWLog_Formatter.h b/ASWLog/ASWLog_Formatter.h index ca9b42c..d5c9922 100644 --- a/ASWLog/ASWLog_Formatter.h +++ b/ASWLog/ASWLog_Formatter.h @@ -2,9 +2,11 @@ ASWLog_Formatter.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. -Source for the ASWLog line formatters. +IASWLogFormatter, which turns an entry into the line a text logger writes, +and the built-in formatters: TASWTextFormatter (the default layout), +TASWPatternFormatter and TASWJSONFormatter (JSON Lines). Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_Interface.h b/ASWLog/ASWLog_Interface.h index e3cc936..ee8a5d0 100644 --- a/ASWLog/ASWLog_Interface.h +++ b/ASWLog/ASWLog_Interface.h @@ -2,7 +2,10 @@ ASWLog_Interface.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. + +IASWLog, the interface of every logger, with its logging methods: plain, +formatted (*Fmt, checked at compile time), raw and forced. Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_Limiter.h b/ASWLog/ASWLog_Limiter.h index 8bcf342..6639388 100644 --- a/ASWLog/ASWLog_Limiter.h +++ b/ASWLog/ASWLog_Limiter.h @@ -4,6 +4,9 @@ Author: Anthony S. West - ASW Software A logging library. +Rate-limited logging for entries that can repeat often: the ASWLOG_ONCE, +ASWLOG_EVERY_N and ASWLOG_EVERY_INTERVAL macros, and TASWLogLimiter. + Requires C++ 20 or higher. Copyright 2026 ASW Software diff --git a/ASWLog/ASWLog_MemoryLog.h b/ASWLog/ASWLog_MemoryLog.h index b0bc05c..7918925 100644 --- a/ASWLog/ASWLog_MemoryLog.h +++ b/ASWLog/ASWLog_MemoryLog.h @@ -2,7 +2,10 @@ ASWLog_MemoryLog.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. + +TASWMemoryLog, a text logger that keeps its newest lines in memory, e.g. +for an in-app log viewer, a crash report or tests. Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_MultiLog.h b/ASWLog/ASWLog_MultiLog.h index eaa6ebd..a169b0f 100644 --- a/ASWLog/ASWLog_MultiLog.h +++ b/ASWLog/ASWLog_MultiLog.h @@ -2,7 +2,10 @@ ASWLog_MultiLog.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. + +TASWMultiLog, which passes each entry on to several loggers, e.g. a file +logger and a console logger. Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_NullLog.h b/ASWLog/ASWLog_NullLog.h index 52a654e..03fdb42 100644 --- a/ASWLog/ASWLog_NullLog.h +++ b/ASWLog/ASWLog_NullLog.h @@ -2,7 +2,10 @@ ASWLog_NullLog.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. + +TASWNullLog, a logger that writes nothing, e.g. to turn logging off without +changing the code that logs, or to pass to code under test. Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_SyslogLog.h b/ASWLog/ASWLog_SyslogLog.h index 013dec8..a2e126f 100644 --- a/ASWLog/ASWLog_SyslogLog.h +++ b/ASWLog/ASWLog_SyslogLog.h @@ -2,7 +2,10 @@ ASWLog_SyslogLog.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. + +TASWSyslogLog, a text logger that sends its lines to the Linux system log +(and the systemd journal); it writes nothing on Windows. Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_TextLogBase.cpp b/ASWLog/ASWLog_TextLogBase.cpp index 1e23e66..cd4ffb0 100644 --- a/ASWLog/ASWLog_TextLogBase.cpp +++ b/ASWLog/ASWLog_TextLogBase.cpp @@ -31,6 +31,7 @@ limitations under the License. //--------------------------------------------------------------------------- #include "ASWLog_CrashHandler.h" #include "ASWLog_Utils.h" +#include "ASWLog_Version.h" //--------------------------------------------------------------------------- namespace ASWLog @@ -260,22 +261,8 @@ bool TASWTextLogBase::Initialize(const TASWLogConfig& config) noexcept Detail::TCrashHandling::Register(*this); - // The output is ready, so the startup lines are best effort: if one throws (e.g. a formatter that throws, or - // out of memory while gathering the system info), the rest are skipped but the logger is still initialized - try - { - if (!GetConfigUnlocked().Startup.Banner.empty()) - { - WriteInfoLine(GetConfigUnlocked().Startup.Banner); - } - - WriteInitializationInfo(); - } - catch (...) - { - ReportCurrentExceptionUnlocked("Skipped the startup lines"); - } - + // The output is ready, so the startup lines are best effort: a failed one doesn't fail Initialize() + WriteStartupLinesUnlocked(); AfterEntryUnlocked(); return true; @@ -862,7 +849,18 @@ void TASWTextLogBase::WaitForQueuedEntries() //--------------------------------------------------------------------------- void TASWTextLogBase::WriteApplicationInfo() { - auto applicationInfo = std::format("app_exe='{}', app_target=", PathToUTF8String(GetExecutablePath())); + const auto executablePath = GetExecutablePath(); + auto applicationInfo = std::format("app_exe='{}'", PathToUTF8String(executablePath)); + + // The configured version, or else the executable's file version (Windows) + auto applicationVersion = GetConfigUnlocked().Startup.ApplicationVersion; + if (applicationVersion.empty()) + applicationVersion = GetFileVersionString(executablePath); + + if (!applicationVersion.empty()) + applicationInfo += std::format(", app_version='{}'", applicationVersion); + + applicationInfo += ", app_target="; #if defined(_WIN64) applicationInfo += "Win64"; @@ -977,6 +975,9 @@ void TASWTextLogBase::WriteInfoLine(std::string_view message, std::source_locati //--------------------------------------------------------------------------- void TASWTextLogBase::WriteInitializationInfo() { + if (GetConfigUnlocked().Startup.WriteLoggerVersion) + WriteLoggerVersionInfo(); + if (GetConfigUnlocked().Startup.WriteTimeInfo) WriteTimeInfo(); @@ -1019,6 +1020,12 @@ std::string TASWTextLogBase::WriteLogEntry(const TASWLogRecord& record) return line; } +//--------------------------------------------------------------------------- +void TASWTextLogBase::WriteLoggerVersionInfo() +{ + WriteInfoLine(std::format("ASWLog: version={}, logger={}", Version, GetLoggerClassName())); +} + //--------------------------------------------------------------------------- void TASWTextLogBase::WriteMemoryUsageInfo() { @@ -1167,6 +1174,31 @@ void TASWTextLogBase::WriteRecord(const TASWLogRecord& record) DispatchLogCallback(*callbackConfig, record, writtenLine); } +//--------------------------------------------------------------------------- +void TASWTextLogBase::WriteStartupLinesUnlocked(std::string_view firstLine) noexcept +{ + m_IsWritingStartupLines = true; + + // If a line throws (e.g. a formatter that throws, or out of memory while gathering the system info), the rest are + // skipped + try + { + if (!firstLine.empty()) + WriteInfoLine(firstLine); + + if (!GetConfigUnlocked().Startup.Banner.empty()) + WriteInfoLine(GetConfigUnlocked().Startup.Banner); + + WriteInitializationInfo(); + } + catch (...) + { + ReportCurrentExceptionUnlocked("Skipped the startup lines"); + } + + m_IsWritingStartupLines = false; +} + //--------------------------------------------------------------------------- void TASWTextLogBase::WriteSystemMemoryInfo() { diff --git a/ASWLog/ASWLog_TextLogBase.h b/ASWLog/ASWLog_TextLogBase.h index 69c49b9..6b15d77 100644 --- a/ASWLog/ASWLog_TextLogBase.h +++ b/ASWLog/ASWLog_TextLogBase.h @@ -2,7 +2,11 @@ ASWLog_TextLogBase.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. + +TASWTextLogBase, the base of the loggers that write each entry as a line of +text: locking, formatting, asynchronous writing and the startup lines. A +custom text logger derives from it and implements only its output. Requires C++ 20 or higher. @@ -141,6 +145,7 @@ class TASWTextLogBase : public TASWLogBase std::atomic m_IsAsync{ false }; bool m_IsWritingQueuedEntries = false; // Guarded by m_Mutex (see IsWritingQueuedEntriesUnlocked()) + bool m_IsWritingStartupLines = false; // Guarded by m_Mutex (see IsWritingStartupLinesUnlocked()) // This logger's place on the list of loggers a crash flushes, guarded by that list's lock (see // Detail::TCrashHandling) @@ -205,6 +210,7 @@ class TASWTextLogBase : public TASWLogBase void WriteInfoLine(std::string_view message, std::source_location loc = std::source_location::current()); void WriteInitializationInfo(); std::string WriteLogEntry(const TASWLogRecord& record); + void WriteLoggerVersionInfo(); void WriteMemoryUsageInfo(); void WriteOSInfo(); void WriteQueuedEntries(std::deque& entries, std::size_t droppedCount) noexcept; @@ -290,6 +296,13 @@ class TASWTextLogBase : public TASWLogBase return m_IsWritingQueuedEntries; } + // True while the startup lines are written (see WriteStartupLinesUnlocked()), so that PrepareWriteUnlocked() can + // leave starting a new output (e.g. rotating a file) until after them. + bool IsWritingStartupLinesUnlocked() const noexcept + { + return m_IsWritingStartupLines; + } + // Called on the worker thread, every GetWorkerIntervalUnlocked() (measured from the start of the previous call), // e.g. to flush the output. Reports a failure with ReportErrorUnlocked(): the worker passes it on to OnError after // the unlock, on its own thread. An exception is caught and reported. Does nothing by default. @@ -328,6 +341,12 @@ class TASWTextLogBase : public TASWLogBase // as is; see IASWLogFormatter::FormatsRawEntries()). virtual void WriteLineUnlocked(Level level, std::string_view line, bool endsLine) = 0; + // Writes the startup lines as Initialize() does: 'firstLine' if it isn't empty, the banner, then the lines + // TASWStartupConfig turns on, each as an Info line through PrepareWriteUnlocked() and WriteLineUnlocked(). For a + // derived logger that starts a new output, e.g. a file logger's next file after a rotation. A line that fails (e.g. + // its formatter throws) is reported and the rest are skipped. Never throws. + void WriteStartupLinesUnlocked(std::string_view firstLine = {}) noexcept; + protected: // TASWLogBase hook // Writes the entry under m_Mutex, through the hooks above, then calls OnLogEntry. An asynchronous logger formats the // entry and queues it instead (see TASWAsyncConfig). diff --git a/ASWLog/ASWLog_Types.h b/ASWLog/ASWLog_Types.h index 95ea589..de6855f 100644 --- a/ASWLog/ASWLog_Types.h +++ b/ASWLog/ASWLog_Types.h @@ -2,9 +2,10 @@ ASWLog_Types.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. -Source for the ASWLog types. +The basic types: Level and the other enums, TASWLogRecord (an entry as a +logger receives it) and TASWLogError (a logger's internal failure). Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_Unicode.h b/ASWLog/ASWLog_Unicode.h index e64cb93..0f68a10 100644 --- a/ASWLog/ASWLog_Unicode.h +++ b/ASWLog/ASWLog_Unicode.h @@ -4,8 +4,9 @@ Author: Anthony S. West - ASW Software A logging library. -Source for logging wide (UTF-16 and UTF-32) text: conversions to and from UTF-8, and the text wrappers the logging -methods and the *Fmt arguments take. +Wide-string support: TASWWideText (wchar_t and char16_t messages), the +Wide() and UTF8() wrappers for *Fmt arguments, and conversions between +UTF-8 and UTF-16 or UTF-32. Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_Utils.cpp b/ASWLog/ASWLog_Utils.cpp index ca1688b..9f9ae6a 100644 --- a/ASWLog/ASWLog_Utils.cpp +++ b/ASWLog/ASWLog_Utils.cpp @@ -30,18 +30,22 @@ limitations under the License. #include #include #include +#include #include #include #include #include #include #include +#include +#include #include #include #if defined(_WIN32) #include #include +#include #else #include #include @@ -113,13 +117,6 @@ std::string GenerateLogFileName(std::string_view prefix, std::string_view custom return name; } -//--------------------------------------------------------------------------- -std::string GetApplicationInfoString() -{ - auto exePath = GetExecutablePath(); - return std::format("application_exe='{}', command_line='{}'", PathToUTF8String(exePath), GetCommandLineString()); -} - //--------------------------------------------------------------------------- std::string GetCommandLineString() { @@ -206,6 +203,62 @@ std::filesystem::path GetExecutablePath() return std::filesystem::path("unknown"); } +//--------------------------------------------------------------------------- +std::string GetFileVersionString(const std::filesystem::path& file) +{ +#if defined(_WIN32) + // version.dll is loaded only for this call, from the system folder, so that no project has to link it + using FreeLibraryFunction = BOOL(WINAPI*)(HMODULE); + using GetFileVersionInfoFunction = BOOL(WINAPI*)(LPCWSTR, DWORD, DWORD, LPVOID); + using GetFileVersionInfoSizeFunction = DWORD(WINAPI*)(LPCWSTR, LPDWORD); + using VerQueryValueFunction = BOOL(WINAPI*)(LPCVOID, LPCWSTR, LPVOID*, PUINT); + + const HMODULE versionLibrary = LoadLibraryExW(L"version.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (versionLibrary == nullptr) + return {}; + + const std::unique_ptr, FreeLibraryFunction> libraryOwner(versionLibrary, &FreeLibrary); + + // Through void (*)() so that GCC doesn't warn about casting between function types + const auto getVersionInfoSize = reinterpret_cast( + reinterpret_cast(GetProcAddress(versionLibrary, "GetFileVersionInfoSizeW"))); + const auto getVersionInfo = reinterpret_cast( + reinterpret_cast(GetProcAddress(versionLibrary, "GetFileVersionInfoW"))); + const auto queryVersionValue = reinterpret_cast( + reinterpret_cast(GetProcAddress(versionLibrary, "VerQueryValueW"))); + if (getVersionInfoSize == nullptr || getVersionInfo == nullptr || queryVersionValue == nullptr) + return {}; + + const std::wstring path = file.wstring(); + DWORD handle = 0; + const DWORD size = getVersionInfoSize(path.c_str(), &handle); + if (size == 0) + return {}; + + std::vector versionInfo(size); + void* fixedInfo = nullptr; + UINT fixedInfoSize = 0; + if (!getVersionInfo(path.c_str(), 0, size, versionInfo.data()) || + !queryVersionValue(versionInfo.data(), L"\\", &fixedInfo, &fixedInfoSize) || fixedInfo == nullptr || + fixedInfoSize < sizeof(VS_FIXEDFILEINFO)) + { + return {}; + } + + VS_FIXEDFILEINFO fileInfo{}; + std::memcpy(&fileInfo, fixedInfo, sizeof(fileInfo)); + if (fileInfo.dwSignature != VS_FFI_SIGNATURE) + return {}; + + return std::format("{}.{}.{}.{}", HIWORD(fileInfo.dwFileVersionMS), LOWORD(fileInfo.dwFileVersionMS), + HIWORD(fileInfo.dwFileVersionLS), LOWORD(fileInfo.dwFileVersionLS)); +#else + static_cast(file); + + return {}; +#endif +} + //--------------------------------------------------------------------------- TMemoryUsage GetMemoryUsage() { diff --git a/ASWLog/ASWLog_Utils.h b/ASWLog/ASWLog_Utils.h index 99d1bf5..1a7dc25 100644 --- a/ASWLog/ASWLog_Utils.h +++ b/ASWLog/ASWLog_Utils.h @@ -2,9 +2,10 @@ ASWLog_Utils.h Author: Anthony S. West - ASW Software -A light-weight logging tool. +A logging library. -Source for the ASWLog utility functions, etc. +Helpers the loggers use, which an application can use too: log file names, +process and system information, paths, time formatting and JSON escaping. Requires C++ 20 or higher. @@ -75,7 +76,6 @@ struct TSystemMemoryUsage */ [[nodiscard]] std::string GenerateLogFileName(std::string_view prefix, std::string_view customPostfix); -[[nodiscard]] std::string GetApplicationInfoString(); [[nodiscard]] std::string GetCommandLineString(); // The operating system's id for the current process, as shown by Task Manager, ps, and debuggers. @@ -87,6 +87,13 @@ struct TSystemMemoryUsage [[nodiscard]] std::string GetDriveInfoString(); [[nodiscard]] std::filesystem::path GetExecutablePath(); + +// The file version in the version resource of 'file' (e.g. an executable or DLL), as four numbers, e.g. "2.3.1.45", +// what Explorer shows as "File version". Empty if the file has no version resource or can't be read, and always on +// Linux, which has no version resources. Windows may report its own system files' version differently (e.g. 6.2 for +// 10.0) to an application without a compatibility manifest. Throws std::bad_alloc if out of memory. +[[nodiscard]] std::string GetFileVersionString(const std::filesystem::path& file); + [[nodiscard]] TMemoryUsage GetMemoryUsage(); [[nodiscard]] std::string GetMemoryUsageString(); [[nodiscard]] std::string GetOSInfoString(); diff --git a/ASWLog/ASWLog_Version.h b/ASWLog/ASWLog_Version.h index cb65b4a..cb787d0 100644 --- a/ASWLog/ASWLog_Version.h +++ b/ASWLog/ASWLog_Version.h @@ -2,7 +2,10 @@ ASWLog_Version.h Author: Anthony S. West - ASW Software -Single source for the ASWLog semantic version (https://semver.org). +A logging library. + +The ASWLog version (https://semver.org), as macros and constants: the only +place where the version is set. Requires C++ 20 or higher. diff --git a/ASWLog/ASWLog_WindowsEventLog.h b/ASWLog/ASWLog_WindowsEventLog.h index 787075d..048fcfe 100644 --- a/ASWLog/ASWLog_WindowsEventLog.h +++ b/ASWLog/ASWLog_WindowsEventLog.h @@ -4,6 +4,9 @@ Author: Anthony S. West - ASW Software A logging library. +TASWWindowsEventLog, a text logger that reports its lines to the Windows +Event Log; it writes nothing on other platforms. + Requires C++ 20 or higher. Copyright 2026 ASW Software diff --git a/CHANGELOG.md b/CHANGELOG.md index d171850..9d63729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -254,6 +254,32 @@ see [0.26.1](#0261---2026-09-17) for the initial versioned baseline. the library is now named `ASWLogd` (e.g. `ASWLogd.lib`), so Debug and Release can share an install prefix; nothing changes for code that links to the `ASWLog::ASWLog` target. +- The ASWLog version in the startup lines: right after the banner, a text + logger writes e.g. `ASWLog: version=1.0.0, logger=TASWFileLog`, so a log + from the field says which ASWLog (and which logger) wrote it. On by + default; `Startup.WriteLoggerVersion = false` leaves it out, which code that + turns off every startup line one by one must now add. +- The application's version in the startup lines: the "App:" line now has + `app_version='...'` after the executable's path, from the new + `Startup.ApplicationVersion`, or, when that is empty, on Windows, from the + executable's file version in its version resource (e.g. `2.3.1.45`, as + RAD Studio's Version Info sets it). Without either, the line is as before. + The new `ASWLog::GetFileVersionString(path)` reads any file's version + (Windows; empty on Linux). It loads `version.dll` at run time, so no + project needs a new library to link. +- The startup lines at the top of each new log file: a file logger now starts + every file it begins after `Initialize()` (size rotation, daily rolling, + `RotateLogFiles()`, `Reconfigure()` to a new path) with a line saying where + the log continues from, e.g. `Log continued from + myapp.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 (the banner and each line its `Startup` setting + turns on), so a rotated log still says which version and system wrote it. + They come before the entry that started the file. On by default; the new + `File.WriteStartupLinesInNewFiles = false` keeps the old output, in which a + new file starts with that entry. A rotation no longer happens in the middle + of the startup lines: if they reach `File.MaxFileSizeBytes`, the next entry + rotates the file. ### Changed @@ -269,6 +295,14 @@ see [0.26.1](#0261---2026-09-17) for the initial versioned baseline. custom formatter. A time before 1970 is now written as the day and millisecond it falls in, instead of a malformed time. +### Removed + +- `GetApplicationInfoString()` (from `ASWLog_Utils.h`). It was an older form + of the startup "App:" line and nothing in ASWLog used it. Breaking for code + that called it: build the text from `GetExecutablePath()` (with + `PathToUTF8String()`) and `GetCommandLineString()`, which it combined as + `application_exe='', command_line=''`. + ## [0.75.0] - 2026-10-06 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 0117f0b..e881b47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,8 @@ endforeach() project(ASWLog VERSION ${ASWLOG_VERSION_MAJOR}.${ASWLOG_VERSION_MINOR}.${ASWLOG_VERSION_PATCH} + DESCRIPTION "A thread-safe, dependency-free C++20 logging library for Windows and Linux" + HOMEPAGE_URL "https://github.com/ASWSoftware/asw-log" LANGUAGES CXX ) diff --git a/Deploy.bat b/Deploy.bat index 8657210..d85cd89 100644 --- a/Deploy.bat +++ b/Deploy.bat @@ -67,6 +67,13 @@ if errorlevel 1 exit /b 1 call :CopyFile "cmake\ASWLogConfig.cmake.in" "%TARGET_DIR%\cmake\ASWLogConfig.cmake.in" if errorlevel 1 exit /b 1 +call :CopyFile "docs\Configuration.md" "%TARGET_DIR%\docs\Configuration.md" +if errorlevel 1 exit /b 1 +call :CopyFile "docs\CustomLoggers.md" "%TARGET_DIR%\docs\CustomLoggers.md" +if errorlevel 1 exit /b 1 +call :CopyFile "docs\Guarantees.md" "%TARGET_DIR%\docs\Guarantees.md" +if errorlevel 1 exit /b 1 + call :CopyFile "ASWLog\ASWLog_Base.cpp" "%TARGET_DIR%\ASWLog\ASWLog_Base.cpp" if errorlevel 1 exit /b 1 call :CopyFile "ASWLog\ASWLog_Base.h" "%TARGET_DIR%\ASWLog\ASWLog_Base.h" diff --git a/README.md b/README.md index 3f4ce05..2bfd6bc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # ASWLog - https://github.com/ASWSoftware/asw-log -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. + +## Design + +ASWLog is a folder of standard C++ sources, or one CMake target, with no third-party code. Its defaults put crash +durability first: a logging call is synchronous and every entry is flushed before the call returns, so the last +entries before a crash are in the file. A call below the logger's level costs a few nanoseconds, so Debug and Trace +calls can stay in release builds and be turned on at run time. The rest (asynchronous writing, backtrace, JSON and +pattern formatters, crash handlers, and the other extras below) is opt-in. ## Features @@ -16,7 +24,9 @@ ASWLog is a thread-safe light-weight C++ logging tool for Windows and Linux proj against their arguments at compile time; wrap one built at run time in `ASWLog::RuntimeFormat()`. - Runtime `Open()`, `Close()`, `Flush()` and log rotation controls, and `Reconfigure()` to change settings safely while other threads log. -- Optional application and system memory, OS, drive, time, and command-line diagnostics. +- Optional startup diagnostics: the ASWLog and application versions (on Windows, the application's is read from its + version resource unless set), application and system memory, OS, drive, time, and the command line, written again + at the top of each new file after a rotation, saying which file the log continues from. - Retry handling for temporary file access conflicts and Windows reader-sharing support. - Crash durability: every entry is flushed by default, and with buffered flushing Error and Critical entries still are (`File.FlushImmediatelyAtLevel`). Entries at a chosen level can also be synced to disk to survive a power loss @@ -47,6 +57,10 @@ ASWLog is a thread-safe light-weight C++ logging tool for Windows and Linux proj - Rate-limited logging for entries that can repeat often: `ASWLOG_ONCE`, `ASWLOG_EVERY_N` and `ASWLOG_EVERY_INTERVAL` (`ASWLog_Limiter.h`), e.g. `ASWLOG_EVERY_INTERVAL(log, std::chrono::seconds(5), ASWLog::Level::Warn, "Retrying {}", host);`, which counts the skipped entries in a `suppressed` field. +- Settings from environment variables (`TASWLogConfig::ApplyEnvironment()`), e.g. `ASWLOG_LEVEL=Trace` to see + everything a deployed release build logs, without rebuilding it. +- Wide-string messages: every logging method also takes `wchar_t` or `char16_t` text (e.g. `L"..."`, a + `std::wstring`, or C++Builder's `UnicodeString::c_str()`), converted to UTF-8 only if the entry is written. - A hook to drop or change entries before they are written (`TASWLogConfig::OnBeforeWrite`), e.g. to redact secrets or drop noisy entries by content, category or field. - Structured fields per entry, e.g. `logger.LogInfo("Order placed", {{"orderId", 17}})`, and scoped context that every @@ -67,7 +81,7 @@ ASWLog is a thread-safe light-weight C++ logging tool for Windows and Linux proj # Donations: -If you find this tool helpful, donations are always appreciated: +If you find this library helpful, donations are always appreciated: PayPal: donate@aswsoftware.com @@ -77,13 +91,65 @@ Bitcoin: # How to Use -Add the source in `ASWLog` to your C++ project. +## Quick start + +```cpp +#include "ASWLog_CrashHandler.h" +#include "ASWLog_FileLog.h" + +int main() +{ + ASWLog::TASWLogConfig config; + config.File.FilePath = "myapp.txt"; // In the "logs" folder + config.ApplyEnvironment(); // Optional: e.g. ASWLOG_LEVEL=Debug overrides the level + + auto& log = ASWLog::TASWFileLog::GetInstance(); + if (!log.Initialize(config)) + return 1; + + ASWLog::InstallCrashHandlers(); // Flushes the log and adds a "Crash:" line if the application crashes + + log.LogInfo("Started"); + log.LogWarnFmt("Disk {} is {}% full", "C:", 93); // Checked against its arguments at compile time + log.LogInfo("Order placed", { { "orderId", 17 }, { "total", 9.99 } }); + log.LogDebug("Not written: below the minimum level (Info by default)"); + + return 0; +} +``` + +writes `logs/myapp.txt`, after the startup lines (ASWLog version, time, OS, drive, memory, application; see +`TASWLogConfig::Startup`): + +```text +[2026-10-11T00:24:25.089Z][INFO][P:6180][T:6648]: Started +[2026-10-11T00:24:25.089Z][WARN][P:6180][T:6648]: Disk C: is 93% full +[2026-10-11T00:24:25.089Z][INFO][P:6180][T:6648][orderId=17 total=9.99]: Order placed +[2026-10-11T00:24:25.089Z][INFO][P:6180][T:6648]: Logger shutdown: 2026-10-11T00:24:25.089Z +``` + +The `example/src/main.cpp` app shows more: console, multi-log, categories, scopes, rate limiting and wide text. + +## Adding ASWLog to a project + +### Copied sources (any build system) + +Add every `.cpp` file in `ASWLog/` to the project, and the `ASWLog` folder to its include path, and compile as C++20. +On Windows, link `psapi` and `advapi32` (RAD Studio links them by default); on Linux, the threads library +(`-pthread`). `Deploy.bat ` copies the library sources, the examples and the docs to a folder, e.g. for a +release package. + +### RAD Studio -## CMake +Add the `ASWLog/*.cpp` files to the project (Project > Add to Project) and the `ASWLog` folder to the include path +(Project > Options > Building > C++ Shared Options > Include path). RAD Studio 13.1 or later with the modern Clang +toolchain (Win64x). `example/rad370/ASWLogExample.cbproj` is a working project to copy settings from. + +### CMake The repository's root `CMakeLists.txt` defines the static library target `ASWLog::ASWLog`. Linking to it adds the -`ASWLog` include folder, C++20, and the system libraries the logger needs (the threads library, and `psapi` on -Windows). Add it from a copy of the repository, such as a git submodule: +`ASWLog` include folder, C++20, and the system libraries the logger needs (the threads library, and `psapi` and +`advapi32` on Windows). Add it from a copy of the repository, such as a git submodule: ``` add_subdirectory(third_party/asw-log) @@ -138,6 +204,60 @@ cmake --build example/cmake/build --config Release The executable is written to `example/build/bin/Release/ASWLogExample.exe`, alongside RAD Studio output. +## API overview + +**Loggers.** Each implements `ASWLog::IASWLog` (`ASWLog_Interface.h`), so code that logs can take an `IASWLog&` and +not care where the entries go. Most have a `GetInstance()` singleton, and any of them can also be created as an +ordinary object. + +| Class | Header | Writes to | +|---|---|---| +| `TASWFileLog` | `ASWLog_FileLog.h` | A file, with rotation, daily rolling and backup cleanup | +| `TASWConsoleLog` | `ASWLog_ConsoleLog.h` | stdout, and stderr for Warn and above, in color on a terminal | +| `TASWDebuggerLog` | `ASWLog_DebuggerLog.h` | The debugger's output (Windows) | +| `TASWMemoryLog` | `ASWLog_MemoryLog.h` | Memory: the newest lines, e.g. for an in-app log viewer | +| `TASWSyslogLog` | `ASWLog_SyslogLog.h` | The system log and the systemd journal (Linux) | +| `TASWWindowsEventLog` | `ASWLog_WindowsEventLog.h` | The Windows Event Log (Windows) | +| `TASWNullLog` | `ASWLog_NullLog.h` | Nothing | +| `TASWMultiLog` | `ASWLog_MultiLog.h` | Several loggers at once, e.g. a file and the console | +| `TASWCategoryLog` | `ASWLog_CategoryLog.h` | Another logger, marking the entries with a category (e.g. `[Net]`) that can have a level of its own | + +The loggers that write to only one platform build on both and write nothing on the other, so the same code can add +them everywhere. + +**Logging.** On any logger: + +- `LogTrace()`, `LogDebug()`, `LogInfo()`, `LogWarn()`, `LogError()`, `LogCritical()`, and `Log(level, ...)`. +- `LogInfoFmt("Opened {} in {} ms", name, ms)` and the other `*Fmt` methods: `std::format`, with the format string + checked against its arguments at compile time (wrap a format string built at run time in `ASWLog::RuntimeFormat()`). + An entry below the minimum level isn't formatted. +- `LogRaw()` writes the message as it is, without the line layout; `LogForce()` and `LogForceRaw()` write it whatever + the minimum level. +- Fields: `LogInfo("Order placed", {{"orderId", 17}})`, `LogInfoFmt({{"orderId", id}}, "Order {} placed", id)`, and + `ASWLog::TASWLogScope scope{{"requestId", id}};` for fields that every entry on the thread carries while it exists + (`ASWLog_Fields.h`). +- Wide text: `LogInfo(L"...")`, `LogInfoFmt(L"... {}", ...)`, and `ASWLog::Wide(text)` / `ASWLog::UTF8(text)` for + arguments (`ASWLog_Unicode.h`). +- The caller's function and source line are captured automatically (shown with `Line.ShowFunctionName` and + `Line.ShowSourceLine`). + +**Control.** `Initialize(config)`, `Reconfigure(config)`, `GetConfig()`, `SetMinimumLevel()`, `ShouldLog(level)` (to +skip building an expensive message), `SetEnabled()`, `Flush()`, `Open()`, `Close()` and `DumpBacktrace()`; a file +logger also has `RotateLogFiles()` and `DeleteOldLogs()`. + +**More.** `ASWLog::InstallCrashHandlers()` (`ASWLog_CrashHandler.h`), the `ASWLOG_ONCE`, `ASWLOG_EVERY_N` and +`ASWLOG_EVERY_INTERVAL` macros (`ASWLog_Limiter.h`), `TASWLogConfig::ApplyEnvironment()`, the formatters +(`ASWLog_Formatter.h`), and the version (`ASWLog_Version.h`). The header comments document every class and method. + +## Documentation + +- [docs/Configuration.md](docs/Configuration.md): every setting, its default, and which loggers use it, plus the + environment variables. +- [docs/Guarantees.md](docs/Guarantees.md): thread safety, the order of entries, which thread runs each callback, + object lifetimes, and choosing settings for crash durability. +- [docs/CustomLoggers.md](docs/CustomLoggers.md): writing a formatter or a logger of your own. +- [CHANGELOG.md](CHANGELOG.md): the changes in each release. + ## Versions ASWLog follows [Semantic Versioning](https://semver.org). Releases are tagged on `main` (e.g. `v1.1.0`). Between @@ -153,6 +273,15 @@ and as constants in the `ASWLog` namespace: #endif ``` +### Upgrading from 0.x + +1.0.0 is the first release with a stable interface; the 0.x releases changed it as the design settled. The releases +that can need changes in code that uses ASWLog are 0.43.0, 0.45.0, 0.65.0, 0.75.0 (only for a class that implements +`IASWLog` directly) and 1.0.0. In [CHANGELOG.md](CHANGELOG.md), read the **Changed** and **Removed** sections, and +the entries marked "Breaking", of every release after the one you use: each says what existing code must change. +Projects that list the ASWLog sources must also add the files that new releases added (the CHANGELOG names them, e.g. +`ASWLog_Unicode.cpp`). + # Unit Tests Using the logger only requires the `ASWLog` source. The unit tests in `tests` additionally use the @@ -194,6 +323,10 @@ cmake --build tests/cmake/build-tsan CI runs both with Clang. `ASWLOG_SANITIZE` applies to the library and everything that links to it. +CI also measures which lines of the library the tests run (a Linux GCC build with `--coverage`, read by gcovr): the +"Linux (GCC, coverage)" job shows a table on its summary page and keeps an HTML report as the `coverage-report` +artifact. It is informational and never fails the run. + On Windows with RAD Studio, the tests can also run in the ASWUnitTests VCL GUI runner, a window for choosing tests and reading their results: build `tests/vcl/gui/rad370/ASWLogTests_VCL_GUI.cbproj` (or its `Build_Win64x_*.bat` scripts), which writes `ASWLogTests_VCL_GUI.exe` to `tests/build/bin/`. `ASWLogTests_Group.groupproj` in the same folder diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 0000000..0faf451 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,217 @@ +# Configuration reference + +Every logger takes a `ASWLog::TASWLogConfig` (`ASWLog_Config.h`) in `Initialize()`. Its settings are grouped by +concern, and a logger ignores the groups it doesn't use, so one config can be passed to several loggers (a multi-log +passes its config on to the loggers it holds). + +```cpp +ASWLog::TASWLogConfig config; +config.InitialMinimumLevel = ASWLog::Level::Debug; +config.File.FolderPath = "logs"; +config.File.FilePath = "myapp.txt"; +config.File.EnableRotation = true; +config.ApplyEnvironment(); // Optional: lets ASWLOG_* environment variables override the settings (see below) + +auto& log = ASWLog::TASWFileLog::GetInstance(); +log.Initialize(config); +``` + +To change the settings of a running logger, copy its current config, change the copy, and pass it to `Reconfigure()`, +which is safe while other threads log: + +```cpp +auto config = *log.GetConfig(); +config.Line.ShowThreadId = false; +log.Reconfigure(config); +``` + +`Reconfigure()` doesn't change the minimum level (use `SetMinimumLevel()`) and doesn't write the startup lines again, +except in the new file of a file logger moved to a new path (see `File.WriteStartupLinesInNewFiles`). + +The header comments in `ASWLog_Config.h` have the full details of each setting; this page is the overview. + +## Which logger uses what + +| Group | Used by | +|---|---| +| `InitialMinimumLevel` | Every logger, including the multi-log (its own pre-filter) | +| `Line`, `Startup`, `Shutdown`, `Async` | The text loggers: file, console, debugger, memory, syslog, Windows Event Log, and loggers derived from `TASWTextLogBase` | +| `File` | `TASWFileLog` | +| `Backtrace` | Every logger except the multi-log (each of its loggers keeps its own), the category logger and the null logger | +| `Memory` | `TASWMemoryLog` | +| `Syslog` | `TASWSyslogLog` | +| `EventLog` | `TASWWindowsEventLog` | +| `OnBeforeWrite` | Every logger; a multi-log calls its own once per entry, before passing the entry on | +| `OnLogEntry`, `OnLogEntryMinimumLevel` | The text loggers | +| `OnError`, `ErrorReportInterval` | Every logger; a multi-log passes them on, and each of its loggers reports its own failures | + +A category logger (`TASWCategoryLog`) and the null logger (`TASWNullLog`) take no config: the category's output +belongs to the logger it wraps. + +## Top level + +| Setting | Default | What it does | +|---|---|---| +| `InitialMinimumLevel` | `Level::Info` | The minimum level `Initialize()` sets. Change it later with `SetMinimumLevel()`; this field doesn't follow it. | +| `OnBeforeWrite` | empty | Called for each entry that passes the level checks, before it is written: return `false` to drop it, or change it with `entry.SetMessage()`, `SetField()` or `RemoveField()` (e.g. to redact secrets). Runs on the logging thread, before the logger's lock, possibly on several threads at once. | +| `OnLogEntry` | empty | Called after an entry at or above `OnLogEntryMinimumLevel` was written, with the record and the line as written (e.g. for alerting). Runs outside the logger's lock; with `Async.Enabled`, on the logger's own thread. Exceptions are swallowed. | +| `OnLogEntryMinimumLevel` | `Level::Error` | The level an entry needs for `OnLogEntry`. `Level::Off` = never. | +| `OnError` | empty | Receives the logger's internal failures (a file that can't be opened, written, flushed or rotated, an invalid setting, ...) as a `TASWLogError`. Empty: each is written to stderr as one line. Runs outside the logger's lock. | +| `ErrorReportInterval` | 1 minute | Each kind of failure is reported at most once per interval; the next report says how many were left out. 0 = report every failure. | + +## `Line`: the line layout (`TASWLineConfig`) + +| Setting | Default | What it does | +|---|---|---| +| `Formatter` | empty | Formats each line. Empty: the built-in layout (`TASWTextFormatter`) from the `Show*` settings. Assign a `TASWPatternFormatter` (e.g. `"{time} {level:5} {message}"`), a `TASWJSONFormatter` (JSON Lines) or your own `IASWLogFormatter`. One formatter can be shared by several loggers. | +| `Ending` | `LineEnding::LF` | Added after each formatted line (`LF` or `CRLF`); not after `LogRaw()` entries written as they are. | +| `Multiline` | `MultilineMode::Preserve` | The line breaks inside an entry: written as they are (`Preserve`), each followed by ` \| ` (`Indent`), or as `\r` and `\n` (`Escape`). | +| `TimestampZone` | `TimeZone::UTC` | `UTC` (e.g. `2026-09-28T21:02:44.342Z`) or `Local` (e.g. `2026-09-28T16:02:44.342-05:00`). Also sets when daily rolling starts a new file and the time in backup names. | +| `TimestampPrecision` | `TimePrecision::Milliseconds` | `Milliseconds`, `Microseconds` or `Nanoseconds`. | +| `ShowTimestamp` | `true` | The entry's time. | +| `ShowLevel` | `true` | The entry's level, e.g. `[INFO]`. | +| `ShowCategory` | `true` | The entry's category, e.g. `[Net]`, if it has one. | +| `ShowProcessId` | `true` | The OS process id. | +| `ShowThreadId` | `true` | The OS id of the thread that logged the entry. | +| `ShowWorkingSet` | `false` | The process's memory use. | +| `ShowPeakWorkingSet` | `false` | The process's peak memory use. | +| `ShowFunctionName` | `false` | The function that logged the entry. | +| `ShowSourceLine` | `false` | The source file and line that logged the entry. | +| `ShowFields` | `true` | The entry's fields (its own and its scopes'), if it has any. | + +The `Show*` settings are read by the built-in layout and by `TASWJSONFormatter`; a pattern decides its own fields. + +## `Startup`: the lines `Initialize()` writes (`TASWStartupConfig`) + +A file logger also writes them at the top of each new file it starts (see `File.WriteStartupLinesInNewFiles`). + +| Setting | Default | What it does | +|---|---|---| +| `ApplicationVersion` | empty | The application's version, written on the application line as `app_version='...'`. Empty: on Windows, the executable's file version from its version resource (e.g. `2.3.1.45`, set in RAD Studio under Project > Options > Application > Version Info), if it has one; nothing on Linux. | +| `Banner` | empty | A line written first, if set. | +| `WriteLoggerVersion` | `true` | The ASWLog version and the logger's class, right after the banner, e.g. `ASWLog: version=1.0.0, logger=TASWFileLog`, so a log says which ASWLog wrote it. | +| `WriteApplicationInfo` | `true` | The executable's path, the application's version (see `ApplicationVersion`) and the build target, e.g. `App: app_exe='C:\MyApp\MyApp.exe', app_version='2.3.1.45', app_target=Win64`. | +| `WriteCommandLine` | `false` | The command line, added to the application line. | +| `WriteDriveInfo` | `true` | The disk's total, free and used space. | +| `WriteMemoryUsage` | `true` | The process's memory use (working set and peak). | +| `WriteOSInfo` | `true` | The operating system, its version and the architecture. | +| `WriteSystemMemoryInfo` | `true` | The system's total and available memory. | +| `WriteTimeInfo` | `true` | The time in UTC, and in local time with its offset. | + +## `Shutdown`: the lines at shutdown and on a crash (`TASWShutdownConfig`) + +| Setting | Default | What it does | +|---|---|---| +| `WriteLine` | `true` | "Logger shutdown: