First of all I need to tell that this article is about our C++ 23 stack currently used in production and produced good performance. Mostly all our libraries based on top of three basic libraries:
- uvent – asynchronous cross-platform engine which provides coroutines wrappers (like in boost.asio) and cross-platform I/O for all of our libraries.
-
unet – asynchronous web-server based on
uventwhich allows to use coroutines as handlers for endpoints (a good advantage, to be explained later). - ureflect – compile time reflection (without need to use a preprocessor).
In this article we'll also use some other our libraries:
-
upq – asynchronous PostgreSQL client library build on top of libpq,
uvent,ureflect. Provides ability to query data from data base with or without reflection (JSON/JSONB also can be parsed into datastructures via reflection). -
uredis – asynchronous redis library build on top of
uventwith own RESP3 implementation. -
ulog – logger inspired by spdlog build on top of
uvent. Instead of using own thread spawns flushing coroutine. - ujson – simple json library with reflection.
This article isn't a guide how to get into coroutines however it shows what can be done by using them in correct way.
Configuring databases
PostgreSQL
Before we start using libraries we need to startup PostgreSQL instance. Here is the docker-compose.yaml file:
version: "3.9"
services:
postgres:
image: postgres:16
container_name: local-postgres
restart: unless-stopped
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: devpass
POSTGRES_DB: devdb
ports:
- "5432:5432"
volumes:
- pgdata_local:/var/lib/postgresql/data
volumes:
pgdata_local:
KeyDB
We need also to add KeyDB as cache. Let's change docker-compose.yaml to:
version: "3.9"
services:
postgres:
image: postgres:16
container_name: local-postgres
restart: unless-stopped
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: devpass
POSTGRES_DB: devdb
ports:
- "5432:5432"
volumes:
- pgdata_local:/var/lib/postgresql/data
keydb:
image: eqalpha/keydb:latest
container_name: local-keydb
restart: unless-stopped
command: ["keydb-server", "/etc/keydb/keydb.conf"]
ports:
- "6379:6379"
volumes:
- keydbdata_local:/data
- ./keydb.conf:/etc/keydb/keydb.conf:ro
volumes:
pgdata_local:
keydbdata_local:
keydb.conf:
bind 0.0.0.0
port 6379
protected-mode yes
requirepass devpass
appendonly yes
dir /data
CMake
After setting up our databases we're able to begin configuring our project. Before we start writing code let's setup CMakeLists.txt file correctly:
cmake_minimum_required(VERSION 3.27)
project(article)
set(CMAKE_CXX_STANDARD 23)
set(UREDIS_BUILD_EXAMPLES OFF)
set(UREDIS_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(UREDIS_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(UREDIS_LOGS OFF CACHE BOOL "" FORCE)
set(UPQ_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
find_package(OpenSSL REQUIRED)
find_package(ZLIB REQUIRED)
include(FetchContent)
include_directories(${article_SOURCE_DIR})
FetchContent_Declare(
uvent
GIT_REPOSITORY https://github.com/Usub-development/uvent.git
GIT_TAG main
OVERRIDE_FIND_PACKAGE
)
FetchContent_Declare(
unet
GIT_REPOSITORY https://github.com/Usub-development/unet.git
GIT_TAG main
OVERRIDE_FIND_PACKAGE
)
# Loading ujson from upq
FetchContent_Declare(
upq
GIT_REPOSITORY https://github.com/Usub-development/upq.git
GIT_TAG main
OVERRIDE_FIND_PACKAGE
)
FetchContent_Declare(
ulog
GIT_REPOSITORY https://github.com/Usub-development/ulog.git
GIT_TAG main
OVERRIDE_FIND_PACKAGE
)
FetchContent_Declare(
uredis
GIT_REPOSITORY https://github.com/Usub-development/uredis.git
GIT_TAG main
FIND_PACKAGE_ARGS
)
FetchContent_MakeAvailable(uvent unet ulog upq uredis)
add_executable(${PROJECT_NAME}
src/main.cpp
)
target_include_directories(${PROJECT_NAME} PRIVATE
/usr/local/include
${CMAKE_CURRENT_LIST_DIR}/include
)
target_link_libraries(${PROJECT_NAME} PRIVATE
-lpq
OpenSSL::Crypto
usub::uvent
usub::server
ZLIB::ZLIB
usub::upq
usub::ulog
usub::uredis
)
Our project structure should look like:
.
├── CMakeLists.txt
├── docker-compose.yaml
├── include
├── keydb.conf
└── src
└── main.cpp
main.cpp
Now we're able to start configuring our main.cpp.
Add necessary includes:
#include <server/server.h>
#include <upq/PgRouting.h>
#include <upq/PgRoutingBuilder.h>
#include <ulog/ulog.h>
#include <uredis/RedisClusterClient.h>
Don't worry about
RedisClusterClient, it'll fallback to basic client if KeyDB (same as Redis) not in cluster mode.
Next configure ulog:
int main() {
usub::ulog::ULogInit cfg{
.trace_path = nullptr, // nullptr means stdout
.debug_path = nullptr, // -//-
.info_path = nullptr, // -//-
.warn_path = nullptr, // -//-
.error_path = nullptr, // -//-
.critical_path = nullptr, // -//-
.fatal_path = nullptr, // -//-
.flush_interval_ns = 5'000'000'000ULL, // 5 seconds
.queue_capacity = 1024,
.batch_size = 512,
.enable_color_stdout = true,
.json_mode = false,
.track_metrics = true
};
usub::ulog::init(cfg);
return 0;
}
Let's clarify each parameter:
-
trace_path,debug_pathetc. – log paths.nullptrmeans logs will be printed to stdout -
flush_interval_ns– how often flushing coroutine will be woken up and flush queues. -
queue_capacity– capacity of lock-free queue which is used as log storage by default. If capacity exceeded it'll fallback to queue with mutex. You can track fallback metrics like that:
if (auto* lg = usub::ulog::Logger::try_instance())
{
auto overflows = lg->get_overflow_events();
ulog::info("logger overflows (mpmc full -> mutex fallback) = {}", overflows);
}
-
batch_size– how many elements will be dequeued from storage (both lock-free and non-lock free queue) at once. -
enable_color_stdout– responsible for colorful logs. -
json_mode– ulog is able to flush logs as json, so they'll look like:
{"time":"2025-10-28 12:03:44.861","thread":3,"level":"I","msg":"starting event loop..."}
-
track_metrics– should ulog track fallback metrics or not.
usub::ulog::init(cfg);initializes the global logger. Call it before any logging; otherwise the program may crash (e.g., segfault).
Top comments (0)