| // Copyright 2015 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "webservd/log_manager.h" |
| |
| #include <arpa/inet.h> |
| #include <cinttypes> |
| #include <netinet/in.h> |
| |
| #include <set> |
| #include <utility> |
| |
| #include <base/bind.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_util.h> |
| #include <base/lazy_instance.h> |
| #include <base/strings/stringprintf.h> |
| #include <brillo/strings/string_utils.h> |
| |
| namespace webservd { |
| |
| namespace { |
| |
| // Singleton instance of LogManager class. |
| base::LazyInstance<LogManager>::DestructorAtExit g_log_manager = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| // The number of files to keep in the log directory. Since there is one log |
| // file per day of logging, this is essentially how many days' worth of logs |
| // to keep. This also controls the total maximum size of the log data, which |
| // is (kLogFilesToKeep * kMaxLogFileSize). |
| const size_t kLogFilesToKeep = 7; |
| |
| // Maximum log file size. |
| const int64_t kMaxLogFileSize = 1024 * 1024; // 1 MB |
| |
| // Obtain an IP address as a human-readable string for logging. |
| std::string GetIPAddress(const sockaddr* addr) { |
| static_assert(INET6_ADDRSTRLEN > INET_ADDRSTRLEN, "Unexpected IP addr len."); |
| char buf[INET6_ADDRSTRLEN] = "-"; |
| if (!addr) |
| return buf; |
| |
| switch (addr->sa_family) { |
| case AF_INET: { |
| auto addr_in = reinterpret_cast<const sockaddr_in*>(addr); |
| if (!inet_ntop(AF_INET, &addr_in->sin_addr, buf, sizeof(buf))) |
| PLOG(ERROR) << "Unable to get IP address string (IPv4)"; |
| } break; |
| |
| case AF_INET6: { |
| auto addr_in6 = reinterpret_cast<const sockaddr_in6*>(addr); |
| |
| // Note that inet_ntop(3) doesn't handle IPv4-mapped IPv6 |
| // addresses [1] the way you'd expect .. for example, it returns |
| // "::ffff:172.22.72.163" instead of the more traditional IPv4 |
| // notation "172.22.72.163". Fortunately, this is pretty easy to |
| // fix ourselves. |
| // |
| // [1] : see RFC 4291, section 2.5.5.2 for what that means |
| // http://tools.ietf.org/html/rfc4291#section-2.5.5 |
| // |
| auto dwords = reinterpret_cast<const uint32_t*>(&addr_in6->sin6_addr); |
| if (dwords[0] == 0x00000000 && dwords[1] == 0x00000000 && |
| dwords[2] == htonl(0x0000ffff)) { |
| auto bytes = reinterpret_cast<const uint8_t*>(&addr_in6->sin6_addr); |
| return base::StringPrintf("%d.%d.%d.%d", bytes[12], bytes[13], |
| bytes[14], bytes[15]); |
| } else if (!inet_ntop(AF_INET6, &addr_in6->sin6_addr, buf, sizeof(buf))) { |
| PLOG(ERROR) << "Unable to get IP address string (IPv6)"; |
| } |
| } break; |
| |
| default: |
| LOG(ERROR) << "Unsupported address family " << addr->sa_family; |
| break; |
| } |
| return buf; |
| } |
| |
| } // Anonymous namespace |
| |
| // Logger class to write the log data to a log file. |
| class FileLogger final : public LogManager::LoggerInterface { |
| public: |
| FileLogger(const base::FilePath& log_directory, LogManager* log_manager) |
| : log_directory_(log_directory), log_manager_{log_manager} {} |
| |
| // Write the log entry to today's log file. |
| void Log(const base::Time& timestamp, const std::string& entry) override { |
| tm time_buf = {}; |
| char file_name[32] = {}; |
| // Create the file name in year-month-day format so that string sort would |
| // correspond to date sort. |
| time_t time = timestamp.ToTimeT(); |
| strftime(file_name, sizeof(file_name), "%Y-%m-%d.log", |
| localtime_r(&time, &time_buf)); |
| base::FilePath file_path = log_directory_.Append(file_name); |
| bool success = false; |
| bool exists = base::PathExists(file_path); |
| // If the file already exists, check its size. If it is going to be larger |
| // then the maximum allowed log size, archive the current log file and |
| // create a new, empty one. |
| if (exists) { |
| int64_t file_size = 0; |
| bool got_size = base::GetFileSize(file_path, &file_size); |
| if (got_size && (file_size + entry.size() > kMaxLogFileSize)) |
| exists = !ArchiveLogFile(file_name); |
| } |
| |
| if (exists) { |
| success = base::AppendToFile(file_path, entry.data(), entry.size()); |
| } else { |
| int size = static_cast<int>(entry.size()); |
| success = (base::WriteFile(file_path, entry.data(), size) == size); |
| if (success) { |
| // We just created a new file, see if we need to purge old ones. |
| log_manager_->PerformLogMaintenance(); |
| } |
| } |
| PLOG_IF(ERROR, !success) |
| << "Failed to append a log entry to log file at " << file_path.value(); |
| } |
| |
| private: |
| // Renames the log file to a next available suffix-appended archive when |
| // the log file size starts to exceed the pre-defined maximum size. |
| // The existing log file is renamed by changing the original |file_name| to |
| // YYYY-MM-DD-<suffix>.log where suffix is characters 'a', 'b', ... |
| // Since '-' comes before '.', "2015-02-25-a.log" will come before |
| // "2015-02-25.log" in sort order and the previously-renamed files will be |
| // considered "older" than the current one, which is what we need. |
| // Returns true if the file has been successfully renamed. |
| bool ArchiveLogFile(const std::string& file_name) { |
| char suffix = 'a'; |
| auto pair = brillo::string_utils::SplitAtFirst(file_name, "."); |
| // If we try all the suffixes from 'a' to 'z' and still can't find a name, |
| // abandon this strategy and keep appending to the current file. |
| while (suffix <= 'z') { |
| base::FilePath archive_file_path = |
| log_directory_.Append(base::StringPrintf( |
| "%s-%c.%s", pair.first.c_str(), suffix, pair.second.c_str())); |
| if (!base::PathExists(archive_file_path)) { |
| base::FilePath file_path = log_directory_.Append(file_name); |
| if (base::Move(file_path, archive_file_path)) { |
| // Successfully renamed, start a new log file. |
| return true; |
| } else { |
| PLOG(ERROR) << "Failed to rename log file from " << file_path.value() |
| << " to " << archive_file_path.value(); |
| } |
| break; |
| } |
| suffix++; |
| } |
| return false; |
| } |
| |
| base::FilePath log_directory_; |
| LogManager* log_manager_{nullptr}; |
| }; |
| |
| void LogManager::Init(const base::FilePath& log_directory) { |
| LogManager* inst = GetInstance(); |
| inst->log_directory_ = log_directory; |
| inst->SetLogger( |
| std::unique_ptr<LoggerInterface>{new FileLogger{log_directory, inst}}); |
| inst->PerformLogMaintenance(); |
| } |
| |
| void LogManager::OnRequestCompleted(const base::Time& timestamp, |
| const sockaddr* client_addr, |
| const std::string& method, |
| const std::string& url, |
| const std::string& version, |
| int status_code, |
| int64_t response_size) { |
| std::string ip_address = GetIPAddress(client_addr); |
| tm time_buf = {}; |
| char str_buf[32] = {}; |
| // Format the date/time as "25/Feb/2015:03:29:12 -0800". |
| time_t time = timestamp.ToTimeT(); |
| strftime(str_buf, sizeof(str_buf), "%d/%b/%Y:%H:%M:%S %z", |
| localtime_r(&time, &time_buf)); |
| |
| // Log file entry for one HTTP request looking like this: |
| // 127.0.0.1 - - [25/Feb/2015:03:29:12 -0800] "GET /test HTTP/1.1" 200 2326 |
| std::string size_string{"-"}; |
| if (response_size >= 0) |
| size_string = std::to_string(response_size); |
| std::string log_entry = |
| base::StringPrintf("%s - - [%s] \"%s %s %s\" %d %s\n", ip_address.c_str(), |
| str_buf, method.c_str(), url.c_str(), version.c_str(), |
| status_code, size_string.c_str()); |
| GetInstance()->logger_->Log(timestamp, log_entry); |
| } |
| |
| void LogManager::SetLogger(std::unique_ptr<LoggerInterface> logger) { |
| GetInstance()->logger_ = std::move(logger); |
| } |
| |
| LogManager* LogManager::GetInstance() { |
| return g_log_manager.Pointer(); |
| } |
| |
| void LogManager::PerformLogMaintenance() { |
| // Get the list of all the log files in the log directory and put them into |
| // a set which will sort the files by name (and effectively by the date since |
| // we chose the file naming scheme deliberately to guarantee proper sorting |
| // order). |
| std::set<base::FilePath> log_files; |
| base::FileEnumerator enumerator{log_directory_, false, |
| base::FileEnumerator::FILES, "*.log"}; |
| base::FilePath file = enumerator.Next(); |
| while (!file.empty()) { |
| log_files.insert(file); |
| file = enumerator.Next(); |
| } |
| |
| // Now, if we have more files than we want to keep, purge the old files. |
| while (log_files.size() > kLogFilesToKeep) { |
| auto front_it = log_files.begin(); |
| PLOG_IF(WARNING, !base::DeleteFile(*front_it, false)) |
| << "Failed to delete an old log file: " << front_it->value(); |
| log_files.erase(front_it); |
| } |
| } |
| |
| } // namespace webservd |