diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 18def90b9f..d132fb7ae1 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -28,7 +28,7 @@ jobs: image: ${{ matrix.config.compiler == 'clang' && 'teeks99/clang-ubuntu' || matrix.config.compiler }}:${{ matrix.config.version }} name: "${{ matrix.config.compiler}} ${{ matrix.config.version }} (C++${{ matrix.config.cppstd }} ${{ matrix.config.build_type }} ${{ matrix.config.asan == 'ON' && 'ASAN' || '' }}${{ matrix.config.tsan == 'ON' && 'TSAN' || '' }})" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup run: | apt-get update diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index fab57b861c..f4852963e2 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -10,7 +10,7 @@ jobs: runs-on: macOS-latest name: "macOS Clang (C++17, Release)" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build run: | mkdir -p build && cd build diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 2991caed88..542a7075fe 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: CMake ${{ matrix.config.GENERATOR }} CXX=${{matrix.config.CXX_STANDARD}} shell: pwsh @@ -85,7 +85,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: CMake ${{ matrix.config.GENERATOR }} CXX=${{matrix.config.CXX_STANDARD}} shell: pwsh diff --git a/CMakeLists.txt b/CMakeLists.txt index 486d1ae3f3..c3e81f66b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,11 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) # Set default build to release # --------------------------------------------------------------------------------------- if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose Release or Debug" FORCE) + # Set CMAKE_BUILD_TYPE only if this project is top-level (avoid overriding parent) + if ((DEFINED PROJECT_IS_TOP_LEVEL AND PROJECT_IS_TOP_LEVEL) OR + (NOT DEFINED PROJECT_IS_TOP_LEVEL AND CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)) + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose Release or Debug" FORCE) + endif () endif () # --------------------------------------------------------------------------------------- # Compiler config @@ -48,7 +52,11 @@ option(SPDLOG_BUILD_SHARED "Build shared library" OFF) option(SPDLOG_BUILD_EXAMPLE "Build example" ${SPDLOG_MASTER_PROJECT}) option(SPDLOG_BUILD_TESTS "Build tests" OFF) option(SPDLOG_BUILD_BENCH "Build benchmarks (Requires https://github.com/google/benchmark.git to be installed)" OFF) -option(SPDLOG_SANITIZE_ADDRESS "Enable address sanitizer in tests" OFF) +option(SPDLOG_SANITIZE_ADDRESS "Enable address sanitizer for the spdlog library and anything that links it" OFF) +option(SPDLOG_SANITIZE_THREAD "Enable thread sanitizer for the spdlog library and anything that links it" OFF) +if (SPDLOG_SANITIZE_ADDRESS AND SPDLOG_SANITIZE_THREAD) + message(FATAL_ERROR "SPDLOG_SANITIZE_ADDRESS and SPDLOG_SANITIZE_THREAD are mutually exclusive") +endif () option(SPDLOG_BUILD_WARNINGS "Enable compiler warnings" OFF) option(SPDLOG_SYSTEM_INCLUDES "Include as system headers (skip for clang-tidy)." OFF) option(SPDLOG_INSTALL "Generate the install target" ${SPDLOG_MASTER_PROJECT}) @@ -62,7 +70,16 @@ option(SPDLOG_PREVENT_CHILD_FD "Prevent from child processes to inherit log file option(SPDLOG_NO_THREAD_ID "prevent spdlog from querying the thread id on each log call if thread id is not needed" OFF) option(SPDLOG_DISABLE_GLOBAL_LOGGER "Disable global logger creation" OFF) option(SPDLOG_NO_TLS "Disable thread local storage" OFF) +option(SPDLOG_NO_TZ_OFFSET "Omit %z timezone offset (use on platforms without tm_gmtoff / full offset)" OFF) option(SPDLOG_TIDY "run clang-tidy" OFF) +set(SPDLOG_DEBUG_POSTFIX "-${SPDLOG_VERSION_MAJOR}.${SPDLOG_VERSION_MINOR}d" CACHE STRING + "Filename postfix for libraries in debug builds (empty string allowed)") +option(SPDLOG_MSVC_UTF8 "Enable/disable MSVC /utf-8 flag (recommended for fmt)" ON) +if (WIN32) + option(SPDLOG_WCHAR_CONSOLE "Decode UTF-8 and write with WriteConsoleW (Unicode console output)" OFF) +else () + set(SPDLOG_WCHAR_CONSOLE OFF CACHE BOOL "non supported option" FORCE) +endif () if (SPDLOG_TIDY) set(CMAKE_CXX_CLANG_TIDY "clang-tidy") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -91,7 +108,7 @@ message(STATUS "spdlog fmt external: " ${SPDLOG_FMT_EXTERNAL}) # Find {fmt} library # --------------------------------------------------------------------------------------- if (SPDLOG_FMT_EXTERNAL) - find_package(fmt REQUIRED) + find_package(fmt REQUIRED CONFIG) message(STATUS "Using external fmt lib version: ${fmt_VERSION}") else () include(cmake/fmtlib.cmake) @@ -105,6 +122,8 @@ find_package(Threads REQUIRED) # --------------------------------------------------------------------------------------- set(SPDLOG_HEADERS "include/spdlog/common.h" + "include/spdlog/file_event_handlers.h" + "include/spdlog/filename_t.h" "include/spdlog/formatter.h" "include/spdlog/fwd.h" "include/spdlog/logger.h" @@ -227,11 +246,22 @@ target_include_directories(spdlog ${SPDLOG_INCLUDES_LEVEL} PUBLIC "$") target_link_libraries(spdlog PUBLIC Threads::Threads) target_link_libraries(spdlog PUBLIC fmt::fmt) +if (MSVC) + if (SPDLOG_MSVC_UTF8) + # Pass /utf-8 only for real MSVC (not clang-cl); see fmtlib and #3260. + target_compile_options(spdlog PUBLIC $<$,$>:/utf-8>) + endif () +endif () spdlog_enable_warnings(spdlog) +if (SPDLOG_SANITIZE_ADDRESS) + spdlog_enable_addr_sanitizer(spdlog) +elseif (SPDLOG_SANITIZE_THREAD) + spdlog_enable_thread_sanitizer(spdlog) +endif () set_target_properties(spdlog PROPERTIES VERSION ${SPDLOG_VERSION} SOVERSION ${SPDLOG_VERSION_MAJOR}.${SPDLOG_VERSION_MINOR}) set(SPDLOG_NAME spdlog-${SPDLOG_VERSION_MAJOR}) -set_target_properties(spdlog PROPERTIES DEBUG_POSTFIX "-${SPDLOG_VERSION_MAJOR}.${SPDLOG_VERSION_MINOR}d") +set_target_properties(spdlog PROPERTIES DEBUG_POSTFIX "${SPDLOG_DEBUG_POSTFIX}") # --------------------------------------------------------------------------------------- # Set prefix and source group for visual studio # --------------------------------------------------------------------------------------- @@ -249,17 +279,22 @@ endif () # --------------------------------------------------------------------------------------- # Private defines according to the options # --------------------------------------------------------------------------------------- +set(SPDLOG_UTF8_TO_WCHAR_CONSOLE ${SPDLOG_WCHAR_CONSOLE}) foreach (SPDLOG_OPTION SPDLOG_CLOCK_COARSE SPDLOG_PREVENT_CHILD_FD SPDLOG_NO_THREAD_ID SPDLOG_DISABLE_GLOBAL_LOGGER SPDLOG_NO_TLS - SPDLOG_FWRITE_UNLOCKED) + SPDLOG_FWRITE_UNLOCKED + SPDLOG_UTF8_TO_WCHAR_CONSOLE) if (${SPDLOG_OPTION}) target_compile_definitions(spdlog PRIVATE ${SPDLOG_OPTION}) endif () endforeach () +if (SPDLOG_NO_TZ_OFFSET) + target_compile_definitions(spdlog PUBLIC SPDLOG_NO_TZ_OFFSET) +endif () # --------------------------------------------------------------------------------------- # Build binaries # --------------------------------------------------------------------------------------- diff --git a/README.md b/README.md index 89c83611c3..15a63c72af 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ see example [CMakeLists.txt](https://github.com/gabime/spdlog/blob/v2.x/example/ ## Features * Very fast (see [benchmarks](#benchmarks) below). * Headers only or compiled -* Feature-rich formatting, using the excellent [fmt](https://github.com/fmtlib/fmt) library. +* Feature-rich formatting, using the excellent [fmt](https://github.com/fmtlib/fmt) library (bundled **12.1.0** by default; use `SPDLOG_FMT_EXTERNAL=ON` for a system **fmt**, **12.x** recommended). * Asynchronous mode (optional) * [Custom](https://github.com/gabime/spdlog/wiki/3.-Custom-formatting) formatting. * Multi/Single threaded loggers. @@ -258,7 +258,7 @@ struct fmt::formatter : fmt::formatter { auto format(my_type my, format_context &ctx) const -> decltype(ctx.out()) { - return format_to(ctx.out(), "[my_type i={}]", my.i); + return fmt::format_to(ctx.out(), "[my_type i={}]", my.i); } }; diff --git a/cmake/fmtlib.cmake b/cmake/fmtlib.cmake index ccca716c8e..f663a0aa41 100644 --- a/cmake/fmtlib.cmake +++ b/cmake/fmtlib.cmake @@ -3,13 +3,25 @@ include(FetchContent) FetchContent_Declare( fmt DOWNLOAD_EXTRACT_TIMESTAMP FALSE - URL https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz - URL_HASH SHA256=ac366b7b4c2e9f0dde63a59b3feb5ee59b67974b14ee5dc9ea8ad78aa2c1ee1e) + URL https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz + URL_HASH SHA256=ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea) FetchContent_GetProperties(fmt) if(NOT fmt_POPULATED) # We do not require os features of fmt set(FMT_OS OFF CACHE BOOL "Disable FMT_OS" FORCE) + # fmt 12+ defaults FMT_INSTALL to OFF when built as a subproject; spdlog's + # install(EXPORT) requires fmt to participate in an export set (CMake 3.23+). + # Only enable fmt's install rules when spdlog installs + if(SPDLOG_INSTALL) + set(FMT_INSTALL ON CACHE BOOL "Generate the install target." FORCE) + else() + set(FMT_INSTALL OFF CACHE BOOL "Generate the install target." FORCE) + endif() FetchContent_MakeAvailable(fmt) set_target_properties(fmt PROPERTIES FOLDER "third-party") + # fmt 12.1.0: MSVC C4834 on locale_ref ctor (discarded [[nodiscard]] from isalpha); fixed on fmt master after 12.1.0. + if (MSVC) + target_compile_options(fmt PRIVATE /wd4834) + endif () endif() diff --git a/cmake/spdlogCPack.cmake b/cmake/spdlogCPack.cmake index d05ea14b85..f21b840cf5 100644 --- a/cmake/spdlogCPack.cmake +++ b/cmake/spdlogCPack.cmake @@ -22,7 +22,7 @@ set(CPACK_DEBIAN_PACKAGE_SECTION "libs") set(CPACK_RPM_PACKAGE_URL ${CPACK_PROJECT_URL}) set(CPACK_DEBIAN_PACKAGE_HOMEPAGE ${CPACK_PROJECT_URL}) set(CPACK_RPM_PACKAGE_DESCRIPTION "Fast C++ logging library.") -set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "FastC++ logging library.") +set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "Fast C++ logging library.") if(CPACK_PACKAGE_NAME) set(CPACK_RPM_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}") diff --git a/cmake/spdlogConfig.cmake.in b/cmake/spdlogConfig.cmake.in index fd21d51e51..47866bb49f 100644 --- a/cmake/spdlogConfig.cmake.in +++ b/cmake/spdlogConfig.cmake.in @@ -7,7 +7,7 @@ include(CMakeFindDependencyMacro) find_package(Threads REQUIRED) -find_dependency(fmt 11 CONFIG) +find_dependency(fmt CONFIG) set(config_targets_file @config_targets_file@) include("${CMAKE_CURRENT_LIST_DIR}/${config_targets_file}") diff --git a/example/example.cpp b/example/example.cpp index 8f645ff019..acd936014d 100644 --- a/example/example.cpp +++ b/example/example.cpp @@ -203,15 +203,15 @@ void multi_sink_example() { // User defined types logging struct my_type { - int i = 0; - explicit my_type(int i) - : i(i) {} + int value_ = 0; + explicit my_type(int value) + : value_(value) {} }; template <> struct fmt::formatter : fmt::formatter { auto format(my_type my, format_context &ctx) const -> decltype(ctx.out()) { - return fmt::format_to(ctx.out(), "[my_type i={}]", my.i); + return fmt::format_to(ctx.out(), "[my_type value={}]", my.value_); } }; diff --git a/include/spdlog/details/circular_q.h b/include/spdlog/details/circular_q.h index f858a48b82..f68291398f 100644 --- a/include/spdlog/details/circular_q.h +++ b/include/spdlog/details/circular_q.h @@ -7,6 +7,8 @@ #include #include +#include + namespace spdlog { namespace details { template diff --git a/include/spdlog/details/mpmc_blocking_q.h b/include/spdlog/details/mpmc_blocking_q.h index f8e5a288da..c2270f6ae7 100644 --- a/include/spdlog/details/mpmc_blocking_q.h +++ b/include/spdlog/details/mpmc_blocking_q.h @@ -5,8 +5,8 @@ // multi producer-multi consumer blocking queue. // enqueue(..) - will block until room found to put the new message. -// enqueue_nowait(..) - will return immediately with false if no room left in -// the queue. +// enqueue_nowait(..) - enqueue immediately; overruns oldest message if no +// room left. // dequeue_for(..) - will block until the queue is not empty or timeout have // passed. diff --git a/include/spdlog/details/null_mutex.h b/include/spdlog/details/null_mutex.h index a0f5045888..e9e1e77092 100644 --- a/include/spdlog/details/null_mutex.h +++ b/include/spdlog/details/null_mutex.h @@ -16,7 +16,7 @@ struct null_mutex { template struct null_atomic { - T value; + T value{}; null_atomic() = default; diff --git a/include/spdlog/details/os.h b/include/spdlog/details/os.h index 346e442d61..e1daa182ba 100644 --- a/include/spdlog/details/os.h +++ b/include/spdlog/details/os.h @@ -36,7 +36,7 @@ SPDLOG_API bool fopen_s(FILE **fp, const filename_t &filename, const filename_t // Return file size according to open FILE* object SPDLOG_API size_t filesize(FILE *f); -// Return utc offset in minutes or throw spdlog_ex on failure +// Return utc offset in minutes (0 on failure to compute offset) SPDLOG_API int utc_minutes_offset(const std::tm &tm = details::os::localtime()); // Return current thread id as size_t diff --git a/include/spdlog/details/tcp_client_unix.h b/include/spdlog/details/tcp_client_unix.h index 945ef2df5f..a3a8f51c53 100644 --- a/include/spdlog/details/tcp_client_unix.h +++ b/include/spdlog/details/tcp_client_unix.h @@ -13,9 +13,13 @@ #include #include #include +#include #include +#include #include +#include +#include #include #include "../common.h" @@ -40,8 +44,67 @@ class tcp_client_unix { ~tcp_client_unix() { close(); } + static int connect_socket_with_timeout(int sockfd, + const struct sockaddr *addr, + socklen_t addrlen, + const struct timeval &tv) { + if (tv.tv_sec == 0 && tv.tv_usec == 0) { + int rv = ::connect(sockfd, addr, addrlen); + if (rv < 0 && errno == EISCONN) { + return 0; + } + return rv; + } + + int orig_flags = ::fcntl(sockfd, F_GETFL, 0); + if (orig_flags < 0) { + return -1; + } + if (::fcntl(sockfd, F_SETFL, orig_flags | O_NONBLOCK) < 0) { + return -1; + } + + int rv = ::connect(sockfd, addr, addrlen); + if (rv == 0 || (rv < 0 && errno == EISCONN)) { + ::fcntl(sockfd, F_SETFL, orig_flags); + return 0; + } + if (errno != EINPROGRESS) { + ::fcntl(sockfd, F_SETFL, orig_flags); + return -1; + } + + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sockfd, &wfds); + + struct timeval tv_copy = tv; + rv = ::select(sockfd + 1, nullptr, &wfds, nullptr, &tv_copy); + if (rv <= 0) { + ::fcntl(sockfd, F_SETFL, orig_flags); + if (rv == 0) { + errno = ETIMEDOUT; + } + return -1; + } + + int so_error = 0; + socklen_t len = sizeof(so_error); + if (::getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_error, &len) < 0) { + ::fcntl(sockfd, F_SETFL, orig_flags); + return -1; + } + ::fcntl(sockfd, F_SETFL, orig_flags); + if (so_error != 0 && so_error != EISCONN) { + errno = so_error; + return -1; + } + + return 0; + } + // try to connect or throw on failure - void connect(const std::string &host, int port) { + void connect(const std::string &host, int port, int timeout_ms = 0) { close(); struct addrinfo hints {}; memset(&hints, 0, sizeof(struct addrinfo)); @@ -50,6 +113,11 @@ class tcp_client_unix { hints.ai_flags = AI_NUMERICSERV; // port passed as as numeric value hints.ai_protocol = 0; + const int validated_timeout_ms = timeout_ms > 0 ? timeout_ms : 0; + struct timeval tv; + tv.tv_sec = validated_timeout_ms / 1000; + tv.tv_usec = (validated_timeout_ms % 1000) * 1000; + auto port_str = std::to_string(port); struct addrinfo *addrinfo_result; auto rv = ::getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrinfo_result); @@ -70,8 +138,11 @@ class tcp_client_unix { last_errno = errno; continue; } - rv = ::connect(socket_, rp->ai_addr, rp->ai_addrlen); - if (rv == 0) { +#ifndef SOCK_CLOEXEC + ::fcntl(socket_, F_SETFD, FD_CLOEXEC); +#endif + if (connect_socket_with_timeout(socket_, rp->ai_addr, rp->ai_addrlen, tv) == 0) { + last_errno = 0; break; } last_errno = errno; @@ -83,6 +154,11 @@ class tcp_client_unix { throw_spdlog_ex("::connect failed", last_errno); } + if (validated_timeout_ms > 0) { + ::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + ::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + } + // set TCP_NODELAY int enable_flag = 1; ::setsockopt(socket_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enable_flag), sizeof(enable_flag)); diff --git a/include/spdlog/details/tcp_client_windows.h b/include/spdlog/details/tcp_client_windows.h index c626e473ea..135b8c063e 100644 --- a/include/spdlog/details/tcp_client_windows.h +++ b/include/spdlog/details/tcp_client_windows.h @@ -58,8 +58,76 @@ class tcp_client_unix { SOCKET fd() const { return socket_; } + int connect_socket_with_timeout(SOCKET sockfd, + const struct sockaddr *addr, + int addrlen, + const timeval &tv) { + if (tv.tv_sec == 0 && tv.tv_usec == 0) { + int rv = ::connect(sockfd, addr, addrlen); + if (rv == SOCKET_ERROR && WSAGetLastError() == WSAEISCONN) { + return 0; + } + return rv; + } + + u_long mode = 1UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) { + return SOCKET_ERROR; + } + + int rv = ::connect(sockfd, addr, addrlen); + int last_error = WSAGetLastError(); + if (rv == 0 || last_error == WSAEISCONN) { + mode = 0UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) { + return SOCKET_ERROR; + } + return 0; + } + if (last_error != WSAEWOULDBLOCK) { + mode = 0UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode)) { + return SOCKET_ERROR; + } + return SOCKET_ERROR; + } + + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sockfd, &wfds); + + timeval tv_copy = tv; + rv = ::select(0, nullptr, &wfds, nullptr, &tv_copy); + + mode = 0UL; + if (::ioctlsocket(sockfd, FIONBIO, &mode) == SOCKET_ERROR) { + return SOCKET_ERROR; + } + + if (rv == 0) { + WSASetLastError(WSAETIMEDOUT); + return SOCKET_ERROR; + } + if (rv == SOCKET_ERROR) { + return SOCKET_ERROR; + } + + int so_error = 0; + int len = sizeof(so_error); + if (::getsockopt(sockfd, SOL_SOCKET, SO_ERROR, reinterpret_cast(&so_error), &len) == + SOCKET_ERROR) { + return SOCKET_ERROR; + } + if (so_error != 0 && so_error != WSAEISCONN) { + WSASetLastError(so_error); + return SOCKET_ERROR; + } + + return 0; + } + // try to connect or throw on failure - void connect(const std::string &host, int port) { + void connect(const std::string &host, int port, int timeout_ms = 0) { if (is_connected()) { close(); } @@ -71,13 +139,17 @@ class tcp_client_unix { hints.ai_flags = AI_NUMERICSERV; // port passed as as numeric value hints.ai_protocol = 0; + const int validated_timeout_ms = timeout_ms > 0 ? timeout_ms : 0; + timeval tv; + tv.tv_sec = validated_timeout_ms / 1000; + tv.tv_usec = (validated_timeout_ms % 1000) * 1000; + auto port_str = std::to_string(port); struct addrinfo *addrinfo_result; auto rv = ::getaddrinfo(host.c_str(), port_str.c_str(), &hints, &addrinfo_result); int last_error = 0; if (rv != 0) { last_error = ::WSAGetLastError(); - WSACleanup(); throw_winsock_error_("getaddrinfo failed", last_error); } @@ -87,21 +159,25 @@ class tcp_client_unix { socket_ = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (socket_ == INVALID_SOCKET) { last_error = ::WSAGetLastError(); - WSACleanup(); continue; } - if (::connect(socket_, rp->ai_addr, (int)rp->ai_addrlen) == 0) { + if (connect_socket_with_timeout(socket_, rp->ai_addr, (int)rp->ai_addrlen, tv) == 0) { + last_error = 0; break; - } else { - last_error = ::WSAGetLastError(); - close(); } + last_error = WSAGetLastError(); + ::closesocket(socket_); + socket_ = INVALID_SOCKET; } ::freeaddrinfo(addrinfo_result); if (socket_ == INVALID_SOCKET) { - WSACleanup(); throw_winsock_error_("connect failed", last_error); } + if (validated_timeout_ms > 0) { + DWORD timeout_dword = static_cast(validated_timeout_ms); + ::setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout_dword, sizeof(timeout_dword)); + ::setsockopt(socket_, SOL_SOCKET, SO_SNDTIMEO, (const char *)&timeout_dword, sizeof(timeout_dword)); + } // set TCP_NODELAY int enable_flag = 1; diff --git a/include/spdlog/details/udp_client_unix.h b/include/spdlog/details/udp_client_unix.h index 1b46da4a67..3185fb34df 100644 --- a/include/spdlog/details/udp_client_unix.h +++ b/include/spdlog/details/udp_client_unix.h @@ -70,9 +70,9 @@ class udp_client_unix { // Send exactly n_bytes of the given data. // On error close the connection and throw. void send(const char *data, size_t n_bytes) { - ssize_t toslen = 0; - socklen_t tolen = sizeof(struct sockaddr); - if ((toslen = ::sendto(socket_, data, n_bytes, 0, (struct sockaddr *)&sockAddr_, tolen)) == -1) { + socklen_t tolen = sizeof(sockAddr_); + if (::sendto(socket_, data, n_bytes, 0, reinterpret_cast(&sockAddr_), + tolen) == -1) { throw_spdlog_ex("sendto(2) failed", errno); } } diff --git a/include/spdlog/logger.h b/include/spdlog/logger.h index ae025df060..8f71f5d6fe 100644 --- a/include/spdlog/logger.h +++ b/include/spdlog/logger.h @@ -123,7 +123,7 @@ class SPDLOG_API logger { // return true if the given message should be flushed [[nodiscard]] bool should_flush(const details::log_msg &msg) const noexcept { - return (msg.log_level >= flush_level_.load(std::memory_order_relaxed)) && (msg.log_level != level::off); + return (msg.log_level >= flush_level()) && (msg.log_level != level::off); } // set the level of logging diff --git a/include/spdlog/sinks/ansicolor_sink.h b/include/spdlog/sinks/ansicolor_sink.h index 418c068b43..33b71c659a 100644 --- a/include/spdlog/sinks/ansicolor_sink.h +++ b/include/spdlog/sinks/ansicolor_sink.h @@ -69,8 +69,10 @@ class ansicolor_sink : public base_sink { static constexpr std::string_view red_bold = "\033[31m\033[1m"; static constexpr std::string_view bold_on_red = "\033[1m\033[41m"; -private: +protected: FILE *target_file_; + +private: bool should_do_colors_; std::array colors_; diff --git a/include/spdlog/sinks/basic_file_sink.h b/include/spdlog/sinks/basic_file_sink.h index 3377061a8c..fe68af3d96 100644 --- a/include/spdlog/sinks/basic_file_sink.h +++ b/include/spdlog/sinks/basic_file_sink.h @@ -20,6 +20,7 @@ class basic_file_sink final : public base_sink { public: explicit basic_file_sink(const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {}); const filename_t &filename() const; + void truncate(); protected: void sink_it_(const details::log_msg &msg) override; diff --git a/include/spdlog/sinks/daily_file_sink.h b/include/spdlog/sinks/daily_file_sink.h index 481321e3a5..bb822c501a 100644 --- a/include/spdlog/sinks/daily_file_sink.h +++ b/include/spdlog/sinks/daily_file_sink.h @@ -55,7 +55,6 @@ struct daily_filename_format_calculator { * Rotating file sink based on date. * If truncate != false , the created file will be truncated. * If max_files > 0, retain only the last max_files and delete previous. - * If max_files > 0, retain only the last max_files and delete previous. * Note that old log files from previous executions will not be deleted by this class, * rotation and deletion is only applied while the program is running. */ @@ -81,8 +80,8 @@ class daily_file_sink final : public base_sink { } auto now = log_clock::now(); - auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); - file_helper_.open(filename, truncate_); + const auto new_filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); + file_helper_.open(new_filename, truncate_); rotation_tp_ = next_rotation_tp_(); if (max_files_ > 0) { @@ -100,8 +99,8 @@ class daily_file_sink final : public base_sink { auto time = msg.time; bool should_rotate = time >= rotation_tp_; if (should_rotate) { - auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(time)); - file_helper_.open(filename, truncate_); + const auto new_filename = FileNameCalc::calc_filename(base_filename_, now_tm(time)); + file_helper_.open(new_filename, truncate_); rotation_tp_ = next_rotation_tp_(); } memory_buf_t formatted; @@ -124,11 +123,11 @@ class daily_file_sink final : public base_sink { std::vector filenames; auto now = log_clock::now(); while (filenames.size() < max_files_) { - auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); - if (!path_exists(filename)) { + const auto new_filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); + if (!path_exists(new_filename)) { break; } - filenames.emplace_back(filename); + filenames.emplace_back(new_filename); now -= std::chrono::hours(24); } for (auto iter = filenames.rbegin(); iter != filenames.rend(); ++iter) { diff --git a/include/spdlog/sinks/dup_filter_sink.h b/include/spdlog/sinks/dup_filter_sink.h index f0530d578b..70f8ecb121 100644 --- a/include/spdlog/sinks/dup_filter_sink.h +++ b/include/spdlog/sinks/dup_filter_sink.h @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include #include "../details/log_msg.h" #include "../details/null_mutex.h" @@ -20,8 +22,8 @@ // #include "spdlog/sinks/dup_filter_sink.h" // // int main() { -// auto dup_filter = std::make_shared(std::chrono::seconds(5), -// level::info); dup_filter->add_sink(std::make_shared()); +// auto dup_filter = std::make_shared(std::chrono::seconds(5)); +// dup_filter->add_sink(std::make_shared()); // spdlog::logger l("logger", dup_filter); // l.info("Hello"); // l.info("Hello"); @@ -40,21 +42,39 @@ template class dup_filter_sink final : public dist_sink { public: template - explicit dup_filter_sink(std::chrono::duration max_skip_duration, level notification_level = level::info) + explicit dup_filter_sink(std::chrono::duration max_skip_duration) + : max_skip_duration_{max_skip_duration} {} + + // Optional: force the "Skipped N duplicate..." line to a fixed level. + template + explicit dup_filter_sink(std::chrono::duration max_skip_duration, + level notification_level) : max_skip_duration_{max_skip_duration}, - log_level_{notification_level} {} + use_fixed_notification_summary_level_{true}, + fixed_notification_summary_level_{notification_level} {} + + template + explicit dup_filter_sink(std::chrono::duration max_skip_duration, + std::vector> sinks) + : dist_sink(std::move(sinks)), + max_skip_duration_{max_skip_duration} {} protected: std::chrono::microseconds max_skip_duration_; log_clock::time_point last_msg_time_; std::string last_msg_payload_; size_t skip_counter_ = 0; - level log_level_; + level skipped_msg_log_level_{level::off}; + bool use_fixed_notification_summary_level_{false}; + level fixed_notification_summary_level_{level::info}; void sink_it_(const details::log_msg &msg) override { bool filtered = filter_(msg); if (!filtered) { skip_counter_ += 1; + if (!use_fixed_notification_summary_level_) { + skipped_msg_log_level_ = msg.log_level; + } return; } @@ -63,7 +83,10 @@ class dup_filter_sink final : public dist_sink { char buf[64]; auto msg_size = ::snprintf(buf, sizeof(buf), "Skipped %u duplicate messages..", static_cast(skip_counter_)); if (msg_size > 0 && static_cast(msg_size) < sizeof(buf)) { - details::log_msg skipped_msg{msg.source, msg.logger_name, log_level_, + const level summary_level = use_fixed_notification_summary_level_ + ? fixed_notification_summary_level_ + : skipped_msg_log_level_; + details::log_msg skipped_msg{msg.source, msg.logger_name, summary_level, string_view_t{buf, static_cast(msg_size)}}; dist_sink::sink_it_(skipped_msg); } @@ -77,8 +100,8 @@ class dup_filter_sink final : public dist_sink { } // return whether the log msg should be displayed (true) or skipped (false) - bool filter_(const details::log_msg &msg) { - auto filter_duration = msg.time - last_msg_time_; + bool filter_(const details::log_msg &msg) const { + const auto filter_duration = msg.time - last_msg_time_; return (filter_duration > max_skip_duration_) || (msg.payload != last_msg_payload_); } }; diff --git a/include/spdlog/sinks/qt_sinks.h b/include/spdlog/sinks/qt_sinks.h index 303e95098e..8b9d36da9f 100644 --- a/include/spdlog/sinks/qt_sinks.h +++ b/include/spdlog/sinks/qt_sinks.h @@ -154,8 +154,10 @@ class qt_color_sink : public base_sink { payload = QString::fromUtf8(str.data(), static_cast(str.size())); // convert color ranges from byte index to character index. if (msg.color_range_start < msg.color_range_end) { - color_range_start = QString::fromUtf8(str.data(), msg.color_range_start).size(); - color_range_end = QString::fromUtf8(str.data(), msg.color_range_end).size(); + color_range_start = + QString::fromUtf8(str.data(), static_cast(msg.color_range_start)).size(); + color_range_end = + QString::fromUtf8(str.data(), static_cast(msg.color_range_end)).size(); } } else { payload = QString::fromLatin1(str.data(), static_cast(str.size())); @@ -165,7 +167,7 @@ class qt_color_sink : public base_sink { qt_text_edit_, // text edit to append to std::move(payload), // text to append default_color_, // default color - colors_.at(msg.log_level), // color to apply + colors_.at(static_cast(msg.log_level)), // color to apply color_range_start, // color range start color_range_end}; // color range end diff --git a/include/spdlog/sinks/ringbuffer_sink.h b/include/spdlog/sinks/ringbuffer_sink.h index b6964190ed..d0bbc1f61e 100644 --- a/include/spdlog/sinks/ringbuffer_sink.h +++ b/include/spdlog/sinks/ringbuffer_sink.h @@ -24,7 +24,11 @@ template class ringbuffer_sink final : public base_sink { public: explicit ringbuffer_sink(size_t n_items) - : q_{n_items} {} + : q_{n_items} { + if (n_items == 0) { + throw_spdlog_ex("ringbuffer_sink: n_items cannot be zero"); + } + } void drain_raw(std::function callback) { std::lock_guard lock(base_sink::mutex_); diff --git a/include/spdlog/sinks/tcp_sink.h b/include/spdlog/sinks/tcp_sink.h index be7bcc3be3..0ddf2e0e7a 100644 --- a/include/spdlog/sinks/tcp_sink.h +++ b/include/spdlog/sinks/tcp_sink.h @@ -17,8 +17,6 @@ #include #include -#pragma once - // Simple tcp client sink // Connects to remote address and send the formatted log. // Will attempt to reconnect if connection drops. @@ -31,6 +29,7 @@ namespace sinks { struct tcp_sink_config { std::string server_host; int server_port; + int timeout_ms = 0; // timeout for connect, send, and recv (milliseconds) bool lazy_connect = false; // if true connect on first log call instead of on construction tcp_sink_config(std::string host, int port) @@ -44,10 +43,22 @@ class tcp_sink final : public spdlog::sinks::base_sink { // connect to tcp host/port or throw if failed // host can be hostname or ip address + explicit tcp_sink(const std::string &host, + int port, + int timeout_ms = 0, + bool lazy_connect = false) + : config_{host, port} { + config_.timeout_ms = timeout_ms; + config_.lazy_connect = lazy_connect; + if (!config_.lazy_connect) { + client_.connect(config_.server_host, config_.server_port, config_.timeout_ms); + } + } + explicit tcp_sink(tcp_sink_config sink_config) : config_{std::move(sink_config)} { if (!config_.lazy_connect) { - this->client_.connect(config_.server_host, config_.server_port); + client_.connect(config_.server_host, config_.server_port, config_.timeout_ms); } } @@ -58,7 +69,7 @@ class tcp_sink final : public spdlog::sinks::base_sink { spdlog::memory_buf_t formatted; spdlog::sinks::base_sink::formatter_->format(msg, formatted); if (!client_.is_connected()) { - client_.connect(config_.server_host, config_.server_port); + client_.connect(config_.server_host, config_.server_port, config_.timeout_ms); } client_.send(formatted.data(), formatted.size()); } diff --git a/include/spdlog/sinks/udp_sink.h b/include/spdlog/sinks/udp_sink.h index 0f0951daaf..c70d67e873 100644 --- a/include/spdlog/sinks/udp_sink.h +++ b/include/spdlog/sinks/udp_sink.h @@ -36,7 +36,7 @@ template class udp_sink final : public base_sink { public: // host can be hostname or ip address - explicit udp_sink(udp_sink_config sink_config) + explicit udp_sink(const udp_sink_config &sink_config) : client_{sink_config.server_host, sink_config.server_port} {} ~udp_sink() override = default; diff --git a/include/spdlog/spdlog.h b/include/spdlog/spdlog.h index 45527eb490..4854e16e67 100644 --- a/include/spdlog/spdlog.h +++ b/include/spdlog/spdlog.h @@ -40,7 +40,7 @@ SPDLOG_API level get_level(); SPDLOG_API void set_level(level level); // Determine whether the global logger should log messages with a certain level -SPDLOG_API bool should_log(level level); +SPDLOG_API bool should_log(level log_level); // Set flush level of the global logger. SPDLOG_API void flush_on(level level); diff --git a/src/common.cpp b/src/common.cpp index 82816cc721..1840f79702 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -4,19 +4,41 @@ #include "spdlog/common.h" #include +#include #include namespace spdlog { +namespace { +bool iequals(const std::string &a, const std::string &b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), [](char ac, char bc) { + return std::tolower(static_cast(ac)) == + std::tolower(static_cast(bc)); + }); +} +} // namespace + spdlog::level level_from_str(const std::string &name) noexcept { - const auto it = std::find(std::begin(level_string_views), std::end(level_string_views), name); - if (it != std::end(level_string_views)) return static_cast(std::distance(std::begin(level_string_views), it)); + const auto it = + std::find_if(std::begin(level_string_views), std::end(level_string_views), + [&name](const string_view_t &level_name) { + return level_name.size() == name.size() && + std::equal(name.begin(), name.end(), level_name.begin(), + [](char a, char b) { + return std::tolower(static_cast(a)) == + std::tolower(static_cast(b)); + }); + }); + if (it != std::end(level_string_views)) { + return static_cast(std::distance(std::begin(level_string_views), it)); + } // check also for "warn" and "err" before giving up - if (name == "warn") { + if (iequals(name, "warn")) { return spdlog::level::warn; } - if (name == "err") { + if (iequals(name, "err")) { return level::err; } return level::off; diff --git a/src/details/os_unix.cpp b/src/details/os_unix.cpp index 5c5828f333..db73e0b261 100644 --- a/src/details/os_unix.cpp +++ b/src/details/os_unix.cpp @@ -127,8 +127,12 @@ size_t filesize(FILE *f) { return 0; // will not be reached. } -// Return utc offset in minutes or throw spdlog_ex on failure +// Return utc offset in minutes (0 on failure to compute offset) int utc_minutes_offset(const std::tm &tm) { +#if defined(SPDLOG_NO_TZ_OFFSET) + (void)tm; + return 0; +#else #if defined(sun) || defined(__sun) || defined(_AIX) || \ (defined(__NEWLIB__) && !defined(__TM_GMTOFF)) || \ (!defined(__APPLE__) && !defined(_BSD_SOURCE) && !defined(_GNU_SOURCE) && \ @@ -165,6 +169,7 @@ int utc_minutes_offset(const std::tm &tm) { auto offset_seconds = tm.tm_gmtoff; #endif return static_cast(offset_seconds / 60); +#endif // SPDLOG_NO_TZ_OFFSET } // Return current thread id as size_t @@ -269,8 +274,8 @@ bool is_color_terminal() noexcept { bool in_terminal(FILE *file) noexcept { return ::isatty(fileno(file)) != 0; } std::string getenv(const char *field) { - char *buf = ::getenv(field); - return buf != nullptr ? buf : std::string{}; + char *buf = std::getenv(field); + return buf != nullptr ? std::string(buf) : std::string{}; } // Do fsync by FILE pointer diff --git a/src/details/os_windows.cpp b/src/details/os_windows.cpp index e9ea0b15ec..2cb06e946f 100644 --- a/src/details/os_windows.cpp +++ b/src/details/os_windows.cpp @@ -6,10 +6,9 @@ #endif // clang-format off -#include "spdlog/details/windows_include.h" // must be included before fileapi.h etc. +#include "spdlog/details/windows_include.h" // must be first; provides FlushFileBuffers via Windows headers // clang-format on -#include // for FlushFileBuffers #include // for _get_osfhandle, _isatty, _fileno #include // for _get_pid #include @@ -107,24 +106,27 @@ size_t filesize(FILE *f) { #pragma warning(pop) #endif -// Return utc offset in minutes or throw spdlog_ex on failure +// Compare the timestamp as local (mktime) vs UTC (_mkgmtime) to get the offset. +// Matches v1.x behavior: better historical DST handling than GetTimeZoneInformation alone. int utc_minutes_offset(const std::tm &tm) { -#if _WIN32_WINNT < _WIN32_WINNT_WS08 - TIME_ZONE_INFORMATION tzinfo; - auto rv = ::GetTimeZoneInformation(&tzinfo); +#if defined(SPDLOG_NO_TZ_OFFSET) + (void)tm; + return 0; #else - DYNAMIC_TIME_ZONE_INFORMATION tzinfo; - auto rv = ::GetDynamicTimeZoneInformation(&tzinfo); -#endif - if (rv == TIME_ZONE_ID_INVALID) throw_spdlog_ex("Failed getting timezone info. ", errno); + std::tm local_tm = tm; // copy since mktime might adjust it (normalize dates, set tm_isdst) + std::time_t local_time_t = std::mktime(&local_tm); + if (local_time_t == static_cast(-1)) { + return 0; // fallback + } - int offset = -tzinfo.Bias; - if (tm.tm_isdst) { - offset -= tzinfo.DaylightBias; - } else { - offset -= tzinfo.StandardBias; + std::time_t utc_time_t = _mkgmtime(&local_tm); + if (utc_time_t == static_cast(-1)) { + return 0; // fallback } - return offset; + + const auto offset_seconds = utc_time_t - local_time_t; + return static_cast(offset_seconds / 60); +#endif } // Return current thread id as size_t @@ -212,7 +214,7 @@ void utf8_to_wstrbuf(string_view_t str, wmemory_buf_t &target) { target.resize(result_size); result_size = ::MultiByteToWideChar(CP_UTF8, 0, str.data(), str_size, target.data(), result_size); if (result_size > 0) { - assert(result_size == target.size()); + assert(result_size == static_cast(target.size())); return; } } @@ -220,21 +222,22 @@ void utf8_to_wstrbuf(string_view_t str, wmemory_buf_t &target) { throw_spdlog_ex(fmt_lib::format("MultiByteToWideChar failed. Last error: {}", ::GetLastError())); } +#ifdef _MSC_VER + #pragma warning(push) + #pragma warning(disable : 4996) +#endif std::string getenv(const char *field) { -#if defined(_MSC_VER) - #if defined(__cplusplus_winrt) - return std::string{}; // not supported under uwp - #else - size_t len = 0; - char buf[128]; - bool ok = ::getenv_s(&len, buf, sizeof(buf), field) == 0; - return ok ? buf : std::string{}; - #endif -#else // revert to getenv - char *buf = ::getenv(field); - return buf != nullptr ? buf : std::string{}; +#if defined(_MSC_VER) && defined(WINAPI_FAMILY) && defined(WINAPI_FAMILY_DESKTOP_APP) && \ + (WINAPI_FAMILY != WINAPI_FAMILY_DESKTOP_APP) + return std::string{}; // not supported on UWP / non-desktop WinRT targets (#3489) +#else + char *buf = std::getenv(field); + return buf != nullptr ? std::string(buf) : std::string{}; #endif } +#ifdef _MSC_VER + #pragma warning(pop) +#endif // Do fsync by FILE handlerpointer // Return true on success diff --git a/src/pattern_formatter.cpp b/src/pattern_formatter.cpp index 79c8c62bcb..f4ff0a3e91 100644 --- a/src/pattern_formatter.cpp +++ b/src/pattern_formatter.cpp @@ -496,10 +496,10 @@ class T_formatter final : public flag_formatter { template class z_formatter final : public flag_formatter { public: - explicit z_formatter(padding_info padinfo) - : flag_formatter(padinfo) {} + explicit z_formatter(padding_info padinfo, pattern_time_type time_type) + : flag_formatter(padinfo), + time_type_(time_type) {} - z_formatter() = default; ~z_formatter() override = default; z_formatter(const z_formatter &) = delete; z_formatter &operator=(const z_formatter &) = delete; @@ -509,6 +509,17 @@ class z_formatter final : public flag_formatter { void format(const details::log_msg &msg, const std::tm &tm_time, memory_buf_t &dest) override { constexpr size_t field_size = 6; ScopedPadder p(field_size, padinfo_, dest); + +#ifdef SPDLOG_NO_TZ_OFFSET + const char *const placeholder = "+??:??"; + dest.append(placeholder, placeholder + 6); +#else + if (time_type_ == pattern_time_type::utc) { + const char *zeroes = "+00:00"; + dest.append(zeroes, zeroes + 6); + return; + } + auto total_minutes = get_cached_offset(msg, tm_time); if (total_minutes < 0) { total_minutes = -total_minutes; @@ -519,9 +530,11 @@ class z_formatter final : public flag_formatter { fmt_helper::pad2(total_minutes / 60, dest); // hours dest.push_back(':'); fmt_helper::pad2(total_minutes % 60, dest); // minutes +#endif } private: + pattern_time_type time_type_; log_clock::time_point last_update_{std::chrono::seconds(0)}; int offset_minutes_{0}; @@ -1051,7 +1064,8 @@ void pattern_formatter::handle_flag_(char flag, details::padding_info padding) { break; case ('z'): // timezone - formatters_.push_back(std::make_unique>(padding)); + formatters_.push_back( + std::make_unique>(padding, pattern_time_type_)); need_localtime_ = true; break; diff --git a/src/sinks/ansicolor_sink.cpp b/src/sinks/ansicolor_sink.cpp index b684ea0fda..2a4aa06b2a 100644 --- a/src/sinks/ansicolor_sink.cpp +++ b/src/sinks/ansicolor_sink.cpp @@ -43,7 +43,6 @@ void ansicolor_sink::set_color_mode(color_mode mode) { template void ansicolor_sink::set_color_mode_(color_mode mode) { - std::lock_guard lock(base_sink::mutex_); switch (mode) { case color_mode::always: should_do_colors_ = true; diff --git a/src/sinks/basic_file_sink.cpp b/src/sinks/basic_file_sink.cpp index d324427fc1..9443ba8662 100644 --- a/src/sinks/basic_file_sink.cpp +++ b/src/sinks/basic_file_sink.cpp @@ -21,6 +21,12 @@ const filename_t &basic_file_sink::filename() const { return file_helper_.filename(); } +template +void basic_file_sink::truncate() { + std::lock_guard lock(base_sink::mutex_); + file_helper_.reopen(true); +} + template void basic_file_sink::sink_it_(const details::log_msg &msg) { memory_buf_t formatted; diff --git a/src/sinks/rotating_file_sink.cpp b/src/sinks/rotating_file_sink.cpp index b43c462862..d652183cd3 100644 --- a/src/sinks/rotating_file_sink.cpp +++ b/src/sinks/rotating_file_sink.cpp @@ -65,6 +65,7 @@ filename_t rotating_file_sink::filename() { template void rotating_file_sink::rotate_now() { + std::lock_guard lock(base_sink::mutex_); rotate_(); } diff --git a/src/sinks/wincolor_sink.cpp b/src/sinks/wincolor_sink.cpp index 20c0a4ed38..bc6b137a45 100644 --- a/src/sinks/wincolor_sink.cpp +++ b/src/sinks/wincolor_sink.cpp @@ -10,6 +10,7 @@ #include "spdlog/sinks/wincolor_sink.h" #include "spdlog/common.h" +#include "spdlog/details/os.h" namespace spdlog { namespace sinks { @@ -108,8 +109,15 @@ std::uint16_t wincolor_sink::set_foreground_color_(std::uint16_t attribs) template void wincolor_sink::print_range_(const memory_buf_t &formatted, size_t start, size_t end) { if (end > start) { +#if defined(SPDLOG_UTF8_TO_WCHAR_CONSOLE) + wmemory_buf_t wformatted; + details::os::utf8_to_wstrbuf(string_view_t(formatted.data() + start, end - start), wformatted); + auto size = static_cast(wformatted.size()); + auto ignored = ::WriteConsoleW(static_cast(out_handle_), wformatted.data(), size, nullptr, nullptr); +#else auto size = static_cast(end - start); auto ignored = ::WriteConsoleA(static_cast(out_handle_), formatted.data() + start, size, nullptr, nullptr); +#endif (void)(ignored); } } diff --git a/src/spdlog.cpp b/src/spdlog.cpp index 415cecaddb..f749dfb92c 100644 --- a/src/spdlog.cpp +++ b/src/spdlog.cpp @@ -33,7 +33,7 @@ void set_pattern(std::string pattern, pattern_time_type time_type) { level get_level() { return global_logger()->log_level(); } -bool should_log(level level) { return global_logger()->should_log(level); } +bool should_log(level log_level) { return global_logger()->should_log(log_level); } void set_level(level level) { global_logger()->set_level(level); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index acd02d024b..b3ff2f1c3c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -52,6 +52,7 @@ set(SPDLOG_UTESTS_SOURCES test_log_level.cpp test_include_sinks.cpp test_bin_to_hex.cpp + test_timezone.cpp test_errors.cpp) if(WIN32) diff --git a/tests/includes.h b/tests/includes.h index e2fb6aaddb..da66648f2d 100644 --- a/tests/includes.h +++ b/tests/includes.h @@ -25,6 +25,9 @@ #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG +#undef SPDLOG_LEVEL_NAMES +#undef SPDLOG_SHORT_LEVEL_NAMES + #include "spdlog/details/fmt_helper.h" #include "spdlog/pattern_formatter.h" #include "spdlog/sinks/null_sink.h" diff --git a/tests/test_custom_callbacks.cpp b/tests/test_custom_callbacks.cpp index cfae2119ce..7a095b3749 100644 --- a/tests/test_custom_callbacks.cpp +++ b/tests/test_custom_callbacks.cpp @@ -3,6 +3,9 @@ * https://raw.githubusercontent.com/gabime/spdlog/v2.x/LICENSE */ #include "includes.h" + +#include + #include "spdlog/common.h" #include "spdlog/sinks/callback_sink.h" #include "test_sink.h" @@ -14,7 +17,8 @@ TEST_CASE("custom_callback_logger", "[custom_callback_logger]") { spdlog::memory_buf_t formatted; formatter.format(msg, formatted); auto eol_len = strlen(spdlog::details::os::default_eol); - lines.emplace_back(formatted.begin(), formatted.end() - eol_len); + using diff_t = typename std::iterator_traits::difference_type; + lines.emplace_back(formatted.begin(), formatted.end() - static_cast(eol_len)); }); std::shared_ptr test_sink(new spdlog::sinks::test_sink_st); diff --git a/tests/test_dup_filter.cpp b/tests/test_dup_filter.cpp index 23e5f96fae..172fabc9c4 100644 --- a/tests/test_dup_filter.cpp +++ b/tests/test_dup_filter.cpp @@ -80,3 +80,41 @@ TEST_CASE("dup_filter_test5", "[dup_filter_sink]") { REQUIRE(test_sink->msg_counter() == 3); // skip 2 messages but log the "skipped.." message before message2 REQUIRE(test_sink->lines()[1] == "Skipped 2 duplicate messages.."); } + +TEST_CASE("dup_filter_skipped_notification_uses_last_duplicate_level", "[dup_filter_sink]") { + using spdlog::sinks::dup_filter_sink_st; + using spdlog::sinks::test_sink_mt; + + dup_filter_sink_st dup_sink{std::chrono::seconds{5}}; + auto test_sink = std::make_shared(); + test_sink->set_pattern("%L"); + dup_sink.add_sink(test_sink); + + dup_sink.log(spdlog::details::log_msg{"test", spdlog::level::warn, "same"}); + dup_sink.log(spdlog::details::log_msg{"test", spdlog::level::warn, "same"}); + dup_sink.log(spdlog::details::log_msg{"test", spdlog::level::info, "diff"}); + + REQUIRE(test_sink->lines().size() == 3); + REQUIRE(test_sink->lines()[0] == "W"); + REQUIRE(test_sink->lines()[1] == "W"); + REQUIRE(test_sink->lines()[2] == "I"); +} + +TEST_CASE("dup_filter_skipped_notification_fixed_level", "[dup_filter_sink]") { + using spdlog::sinks::dup_filter_sink_st; + using spdlog::sinks::test_sink_mt; + + dup_filter_sink_st dup_sink{std::chrono::seconds{5}, spdlog::level::info}; + auto test_sink = std::make_shared(); + test_sink->set_pattern("%L"); + dup_sink.add_sink(test_sink); + + dup_sink.log(spdlog::details::log_msg{"test", spdlog::level::warn, "same"}); + dup_sink.log(spdlog::details::log_msg{"test", spdlog::level::warn, "same"}); + dup_sink.log(spdlog::details::log_msg{"test", spdlog::level::info, "diff"}); + + REQUIRE(test_sink->lines().size() == 3); + REQUIRE(test_sink->lines()[0] == "W"); + REQUIRE(test_sink->lines()[1] == "I"); + REQUIRE(test_sink->lines()[2] == "I"); +} diff --git a/tests/test_file_logging.cpp b/tests/test_file_logging.cpp index 56c7650ebb..2fcaa852f2 100644 --- a/tests/test_file_logging.cpp +++ b/tests/test_file_logging.cpp @@ -41,6 +41,26 @@ TEST_CASE("flush_on", "[flush_on]") { default_eol, default_eol, default_eol)); } +TEST_CASE("basic_file_sink_truncate", "[truncate]") { + prepare_logdir(); + const spdlog::filename_t filename = SPDLOG_FILENAME_T(SIMPLE_LOG); + const bool truncate = true; + const auto sink = std::make_shared(filename, truncate); + const auto logger = std::make_shared("simple_file_logger", sink); + + logger->info("Test message {}", 3.14); + logger->info("Test message {}", 2.71); + logger->flush(); + REQUIRE(count_lines(SIMPLE_LOG) == 2); + + sink->truncate(); + REQUIRE(count_lines(SIMPLE_LOG) == 0); + + logger->info("Test message {}", 6.28); + logger->flush(); + REQUIRE(count_lines(SIMPLE_LOG) == 1); +} + TEST_CASE("rotating_file_logger1", "[rotating_logger]") { prepare_logdir(); size_t max_size = 1024 * 10; diff --git a/tests/test_misc.cpp b/tests/test_misc.cpp index d73be2a52b..734224ec11 100644 --- a/tests/test_misc.cpp +++ b/tests/test_misc.cpp @@ -72,6 +72,17 @@ TEST_CASE("to_level_enum", "[convert_to_level_enum]") { REQUIRE(spdlog::level_from_str("critical") == spdlog::level::critical); REQUIRE(spdlog::level_from_str("off") == spdlog::level::off); REQUIRE(spdlog::level_from_str("null") == spdlog::level::off); + REQUIRE(spdlog::level_from_str("TRACE") == spdlog::level::trace); + REQUIRE(spdlog::level_from_str("DEBUG") == spdlog::level::debug); + REQUIRE(spdlog::level_from_str("INFO") == spdlog::level::info); + REQUIRE(spdlog::level_from_str("WARNING") == spdlog::level::warn); + REQUIRE(spdlog::level_from_str("WARN") == spdlog::level::warn); + REQUIRE(spdlog::level_from_str("ERROR") == spdlog::level::err); + REQUIRE(spdlog::level_from_str("ERR") == spdlog::level::err); + REQUIRE(spdlog::level_from_str("CRITICAL") == spdlog::level::critical); + REQUIRE(spdlog::level_from_str("OFF") == spdlog::level::off); + REQUIRE(spdlog::level_from_str("TrAcE") == spdlog::level::trace); + REQUIRE(spdlog::level_from_str("DeBuG") == spdlog::level::debug); } TEST_CASE("copy_ctor", "[copy_ctor]") { diff --git a/tests/test_pattern_formatter.cpp b/tests/test_pattern_formatter.cpp index bcc61f1827..0d9b166b6b 100644 --- a/tests/test_pattern_formatter.cpp +++ b/tests/test_pattern_formatter.cpp @@ -3,6 +3,7 @@ #include "test_sink.h" #include +#include using spdlog::memory_buf_t; @@ -69,13 +70,34 @@ TEST_CASE("date MM/DD/YY ", "[pattern_formatter]") { REQUIRE(log_to_str("Some message", "%D %v", spdlog::pattern_time_type::local, "\n") == oss.str()); } -TEST_CASE("GMT offset ", "[pattern_formatter]") { +TEST_CASE("%z with UTC pattern time", "[pattern_formatter]") { using namespace std::chrono_literals; const auto now = std::chrono::system_clock::now(); const auto yesterday = now - 24h; +#ifndef SPDLOG_NO_TZ_OFFSET REQUIRE(log_to_str_with_time(yesterday, "Some message", "%z", spdlog::pattern_time_type::utc, "\n") == "+00:00\n"); +#else + REQUIRE(log_to_str_with_time(yesterday, "Some message", "%z", spdlog::pattern_time_type::utc, "\n") == + "+??:??\n"); +#endif +} + +// see test_timezone.cpp for actual UTC offset calculation tests +TEST_CASE("UTC offset", "[pattern_formatter]") { + using namespace std::chrono_literals; + const auto now = std::chrono::system_clock::now(); + std::string result = + log_to_str_with_time(now, "Some message", "%z", spdlog::pattern_time_type::local, "\n"); + +#ifndef SPDLOG_NO_TZ_OFFSET + // Match format: +HH:MM or -HH:MM + std::regex re(R"([+-]\d{2}:[0-5]\d\n)"); + REQUIRE(std::regex_match(result, re)); +#else + REQUIRE(result == "+??:??\n"); +#endif } TEST_CASE("color range test1", "[pattern_formatter]") { diff --git a/tests/test_ringbuffer_sink.cpp b/tests/test_ringbuffer_sink.cpp index be6ac28e96..f0239e4204 100644 --- a/tests/test_ringbuffer_sink.cpp +++ b/tests/test_ringbuffer_sink.cpp @@ -57,15 +57,5 @@ TEST_CASE("test_empty", "[ringbuffer_sink]") { } TEST_CASE("test_empty_size", "[ringbuffer_sink]") { - const size_t sink_size = 0; - auto sink = std::make_shared(sink_size); - spdlog::logger l("logger", sink); - - for (size_t i = 0; i < sink_size + 1; ++i) { - l.info("{}", i); - } - - sink->drain([&](std::string_view) { - REQUIRE_FALSE(true); // should not be called since the sink size is 0 - }); + REQUIRE_THROWS_AS((void)spdlog::sinks::ringbuffer_sink_mt(0), spdlog::spdlog_ex); } diff --git a/tests/test_sink.h b/tests/test_sink.h index ae7da23720..09318712b6 100644 --- a/tests/test_sink.h +++ b/tests/test_sink.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -57,7 +58,8 @@ class test_sink : public base_sink { // save the line without the eol auto eol_len = strlen(details::os::default_eol); if (lines_.size() < lines_to_save) { - lines_.emplace_back(formatted.begin(), formatted.end() - eol_len); + using diff_t = typename std::iterator_traits::difference_type; + lines_.emplace_back(formatted.begin(), formatted.end() - static_cast(eol_len)); } msg_counter_++; std::this_thread::sleep_for(delay_); diff --git a/tests/test_stopwatch.cpp b/tests/test_stopwatch.cpp index ba4c8f62d7..02a6ce3f42 100644 --- a/tests/test_stopwatch.cpp +++ b/tests/test_stopwatch.cpp @@ -5,7 +5,7 @@ TEST_CASE("stopwatch1", "[stopwatch]") { using std::chrono::milliseconds; using clock = std::chrono::steady_clock; - milliseconds wait_ms(200); + milliseconds wait_ms(500); milliseconds tolerance_ms(250); auto start = clock::now(); spdlog::stopwatch sw; @@ -22,7 +22,7 @@ TEST_CASE("stopwatch2", "[stopwatch]") { using std::chrono::milliseconds; using clock = std::chrono::steady_clock; - clock::duration wait_duration(milliseconds(200)); + clock::duration wait_duration(milliseconds(500)); clock::duration tolerance_duration(milliseconds(250)); auto test_sink = std::make_shared(); diff --git a/tests/test_timezone.cpp b/tests/test_timezone.cpp new file mode 100644 index 0000000000..120e1daa3b --- /dev/null +++ b/tests/test_timezone.cpp @@ -0,0 +1,194 @@ +#ifndef SPDLOG_NO_TZ_OFFSET + +#include "includes.h" +#include +#include +#include + +// Helper to construct a simple std::tm from components +std::tm make_tm(int year, int month, int day, int hour, int minute) { + std::tm t; + std::memset(&t, 0, sizeof(t)); + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = 0; + t.tm_isdst = -1; + std::mktime(&t); + return t; +} + +// Cross-platform RAII Helper to safely set/restore process timezone +class ScopedTZ { + std::string original_tz_; + bool has_original_ = false; + +public: + explicit ScopedTZ(const std::string &tz_name) { + // save current TZ +#ifdef _WIN32 + char *buf = nullptr; + size_t len = 0; + if (_dupenv_s(&buf, &len, "TZ") == 0 && buf != nullptr) { + original_tz_ = std::string(buf); + has_original_ = true; + free(buf); + } +#else + const char *tz = std::getenv("TZ"); + if (tz) { + original_tz_ = tz; + has_original_ = true; + } +#endif + + // set new TZ +#ifdef _WIN32 + _putenv_s("TZ", tz_name.c_str()); + _tzset(); +#else + setenv("TZ", tz_name.c_str(), 1); + tzset(); +#endif + } + + ~ScopedTZ() { + // restore original TZ +#ifdef _WIN32 + if (has_original_) { + _putenv_s("TZ", original_tz_.c_str()); + } else { + _putenv_s("TZ", ""); + } + _tzset(); +#else + if (has_original_) { + setenv("TZ", original_tz_.c_str(), 1); + } else { + unsetenv("TZ"); + } + tzset(); +#endif + } +}; + +using spdlog::details::os::utc_minutes_offset; + +/* + * POSIX 2024 defines three formats for the TZ environment variable, + * + * 1. Implementation defined format which always starts with a colon: + * ":characters". + * 2. A specifier which fully describes the timezone rule in format + * "stdoffset[dst[offset][,start[/time],end[/time]]]". Note the + * offset and start/end part could be omitted, in which case one hour + * is implied, or it's considered implementation-defined when changing + * to and from Daylight Saving Time occurs. + * 3. Geographical or special timezone from an implementation-defined + * timezone database. + * + * On POSIX-compilant systems, we prefer format 2, and explicitly specify the + * DST rules to avoid implementation-defined behavior. + * + * See also IEEE 1003.1-2024 8.3 Other Environment Variables. + */ +#ifndef _WIN32 +/* + * Standard time is UTC-5 ("EST"), DST time is UTC-4 ("EDT"). DST is active + * from 2:00 on the 2nd Sunday in March, to 2:00 on 1st Sunday in November. + */ +#define EST5EDT "EST5EDT,M3.2.0,M11.1.0" +/* + * Standard time is UTC+2 ("IST"), DST time is UTC+3 ("IDT"). DST is active + * from 2:00 on following day of the 4th Thursday in March, to 2:00 on the + * last Sunday in October. + */ +#define IST_MINUS2_IDT "IST-2IDT,M3.4.4/26,M10.5.0" +#else +/* + * However, Windows doesn't follow the POSIX rules and only accept a TZ + * environment variable in format + * + * tzn [+|-]hh[:mm[:ss] ][dzn] + * + * thus we couldn't specify the DST rules. Luckily, Windows C runtime library + * assumes the United State's rules for implementing the calculation of DST, + * which is fine for our test cases. + * + * See also https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/tzset?view=msvc-170 + */ +#define EST5EDT "EST5EDT" +#define IST_MINUS2_IDT "IST-2IDT" +#endif + +TEST_CASE("UTC Offset - Western Hemisphere (USA - Standard Time)", "[timezone][west]") { + // EST5EDT: Eastern Standard Time (UTC-5) + ScopedTZ tz(EST5EDT); + + // Jan 15th (Winter) + auto tm = make_tm(2023, 1, 15, 12, 0); + REQUIRE(utc_minutes_offset(tm) == -300); +} + +TEST_CASE("UTC Offset - Eastern Hemisphere (Europe/Israel - Standard Time)", "[timezone][east]") { + // IST-2IDT: Israel Standard Time (UTC+2) + ScopedTZ tz(IST_MINUS2_IDT); + + // Jan 15th (Winter) + auto tm = make_tm(2023, 1, 15, 12, 0); + REQUIRE(utc_minutes_offset(tm) == 120); +} + +TEST_CASE("UTC Offset - Zero Offset (UTC/GMT)", "[timezone][utc]") { + ScopedTZ tz("GMT0"); + + // Check Winter + auto tm_winter = make_tm(2023, 1, 15, 12, 0); + REQUIRE(utc_minutes_offset(tm_winter) == 0); + + // Check Summer (GMT never shifts, so this should also be 0) + auto tm_summer = make_tm(2023, 7, 15, 12, 0); + REQUIRE(utc_minutes_offset(tm_summer) == 0); +} + +TEST_CASE("UTC Offset - Non-Integer Hour Offsets (India)", "[timezone][partial]") { + // IST-5:30: India Standard Time (UTC+5:30) + ScopedTZ tz("IST-5:30"); + + auto tm = make_tm(2023, 1, 15, 12, 0); + REQUIRE(utc_minutes_offset(tm) == 330); +} + +TEST_CASE("UTC Offset - Edge Case: Negative Offset Crossing Midnight", "[timezone][edge]") { + ScopedTZ tz(EST5EDT); + // Late night Dec 31st, 2023 + auto tm = make_tm(2023, 12, 31, 23, 59); + REQUIRE(utc_minutes_offset(tm) == -300); +} + +TEST_CASE("UTC Offset - Edge Case: Leap Year", "[timezone][edge]") { + ScopedTZ tz(EST5EDT); + // Feb 29, 2024 (Leap Day) - Winter + auto tm = make_tm(2024, 2, 29, 12, 0); + REQUIRE(utc_minutes_offset(tm) == -300); +} + +TEST_CASE("UTC Offset - Edge Case: Invalid Date (Pre-Epoch)", "[timezone][edge]") { +#ifdef _WIN32 + // Windows mktime returns -1 for dates before 1970. + // We expect the function to safely return 0 (fallback). + auto tm = make_tm(1960, 1, 1, 12, 0); + REQUIRE(utc_minutes_offset(tm) == 0); +#else + // Unix mktime handles pre-1970 dates correctly. + // We expect the actual historical offset (EST was UTC-5 in 1960). + ScopedTZ tz(EST5EDT); + auto tm = make_tm(1960, 1, 1, 12, 0); + REQUIRE(utc_minutes_offset(tm) == -300); +#endif +} + +#endif // !SPDLOG_NO_TZ_OFFSET +