Skip to content
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
137 changes: 131 additions & 6 deletions source/utils/ze_logger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ static bool winEnableAnsiColor(int fd) {

#else
#include <unistd.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pwd.h>
Expand All @@ -70,6 +71,117 @@ 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<unsigned char>(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;
}

// Compute the token values per call. This is not a hot path -- createLogger()
// resolves the filename once per process at logger creation -- and computing
// the pid here (rather than caching it) keeps %P correct after fork(): a
// cached static would otherwise expand to the parent's pid in the child.
const std::string pid = std::to_string(static_cast<long long>(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() + timestamp.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
Expand Down Expand Up @@ -574,12 +686,11 @@ std::shared_ptr<ZeLogger> createLogger(const std::string &caller) {
if (loader_file.empty()) {
loader_file = LOADER_LOG_FILE;
}

#ifdef _WIN32
std::string full_log_file_path = log_directory + "\\" + loader_file;
#else
std::string full_log_file_path = log_directory + "/" + loader_file;
#endif
// ZEL_LOADER_LOG_FILE pattern tokens (%P, %N, %T, %%) are expanded lazily,
// only when a file sink is actually created (see below), so the no-op and
// console paths never pay for the pid/exe-path/time lookups. A filename
// without tokens is used unchanged, preserving existing behaviour.
std::string resolved_loader_file;

const uint32_t logging_mode = getenv_tomode("ZEL_ENABLE_LOADER_LOGGING");
const bool logging_enabled = (logging_mode != 0);
Expand Down Expand Up @@ -667,6 +778,17 @@ std::shared_ptr<ZeLogger> createLogger(const std::string &caller) {
}
}
#endif
// Resolve the %P/%N/%T/%% tokens now that a file sink is definitely used.
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 + "\\" + resolved_loader_file;
#else
std::string full_log_file_path = log_directory + "/" + resolved_loader_file;
#endif

logger = std::shared_ptr<ZeLogger>(new ZeLogger(full_log_file_path, level, log_pattern));
output_dest = full_log_file_path;
}
Expand All @@ -679,6 +801,9 @@ std::shared_ptr<ZeLogger> 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;
if (!log_console) {
cfg += "\n Resolved log filename : " + resolved_loader_file;
}
cfg += "\n ZEL_LOADER_LOG_PATTERN : " + log_pattern;
cfg += "\n Output : " + output_dest;
logger->info(cfg);
Expand Down
11 changes: 11 additions & 0 deletions source/utils/ze_logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<ZeLogger> 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.
Expand Down
16 changes: 16 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
140 changes: 140 additions & 0 deletions test/ze_logger_filename_pattern_unit_tests.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#include "ze_logger.h"

#include <regex>
#include <string>

#if defined(_WIN32)
#include <process.h>
#define TEST_GET_PID() _getpid()
#else
#include <unistd.h>
#define TEST_GET_PID() getpid()
#endif

namespace {

std::string currentPidString() {
return std::to_string(static_cast<long long>(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, GivenPlainNameThenReturnsUnchanged) {
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("a<b>c: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, GivenNoTokenThenReturnsUnchanged) {
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