| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "util/Log.hpp" |
| |
| #include <algorithm> |
| #include <cctype> |
| #include <chrono> |
| #include <cstdio> |
| #include <ctime> |
| #include <iostream> |
| #include <map> |
| #include <mutex> |
| #include <sstream> |
| #include <string> |
| #include <thread> |
| #include <utility> |
| |
| #include <boost/asio/ip/host_name.hpp> |
| #include <boost/filesystem.hpp> |
| #include <boost/process/environment.hpp> |
| #include <boost/regex.hpp> |
| |
| #include <geode/ExceptionTypes.hpp> |
| #include <geode/util/LogLevel.hpp> |
| |
| #include "geodeBanner.hpp" |
| #include "util/chrono/time_point.hpp" |
| |
| namespace { |
| |
| static size_t g_bytesWritten = 0; |
| |
| static size_t g_fileSizeLimit = GEODE_MAX_LOG_FILE_LIMIT; |
| static size_t g_diskSpaceLimit = GEODE_MAX_LOG_DISK_LIMIT; |
| |
| static std::mutex g_logMutex; |
| |
| static int g_rollIndex = 0; |
| static size_t g_spaceUsed = 0; |
| |
| static boost::filesystem::path g_fullpath; |
| static std::map<int32_t, boost::filesystem::path> g_rollFiles; |
| |
| static FILE* g_log = nullptr; |
| |
| static std::string g_hostName; |
| |
| static thread_local std::string g_threadName; |
| |
| const int __1K__ = 1024; |
| const int __1M__ = (__1K__ * __1K__); |
| |
| } // namespace |
| |
| namespace apache { |
| namespace geode { |
| namespace client { |
| |
| LogLevel Log::s_logLevel = LogLevel::Default; |
| |
| /*****************************************************************************/ |
| |
| LogLevel Log::logLevel() { return s_logLevel; } |
| |
| /** |
| * Set the current log level. |
| */ |
| void Log::setLogLevel(LogLevel level) { s_logLevel = level; } |
| |
| void Log::validateSizeLimits(int64_t fileSizeLimit, int64_t diskSpaceLimit) { |
| if (fileSizeLimit * __1M__ > GEODE_MAX_LOG_FILE_LIMIT) { |
| throw IllegalArgumentException( |
| "Specified file size limit larger than max allowed (1GB)"); |
| } else if (fileSizeLimit < 0) { |
| throw IllegalArgumentException("Specified file size limit must be >= 0"); |
| } |
| |
| if (diskSpaceLimit * __1M__ > GEODE_MAX_LOG_DISK_LIMIT) { |
| throw IllegalArgumentException( |
| "Specified disk space limit larger than max allowed (1TB)"); |
| } else if (diskSpaceLimit < 0) { |
| throw IllegalArgumentException("Specified disk space limit must be >= 0"); |
| } |
| |
| if (fileSizeLimit > diskSpaceLimit && diskSpaceLimit != 0) { |
| throw IllegalArgumentException( |
| "Disk space limit must be larger than file size limit"); |
| } |
| } |
| |
| void Log::init(LogLevel level, const char* logFileName, int32_t logFileLimit, |
| int64_t logDiskSpaceLimit) { |
| auto logFileNameString = |
| logFileName ? std::string(logFileName) : std::string("geode-native.log"); |
| init(level, logFileNameString, logFileLimit, logDiskSpaceLimit); |
| } |
| |
| void Log::rollLogFile() { |
| if (g_log) { |
| fclose(g_log); |
| g_log = nullptr; |
| } |
| |
| auto rollFileName = |
| (g_fullpath.parent_path() / |
| (g_fullpath.stem().string() + "-" + std::to_string(g_rollIndex) + |
| g_fullpath.extension().string())) |
| .string(); |
| try { |
| auto rollFile = boost::filesystem::path(rollFileName); |
| boost::filesystem::rename(g_fullpath, rollFile); |
| g_rollFiles[g_rollIndex] = rollFile; |
| g_rollIndex++; |
| } catch (const boost::filesystem::filesystem_error&) { |
| throw IllegalStateException("Failed to roll log file"); |
| } |
| } |
| |
| void Log::removeOldestRolledLogFile() { |
| if (g_rollFiles.size()) { |
| auto index = g_rollFiles.begin()->first; |
| auto fileToRemove = g_rollFiles.begin()->second; |
| auto fileSize = boost::filesystem::file_size(fileToRemove); |
| boost::filesystem::remove(fileToRemove); |
| g_rollFiles.erase(index); |
| g_spaceUsed -= fileSize; |
| } else { |
| throw IllegalStateException( |
| "Failed to free sufficient disk space for logs"); |
| } |
| } |
| |
| void Log::calculateUsedDiskSpace() { |
| g_spaceUsed = 0; |
| if (boost::filesystem::exists(g_fullpath)) { |
| g_spaceUsed = boost::filesystem::file_size(g_fullpath); |
| for (auto const& item : g_rollFiles) { |
| g_spaceUsed += boost::filesystem::file_size(item.second); |
| } |
| } |
| } |
| |
| void Log::buildRollFileMapping() { |
| const auto filterstring = g_fullpath.stem().string() + "-(\\d+)\\.log$"; |
| const boost::regex my_filter(filterstring); |
| |
| g_rollFiles.clear(); |
| |
| boost::filesystem::directory_iterator end_itr; |
| for (boost::filesystem::directory_iterator i( |
| g_fullpath.parent_path().string()); |
| i != end_itr; ++i) { |
| if (boost::filesystem::is_regular_file(i->status())) { |
| std::string filename = i->path().filename().string(); |
| boost::regex testPattern(filterstring); |
| boost::match_results<std::string::const_iterator> testMatches; |
| if (boost::regex_search(std::string::const_iterator(filename.begin()), |
| filename.cend(), testMatches, testPattern)) { |
| auto index = std::atoi( |
| std::string(testMatches[1].first, testMatches[1].second).c_str()); |
| g_rollFiles[index] = i->path(); |
| } |
| } |
| } |
| } |
| |
| void Log::setRollFileIndex() { |
| g_rollIndex = 0; |
| if (g_rollFiles.size()) { |
| g_rollIndex = g_rollFiles.rbegin()->first + 1; |
| } |
| } |
| |
| void Log::setSizeLimits(int32_t logFileLimit, int64_t logDiskSpaceLimit) { |
| validateSizeLimits(logFileLimit, logDiskSpaceLimit); |
| |
| // Default to 10MB file limit and 1GB disk limit |
| if (logFileLimit == 0 && logDiskSpaceLimit == 0) { |
| g_fileSizeLimit = 10 * __1M__; |
| g_diskSpaceLimit = 1000 * __1M__; |
| } |
| // disk space specified but file size is defaulted. Just use a single |
| // log file, i.e. set file limit == disk limit |
| else if (logFileLimit == 0) { |
| g_diskSpaceLimit = logDiskSpaceLimit * __1M__; |
| g_fileSizeLimit = g_diskSpaceLimit; |
| } else if (logDiskSpaceLimit == 0) { |
| g_fileSizeLimit = logFileLimit * __1M__; |
| g_diskSpaceLimit = g_fileSizeLimit; |
| } else { |
| g_fileSizeLimit = logFileLimit * __1M__; |
| g_diskSpaceLimit = logDiskSpaceLimit * __1M__; |
| } |
| } |
| |
| void Log::init(LogLevel level, const std::string& logFileName, |
| int32_t logFileLimit, int64_t logDiskSpaceLimit) { |
| if (g_log != nullptr) { |
| throw IllegalStateException( |
| "The Log has already been initialized. " |
| "Call Log::close() before calling Log::init again."); |
| } |
| s_logLevel = level; |
| |
| try { |
| std::lock_guard<decltype(g_logMutex)> guard(g_logMutex); |
| |
| g_hostName = boost::asio::ip::host_name(); |
| |
| g_fullpath = |
| boost::filesystem::absolute(boost::filesystem::path(logFileName)); |
| |
| // if no extension then add .log extension |
| if (g_fullpath.extension().empty() || (g_fullpath.extension() != ".log")) { |
| g_fullpath = g_fullpath.string() + ".log"; |
| } |
| |
| setSizeLimits(logFileLimit, logDiskSpaceLimit); |
| |
| g_bytesWritten = 0; |
| g_spaceUsed = 0; |
| |
| // Ensure that directory exists for log files. We're going to attempt |
| // to iterate through files in that folder, and if it doesn't exist boost |
| // will throw an exception. |
| const auto target_path = g_fullpath.parent_path().string(); |
| if (!boost::filesystem::exists(target_path)) { |
| boost::filesystem::create_directories(target_path); |
| } |
| |
| buildRollFileMapping(); |
| setRollFileIndex(); |
| calculateUsedDiskSpace(); |
| while (g_spaceUsed > g_diskSpaceLimit) { |
| removeOldestRolledLogFile(); |
| } |
| |
| if (boost::filesystem::exists(g_fullpath) && logFileLimit > 0) { |
| rollLogFile(); |
| } |
| writeBanner(); |
| } catch (const boost::exception&) { |
| auto msg = std::string("Unable to log to file '") + logFileName + "'"; |
| throw IllegalArgumentException(msg.c_str()); |
| } catch (const std::exception& ex) { |
| auto msg = std::string("Unable to log to file '") + logFileName + |
| "': " + ex.what(); |
| throw IllegalArgumentException(msg.c_str()); |
| } |
| } |
| |
| void Log::close() { |
| std::lock_guard<decltype(g_logMutex)> guard(g_logMutex); |
| |
| if (g_log) { |
| fclose(g_log); |
| g_log = nullptr; |
| } |
| g_fullpath = ""; |
| } |
| |
| void Log::writeBanner() { |
| if (s_logLevel != LogLevel::None) { |
| std::string bannertext = geodeBanner::getBanner(); |
| |
| // fullpath empty --> we're logging to stdout |
| if (g_fullpath.string().empty()) { |
| std::cout << bannertext << std::flush; |
| } else { |
| if (boost::filesystem::exists( |
| g_fullpath.parent_path().string().c_str()) || |
| boost::filesystem::create_directories(g_fullpath.parent_path())) { |
| g_log = fopen(g_fullpath.string().c_str(), "a"); |
| if (g_log) { |
| if (fwrite(bannertext.c_str(), sizeof(char), bannertext.length(), |
| g_log) == bannertext.length()) { |
| g_bytesWritten += static_cast<int32_t>(bannertext.length()); |
| fflush(g_log); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| const char* Log::levelToChars(LogLevel level) { |
| switch (level) { |
| case LogLevel::None: |
| return "none"; |
| |
| case LogLevel::Error: |
| return "error"; |
| |
| case LogLevel::Warning: |
| return "warning"; |
| |
| case LogLevel::Info: |
| return "info"; |
| |
| case LogLevel::Default: |
| return "default"; |
| |
| case LogLevel::Config: |
| return "config"; |
| |
| case LogLevel::Fine: |
| return "fine"; |
| |
| case LogLevel::Finer: |
| return "finer"; |
| |
| case LogLevel::Finest: |
| return "finest"; |
| |
| case LogLevel::Debug: |
| return "debug"; |
| |
| case LogLevel::All: |
| return "all"; |
| } |
| throw IllegalArgumentException(std::string("Unexpected log level: ") + |
| std::to_string(static_cast<int>(level))); |
| } |
| |
| LogLevel Log::charsToLevel(const std::string& chars) { |
| std::string level = chars; |
| |
| if (level.empty()) return LogLevel::None; |
| |
| std::transform(level.begin(), level.end(), level.begin(), ::tolower); |
| |
| if (level == "none") { |
| return LogLevel::None; |
| } else if (level == "error") { |
| return LogLevel::Error; |
| } else if (level == "warning") { |
| return LogLevel::Warning; |
| } else if (level == "info") { |
| return LogLevel::Info; |
| } else if (level == "default") { |
| return LogLevel::Default; |
| } else if (level == "config") { |
| return LogLevel::Config; |
| } else if (level == "fine") { |
| return LogLevel::Fine; |
| } else if (level == "finer") { |
| return LogLevel::Finer; |
| } else if (level == "finest") { |
| return LogLevel::Finest; |
| } else if (level == "debug") { |
| return LogLevel::Debug; |
| } else if (level == "all") { |
| return LogLevel::All; |
| } else { |
| throw IllegalArgumentException(("Unexpected log level: " + level).c_str()); |
| } |
| } |
| |
| void Log::setThreadName(const std::string& threadName) { |
| if (threadName.empty()) { |
| throw IllegalArgumentException("Thread name is empty."); |
| } |
| |
| g_threadName = threadName; |
| |
| #if defined(HAVE_pthread_setname_np) |
| |
| pthread_setname_np(threadName.c_str()); |
| |
| #elif defined(_WIN32) |
| |
| const DWORD MS_VC_EXCEPTION = 0x406D1388; |
| |
| #pragma pack(push, 8) |
| typedef struct tagTHREADNAME_INFO { |
| DWORD dwType; // Must be 0x1000. |
| LPCSTR szName; // Pointer to name (in user addr space). |
| DWORD dwThreadID; // Thread ID (-1=caller thread). |
| DWORD dwFlags; // Reserved for future use, must be zero. |
| } THREADNAME_INFO; |
| #pragma pack(pop) |
| |
| THREADNAME_INFO info; |
| info.dwType = 0x1000; |
| info.szName = threadName.c_str(); |
| info.dwThreadID = -1; |
| info.dwFlags = 0; |
| |
| __try { |
| RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), |
| (ULONG_PTR*)&info); |
| } __except (EXCEPTION_EXECUTE_HANDLER) { |
| } |
| |
| #endif |
| } |
| |
| std::string Log::formatLogLine(LogLevel level) { |
| std::stringstream msg; |
| const auto now = std::chrono::system_clock::now(); |
| const auto secs = std::chrono::system_clock::to_time_t(now); |
| const auto microseconds = |
| std::chrono::duration_cast<std::chrono::microseconds>( |
| now - std::chrono::system_clock::from_time_t(secs)); |
| const auto tm_val = apache::geode::util::chrono::localtime(secs); |
| |
| msg << '[' << Log::levelToChars(level) << ' ' |
| << std::put_time(&tm_val, "%Y/%m/%d %H:%M:%S") << '.' << std::setfill('0') |
| << std::setw(6) << microseconds.count() << ' ' |
| << std::put_time(&tm_val, "%z ") << g_hostName << ':' |
| << boost::this_process::get_id() << ' ' << std::this_thread::get_id() |
| << " (" << g_threadName << ")] "; |
| |
| return msg.str(); |
| } |
| |
| void Log::log(LogLevel level, const std::string& msg) { |
| Log::logInternal(level, msg); |
| } |
| |
| void Log::logInternal(LogLevel level, const std::string& msg) { |
| std::lock_guard<decltype(g_logMutex)> guard(g_logMutex); |
| |
| std::string buf; |
| char fullpath[512] = {0}; |
| |
| if (g_fullpath.string().empty()) { |
| std::cout << formatLogLine(level) << msg << "\n" << std::flush; |
| } else { |
| if (!g_log) { |
| g_log = fopen(g_fullpath.string().c_str(), "a"); |
| } |
| |
| if (g_log) { |
| buf = formatLogLine(level); |
| auto numChars = static_cast<int>(buf.length() + msg.length()); |
| g_bytesWritten += |
| numChars + 2; // bcoz we have to count trailing new line (\n) |
| |
| if ((g_fileSizeLimit != 0) && (g_bytesWritten >= g_fileSizeLimit)) { |
| rollLogFile(); |
| g_bytesWritten = numChars + 2; // Account for trailing newline |
| writeBanner(); |
| } |
| |
| g_spaceUsed += numChars + 2; |
| |
| // Remove existing rolled log files until we're below the limit |
| while (g_spaceUsed >= g_diskSpaceLimit) { |
| removeOldestRolledLogFile(); |
| } |
| |
| auto logLine = buf + msg + "\n"; |
| if (fwrite(logLine.c_str(), sizeof(char), logLine.length(), g_log) != |
| logLine.length() || |
| ferror(g_log)) { |
| // Let's continue without throwing the exception. It should not cause |
| // process to terminate |
| fclose(g_log); |
| g_log = nullptr; |
| } else { |
| fflush(g_log); |
| } |
| } |
| } |
| } |
| |
| void Log::log(LogLevel level, const char* fmt, ...) { |
| char msg[_GEODE_LOG_MESSAGE_LIMIT] = {0}; |
| va_list argp; |
| va_start(argp, fmt); |
| // NOLINTNEXTLINE(clang-analyzer-valist.Uninitialized): clang-tidy bug |
| std::vsnprintf(msg, sizeof(msg), fmt, argp); |
| Log::logInternal(level, std::string(msg)); |
| va_end(argp); |
| } |
| |
| void Log::logCatch(LogLevel level, const char* msg, const Exception& ex) { |
| if (enabled(level)) { |
| std::string message = "Geode exception " + ex.getName() + |
| " caught: " + ex.getMessage() + "\n" + msg; |
| log(level, message); |
| } |
| } |
| |
| bool Log::enabled(LogLevel level) { |
| return (level != LogLevel::None && level <= logLevel()); |
| } |
| |
| } // namespace client |
| } // namespace geode |
| } // namespace apache |