diff --git a/resource_detectors/BUILD b/resource_detectors/BUILD index 503949ca28..d68c1a710d 100644 --- a/resource_detectors/BUILD +++ b/resource_detectors/BUILD @@ -14,7 +14,10 @@ cc_library( srcs = [ "container_detector.cc", "container_detector_utils.cc", + "process_detector.cc", + "process_detector_utils.cc", ], + copts = ["-fexceptions"], deps = [ "//api", "//resource_detectors:headers", diff --git a/resource_detectors/CMakeLists.txt b/resource_detectors/CMakeLists.txt index 108ca00bad..7fdba7d73b 100644 --- a/resource_detectors/CMakeLists.txt +++ b/resource_detectors/CMakeLists.txt @@ -1,8 +1,10 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -add_library(opentelemetry_resource_detectors container_detector_utils.cc - container_detector.cc) +add_library( + opentelemetry_resource_detectors + container_detector_utils.cc container_detector.cc process_detector.cc + process_detector_utils.cc) set_target_properties(opentelemetry_resource_detectors PROPERTIES EXPORT_NAME resource_detectors) @@ -28,7 +30,7 @@ otel_add_component( PATTERN "*.h" PATTERN - "container_detector_utils.h" + "resource_detectors/detail/*" EXCLUDE) if(BUILD_TESTING) diff --git a/resource_detectors/container_detector.cc b/resource_detectors/container_detector.cc index 3cf27d4b9e..a71c7cbc32 100644 --- a/resource_detectors/container_detector.cc +++ b/resource_detectors/container_detector.cc @@ -3,7 +3,7 @@ #include "opentelemetry/resource_detectors/container_detector.h" #include "opentelemetry/nostd/variant.h" -#include "opentelemetry/resource_detectors/container_detector_utils.h" +#include "opentelemetry/resource_detectors/detail/container_detector_utils.h" #include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/resource/resource_detector.h" #include "opentelemetry/semconv/incubating/container_attributes.h" diff --git a/resource_detectors/container_detector_utils.cc b/resource_detectors/container_detector_utils.cc index a1c54e2f47..f0a0c383b2 100644 --- a/resource_detectors/container_detector_utils.cc +++ b/resource_detectors/container_detector_utils.cc @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#include "opentelemetry/resource_detectors/container_detector_utils.h" +#include "opentelemetry/resource_detectors/detail/container_detector_utils.h" #include "opentelemetry/nostd/string_view.h" #include diff --git a/resource_detectors/include/opentelemetry/resource_detectors/container_detector_utils.h b/resource_detectors/include/opentelemetry/resource_detectors/detail/container_detector_utils.h similarity index 100% rename from resource_detectors/include/opentelemetry/resource_detectors/container_detector_utils.h rename to resource_detectors/include/opentelemetry/resource_detectors/detail/container_detector_utils.h 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 new file mode 100644 index 0000000000..aeb61a8e28 --- /dev/null +++ b/resource_detectors/include/opentelemetry/resource_detectors/detail/process_detector_utils.h @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace resource_detector +{ +namespace detail +{ + +/** + * Forms a file path for a process type based on the given PID. + * for example - /proc//cmdline, /proc//exe + */ +std::string FormFilePath(const int32_t &pid, const char *process_type); + +/** + * Retrieves the absolute file system path to the executable for a given PID. + * Platform-specific behavior: + * - Windows: Uses OpenProcess() + GetProcessImageFileNameW(). + * - Linux/Unix: Reads the /proc//exe symbolic link. + * - TODO: Need to implement for Darwin + * + * @param pid Process ID. + */ +std::string GetExecutablePath(const int32_t &pid); + +/** + * Retrieves the command used to launch the process for a given PID. + * Platform-specific behavior: + * - Windows: Uses GetCommandLineW() to get the command of the current process. + * - Linux/Unix: Reads the zeroth string of /proc//cmdline file. + * - TODO: Need to implement for Darwin + */ +std::string ExtractCommand(const std::string &command_line_path); + +/** + * Retrieves the command used to launch the process for a given PID. + * This function is a wrapper around ExtractCommand() and is provided for convenience and + * testability of ExtractCommand(). + */ +std::string GetCommand(const int32_t &pid); + +} // 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 new file mode 100644 index 0000000000..229693a051 --- /dev/null +++ b/resource_detectors/include/opentelemetry/resource_detectors/process_detector.h @@ -0,0 +1,35 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/resource_detector.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +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. + */ +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. + */ + opentelemetry::sdk::resource::Resource Detect() noexcept override; +}; + +} // namespace resource_detector +OPENTELEMETRY_END_NAMESPACE diff --git a/resource_detectors/process_detector.cc b/resource_detectors/process_detector.cc new file mode 100644 index 0000000000..c0bde2555a --- /dev/null +++ b/resource_detectors/process_detector.cc @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/resource_detectors/process_detector.h" +#include "opentelemetry/nostd/variant.h" +#include "opentelemetry/resource_detectors/detail/process_detector_utils.h" +#include "opentelemetry/sdk/common/global_log_handler.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/resource_detector.h" +#include "opentelemetry/semconv/incubating/process_attributes.h" +#include "opentelemetry/version.h" + +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +# include +# define getpid _getpid +#else +# include +#endif + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace resource_detector +{ + +opentelemetry::sdk::resource::Resource ProcessResourceDetector::Detect() noexcept +{ + int32_t pid = getpid(); + opentelemetry::sdk::resource::ResourceAttributes attributes; + attributes[semconv::process::kProcessPid] = pid; + + try + { + std::string executable_path = opentelemetry::resource_detector::detail::GetExecutablePath(pid); + if (!executable_path.empty()) + { + attributes[semconv::process::kProcessExecutablePath] = std::move(executable_path); + } + } + catch (const ::std::exception &ex) + { + OTEL_INTERNAL_LOG_ERROR("[Process Resource Detector] " + << "Error extracting the executable path: " << ex.what()); + } + + try + { + std::string command = opentelemetry::resource_detector::detail::GetCommand(pid); + if (!command.empty()) + { + attributes[semconv::process::kProcessCommand] = std::move(command); + } + } + catch (const std::exception &ex) + { + OTEL_INTERNAL_LOG_ERROR("[Process Resource Detector] " << "Error extracting command: " + << ex.what()); + } + + return ResourceDetector::Create(attributes); +} + +} // namespace resource_detector +OPENTELEMETRY_END_NAMESPACE diff --git a/resource_detectors/process_detector_utils.cc b/resource_detectors/process_detector_utils.cc new file mode 100644 index 0000000000..27a083ea7e --- /dev/null +++ b/resource_detectors/process_detector_utils.cc @@ -0,0 +1,126 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/resource_detectors/detail/process_detector_utils.h" + +#include +#include + +#ifdef _MSC_VER +// clang-format off +# include +# include +// clang-format on +#else +# include +# include +# include +#endif + +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace resource_detector +{ +namespace detail +{ + +constexpr const char *kExecutableName = "exe"; +constexpr const char *kCmdlineName = "cmdline"; + +std::string GetExecutablePath(const int32_t &pid) +{ +#ifdef _MSC_VER + HANDLE hProcess = + OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, static_cast(pid)); + if (!hProcess) + { + return std::string(); + } + + WCHAR wbuffer[MAX_PATH]; + DWORD len = GetProcessImageFileNameW(hProcess, wbuffer, MAX_PATH); + CloseHandle(hProcess); + + if (len == 0) + { + return std::string(); + } + + // Convert UTF-16 to UTF-8 + 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); + + return utf8_path; +#else + std::string path = FormFilePath(pid, kExecutableName); + char buffer[4096]; + + ssize_t len = readlink(path.c_str(), buffer, sizeof(buffer) - 1); + if (len != -1) + { + buffer[len] = '\0'; + return std::string(buffer); + } + + return std::string(); +#endif +} + +std::string GetCommand(const int32_t &pid) +{ +#ifdef _MSC_VER + // On Windows, GetCommandLineW only works for the CURRENT process, + // so we ignore `pid` and just return the current process's command line. + LPCWSTR wcmd = GetCommandLineW(); + if (!wcmd) + { + return std::string(); + } + + // Convert UTF-16 to UTF-8 + int size_needed = WideCharToMultiByte(CP_UTF8, 0, wcmd, -1, NULL, 0, NULL, NULL); + if (size_needed <= 0) + { + return std::string(); + } + + std::string utf8_command(size_needed - 1, 0); // exclude null terminator + WideCharToMultiByte(CP_UTF8, 0, wcmd, -1, &utf8_command[0], size_needed, NULL, NULL); + + return utf8_command; +#else + // This is the path to get the command that was used to start the process + std::string command_line_path = FormFilePath(pid, kCmdlineName); + return ExtractCommand(command_line_path); +#endif +} + +std::string ExtractCommand(const std::string &command_line_path) +{ + std::string command; + std::ifstream command_line_file(command_line_path, std::ios::in | std::ios::binary); + std::getline(command_line_file, command, '\0'); + return command; +} + +std::string FormFilePath(const int32_t &pid, const char *process_type) +{ + char buff[64]; + int len = std::snprintf(buff, sizeof(buff), "/proc/%d/%s", pid, process_type); + if (len < 0) + { + // in case snprintf fails + return std::string(); + } + if (len >= static_cast(sizeof(buff))) + { + return std::string(buff, sizeof(buff) - 1); + } + return std::string(buff, len); +} + +} // namespace detail +} // namespace resource_detector +OPENTELEMETRY_END_NAMESPACE diff --git a/resource_detectors/test/BUILD b/resource_detectors/test/BUILD index 2cac8174dc..0c12439654 100644 --- a/resource_detectors/test/BUILD +++ b/resource_detectors/test/BUILD @@ -5,6 +5,7 @@ cc_test( name = "resource_detector_test", srcs = [ "container_detector_test.cc", + "process_detector_test.cc", ], tags = ["test"], deps = [ diff --git a/resource_detectors/test/CMakeLists.txt b/resource_detectors/test/CMakeLists.txt index 9414e64212..4062a278ef 100644 --- a/resource_detectors/test/CMakeLists.txt +++ b/resource_detectors/test/CMakeLists.txt @@ -1,7 +1,8 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -add_executable(resource_detector_test container_detector_test.cc) +add_executable(resource_detector_test container_detector_test.cc + process_detector_test.cc) # Link the required dependencies target_link_libraries( diff --git a/resource_detectors/test/container_detector_test.cc b/resource_detectors/test/container_detector_test.cc index 0243973c46..bc70763370 100644 --- a/resource_detectors/test/container_detector_test.cc +++ b/resource_detectors/test/container_detector_test.cc @@ -7,7 +7,7 @@ #include #include "opentelemetry/nostd/string_view.h" -#include "opentelemetry/resource_detectors/container_detector_utils.h" +#include "opentelemetry/resource_detectors/detail/container_detector_utils.h" TEST(ContainerIdDetectorTest, ExtractValidContainerIdFromLine) { diff --git a/resource_detectors/test/process_detector_test.cc b/resource_detectors/test/process_detector_test.cc new file mode 100644 index 0000000000..374a3a61cb --- /dev/null +++ b/resource_detectors/test/process_detector_test.cc @@ -0,0 +1,148 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#ifdef _MSC_VER +// clang-format off +# include +# include +# include +# define getpid _getpid +// clang-format on +#else +# include +# include +# include +#endif + +#include "opentelemetry/resource_detectors/detail/process_detector_utils.h" + +TEST(ProcessDetectorUtilsTest, FormFilePath) +{ + int32_t pid = 1234; + std::string cmdline_path = opentelemetry::resource_detector::detail::FormFilePath(pid, "cmdline"); + std::string exe_path = opentelemetry::resource_detector::detail::FormFilePath(pid, "exe"); + + EXPECT_EQ(cmdline_path, "/proc/1234/cmdline"); + EXPECT_EQ(exe_path, "/proc/1234/exe"); +} + +TEST(ProcessDetectorUtilsTest, ExtractCommand) +{ + std::string filename{"test_command.txt"}; + + { + std::ofstream outfile(filename, std::ios::binary); + const char raw_data[] = "test_command\0arg1\0arg2\0arg3\0"; + outfile.write(raw_data, sizeof(raw_data) - 1); + } + + std::string command = opentelemetry::resource_detector::detail::ExtractCommand(filename); + EXPECT_EQ(command, std::string{"test_command"}); + + std::remove(filename.c_str()); // Cleanup +} + +TEST(ProcessDetectorUtilsTest, EmptyCommandFile) +{ + std::string filename{"empty_command.txt"}; + std::ofstream outfile(filename, std::ios::binary); + outfile.close(); + + std::string command = opentelemetry::resource_detector::detail::ExtractCommand(filename); + EXPECT_EQ(command, std::string{""}); + + std::remove(filename.c_str()); // Cleanup +} + +TEST(ProcessDetectorUtilsTest, GetExecutablePathTest) +{ + int32_t pid = getpid(); + std::string path; +#ifdef _MSC_VER + HANDLE hProcess = + OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, static_cast(pid)); + if (!hProcess) + { + path = std::string(); + } + else + { + + WCHAR wbuffer[MAX_PATH]; + DWORD len = GetProcessImageFileNameW(hProcess, wbuffer, MAX_PATH); + CloseHandle(hProcess); + + if (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; + } + } +#else + std::string exe_path = opentelemetry::resource_detector::detail::FormFilePath(pid, "exe"); + char buffer[4096]; + + ssize_t len = readlink(exe_path.c_str(), buffer, sizeof(buffer) - 1); + if (len != -1) + { + buffer[len] = '\0'; + path = std::string(buffer); + } + else + { + path = std::string(); + } +#endif + std::string expected_path = opentelemetry::resource_detector::detail::GetExecutablePath(pid); + EXPECT_EQ(path, expected_path); +} + +TEST(ProcessDetectorUtilsTest, GetCommandTest) +{ + int32_t pid = getpid(); + std::string command; +#ifdef _MSC_VER + // On Windows, GetCommandLineW only works for the CURRENT process, + // so we ignore `pid` and just return the current process's command line. + LPCWSTR wcmd = GetCommandLineW(); + if (!wcmd) + { + command = std::string(); + } + else + { + + // Convert UTF-16 to UTF-8 + int size_needed = WideCharToMultiByte(CP_UTF8, 0, wcmd, -1, NULL, 0, NULL, NULL); + if (size_needed <= 0) + { + command = std::string(); + } + else + { + std::string utf8_command(size_needed - 1, 0); // exclude null terminator + WideCharToMultiByte(CP_UTF8, 0, wcmd, -1, &utf8_command[0], size_needed, NULL, NULL); + command = utf8_command; + } + } +#else + // This is the path to get the command that was used to start the process + std::string command_line_path = + opentelemetry::resource_detector::detail::FormFilePath(pid, "cmdline"); + command = opentelemetry::resource_detector::detail::ExtractCommand(command_line_path); +#endif + std::string expected_command = opentelemetry::resource_detector::detail::GetCommand(pid); + EXPECT_EQ(command, expected_command); +}