diff --git a/README.md b/README.md index 448ff199..de34c105 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The Level Zero Loader provides built-in logging controlled via environment varia | `ZEL_LOADER_LOG_CONSOLE` | `0` | Set to `1` to enable console (stderr) logging, overrides file logging | | `ZEL_LOADER_LOGGING_LEVEL` | `warn` | Log level: `trace`, `debug`, `info`, `warn`, `error`, `critical`, `off` | | `ZEL_LOADER_LOG_DIR` | `~/.oneapi_logs` | Directory to write the log file into | -| `ZEL_LOADER_LOG_FILE` | `ze_loader.log` | Log filename | +| `ZEL_LOADER_LOG_FILE` | `ze_loader.log` | Log filename, supports runtime pattern tokens (see below) | | `ZEL_LOADER_LOG_PATTERN` | see below | Custom log format pattern | ## Output destination @@ -87,6 +87,27 @@ The two flags control output as follows: The log directory (`ZEL_LOADER_LOG_DIR`) is created automatically on first use if it does not exist. +## Log file pattern + +`ZEL_LOADER_LOG_FILE` may contain runtime tokens that the loader expands when resolving the log +filename. This makes it possible to keep separate log files per process or per run. A filename +without tokens (the default `ze_loader.log`) is used unchanged. + +Supported filename pattern tokens: +- `%P` — process id +- `%N` — process executable base name +- `%T` — logger startup timestamp formatted as `YYYYMMDD-HHMMSS` +- `%%` — literal percent sign + +A `%` not followed by `P`, `N`, `T`, or `%` is kept verbatim in the filename. + +Examples: +``` +ZEL_LOADER_LOG_FILE=ze_loader-%P.log +ZEL_LOADER_LOG_FILE=%N-%P.log +ZEL_LOADER_LOG_FILE=%N-%T-%P.log +``` + ## Log pattern Default pattern (used when `ZEL_LOADER_LOG_PATTERN` is not set): diff --git a/source/utils/ze_logger.cpp b/source/utils/ze_logger.cpp index 1049dd6e..570c1aec 100644 --- a/source/utils/ze_logger.cpp +++ b/source/utils/ze_logger.cpp @@ -10,6 +10,7 @@ #include "ze_util.h" #include +#include #include #include #include @@ -54,6 +55,7 @@ static bool winEnableAnsiColor(int fd) { #else #include +#include #include #include #include @@ -70,6 +72,113 @@ namespace loader { // --------------------------------------------------------------------------- namespace { +std::string currentProcessName() { +#ifdef _WIN32 + char module_path[MAX_PATH] = {}; + const DWORD len = GetModuleFileNameA(nullptr, module_path, MAX_PATH); + if (len != 0) { + return sanitizeFileNameComponent(baseNameFromPath(std::string(module_path, len))); + } +#else + char module_path[PATH_MAX] = {}; + const ssize_t len = readlink("/proc/self/exe", module_path, sizeof(module_path) - 1); + if (len > 0) { + module_path[len] = '\0'; + return sanitizeFileNameComponent(baseNameFromPath(module_path)); + } +#endif + return "process"; +} + +std::string startupTimestampForFileName() { + const auto now = std::chrono::system_clock::now(); + const auto now_t = std::chrono::system_clock::to_time_t(now); + std::tm tm_buf{}; +#ifdef _WIN32 + localtime_s(&tm_buf, &now_t); +#else + localtime_r(&now_t, &tm_buf); +#endif + + char timestamp[32] = {}; + std::strftime(timestamp, sizeof(timestamp), "%Y%m%d-%H%M%S", &tm_buf); + return timestamp; +} + +} // namespace (internal process-runtime helpers) + +// The filename-pattern helpers below are defined at namespace scope (and +// declared in ze_logger.h) so unit tests can exercise them directly. They are +// pure string transforms except that expandLogFilePattern() reads the process +// pid/name/startup-time to fill the %P/%N/%T tokens. +std::string baseNameFromPath(const std::string &path) { + const std::size_t pos = path.find_last_of("\\/"); + if (pos == std::string::npos) { + return path; + } + return path.substr(pos + 1); +} + +std::string sanitizeFileNameComponent(std::string value) { + if (value.empty()) { + return "process"; + } + for (char &ch : value) { + const unsigned char uch = static_cast(ch); + if (uch < 0x20 || ch == '<' || ch == '>' || ch == ':' || ch == '"' || + ch == '/' || ch == '\\' || ch == '|' || ch == '?' || ch == '*') { + ch = '_'; + } + } + return value; +} + +std::string expandLogFilePattern(const std::string &pattern) { + // Fast path: a filename without any token marker (e.g. the default + // "ze_loader.log") is used as-is, avoiding the pid/process-name/timestamp + // lookups and their syscalls. + if (pattern.find('%') == std::string::npos) { + return pattern; + } + + const std::string pid = std::to_string(static_cast(GET_PID())); + const std::string process_name = currentProcessName(); + const std::string timestamp = startupTimestampForFileName(); + + std::string expanded; + expanded.reserve(pattern.size() + pid.size() + process_name.size()); + + for (std::size_t i = 0; i < pattern.size(); ++i) { + if (pattern[i] == '%' && i + 1 < pattern.size()) { + switch (pattern[i + 1]) { + case '%': + expanded.push_back('%'); + ++i; + continue; + case 'P': + expanded += pid; + ++i; + continue; + case 'N': + expanded += process_name; + ++i; + continue; + case 'T': + expanded += timestamp; + ++i; + continue; + default: + break; + } + } + expanded.push_back(pattern[i]); + } + + return expanded; +} + +namespace { + struct AnsiColor { static const char *reset() { return "\033[0m"; } static const char *trace() { return "\033[37m"; } // white @@ -575,10 +684,17 @@ std::shared_ptr createLogger(const std::string &caller) { loader_file = LOADER_LOG_FILE; } + // Expand filename pattern tokens (%P, %N, %T, %%) within ZEL_LOADER_LOG_FILE. + // A filename without tokens is returned unchanged, preserving existing behaviour. + std::string resolved_loader_file = expandLogFilePattern(loader_file); + if (resolved_loader_file.empty()) { + resolved_loader_file = loader_file; + } + #ifdef _WIN32 - std::string full_log_file_path = log_directory + "\\" + loader_file; + std::string full_log_file_path = log_directory + "\\" + resolved_loader_file; #else - std::string full_log_file_path = log_directory + "/" + loader_file; + std::string full_log_file_path = log_directory + "/" + resolved_loader_file; #endif const uint32_t logging_mode = getenv_tomode("ZEL_ENABLE_LOADER_LOGGING"); @@ -679,6 +795,7 @@ std::shared_ptr createLogger(const std::string &caller) { cfg += "\n ZEL_LOADER_LOGGING_LEVEL : " + log_level; cfg += "\n ZEL_LOADER_LOG_DIR : " + log_directory; cfg += "\n ZEL_LOADER_LOG_FILE : " + loader_file; + cfg += "\n Resolved log filename : " + resolved_loader_file; cfg += "\n ZEL_LOADER_LOG_PATTERN : " + log_pattern; cfg += "\n Output : " + output_dest; logger->info(cfg); diff --git a/source/utils/ze_logger.h b/source/utils/ze_logger.h index d9bf9882..cc842964 100644 --- a/source/utils/ze_logger.h +++ b/source/utils/ze_logger.h @@ -108,6 +108,17 @@ std::string to_string(ze_result_t result); // Factory: reads ZEL_* env vars and constructs an appropriately configured logger. std::shared_ptr createLogger(const std::string &caller = "Loader"); +// Log-filename helpers, exposed for unit testing (not a stable public API). +// baseNameFromPath() strips any directory prefix. sanitizeFileNameComponent() +// replaces path-hostile characters with '_' (and maps empty input to +// "process"). expandLogFilePattern() resolves the runtime tokens documented in +// the README within ZEL_LOADER_LOG_FILE: %P (pid), %N (process base name), +// %T (startup timestamp YYYYMMDD-HHMMSS) and %% (literal percent); a filename +// with no '%' is returned unchanged. +std::string baseNameFromPath(const std::string &path); +std::string sanitizeFileNameComponent(std::string value); +std::string expandLogFilePattern(const std::string &pattern); + // A permanently-alive no-op logger instance suitable for use as a raw-pointer // default in components (e.g. the validation layer) that must never hold a // shared_ptr across dlclose/process-exit boundaries. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9898efb9..d922bbfb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1214,3 +1214,19 @@ if(UNIX AND NOT APPLE) target_link_libraries(ze_logger_teardown_unit_tests PRIVATE pthread) endif() add_test(NAME ze_logger_teardown_unit_tests COMMAND ze_logger_teardown_unit_tests) + +# ----------------------------------------------------------------------------- +# Standalone unit test for the ZEL_LOADER_LOG_FILE filename-pattern expansion +# (%P / %N / %T / %% tokens). Links ONLY level_zero_utils, so it is a true unit +# test independent of the static/dynamic build model and of any hardware. +# ----------------------------------------------------------------------------- +add_executable(ze_logger_filename_pattern_unit_tests ze_logger_filename_pattern_unit_tests.cpp) +target_include_directories(ze_logger_filename_pattern_unit_tests PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/source/inc +) +target_link_libraries(ze_logger_filename_pattern_unit_tests PRIVATE GTest::gtest_main level_zero_utils) +if(UNIX AND NOT APPLE) + target_link_libraries(ze_logger_filename_pattern_unit_tests PRIVATE pthread) +endif() +add_test(NAME ze_logger_filename_pattern_unit_tests COMMAND ze_logger_filename_pattern_unit_tests) diff --git a/test/ze_logger_filename_pattern_unit_tests.cpp b/test/ze_logger_filename_pattern_unit_tests.cpp new file mode 100644 index 00000000..ed041946 --- /dev/null +++ b/test/ze_logger_filename_pattern_unit_tests.cpp @@ -0,0 +1,140 @@ +/* + * + * Copyright (C) 2026 Intel Corporation + * + * SPDX-License-Identifier: MIT + * + */ + +// Unit tests for the ZEL_LOADER_LOG_FILE filename-pattern expansion implemented +// in source/utils/ze_logger.cpp (baseNameFromPath / sanitizeFileNameComponent / +// expandLogFilePattern). They link ONLY level_zero_utils -- no loader, drivers, +// or layers -- so they are true unit tests independent of any hardware. + +#include + +#include "ze_logger.h" + +#include +#include + +#if defined(_WIN32) +#include +#define TEST_GET_PID() _getpid() +#else +#include +#define TEST_GET_PID() getpid() +#endif + +namespace { + +std::string currentPidString() { + return std::to_string(static_cast(TEST_GET_PID())); +} + +// ----------------------------------------------------------------------------- +// baseNameFromPath +// ----------------------------------------------------------------------------- + +TEST(ZeLoggerBaseNameFromPath, GivenNoSeparatorThenReturnsInputUnchanged) { + EXPECT_EQ(std::string("ze_loader.log"), loader::baseNameFromPath("ze_loader.log")); +} + +TEST(ZeLoggerBaseNameFromPath, GivenForwardSlashPathThenReturnsLastComponent) { + EXPECT_EQ(std::string("app.exe"), loader::baseNameFromPath("/usr/bin/app.exe")); +} + +TEST(ZeLoggerBaseNameFromPath, GivenBackslashPathThenReturnsLastComponent) { + EXPECT_EQ(std::string("app.exe"), loader::baseNameFromPath("C:\\Program Files\\app.exe")); +} + +TEST(ZeLoggerBaseNameFromPath, GivenTrailingSeparatorThenReturnsEmpty) { + EXPECT_EQ(std::string(""), loader::baseNameFromPath("/usr/bin/")); +} + +// ----------------------------------------------------------------------------- +// sanitizeFileNameComponent +// ----------------------------------------------------------------------------- + +TEST(ZeLoggerSanitizeFileNameComponent, GivenEmptyThenReturnsProcessPlaceholder) { + EXPECT_EQ(std::string("process"), loader::sanitizeFileNameComponent("")); +} + +TEST(ZeLoggerSanitizeFileNameComponent, GivenPlainNameThenReturnedUnchanged) { + EXPECT_EQ(std::string("app_name-1.2"), loader::sanitizeFileNameComponent("app_name-1.2")); +} + +TEST(ZeLoggerSanitizeFileNameComponent, GivenPathHostileCharactersThenReplacedWithUnderscore) { + EXPECT_EQ(std::string("a_b_c_d_e_f_g_h_i"), + loader::sanitizeFileNameComponent("ac:d\"e/f\\g|h?i")); + EXPECT_EQ(std::string("star_"), loader::sanitizeFileNameComponent("star*")); +} + +TEST(ZeLoggerSanitizeFileNameComponent, GivenControlCharacterThenReplacedWithUnderscore) { + EXPECT_EQ(std::string("a_b"), loader::sanitizeFileNameComponent(std::string("a\x01") + "b")); +} + +// ----------------------------------------------------------------------------- +// expandLogFilePattern +// ----------------------------------------------------------------------------- + +TEST(ZeLoggerExpandLogFilePattern, GivenNoTokenThenReturnedUnchanged) { + EXPECT_EQ(std::string("ze_loader.log"), loader::expandLogFilePattern("ze_loader.log")); +} + +TEST(ZeLoggerExpandLogFilePattern, GivenEmptyThenReturnsEmpty) { + EXPECT_EQ(std::string(""), loader::expandLogFilePattern("")); +} + +TEST(ZeLoggerExpandLogFilePattern, GivenDoublePercentThenCollapsedToSinglePercent) { + EXPECT_EQ(std::string("a%b"), loader::expandLogFilePattern("a%%b")); + EXPECT_EQ(std::string("100%"), loader::expandLogFilePattern("100%%")); +} + +TEST(ZeLoggerExpandLogFilePattern, GivenPidTokenThenReplacedWithProcessId) { + const std::string pid = currentPidString(); + EXPECT_EQ("ze_loader-" + pid + ".log", loader::expandLogFilePattern("ze_loader-%P.log")); + EXPECT_EQ(pid + pid, loader::expandLogFilePattern("%P%P")); +} + +TEST(ZeLoggerExpandLogFilePattern, GivenNameTokenThenReplacedWithSafeNonEmptyComponent) { + const std::string name = loader::expandLogFilePattern("%N"); + EXPECT_FALSE(name.empty()); + // The expansion is a single sanitized filename component: no path + // separators and no other path-hostile characters survive. + EXPECT_EQ(std::string::npos, name.find('/')); + EXPECT_EQ(std::string::npos, name.find('\\')); + EXPECT_EQ(std::string::npos, name.find_first_of("<>:\"|?*")); +} + +TEST(ZeLoggerExpandLogFilePattern, GivenTimestampTokenThenMatchesExpectedFormat) { + const std::string ts = loader::expandLogFilePattern("%T"); + EXPECT_TRUE(std::regex_match(ts, std::regex("[0-9]{8}-[0-9]{6}"))) + << "unexpected timestamp: " << ts; +} + +TEST(ZeLoggerExpandLogFilePattern, GivenUnknownTokenThenKeptVerbatim) { + // An unrecognised token letter is preserved together with its '%'. + EXPECT_EQ(std::string("a%Xb"), loader::expandLogFilePattern("a%Xb")); +} + +TEST(ZeLoggerExpandLogFilePattern, GivenTrailingLonePercentThenKeptVerbatim) { + // A '%' with no following character cannot start a token and is emitted as-is. + EXPECT_EQ(std::string("log%"), loader::expandLogFilePattern("log%")); +} + +TEST(ZeLoggerExpandLogFilePattern, GivenCombinedTokensThenAllExpanded) { + const std::string result = loader::expandLogFilePattern("%N-%T-%P.log"); + const std::string pid = currentPidString(); + + // Ends with the pid token expansion followed by the literal suffix. + const std::string suffix = "-" + pid + ".log"; + ASSERT_GE(result.size(), suffix.size()); + EXPECT_EQ(suffix, result.substr(result.size() - suffix.size())); + + // Contains an embedded timestamp somewhere in the middle. + EXPECT_TRUE(std::regex_search(result, std::regex("[0-9]{8}-[0-9]{6}"))) + << "no timestamp in: " << result; +} + +} // namespace