diff --git a/README.md b/README.md index 1d98f871..659c3275 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,26 @@ 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` + +A `%` not followed by `P`, `N`, or `T` 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..2cb53aef 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,98 @@ namespace loader { // --------------------------------------------------------------------------- namespace { +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 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; +} + +std::string expandLogFilePattern(const std::string &pattern) { + if (pattern.empty()) { + 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 '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; +} + struct AnsiColor { static const char *reset() { return "\033[0m"; } static const char *trace() { return "\033[37m"; } // white @@ -575,10 +669,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 +780,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);