mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-08-18 02:55:12 +00:00
feat(logging): rotating log files (#5485)
This commit is contained in:
@ -1870,7 +1870,8 @@ editing the `conf` file in a text editor. Use the examples as reference.
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
The path where the Sunshine log is stored.
|
||||
The path where the current Sunshine log is stored. Each time Sunshine starts, up to five previous
|
||||
logs are retained by appending <code>.1</code> through <code>.5</code> to this path.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@ -156,6 +156,11 @@ namespace logging {
|
||||
deinit();
|
||||
}
|
||||
|
||||
const auto log_path = std::filesystem::path {std::u8string {log_file.begin(), log_file.end()}};
|
||||
if (const auto rotation_error = rotate_log_file(log_path)) {
|
||||
std::cerr << "Failed to rotate log file '" << log_file << "': " << rotation_error.message() << '\n';
|
||||
}
|
||||
|
||||
#ifndef __ANDROID__
|
||||
setup_av_logging(min_log_level);
|
||||
setup_libdisplaydevice_logging(min_log_level);
|
||||
|
||||
@ -4,6 +4,13 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
// standard includes
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
// lib includes
|
||||
#include <boost/log/common.hpp>
|
||||
#include <boost/log/sinks.hpp>
|
||||
@ -30,6 +37,47 @@ extern boost::log::sources::severity_logger<int> tests;
|
||||
* @brief Handles the initialization and deinitialization of the logging system.
|
||||
*/
|
||||
namespace logging {
|
||||
/**
|
||||
* @brief The number of previous log files retained during rotation.
|
||||
*/
|
||||
inline constexpr std::size_t retained_log_file_count {5};
|
||||
|
||||
/**
|
||||
* @brief Rotate a log file while retaining up to five previous logs.
|
||||
*
|
||||
* The current log is renamed with a `.1` suffix, existing rotated logs are
|
||||
* advanced by one generation, and the previous `.5` log is removed.
|
||||
*
|
||||
* @param log_file Path to the current log file.
|
||||
* @return An error code when rotation fails, or a clear error code on success.
|
||||
*/
|
||||
inline std::error_code rotate_log_file(const std::filesystem::path &log_file) noexcept {
|
||||
const auto rotated_log_path = [&log_file](std::size_t generation) {
|
||||
auto rotated_path = log_file;
|
||||
rotated_path += std::format(".{}", generation);
|
||||
return rotated_path;
|
||||
};
|
||||
|
||||
try {
|
||||
std::filesystem::remove(rotated_log_path(retained_log_file_count));
|
||||
|
||||
for (auto generation = retained_log_file_count; generation > 1; --generation) {
|
||||
const auto previous_path = rotated_log_path(generation - 1);
|
||||
if (std::filesystem::exists(previous_path)) {
|
||||
std::filesystem::rename(previous_path, rotated_log_path(generation));
|
||||
}
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(log_file)) {
|
||||
std::filesystem::rename(log_file, rotated_log_path(1));
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (const std::filesystem::filesystem_error &filesystem_error) {
|
||||
return filesystem_error.code();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief RAII helper that runs shutdown cleanup when destroyed.
|
||||
*/
|
||||
@ -58,7 +106,7 @@ namespace logging {
|
||||
void formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os);
|
||||
|
||||
/**
|
||||
* @brief Initialize the logging system.
|
||||
* @brief Rotate the current log file and initialize the logging system.
|
||||
* @param min_log_level The minimum log level to output.
|
||||
* @param log_file The log file to write to.
|
||||
* @return An object that will deinitialize the logging system when it goes out of scope.
|
||||
|
||||
@ -84,7 +84,6 @@ namespace system_tray {
|
||||
platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher");
|
||||
}
|
||||
|
||||
#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
|
||||
/**
|
||||
* @brief Forwards Qt log messages to Sunshine's BOOST_LOG logger.
|
||||
* @param level Log level: 0=debug, 1=info, 2=warning, 3=error.
|
||||
@ -109,7 +108,6 @@ namespace system_tray {
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item) {
|
||||
BOOST_LOG(info) << "Resetting display device config from system tray"sv;
|
||||
@ -318,9 +316,7 @@ namespace system_tray {
|
||||
tray.icon = tray.allIconPaths[0];
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
|
||||
tray_set_log_callback(qt_log_to_boost);
|
||||
#endif
|
||||
|
||||
tray_set_app_info(PROJECT_NAME, PROJECT_NAME, PROJECT_FQDN);
|
||||
|
||||
|
||||
@ -278,7 +278,7 @@
|
||||
"locale": "Locale",
|
||||
"locale_desc": "The locale used for Sunshine's user interface.",
|
||||
"log_path": "Logfile Path",
|
||||
"log_path_desc": "The file where the current logs of Sunshine are stored.",
|
||||
"log_path_desc": "The file where the current Sunshine log is stored. At startup, up to five previous logs are retained with .1 through .5 suffixes.",
|
||||
"max_bitrate": "Maximum Bitrate",
|
||||
"max_bitrate_desc": "The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight.",
|
||||
"minimum_fps_target": "Minimum FPS Target",
|
||||
|
||||
@ -5,9 +5,14 @@
|
||||
#include "../tests_common.h"
|
||||
#include "../tests_log_checker.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <random>
|
||||
#include <src/logging.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
std::array log_levels = {
|
||||
@ -20,8 +25,103 @@ namespace {
|
||||
};
|
||||
|
||||
constexpr auto log_file = "test_sunshine.log";
|
||||
|
||||
/**
|
||||
* @brief Write test content to a log file.
|
||||
*
|
||||
* @param path Path to write.
|
||||
* @param content Content to write.
|
||||
*/
|
||||
void write_log_file(const std::filesystem::path &path, std::string_view content) {
|
||||
std::ofstream output {path};
|
||||
output << content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read all content from a test log file.
|
||||
*
|
||||
* @param path Path to read.
|
||||
* @return File content.
|
||||
*/
|
||||
std::string read_log_file(const std::filesystem::path &path) {
|
||||
std::ifstream input {path};
|
||||
return {std::istreambuf_iterator<char> {input}, std::istreambuf_iterator<char> {}};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief Test fixture for startup log rotation.
|
||||
*/
|
||||
class LogRotationTest: public BaseTest {
|
||||
protected:
|
||||
/**
|
||||
* @brief Create an empty directory for the current test.
|
||||
*/
|
||||
void SetUp() override {
|
||||
BaseTest::SetUp();
|
||||
std::filesystem::remove_all(test_directory);
|
||||
std::filesystem::create_directories(test_directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remove files created by the current test.
|
||||
*/
|
||||
void TearDown() override {
|
||||
std::filesystem::remove_all(test_directory);
|
||||
BaseTest::TearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build the path for a rotated test log.
|
||||
*
|
||||
* @param generation Rotated log generation number.
|
||||
* @return Path with the generation suffix appended.
|
||||
*/
|
||||
std::filesystem::path rotated_log_path(std::size_t generation) const {
|
||||
auto path = log_path;
|
||||
path += std::format(".{}", generation);
|
||||
return path;
|
||||
}
|
||||
|
||||
const std::filesystem::path test_directory {std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "log_rotation_tests"}; ///< Directory containing log rotation test files.
|
||||
const std::filesystem::path log_path {test_directory / "custom.log"}; ///< Path to the current test log.
|
||||
};
|
||||
|
||||
TEST_F(LogRotationTest, RotatesCurrentLogAndRetainsFivePreviousLogs) {
|
||||
write_log_file(log_path, "current");
|
||||
for (std::size_t generation = 1; generation <= logging::retained_log_file_count; ++generation) {
|
||||
write_log_file(rotated_log_path(generation), std::to_string(generation));
|
||||
}
|
||||
|
||||
EXPECT_FALSE(logging::rotate_log_file(log_path));
|
||||
|
||||
EXPECT_FALSE(std::filesystem::exists(log_path));
|
||||
EXPECT_EQ(read_log_file(rotated_log_path(1)), "current");
|
||||
EXPECT_EQ(read_log_file(rotated_log_path(2)), "1");
|
||||
EXPECT_EQ(read_log_file(rotated_log_path(3)), "2");
|
||||
EXPECT_EQ(read_log_file(rotated_log_path(4)), "3");
|
||||
EXPECT_EQ(read_log_file(rotated_log_path(5)), "4");
|
||||
}
|
||||
|
||||
TEST_F(LogRotationTest, SupportsMissingLogGenerations) {
|
||||
write_log_file(rotated_log_path(2), "second");
|
||||
|
||||
EXPECT_FALSE(logging::rotate_log_file(log_path));
|
||||
|
||||
EXPECT_FALSE(std::filesystem::exists(log_path));
|
||||
EXPECT_FALSE(std::filesystem::exists(rotated_log_path(1)));
|
||||
EXPECT_FALSE(std::filesystem::exists(rotated_log_path(2)));
|
||||
EXPECT_EQ(read_log_file(rotated_log_path(3)), "second");
|
||||
}
|
||||
|
||||
TEST_F(LogRotationTest, ReportsFilesystemErrors) {
|
||||
std::filesystem::create_directories(rotated_log_path(logging::retained_log_file_count) / "child");
|
||||
write_log_file(log_path, "current");
|
||||
|
||||
EXPECT_TRUE(logging::rotate_log_file(log_path));
|
||||
EXPECT_EQ(read_log_file(log_path), "current");
|
||||
}
|
||||
|
||||
struct LogLevelsTest: BaseTest, testing::WithParamInterface<decltype(log_levels)::value_type> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
#include <Windows.h>
|
||||
#include <WtsApi32.h>
|
||||
|
||||
// local includes
|
||||
#include "src/logging.h"
|
||||
|
||||
// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers
|
||||
#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST
|
||||
#define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)
|
||||
@ -123,10 +126,13 @@ HANDLE OpenLogFileHandle() {
|
||||
GetTempPathW(_countof(log_file_name), log_file_name);
|
||||
wcscat_s(log_file_name, L"sunshine.log");
|
||||
|
||||
// Preserve previous service output before opening the current log.
|
||||
logging::rotate_log_file(log_file_name);
|
||||
|
||||
// The file handle must be inheritable for our child process to use it
|
||||
SECURITY_ATTRIBUTES security_attributes = {sizeof(security_attributes), nullptr, TRUE};
|
||||
|
||||
// Overwrite the old sunshine.log
|
||||
// Create the current sunshine.log
|
||||
return CreateFileW(log_file_name, GENERIC_WRITE, FILE_SHARE_READ, &security_attributes, CREATE_ALWAYS, 0, nullptr);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user