diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b17ef3f..a3a43d0a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Increment the: ## [Unreleased] +* [RESOURCE DETECTOR] Add required and recommended attributes for process entity + [#4437](https://github.com/open-telemetry/opentelemetry-cpp/pull/4437) * [CONFIGURATION] Add a configuration builder for the host resource detector [#4451](https://github.com/open-telemetry/opentelemetry-cpp/issues/4451) * [CONFIGURATION] Build the configured resource detectors in SdkBuilder, apply diff --git a/resource_detectors/BUILD b/resource_detectors/BUILD index e0a715f55..c580e47af 100644 --- a/resource_detectors/BUILD +++ b/resource_detectors/BUILD @@ -23,6 +23,7 @@ cc_library( "//resource_detectors:headers", "//sdk:headers", "//sdk/src/resource", + "@boringssl//:crypto", ], ) diff --git a/resource_detectors/CMakeLists.txt b/resource_detectors/CMakeLists.txt index 02c0d88c2..62250545f 100644 --- a/resource_detectors/CMakeLists.txt +++ b/resource_detectors/CMakeLists.txt @@ -5,6 +5,8 @@ # opentelemetry_container_resource_detector # +find_package(OpenSSL REQUIRED) + add_library(opentelemetry_container_resource_detector src/container_detector.cc src/container_detector_utils.cc) @@ -23,6 +25,8 @@ target_include_directories( # opentelemetry_container_resource_detector_builder # +find_package(OpenSSL REQUIRED) + add_library(opentelemetry_container_resource_detector_builder src/container_detector_builder.cc) @@ -109,7 +113,8 @@ set_target_properties(opentelemetry_process_resource_detector set_target_version(opentelemetry_process_resource_detector) target_link_libraries(opentelemetry_process_resource_detector - PUBLIC opentelemetry_resources) + PUBLIC opentelemetry_resources + PRIVATE OpenSSL::Crypto) target_include_directories( opentelemetry_process_resource_detector PUBLIC "$" diff --git a/resource_detectors/README.md b/resource_detectors/README.md index c9ac85ac5..144edb36e 100644 --- a/resource_detectors/README.md +++ b/resource_detectors/README.md @@ -81,6 +81,20 @@ or inaccessible. | --- | --- | --- | --- | --- | | `process.pid` | Process ID | Yes | Yes | Yes | | `process.executable.path` | Path via `/proc` (Linux) or Win32 APIs | Yes | Yes | Yes | +| `process.executable.name` | Basename of the executable path | Yes | Yes | Yes | +| `process.creation.time` | Process start time in ISO 8601 UTC | Yes | Yes | Yes | +| `process.owner` | Username of the process owner | Yes | Yes | Yes | +| `process.executable.build_id.htlhash` | Deterministic SHA256-based build ID | Yes | Yes | Yes | + +Limitations: + +- On macOS, `process.executable.path`, `process.executable.name`, and + `process.executable.build_id.htlhash` are resolved via `_NSGetExecutablePath()`, + which only works for the **current process**. These attributes are always + populated for the running process, but cannot be resolved for an arbitrary PID. +- For executables smaller than 4096 bytes the head and tail slices of the + htlhash algorithm overlap (both cover the entire file), which is correct per + the [spec](https://opentelemetry.io/docs/specs/semconv/attributes-registry/process/#algorithm-for-processexecutablebuild_idhtlhash). ### Env Entity Resource Detector (Experimental) diff --git a/resource_detectors/include/opentelemetry/resource_detectors/detail/process_detector_utils.h b/resource_detectors/include/opentelemetry/resource_detectors/detail/process_detector_utils.h index 95a864867..65003b45d 100644 --- a/resource_detectors/include/opentelemetry/resource_detectors/detail/process_detector_utils.h +++ b/resource_detectors/include/opentelemetry/resource_detectors/detail/process_detector_utils.h @@ -15,6 +15,15 @@ namespace resource_detector namespace detail { +/** + * Contains the path and name of the executable. + */ +struct ExecutableInfo +{ + std::string path; + std::string name; +}; + /** * Forms a file path for a process type based on the given PID. * for example - /proc//cmdline, /proc//exe @@ -22,16 +31,16 @@ namespace detail std::string FormFilePath(const int32_t &pid, const char *process_type); /** - * Retrieves the absolute file system path to the executable for a given PID. + * Retrieves the absolute file system path and the base name of the process executable. * Platform-specific behavior: - * - Windows: Uses OpenProcess() + GetProcessImageFileNameW(). + * - Windows: Uses OpenProcess() + QueryFullProcessImageNameW(). * - Linux/Unix: Reads the /proc//exe symbolic link. * - macOS: Uses _NSGetExecutablePath() for the current process only; returns * an empty string for other PIDs. * * @param pid Process ID. */ -std::string GetExecutablePath(const int32_t &pid); +ExecutableInfo GetExecutableInfo(const int32_t &pid); /** * Extracts the command-line arguments and the command. @@ -49,6 +58,47 @@ std::vector ExtractCommandWithArgs(const std::string &command_line_ */ std::vector GetCommandWithArgs(const int32_t &pid); +/** + * Retrieves the process creation time as an ISO 8601 string (e.g. "2023-11-21T09:25:34.853Z"). + * Platform-specific behavior: + * - Linux: Reads starttime from /proc//stat and combines with boot time. + * - macOS: Uses sysctl(KERN_PROC) to obtain kinfo_proc.kp_proc.p_starttime. + * - Windows: Uses GetProcessTimes() to obtain lpCreationTime (FILETIME). + * Returns an empty string if the information is unavailable. + * + * @param pid Process ID. + */ +std::string GetProcessCreationTime(const int32_t &pid); + +/** + * Retrieves the username of the user that owns the process. + * Platform-specific behavior: + * - Linux/macOS: Uses getuid() + getpwuid_r() to resolve the effective user name. + * - Windows: Uses OpenProcessToken() + GetTokenInformation() + LookupAccountSidW(). + * Returns an empty string if the information is unavailable. + * + * @param pid Process ID. + */ +std::string GetProcessOwner(); + +/** + * Computes the deterministic htlhash build ID for the process executable. + * Algorithm: SHA256(File[:4096] || File[-4096:] || BigEndianUInt64(FileLen)) + * For files <= 4096 bytes the two slices overlap (both equal the whole file), + * matching the spec requirement that inputs are "not padded". + * The result is the first 16 bytes (128 bits) of the digest as a lowercase hex string. + * Returns an empty string if the executable cannot be read. + * + * @param pid Process ID. + */ +std::string GetExecutableBuildIdHtlhash(const int32_t &pid); + +/** + * Computes a SHA-256 hash of the given data and returns it as a lowercase hex string. + * This is exposed primarily for unit testing the internal SHA-256 implementation. + */ +std::string ComputeSha256Hex(const std::string &data); + } // namespace detail } // namespace resource_detector OPENTELEMETRY_END_NAMESPACE diff --git a/resource_detectors/include/opentelemetry/resource_detectors/process_detector.h b/resource_detectors/include/opentelemetry/resource_detectors/process_detector.h index 06d629796..3c0c360be 100644 --- a/resource_detectors/include/opentelemetry/resource_detectors/process_detector.h +++ b/resource_detectors/include/opentelemetry/resource_detectors/process_detector.h @@ -12,20 +12,28 @@ namespace resource_detector /** * ProcessResourceDetector to detect resource attributes when running in a process. - * This detector extracts metadata such as process ID, executable path, and command line arguments - * and sets attributes like process.pid, process.executable.path, and process.command following - * the OpenTelemetry semantic conventions. + * This detector extracts metadata such as process ID, executable path, executable name, + * process creation time, process owner, and executable build ID, then sets attributes + * following the OpenTelemetry semantic conventions: + * + * Process entity attributes: + * - process.pid (required) — current process identifier + * - process.creation.time (required) — ISO 8601 UTC creation timestamp + * - process.owner (recommended) — username of the process owner + * + * Process Executable entity attributes: + * - process.executable.path (recommended) — full path via /proc or Win32 APIs + * - process.executable.name (recommended) — basename of the executable path + * - process.executable.build_id.htlhash (required) — deterministic SHA256-based build ID + * + * Attributes that cannot be determined on the current platform are omitted. */ class ProcessResourceDetector : public opentelemetry::sdk::resource::ResourceDetector { public: /** * Detect retrieves the resource attributes for the current process. - * It reads: - * - process.pid from the current process ID - * - process.executable.path from the executable path of the current process - * - process.command from the command used to launch the process - * and returns a Resource with these attributes set. + * See the class-level documentation for the complete list of attributes populated. */ opentelemetry::sdk::resource::Resource Detect() noexcept override; }; diff --git a/resource_detectors/src/process_detector.cc b/resource_detectors/src/process_detector.cc index 8dd760bd8..673ece152 100644 --- a/resource_detectors/src/process_detector.cc +++ b/resource_detectors/src/process_detector.cc @@ -36,10 +36,15 @@ opentelemetry::sdk::resource::Resource ProcessResourceDetector::Detect() noexcep try { - std::string executable_path = opentelemetry::resource_detector::detail::GetExecutablePath(pid); - if (!executable_path.empty()) + auto exe_info = opentelemetry::resource_detector::detail::GetExecutableInfo(pid); + if (!exe_info.path.empty()) { - attributes[semconv::process::kProcessExecutablePath] = std::move(executable_path); + attributes[semconv::process::kProcessExecutablePath] = std::move(exe_info.path); + + if (!exe_info.name.empty()) + { + attributes[semconv::process::kProcessExecutableName] = std::move(exe_info.name); + } } } catch (const ::std::exception &ex) @@ -65,6 +70,50 @@ opentelemetry::sdk::resource::Resource ProcessResourceDetector::Detect() noexcep << "Error extracting command with arguments: " << ex.what()); } + try + { + std::string creation_time = + opentelemetry::resource_detector::detail::GetProcessCreationTime(pid); + if (!creation_time.empty()) + { + attributes[semconv::process::kProcessCreationTime] = std::move(creation_time); + } + } + catch (const std::exception &ex) + { + OTEL_INTERNAL_LOG_ERROR("[Process Resource Detector] " + << "Error extracting process creation time: " << ex.what()); + } + + try + { + std::string owner = opentelemetry::resource_detector::detail::GetProcessOwner(); + if (!owner.empty()) + { + attributes[semconv::process::kProcessOwner] = std::move(owner); + } + } + catch (const std::exception &ex) + { + OTEL_INTERNAL_LOG_ERROR("[Process Resource Detector] " << "Error extracting process owner: " + << ex.what()); + } + + try + { + std::string build_id = + opentelemetry::resource_detector::detail::GetExecutableBuildIdHtlhash(pid); + if (!build_id.empty()) + { + attributes[semconv::process::kProcessExecutableBuildIdHtlhash] = std::move(build_id); + } + } + catch (const std::exception &ex) + { + OTEL_INTERNAL_LOG_ERROR("[Process Resource Detector] " + << "Error computing executable build id (htlhash): " << ex.what()); + } + return ResourceDetector::Create(attributes); } diff --git a/resource_detectors/src/process_detector_utils.cc b/resource_detectors/src/process_detector_utils.cc index fe4823bf9..8ca014912 100644 --- a/resource_detectors/src/process_detector_utils.cc +++ b/resource_detectors/src/process_detector_utils.cc @@ -3,10 +3,14 @@ #include "opentelemetry/resource_detectors/detail/process_detector_utils.h" +#include +#include #include #include #include +#include + #if defined(__APPLE__) # include #endif @@ -21,7 +25,14 @@ #else # include # include -# include +#endif + +#ifdef __APPLE__ +# include +#endif + +#ifndef _MSC_VER +# include #endif #include "opentelemetry/version.h" @@ -35,58 +46,73 @@ namespace detail constexpr const char *kExecutableName = "exe"; constexpr const char *kCmdlineName = "cmdline"; -std::string GetExecutablePath(const int32_t &pid) +ExecutableInfo GetExecutableInfo(const int32_t &pid) { + ExecutableInfo info; #ifdef _MSC_VER HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, static_cast(pid)); if (!hProcess) { - return std::string(); + return info; } WCHAR wbuffer[MAX_PATH]; - DWORD len = GetProcessImageFileNameW(hProcess, wbuffer, MAX_PATH); + DWORD len = MAX_PATH; + BOOL success = QueryFullProcessImageNameW(hProcess, 0, wbuffer, &len); CloseHandle(hProcess); - if (len == 0) + if (!success || len == 0) { - return std::string(); + return info; } // Convert UTF-16 to UTF-8 int size_needed = WideCharToMultiByte(CP_UTF8, 0, wbuffer, len, NULL, 0, NULL, NULL); + if (size_needed <= 0) + { + return info; + } std::string utf8_path(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, wbuffer, len, &utf8_path[0], size_needed, NULL, NULL); - - return utf8_path; -#elif defined(__APPLE__) - if (pid != static_cast(getpid())) + // cppcheck-suppress containerOutOfBounds + if (WideCharToMultiByte(CP_UTF8, 0, wbuffer, len, &utf8_path[0], size_needed, NULL, NULL) <= 0) { - return std::string(); + return info; } - + info.path = utf8_path; +#elif defined(__APPLE__) char path[4096]; uint32_t size = sizeof(path); - if (_NSGetExecutablePath(path, &size) != 0) + if (_NSGetExecutablePath(path, &size) == 0) { - return std::string(); + info.path = std::string(path); } - return std::string(path); #else - std::string path = FormFilePath(pid, kExecutableName); + std::string proc_path = FormFilePath(pid, kExecutableName); char buffer[4096]; - ssize_t len = readlink(path.c_str(), buffer, sizeof(buffer) - 1); + ssize_t len = readlink(proc_path.c_str(), buffer, sizeof(buffer) - 1); if (len != -1) { buffer[len] = '\0'; - return std::string(buffer); + info.path = std::string(buffer); } - - return std::string(); #endif + + if (!info.path.empty()) + { + std::size_t sep = info.path.find_last_of("/\\"); + if (sep != std::string::npos) + { + info.name = info.path.substr(sep + 1); + } + else + { + info.name = info.path; + } + } + return info; } std::vector ExtractCommandWithArgs(const std::string &command_line_path) @@ -121,8 +147,9 @@ std::vector GetCommandWithArgs(const int32_t &pid) int size_needed = WideCharToMultiByte(CP_UTF8, 0, argvW[i], -1, NULL, 0, NULL, NULL); if (size_needed > 0) { - std::string arg(size_needed - 1, 0); + std::string arg(size_needed, 0); WideCharToMultiByte(CP_UTF8, 0, argvW[i], -1, &arg[0], size_needed, NULL, NULL); + arg.resize(size_needed - 1); args.push_back(arg); } } @@ -151,6 +178,336 @@ std::string FormFilePath(const int32_t &pid, const char *process_type) return std::string(buff, len); } +// --------------------------------------------------------------------------- +// GetProcessCreationTime +// --------------------------------------------------------------------------- + +#ifdef _MSC_VER +namespace +{ +// Convert a FILETIME to an ISO 8601 UTC string "YYYY-MM-DDTHH:MM:SS.mmmZ". +std::string FileTimeToIso8601(const FILETIME &ft) +{ + SYSTEMTIME st; + if (!FileTimeToSystemTime(&ft, &st)) + { + return std::string(); + } + char buf[128]; + std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", static_cast(st.wYear), + static_cast(st.wMonth), static_cast(st.wDay), static_cast(st.wHour), + static_cast(st.wMinute), static_cast(st.wSecond), + static_cast(st.wMilliseconds)); + return std::string(buf); +} +} // namespace +#endif // _MSC_VER + +#if !defined(_MSC_VER) && !defined(__APPLE__) +namespace +{ +// Parse the 22nd field (starttime, in clock ticks since boot) from /proc//stat. +// The comm field (2nd) may contain spaces/parens, so we scan past the closing ')'. +bool ParseStarttimeFromProcStat(const std::string &stat_path, uint64_t &starttime_ticks) +{ + std::ifstream f(stat_path); + if (!f.is_open()) + { + return false; + } + std::string line; + if (!std::getline(f, line)) + { + return false; + } + // Skip past the closing ')' of the comm field. + std::size_t pos = line.rfind(')'); + if (pos == std::string::npos) + { + return false; + } + pos += 2; // skip ') ' + // Fields 3..21 (19 fields) come before starttime (field 22, 0-indexed from 3 → index 19). + for (int i = 0; i < 19; ++i) + { + pos = line.find(' ', pos); + if (pos == std::string::npos) + { + return false; + } + ++pos; + } + starttime_ticks = std::stoull(line.substr(pos)); + return true; +} + +// Read boot time in seconds since epoch from /proc/stat. +bool ReadBootTimeSecs(uint64_t &boot_time) +{ + std::ifstream f("/proc/stat"); + if (!f.is_open()) + { + return false; + } + std::string line; + while (std::getline(f, line)) + { + if (line.substr(0, 6) == "btime ") + { + boot_time = std::stoull(line.substr(6)); + return true; + } + } + return false; +} +} // namespace +#endif // !_MSC_VER && !__APPLE__ + +std::string GetProcessCreationTime(const int32_t &pid) +{ +#ifdef _MSC_VER + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, static_cast(pid)); + if (!hProcess) + { + return std::string(); + } + FILETIME creation_time, exit_time, kernel_time, user_time; + bool ok = GetProcessTimes(hProcess, &creation_time, &exit_time, &kernel_time, &user_time) != 0; + CloseHandle(hProcess); + if (!ok) + { + return std::string(); + } + return FileTimeToIso8601(creation_time); + +#elif defined(__APPLE__) + struct kinfo_proc kp; + std::size_t len = sizeof(kp); + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, static_cast(pid)}; + if (sysctl(mib, 4, &kp, &len, nullptr, 0) != 0 || len == 0) + { + return std::string(); + } + // kp_proc.p_starttime is a struct timeval (seconds + microseconds). + time_t secs = kp.kp_proc.p_starttime.tv_sec; + int64_t usecs = kp.kp_proc.p_starttime.tv_usec; + struct tm utc_time = {}; + gmtime_r(&secs, &utc_time); + char buf[128]; + std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03lldZ", utc_time.tm_year + 1900, + utc_time.tm_mon + 1, utc_time.tm_mday, utc_time.tm_hour, utc_time.tm_min, + utc_time.tm_sec, usecs / 1000); + return std::string(buf); + +#else + // Linux: starttime (ticks since boot) from /proc//stat + btime from /proc/stat. + uint64_t starttime_ticks = 0; + uint64_t boot_time_secs = 0; + + std::string stat_path = FormFilePath(pid, "stat"); + if (!ParseStarttimeFromProcStat(stat_path, starttime_ticks)) + { + return std::string(); + } + if (!ReadBootTimeSecs(boot_time_secs)) + { + return std::string(); + } + + int64_t clk_tck = sysconf(_SC_CLK_TCK); + if (clk_tck <= 0) + { + return std::string(); + } + + const auto clk = static_cast(clk_tck); + uint64_t start_secs = boot_time_secs + starttime_ticks / clk; + uint64_t start_msecs = (starttime_ticks % clk) * 1000 / clk; + + time_t t = static_cast(start_secs); + struct tm utc_time = {}; + gmtime_r(&t, &utc_time); + + char buf[128]; + std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03lluZ", utc_time.tm_year + 1900, + utc_time.tm_mon + 1, utc_time.tm_mday, utc_time.tm_hour, utc_time.tm_min, + utc_time.tm_sec, static_cast(start_msecs)); + return std::string(buf); +#endif +} + +// --------------------------------------------------------------------------- +// GetProcessOwner +// --------------------------------------------------------------------------- + +std::string GetProcessOwner() +{ +#ifdef _MSC_VER + // On Windows, open the current process token and look up the account SID. + HANDLE hToken = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) + { + return std::string(); + } + + DWORD token_info_len = 0; + GetTokenInformation(hToken, TokenUser, nullptr, 0, &token_info_len); + if (token_info_len == 0) + { + CloseHandle(hToken); + return std::string(); + } + + std::vector token_info_buf(token_info_len); + if (!GetTokenInformation(hToken, TokenUser, token_info_buf.data(), token_info_len, + &token_info_len)) + { + CloseHandle(hToken); + return std::string(); + } + CloseHandle(hToken); + + TOKEN_USER *token_user = reinterpret_cast(token_info_buf.data()); + WCHAR name[256]; + WCHAR domain[256]; + DWORD name_len = 256; + DWORD domain_len = 256; + SID_NAME_USE sid_use; + if (!LookupAccountSidW(nullptr, token_user->User.Sid, name, &name_len, domain, &domain_len, + &sid_use)) + { + return std::string(); + } + + // Convert UTF-16 username to UTF-8. + int size_needed = WideCharToMultiByte(CP_UTF8, 0, name, -1, nullptr, 0, nullptr, nullptr); + if (size_needed <= 0) + { + return std::string(); + } + std::string utf8_name(size_needed, '\0'); + WideCharToMultiByte(CP_UTF8, 0, name, -1, &utf8_name[0], size_needed, nullptr, nullptr); + utf8_name.resize(size_needed - 1); + return utf8_name; + +#else + // POSIX (Linux + macOS): resolve effective UID to a username via getpwuid_r. + uid_t uid = getuid(); + struct passwd pw = {}; + struct passwd *result = nullptr; + char buf[1024]; + if (getpwuid_r(uid, &pw, buf, sizeof(buf), &result) != 0 || result == nullptr) + { + return std::string(); + } + return std::string(result->pw_name); +#endif +} + +// --------------------------------------------------------------------------- +// GetExecutableBuildIdHtlhash (self-contained SHA-256, no external deps) +// --------------------------------------------------------------------------- + +namespace +{ + +// Encode the first `byte_count` bytes of `digest` as lowercase hex. +std::string DigestToHex(const uint8_t *digest, std::size_t byte_count) +{ + static const char kHex[] = "0123456789abcdef"; + std::string result; + result.reserve(byte_count * 2); + for (std::size_t i = 0; i < byte_count; ++i) + { + result += kHex[(digest[i] >> 4) & 0x0F]; + result += kHex[digest[i] & 0x0F]; + } + return result; +} + +} // namespace + +std::string GetExecutableBuildIdHtlhash(const int32_t &pid) +{ + std::string exe_path = GetExecutableInfo(pid).path; + if (exe_path.empty()) + { + return std::string(); + } + + std::ifstream f(exe_path, std::ios::binary | std::ios::ate); + if (!f.is_open()) + { + return std::string(); + } + + const auto end_pos = f.tellg(); + if (end_pos < 0) + { + return std::string(); + } + auto file_size = static_cast(end_pos); + + constexpr std::size_t kChunkSize = 4096; + + // Read head (up to 4096 bytes). + std::string head(kChunkSize, '\0'); + f.seekg(0, std::ios::beg); + f.read(&head[0], static_cast(kChunkSize)); + std::size_t head_read = static_cast(f.gcount()); + head.resize(head_read); + + // Read tail (up to 4096 bytes from end). For files <= 4096 bytes the tail + // overlaps the head (both cover the whole file), matching the spec. + std::string tail; + tail.resize(kChunkSize, '\0'); + std::size_t tail_offset = + (file_size < kChunkSize) ? 0 : static_cast(file_size - kChunkSize); + f.seekg(static_cast(tail_offset), std::ios::beg); + f.read(&tail[0], static_cast(kChunkSize)); + std::size_t tail_read = static_cast(f.gcount()); + tail.resize(tail_read); + + // Encode file length as big-endian uint64. + std::uint64_t file_size_be = file_size; + uint8_t len_bytes[8]; + for (int i = 7; i >= 0; --i) + { + len_bytes[i] = static_cast(file_size_be & 0xFF); + file_size_be >>= 8; + } + + // SHA256(head || tail || len_bytes). + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr); + EVP_DigestUpdate(ctx, reinterpret_cast(head.data()), head.size()); + EVP_DigestUpdate(ctx, reinterpret_cast(tail.data()), tail.size()); + EVP_DigestUpdate(ctx, len_bytes, 8); + + uint8_t digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len = 0; + EVP_DigestFinal_ex(ctx, digest, &digest_len); + EVP_MD_CTX_free(ctx); + + // Return first 16 bytes (128 bits) as hex (32 hex chars). + return DigestToHex(digest, 16); +} + +std::string ComputeSha256Hex(const std::string &data) +{ + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr); + EVP_DigestUpdate(ctx, reinterpret_cast(data.data()), data.size()); + + uint8_t digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len = 0; + EVP_DigestFinal_ex(ctx, digest, &digest_len); + EVP_MD_CTX_free(ctx); + + // Return all 32 bytes (256 bits) as hex (64 hex chars). + return DigestToHex(digest, 32); +} + } // namespace detail } // namespace resource_detector OPENTELEMETRY_END_NAMESPACE diff --git a/resource_detectors/src/service_detector_utils.cc b/resource_detectors/src/service_detector_utils.cc index 3a45bb72c..476052f45 100644 --- a/resource_detectors/src/service_detector_utils.cc +++ b/resource_detectors/src/service_detector_utils.cc @@ -73,20 +73,9 @@ std::string GetServiceName() return service_name; } - const std::string executable_path = GetExecutablePath(static_cast(getpid())); - std::string executable_name; - if (!executable_path.empty()) - { - const size_t pos = executable_path.find_last_of("/\\"); - if (pos == std::string::npos) - { - executable_name = executable_path; - } - else - { - executable_name = executable_path.substr(pos + 1); - } - } + const auto exe_info = GetExecutableInfo(static_cast(getpid())); + const std::string &executable_name = exe_info.name; + if (!executable_name.empty()) { std::string fallback_service_name(kUnknownServicePrefix); diff --git a/resource_detectors/test/process_detector_test.cc b/resource_detectors/test/process_detector_test.cc index f331d22d5..de547395b 100644 --- a/resource_detectors/test/process_detector_test.cc +++ b/resource_detectors/test/process_detector_test.cc @@ -24,6 +24,9 @@ #endif #include "opentelemetry/resource_detectors/detail/process_detector_utils.h" +#include "opentelemetry/resource_detectors/process_detector.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/semconv/incubating/process_attributes.h" TEST(ProcessDetectorUtilsTest, FormFilePath) { @@ -78,22 +81,28 @@ TEST(ProcessDetectorUtilsTest, GetExecutablePathTest) } else { - WCHAR wbuffer[MAX_PATH]; - DWORD len = GetProcessImageFileNameW(hProcess, wbuffer, MAX_PATH); + DWORD len = MAX_PATH; + BOOL success = QueryFullProcessImageNameW(hProcess, 0, wbuffer, &len); CloseHandle(hProcess); - if (len == 0) + if (!success || len == 0) { path = std::string(); } else { int size_needed = WideCharToMultiByte(CP_UTF8, 0, wbuffer, len, NULL, 0, NULL, NULL); - std::string utf8_path(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, wbuffer, len, &utf8_path[0], size_needed, NULL, NULL); - - path = utf8_path; + if (size_needed > 0) + { + std::string utf8_path(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, wbuffer, len, &utf8_path[0], size_needed, NULL, NULL); + path = utf8_path; + } + else + { + path = std::string(); + } } } #elif defined(__APPLE__) @@ -122,7 +131,7 @@ TEST(ProcessDetectorUtilsTest, GetExecutablePathTest) path = std::string(); } #endif - std::string expected_path = opentelemetry::resource_detector::detail::GetExecutablePath(pid); + std::string expected_path = opentelemetry::resource_detector::detail::GetExecutableInfo(pid).path; EXPECT_EQ(path, expected_path); } @@ -202,3 +211,149 @@ TEST(ProcessDetectorUtilsTest, GetCommandWithArgsTest) opentelemetry::resource_detector::detail::GetCommandWithArgs(pid); EXPECT_EQ(args, expected_args); } + +// --------------------------------------------------------------------------- +// New utility tests +// --------------------------------------------------------------------------- + +TEST(ProcessDetectorUtilsTest, GetExecutableNameTest) +{ + int32_t pid = getpid(); + auto exe_info = opentelemetry::resource_detector::detail::GetExecutableInfo(pid); + std::string exe_path = exe_info.path; + std::string exe_name = exe_info.name; + + if (exe_path.empty()) + { + EXPECT_TRUE(exe_name.empty()) << "Name should be empty when path is empty"; + return; + } + + EXPECT_FALSE(exe_name.empty()) << "Executable name must not be empty when path is available"; + // The name must be a suffix of the path. + EXPECT_NE(exe_path.find(exe_name), std::string::npos) + << "Executable name '" << exe_name << "' should be a substring of path '" << exe_path << "'"; + // The name must not contain directory separators. + EXPECT_EQ(exe_name.find('/'), std::string::npos) << "Name should not contain '/'"; + EXPECT_EQ(exe_name.find('\\'), std::string::npos) << "Name should not contain '\\'"; +} + +TEST(ProcessDetectorUtilsTest, GetProcessCreationTimeTest) +{ + int32_t pid = getpid(); + std::string iso_time = opentelemetry::resource_detector::detail::GetProcessCreationTime(pid); + +#if defined(_MSC_VER) || defined(__linux__) || defined(__APPLE__) + // On supported platforms we expect a non-empty ISO 8601 result. + EXPECT_FALSE(iso_time.empty()) << "Creation time should be non-empty on this platform"; + + if (!iso_time.empty()) + { + // Very basic format check: "YYYY-MM-DDTHH:MM:SS.mmmZ" = 24 chars. + EXPECT_GE(iso_time.size(), 20u) << "ISO 8601 string too short: " << iso_time; + EXPECT_EQ(iso_time[4], '-') << "Expected '-' at index 4: " << iso_time; + EXPECT_EQ(iso_time[7], '-') << "Expected '-' at index 7: " << iso_time; + EXPECT_EQ(iso_time[10], 'T') << "Expected 'T' at index 10: " << iso_time; + EXPECT_EQ(iso_time.back(), 'Z') << "Expected 'Z' at end: " << iso_time; + } +#else + // On unsupported platforms accept an empty string gracefully. + (void)iso_time; +#endif +} + +TEST(ProcessDetectorUtilsTest, GetProcessOwnerTest) +{ + std::string owner = opentelemetry::resource_detector::detail::GetProcessOwner(); + + // On all supported platforms the effective user name must be non-empty. + EXPECT_FALSE(owner.empty()) << "Process owner should be non-empty"; + // Sanity: no newline characters in the username. + EXPECT_EQ(owner.find('\n'), std::string::npos) << "Owner should not contain newline"; +} + +TEST(ProcessDetectorUtilsTest, GetExecutableBuildIdHtlhashTest) +{ + int32_t pid = getpid(); + std::string exe_path = opentelemetry::resource_detector::detail::GetExecutableInfo(pid).path; + std::string hash1 = opentelemetry::resource_detector::detail::GetExecutableBuildIdHtlhash(pid); + std::string hash2 = opentelemetry::resource_detector::detail::GetExecutableBuildIdHtlhash(pid); + + if (exe_path.empty()) + { + EXPECT_TRUE(hash1.empty()) << "htlhash should be empty when executable path is empty"; + return; + } + + EXPECT_FALSE(hash1.empty()) << "Build ID htlhash must not be empty"; + + if (!hash1.empty()) + { + // Must be exactly 32 lowercase hex characters (16 bytes). + EXPECT_EQ(hash1.size(), 32u) << "htlhash must be 32 hex chars, got: " << hash1; + for (char c : hash1) + { + EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) + << "Non-hex character '" << c << "' in htlhash: " << hash1; + } + // Must be deterministic: two calls on the same process → same result. + EXPECT_EQ(hash1, hash2) << "htlhash must be deterministic"; + } +} + +// --------------------------------------------------------------------------- +// Integration test — Detect() attribute presence +// --------------------------------------------------------------------------- + +TEST(ProcessDetectorUtilsTest, ComputeSha256HexTest) +{ + // Test vectors from NIST FIPS 180-4 + // 1. Empty string + EXPECT_EQ(opentelemetry::resource_detector::detail::ComputeSha256Hex(""), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + + // 2. "abc" + EXPECT_EQ(opentelemetry::resource_detector::detail::ComputeSha256Hex("abc"), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + + // 3. "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + EXPECT_EQ(opentelemetry::resource_detector::detail::ComputeSha256Hex( + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"), + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"); +} + +TEST(ProcessResourceDetectorTest, DetectPopulatesExpectedAttributes) +{ + opentelemetry::resource_detector::ProcessResourceDetector detector; + opentelemetry::sdk::resource::Resource resource = detector.Detect(); + const auto &attrs = resource.GetAttributes(); + + // process.pid — always present. + EXPECT_NE(attrs.find(opentelemetry::semconv::process::kProcessPid), attrs.end()) + << "process.pid must be present"; + +#if defined(_MSC_VER) || defined(__linux__) || defined(__APPLE__) + // process.executable.path — present on Linux, macOS, and Windows. + EXPECT_NE(attrs.find(opentelemetry::semconv::process::kProcessExecutablePath), attrs.end()) + << "process.executable.path must be present on this platform"; + + // process.executable.name — present whenever executable.path is. + EXPECT_NE(attrs.find(opentelemetry::semconv::process::kProcessExecutableName), attrs.end()) + << "process.executable.name must be present on this platform"; + + // process.executable.build_id.htlhash — present on Linux, macOS, and Windows. + EXPECT_NE(attrs.find(opentelemetry::semconv::process::kProcessExecutableBuildIdHtlhash), + attrs.end()) + << "process.executable.build_id.htlhash must be present on this platform"; +#endif + +#if defined(_MSC_VER) || defined(__linux__) || defined(__APPLE__) + // process.creation.time — present on Linux, macOS, and Windows. + EXPECT_NE(attrs.find(opentelemetry::semconv::process::kProcessCreationTime), attrs.end()) + << "process.creation.time must be present on this platform"; + + // process.owner — present on Linux, macOS, and Windows. + EXPECT_NE(attrs.find(opentelemetry::semconv::process::kProcessOwner), attrs.end()) + << "process.owner must be present on this platform"; +#endif +}