1//
2// Copyright(c) 2015 Gabi Melman.
3// Distributed under the MIT License (http://opensource.org/licenses/MIT)
4//
5
6/* -*- MODE: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
7/*
8 *     Copyright 2017 Couchbase, Inc
9 *
10 *   Licensed under the Apache License, Version 2.0 (the "License");
11 *   you may not use this file except in compliance with the License.
12 *   You may obtain a copy of the License at
13 *
14 *       http://www.apache.org/licenses/LICENSE-2.0
15 *
16 *   Unless required by applicable law or agreed to in writing, software
17 *   distributed under the License is distributed on an "AS IS" BASIS,
18 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 *   See the License for the specific language governing permissions and
20 *   limitations under the License.
21 */
22
23#include "custom_rotating_file_sink.h"
24
25#include <platform/dirutils.h>
26#include <spdlog/details/file_helper.h>
27#include <spdlog/details/fmt_helper.h>
28
29#include <memory>
30
31static unsigned long find_first_logfile_id(const std::string& basename) {
32    unsigned long id = 0;
33
34    auto files = cb::io::findFilesWithPrefix(basename);
35    for (auto& file : files) {
36        // the format of the name should be:
37        // fnm.number.txt
38        auto index = file.rfind(".txt");
39        if (index == std::string::npos) {
40            continue;
41        }
42
43        file.resize(index);
44        index = file.rfind('.');
45        if (index != std::string::npos) {
46            try {
47                unsigned long value = std::stoul(file.substr(index + 1));
48                if (value > id) {
49                    id = value + 1;
50                }
51            } catch (...) {
52                // Ignore
53            }
54        }
55    }
56
57    return id;
58}
59
60template <class Mutex>
61custom_rotating_file_sink<Mutex>::custom_rotating_file_sink(
62        const spdlog::filename_t& base_filename,
63        std::size_t max_size,
64        const std::string& log_pattern)
65    : _base_filename(base_filename),
66      _max_size(max_size),
67      _current_size(0),
68      _file_helper(std::make_unique<spdlog::details::file_helper>()),
69      _next_file_id(find_first_logfile_id(base_filename)) {
70    formatter = std::make_unique<spdlog::pattern_formatter>(
71            log_pattern, spdlog::pattern_time_type::local);
72    _file_helper->open(calc_filename());
73    _current_size = _file_helper->size(); // expensive. called only once
74    addHook(openingLogfile);
75}
76
77/* In addition to the functionality of spdlog's rotating_file_sink,
78 * this class adds hooks marking the start and end of a logfile.
79 */
80template <class Mutex>
81void custom_rotating_file_sink<Mutex>::sink_it_(
82        const spdlog::details::log_msg& msg) {
83    _current_size += msg.raw.size();
84    if (_current_size > _max_size) {
85        std::unique_ptr<spdlog::details::file_helper> next =
86                std::make_unique<spdlog::details::file_helper>();
87        try {
88            next->open(calc_filename(), true);
89            addHook(closingLogfile);
90            std::swap(_file_helper, next);
91            _current_size = msg.raw.size();
92            addHook(openingLogfile);
93        } catch (...) {
94            // Keep on logging to the this file, but try swap at the next
95            // insert of data (didn't use the next file we need to
96            // roll back the next_file_id to avoid getting a hole ;-)
97            _next_file_id--;
98        }
99    }
100    fmt::memory_buffer formatted;
101    formatter->format(msg, formatted);
102
103    _file_helper->write(formatted);
104}
105
106template <class Mutex>
107void custom_rotating_file_sink<Mutex>::flush_() {
108    _file_helper->flush();
109}
110
111/* Takes a message, formats it and writes it to file */
112template <class Mutex>
113void custom_rotating_file_sink<Mutex>::addHook(const std::string& hook) {
114    spdlog::details::log_msg msg;
115    msg.time = spdlog::details::os::now();
116    msg.level = spdlog::level::info;
117
118    // Append the hook to the msg
119    spdlog::details::fmt_helper::append_str(hook, msg.raw);
120
121    if (hook == openingLogfile) {
122        spdlog::details::fmt_helper::append_str(
123                std::string(_file_helper->filename().data(),
124                            _file_helper->filename().size()),
125                msg.raw);
126    }
127    fmt::memory_buffer formatted;
128    formatter->format(msg, formatted);
129    _current_size += formatted.size();
130
131    _file_helper->write(formatted);
132}
133
134/**
135 * Create a new filename. If this breaks when updating spdlog then compare
136 * functionality to the calc_filename() method in the spdlog rotating_file_sink
137 * @tparam Mutex
138 * @return An spdlog filename
139 */
140template <class Mutex>
141spdlog::filename_t custom_rotating_file_sink<Mutex>::calc_filename() {
142    char fname[1024];
143    unsigned long try_id = _next_file_id;
144    do {
145        sprintf(fname, "%s.%06lu.txt", _base_filename.c_str(), try_id++);
146    } while (cb::io::isFile(fname));
147
148    _next_file_id = try_id;
149#if defined(_WIN32) && defined(SPDLOG_WCHAR_FILENAMES)
150    return fmt::to_wstring(fname);
151#else
152    return fmt::to_string(fname);
153#endif
154}
155
156template <class Mutex>
157custom_rotating_file_sink<Mutex>::~custom_rotating_file_sink() {
158    addHook(closingLogfile);
159}
160
161template class custom_rotating_file_sink<std::mutex>;
162template class custom_rotating_file_sink<spdlog::details::null_mutex>;
163