Merge pull request #17 from tboudreaux/feature/logging

Added Probe Module
This commit is contained in:
2025-02-23 11:41:05 -05:00
committed by GitHub
11 changed files with 346 additions and 2 deletions

3
.gitignore vendored
View File

@@ -61,5 +61,8 @@ tags
subprojects/mfem/
subprojects/tetgen/
subprojects/yaml-cpp/
subprojects/quill/
.vscode/
*.log

View File

@@ -1,4 +1,5 @@
cmake = import('cmake')
subdir('mfem')
subdir('yaml-cpp')
subdir('yaml-cpp')
subdir('quill')

View File

@@ -0,0 +1,5 @@
quill_sp = cmake.subproject(
'quill'
)
quill_dep = quill_sp.dependency('quill')
add_project_arguments('-I' + meson.current_build_dir() + '/subprojects/quill/__CMake_build', language: 'cpp')

View File

@@ -6,4 +6,5 @@ subdir('dobj')
subdir('const')
subdir('opatIO')
subdir('meshIO')
subdir('config')
subdir('config')
subdir('probe')

22
src/probe/meson.build Normal file
View File

@@ -0,0 +1,22 @@
# Define the library
probe_sources = files(
'private/probe.cpp',
)
probe_headers = files(
'public/probe.h'
)
# Define the liblogger library so it can be linked against by other parts of the build system
libprobe = static_library('probe',
probe_sources,
include_directories: include_directories('public'),
cpp_args: ['-fvisibility=default'],
install : true,
dependencies: [config_dep, mfem_dep, quill_dep]
)
probe_dep = declare_dependency(
include_directories: include_directories('public'),
link_with: libprobe,
)

View File

