Skip to content

Commit

Permalink
logrotate: Support incremental file rotation strategy
Browse files Browse the repository at this point in the history
Advantages of the incremental rotation strategy:
1. Each file is created only once. If the file is traversed and packaged by an external process, there will be no duplication or loss problems in the rename rotation method.
2. You can estimate how much data has been rolled back by looking at the file name.
  • Loading branch information
shawnfeng0 committed Jan 25, 2025
1 parent 2d56017 commit e8d0588
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 154 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use_cmake_policy()
project(ulog)

set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

option(ULOG_BUILD_EXAMPLES "Build examples" OFF)
Expand Down
5 changes: 2 additions & 3 deletions examples/unix/ulog_example_rotate_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

#include "ulog/error.h"
#include "ulog/file/async_rotating_file.h"
#include "ulog/queue/fifo_power_of_two.h"
#include "ulog/queue/mpsc_ring.h"
#include "ulog/queue/spsc_ring.h"
#include "ulog/ulog.h"
Expand Down Expand Up @@ -41,8 +40,8 @@ static void OutputFunc() {

int main() {
std::unique_ptr<ulog::WriterInterface> file_writer = std::make_unique<ulog::FileLimitWriter>(100 * 1024);
ulog::AsyncRotatingFile<ulog::mpsc::Mq> async_rotate(std::move(file_writer), 65536 * 2, "/tmp/ulog/test.txt", 5, true,
std::chrono::seconds{1});
ulog::file::AsyncRotatingFile<ulog::mpsc::Mq> async_rotate(std::move(file_writer), 65536 * 2, "/tmp/ulog/test.txt", 5,
true, std::chrono::seconds{1}, ulog::file::kRename);

// Initial logger
logger_set_user_data(ULOG_GLOBAL, &async_rotate);
Expand Down
5 changes: 2 additions & 3 deletions include/ulog/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
#pragma once

#include <cstdio>
#include <string>

// Precompiler define to get only filename;
#if defined(__FILENAME__)
#define ULOG_ERROR(fmt, ...) fprintf(stderr, "%s:%d" fmt "\n", __FILENAME__, __LINE__, ##__VA_ARGS__)
#define ULOG_ERROR(fmt, ...) fprintf(stderr, "%s:%d " fmt "\n", __FILENAME__, __LINE__, ##__VA_ARGS__)
#else
#define ULOG_ERROR(fmt, ...) fprintf(stderr, "%s:%d" fmt "\n", __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define ULOG_ERROR(fmt, ...) fprintf(stderr, "%s:%d " fmt "\n", __FUNCTION__, __LINE__, ##__VA_ARGS__)
#endif
10 changes: 6 additions & 4 deletions include/ulog/file/async_rotating_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#include "ulog/error.h"
#include "ulog/file/rotating_file.h"

namespace ulog {
namespace ulog::file {

template <typename Queue>
class AsyncRotatingFile {
Expand All @@ -28,11 +28,13 @@ class AsyncRotatingFile {
* @param rotate_on_open Whether to rotate the file when opening
* @param max_flush_period Maximum file flush period (some file systems and platforms only refresh once every 60s
* by default, which is too slow)
* @param rotation_strategy Rotation strategy
*/
AsyncRotatingFile(std::unique_ptr<WriterInterface> &&writer, const size_t fifo_size, const std::string &filename,
const std::size_t max_files, const bool rotate_on_open,
const std::chrono::milliseconds max_flush_period)
: umq_(Queue::Create(fifo_size)), rotating_file_(std::move(writer), filename, max_files, rotate_on_open) {
const std::chrono::milliseconds max_flush_period, const RotationStrategy rotation_strategy)
: umq_(Queue::Create(fifo_size)),
rotating_file_(std::move(writer), filename, max_files, rotate_on_open, rotation_strategy) {
auto async_thread_function = [max_flush_period, this] {
auto last_flush_time = std::chrono::steady_clock::now();
bool need_wait_flush = false; // Whether to wait for the next flush
Expand Down Expand Up @@ -108,4 +110,4 @@ class AsyncRotatingFile {
std::atomic_bool should_exit_{false};
};

} // namespace ulog
} // namespace ulog::file
31 changes: 0 additions & 31 deletions include/ulog/file/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,6 @@ static inline std::tuple<std::string, std::string> SplitByExtension(const std::s
return std::make_tuple(filename.substr(0, ext_index), filename.substr(ext_index));
}

static inline std::string CalcFilename(const std::string &filename, std::size_t index) {
if (index == 0u) {
return filename;
}

std::string basename, ext;
std::tie(basename, ext) = SplitByExtension(filename);
return basename + "." + std::to_string(index) + ext;
}

// Rename the src file to target
// return true on success, false otherwise.
static inline bool RenameFile(const std::string &src_filename, const std::string &target_filename) {
Expand All @@ -140,25 +130,4 @@ static inline bool RenameFile(const std::string &src_filename, const std::string
return std::rename(src_filename.c_str(), target_filename.c_str()) == 0;
}

// Rotate files:
// log.txt -> log.1.txt
// log.1.txt -> log.2.txt
// log.2.txt -> log.3.txt
// log.3.txt -> delete
static void inline RotateFiles(const std::string &filename, const size_t max_files) {
for (auto i = max_files - 1; i > 1; --i) {
std::string src = CalcFilename(filename, i - 1);
if (!ulog::file::path_exists(src)) {
continue;
}
std::string target = CalcFilename(filename, i);
RenameFile(src, target);
}

// "tail -f" may be interrupted when rename is executed, and "tail -F" can
// be used instead, but some "-F" implementations (busybox tail) cannot
// obtain all logs in real time.
RenameFile(filename, CalcFilename(filename, 1));
}

} // namespace ulog::file
33 changes: 23 additions & 10 deletions include/ulog/file/rotating_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,47 @@

#pragma once

#include <fstream>
#include <string>
#include <utility>

#include "file.h"
#include "file_writer.h"
#include "rotation_strategy_incremental.h"
#include "rotation_strategy_rename.h"

namespace ulog {
namespace ulog::file {

enum RotationStrategy {
kRename = 1,
kIncrement = 0,
};

class RotatingFile {
public:
RotatingFile(std::unique_ptr<WriterInterface> &&writer, std::string filename, const std::size_t max_files,
const bool rotate_on_open)
const bool rotate_on_open, const RotationStrategy rotation_strategy)
: max_files_(max_files), writer_(std::move(writer)), filename_(std::move(filename)) {
auto [basename, ext] = SplitByExtension(filename_);
if (rotation_strategy == kIncrement) {
rotator_ = std::make_unique<RotationStrategyIncremental>(basename, ext, max_files_);
} else {
rotator_ = std::make_unique<RotationStrategyRename>(basename, ext, max_files_);
}

if (rotate_on_open) {
file::RotateFiles(filename, max_files_);
rotator_->Rotate();
}

writer_->Open(filename_, rotate_on_open);
writer_->Open(rotator_->LatestFilename(), rotate_on_open);
}

Status SinkIt(const void *buffer, const size_t length) const {
Status status = writer_->Write(buffer, length);

if (status.IsFull()) {
writer_->Close();
file::RotateFiles(filename_, max_files_);
status = writer_->Open(filename_, true);
rotator_->Rotate();

status = writer_->Open(rotator_->LatestFilename(), true);
if (!status) {
return status;
}
Expand All @@ -43,13 +55,14 @@ class RotatingFile {
return status;
}

Status Flush() const { return writer_->Flush(); }
[[nodiscard]] Status Flush() const { return writer_->Flush(); }

private:
const std::size_t max_files_;
std::unique_ptr<WriterInterface> writer_;

std::string filename_;
std::unique_ptr<RotationStrategyInterface> rotator_;
};

} // namespace ulog
} // namespace ulog::file
91 changes: 91 additions & 0 deletions include/ulog/file/rotation_strategy_incremental.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#pragma once

#include <fstream>
#include <iostream>
#include <string>
#include <utility>

#include "rotation_strategy_interface.h"
#include "ulog/status.h"

namespace ulog::file {

class RotationStrategyIncremental final : public RotationStrategyInterface {
public:
RotationStrategyIncremental(std::string basename, std::string ext, const size_t max_files)
: basename_(std::move(basename)), ext_(std::move(ext)), max_files_(max_files == 0 ? 1 : max_files) {
if (const auto status = ReadNumberFile(get_number_filename(), &final_number_); !status.ok()) {
// Maybe first write
final_number_ = 0;
}
}

// Example:
// max_files == 10;
// final_num == 29 + 1;
// remove file: log-20.txt
// remain files: log-21.txt log-22.txt ... log-29.txt
// Next, call LatestFilename() to get log-30.txt for writing
Status Rotate() override {
++final_number_;

if (final_number_ >= max_files_) {
std::remove(get_filename(final_number_ - max_files_).c_str());

// Try to clear the files that were previously configured too much
size_t not_exists = 0;
for (size_t i = final_number_ - max_files_; i != 0; --i) {
std::string filename = get_filename(i);
if (!path_exists(filename)) {
if (++not_exists == 2) break;
continue;
}
std::remove(filename.c_str());
}
std::remove(get_filename(0).c_str());
}
return WriteNumberFile(get_number_filename(), final_number_);
}

std::string LatestFilename() override { return basename_ + "-" + std::to_string(final_number_) + ext_; }

private:
[[nodiscard]] std::string get_number_filename() const { return basename_ + ext_ + ".latest"; }
[[nodiscard]] std::string get_filename(const size_t index) const {
return basename_ + "-" + std::to_string(index) + ext_;
}

static Status ReadNumberFile(const std::string& filename, size_t* const final_number) {
std::ifstream input_file(filename);
if (!input_file.is_open()) {
*final_number = 0;
return Status::IOError("Failed to open file: " + filename);
}

input_file >> *final_number;
input_file.close();
return Status::OK();
}

// Directly writes the latest number to the file
static Status WriteNumberFile(const std::string& filename, const size_t number) {
std::ofstream output_file(filename);

if (!output_file.is_open()) {
return Status::IOError("Failed to write number to: " + filename);
}

output_file << number;
output_file.close();
return Status::OK();
}

// config
const std::string basename_;
const std::string ext_;
const size_t max_files_;

size_t final_number_ = 0;
};

} // namespace ulog::file
16 changes: 16 additions & 0 deletions include/ulog/file/rotation_strategy_interface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include "ulog/status.h"

namespace ulog::file {

class RotationStrategyInterface {
public:
RotationStrategyInterface() = default;
virtual ~RotationStrategyInterface() = default;

virtual Status Rotate() = 0;
virtual std::string LatestFilename() = 0;
};

} // namespace ulog::file
61 changes: 61 additions & 0 deletions include/ulog/file/rotation_strategy_rename.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#pragma once

#include <string>
#include <utility>

#include "ulog/file/file.h"
#include "ulog/file/rotation_strategy_interface.h"

namespace ulog::file {

class RotationStrategyRename final : public RotationStrategyInterface {
public:
RotationStrategyRename(std::string basename, std::string ext, const size_t max_files)
: basename_(std::move(basename)), ext_(std::move(ext)), max_files_(max_files == 0 ? 1 : max_files) {}

// Rotate files:
// log.txt -> log.1.txt
// log.1.txt -> log.2.txt
// log.2.txt -> log.3.txt
// log.3.txt -> delete
// "tail -f" may be interrupted when rename is executed, and "tail -F" can
// be used instead, but some "-F" implementations (busybox tail) cannot
// obtain all logs in real time.
Status Rotate() override {
for (size_t i = max_files_ - 1; i > 0; --i) {
std::string src = get_filename(i - 1);
if (!path_exists(src)) {
continue;
}
RenameFile(src, get_filename(i));
}

// Try to clear the files that were previously configured too much
size_t not_exists = 0;
for (size_t i = max_files_;; ++i) {
std::string filename = get_filename(i);
if (!path_exists(filename)) {
if (++not_exists == 2) break;
continue;
}
std::remove(filename.c_str());
}
return Status::OK();
}

std::string LatestFilename() override { return basename_ + ext_; }

private:
[[nodiscard]] std::string get_filename(const size_t index) const {
if (index == 0u) {
return basename_ + ext_;
}

return basename_ + "." + std::to_string(index) + ext_;
}
std::string basename_;
std::string ext_;
size_t max_files_;
};

} // namespace ulog::file
Loading

0 comments on commit e8d0588

Please sign in to comment.