@@ -0,0 +1,99 @@
#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/Logger.h"
#include "quill/sinks/ConsoleSink.h"
#include "quill/sinks/FileSink.h"
#include <stdexcept>
#include <string>
#include <iostream>
#include <chrono>
#include "mfem.hpp"
#include "config.h"
#include "probe.h"
namespace Probe {
void pause() {
std::cout << "Execution paused. Please press enter to continue..."
<< std::endl; // Use endl to flush
std::cin.get();
}
void wait(int seconds) {
std::this_thread::sleep_for(std::chrono::seconds(seconds));
}
void glVisView(mfem::GridFunction& u, mfem::Mesh& mesh,
const std::string& windowTitle) {
Config& config = Config::getInstance();
if (config.get<bool>("Probe:GLVis:Visualization", true)) {
std::string vishost = config.get<std::string>("Probe:GLVis:Host", "localhost");
int visport = config.get<int>("Probe:GLVis:Port", 19916); // Changed default port
mfem::socketstream sol_sock(vishost.c_str(), visport);
sol_sock.precision(8);
sol_sock << "solution\n" << mesh << u
<< "window_title '" << windowTitle << "'\n" << std::flush; // Added title
}
}
LogManager::LogManager() {
Config& config = Config::getInstance();
quill::Backend::start();
auto CLILogger = quill::Frontend::create_or_get_logger(
"root",
quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1"));
newFileLogger(config.get<std::string>("Probe:LogManager:DefaultLogName", "4DSSE.log"), "log");
loggerMap.emplace("stdout", CLILogger);
}
LogManager::~LogManager() = default;
quill::Logger* LogManager::getLogger(const std::string& loggerName) {
auto it = loggerMap.find(loggerName); // Find *once*
if (it == loggerMap.end()) {
throw std::runtime_error("Cannot find logger " + loggerName);
}
return it->second; // Return the raw pointer from the shared_ptr
}
std::vector<std::string> LogManager::getLoggerNames() {
std::vector<std::string> loggerNames;
loggerNames.reserve(loggerMap.size());
for (const auto& pair : loggerMap) { // Use range-based for loop and const auto&
loggerNames.push_back(pair.first);
}
return loggerNames;
}
std::vector<quill::Logger*> LogManager::getLoggers() {
std::vector<quill::Logger*> loggers;
loggers.reserve(loggerMap.size());
for (const auto& pair : loggerMap) {
loggers.push_back(pair.second); // Get the raw pointer
}
return loggers;
}
quill::Logger* LogManager::newFileLogger(const std::string& filename,
const std::string& loggerName) {
auto file_sink = quill::Frontend::create_or_get_sink<quill::FileSink>(
filename,
[]() {
quill::FileSinkConfig cfg;
cfg.set_open_mode('w');
return cfg;
}(),
quill::FileEventNotifier{});
// Get the raw pointer.
quill::Logger* rawLogger = quill::Frontend::create_or_get_logger(loggerName, std::move(file_sink));
// Create a shared_ptr from the raw pointer.
loggerMap.emplace(loggerName, rawLogger);
return rawLogger;
}
} // namespace Probe

99
src/probe/public/probe.h Normal file
View File

@@ -0,0 +1,99 @@
//=== Probe.h ===
#ifndef PROBE_H
#define PROBE_H
#include <string>
#include <map>
#include <vector>
#include <iostream>
#include "mfem.hpp"
#include "quill/Logger.h"
/**
* @brief The Probe namespace contains utility functions for debugging and logging.
*/
namespace Probe {
/**
* @brief Pause the execution and wait for user input.
*/
void pause();
/**
* @brief Wait for a specified number of seconds.
* @param seconds The number of seconds to wait.
*/
void wait(int seconds);
/**
* @brief Visualize a solution using GLVis.
* @param u The GridFunction to visualize.
* @param mesh The mesh associated with the GridFunction.
* @param windowTitle The title of the visualization window.
*/
void glVisView(mfem::GridFunction& u, mfem::Mesh& mesh,
const std::string& windowTitle = "solution");
/**
* @brief Class to manage logging operations.
*/
class LogManager {
private:
/**
* @brief Private constructor for singleton pattern.
*/
LogManager();
/**
* @brief Destructor.
*/
~LogManager();
// Map to store pointers to quill loggers (raw pointers as quill deals with its own memory managment in a seperated, detatched, thread)
std::map<std::string, quill::Logger*> loggerMap;
// Prevent copying and assignment (Rule of Zero)
LogManager(const LogManager&) = delete;
LogManager& operator=(const LogManager&) = delete;
public:
/**
* @brief Get the singleton instance of LogManager.
* @return The singleton instance of LogManager.
*/
static LogManager& getInstance() {
static LogManager instance;
return instance;
}
/**
* @brief Get a logger by name.
* @param loggerName The name of the logger.
* @return A pointer to the logger.
*/
quill::Logger* getLogger(const std::string& loggerName);
/**
* @brief Get the names of all loggers.
* @return A vector of logger names.
*/
std::vector<std::string> getLoggerNames();
/**
* @brief Get all loggers.
* @return A vector of pointers to the loggers.
*/
std::vector<quill::Logger*> getLoggers();
/**
* @brief Create a new file logger.
* @param filename The name of the log file.
* @param loggerName The name of the logger.
* @return A pointer to the new logger.
*/
quill::Logger* newFileLogger(const std::string& filename,
const std::string& loggerName);
};
} // namespace Probe
#endif

5
subprojects/quill.wrap Normal file
View File

@@ -0,0 +1,5 @@
[wrap-git]
url = https://github.com/odygrd/quill
revision = v8.1.1
[cmake]

View File

@@ -8,6 +8,7 @@ subdir('const')
subdir('opatIO')
subdir('meshIO')
subdir('config')
subdir('probe')
# Subdirectories for sandbox tests
subdir('dobj_sandbox')

20
tests/probe/meson.build Normal file
View File

@@ -0,0 +1,20 @@
# Test files for dobj
test_sources = [
'probeTest.cpp',
]
foreach test_file : test_sources
exe_name = test_file.split('.')[0]
message('Building test: ' + exe_name)
# Create an executable target for each test
test_exe = executable(
exe_name,
test_file,
dependencies: [gtest_dep, probe_dep, mfem_dep, quill_dep],
install_rpath: '@loader_path/../../src' # Ensure runtime library path resolves correctly
)
# Add the executable as a test
test(exe_name, test_exe)
endforeach

88
tests/probe/probeTest.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include <gtest/gtest.h>
#include "probe.h"
#include <iostream>
#include <string>
#include <vector>
#include <set>
#include <sstream>
#include <regex>
#include <source_location>
#include <chrono>
#include "quill/LogMacros.h"
std::string getLastLine(const std::string& filename) {
std::ifstream file(filename);
std::string line, lastLine;
if (!file.is_open()) {
throw std::runtime_error("Could not open file");
}
while (std::getline(file, line)) {
lastLine = line;
}
return lastLine; // Returns the last non-empty line
}
std::string stripTimestamps(const std::string& logLine) {
std::regex logPattern(R"(\d+:\d+:\d+\.\d+\s+\[\d+\]\s+probeTest\.cpp:\d+\s+LOG_INFO\s+[A-Za-z]*\s+(.*))");
std::smatch match;
if (std::regex_match(logLine, match, logPattern) && match.size() > 1) {
return match[1].str(); // Extract log message after timestamp
}
return logLine; // Return as-is if pattern doesn't match
}
class probeTest : public ::testing::Test {};
TEST_F(probeTest, DefaultConstructorTest) {
EXPECT_NO_THROW(Probe::LogManager::getInstance());
}
TEST_F(probeTest, waitTest) {
auto start = std::chrono::high_resolution_clock::now();
Probe::wait(1);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed = end - start;
EXPECT_LE(elapsed.count(), 1.1);
}
TEST_F(probeTest, getLoggerTest) {
Probe::LogManager& logManager = Probe::LogManager::getInstance();
std::string loggerName = "log";
quill::Logger* logger = logManager.getLogger(loggerName);
EXPECT_NE(logger, nullptr);
LOG_INFO(logger, "This is a test message");
// Wait for the log to be written by calling getLastLine until it is non empty
std::string lastLine;
while (lastLine.empty()) {
lastLine = getLastLine("4DSSE.log");
}
EXPECT_EQ(stripTimestamps(lastLine), "This is a test message");
}
TEST_F(probeTest, newFileLoggerTest) {
Probe::LogManager& logManager = Probe::LogManager::getInstance();
const std::string loggerName = "newLog";
const std::string filename = "newLog.log";
quill::Logger* logger = logManager.newFileLogger(filename, loggerName);
EXPECT_NE(logger, nullptr);
LOG_INFO(logger, "This is a new test message");
// Wait for the log to be written by calling getLastLine until it is non empty
std::string lastLine;
while (lastLine.empty()) {
lastLine = getLastLine(filename);
}
EXPECT_EQ(stripTimestamps(lastLine), "This is a new test message");
}
TEST_F(probeTest, getLoggerNames) {
Probe::LogManager& logManager = Probe::LogManager::getInstance();
std::vector<std::string> loggerNames = logManager.getLoggerNames();
EXPECT_EQ(loggerNames.size(), 3);
EXPECT_EQ(loggerNames.at(0), "log");
EXPECT_EQ(loggerNames.at(1), "newLog");
EXPECT_EQ(loggerNames.at(2), "stdout");
}