feat: 新增 C++ 和 Rust 客户端 SDK 及 Docker 构建支持
- 新增 kenaito-conf-cpp-client / kenaito-conf-rust-client 及对应 demo - 新增 Dockerfile.cpp / Dockerfile.rust - README 目录结构补充 C++/Rust 客户端 - README_DOCKERFILE 补充 Windows docker run 命令,新增 C++/Rust 段落
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM alpine:3.20 AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
cmake \
|
||||||
|
curl-dev \
|
||||||
|
git
|
||||||
|
|
||||||
|
# Copy client library
|
||||||
|
COPY kenaito-conf-cpp-client/ ./kenaito-conf-cpp-client/
|
||||||
|
|
||||||
|
# Copy demo project
|
||||||
|
COPY kenaito-conf-cpp-client-demo/ ./kenaito-conf-cpp-client-demo/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
WORKDIR /src/kenaito-conf-cpp-client-demo
|
||||||
|
RUN mkdir build && cd build && \
|
||||||
|
cmake .. && \
|
||||||
|
make
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache libcurl libstdc++ libgcc
|
||||||
|
|
||||||
|
# Copy binary
|
||||||
|
COPY --from=build /src/kenaito-conf-cpp-client-demo/build/demo /app/demo
|
||||||
|
|
||||||
|
ENTRYPOINT ["./demo"]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM rust:1.88 AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy client library
|
||||||
|
COPY kenaito-conf-rust-client/ ./kenaito-conf-rust-client/
|
||||||
|
|
||||||
|
# Copy demo project
|
||||||
|
COPY kenaito-conf-rust-client-demo/ ./kenaito-conf-rust-client-demo/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
WORKDIR /src/kenaito-conf-rust-client-demo
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy binary
|
||||||
|
COPY --from=build /src/kenaito-conf-rust-client-demo/target/release/kenaito-conf-rust-client-demo /app/demo
|
||||||
|
|
||||||
|
ENTRYPOINT ["./demo"]
|
||||||
@@ -31,7 +31,11 @@ kenaito-conf/
|
|||||||
├── kenaito-conf-dotnet-client/ # .NET 客户端 SDK(.NET 6.0+)
|
├── kenaito-conf-dotnet-client/ # .NET 客户端 SDK(.NET 6.0+)
|
||||||
├── kenaito-conf-dotnet-client-demo/ # .NET 客户端示例
|
├── kenaito-conf-dotnet-client-demo/ # .NET 客户端示例
|
||||||
├── kenaito-conf-lua-client/ # Lua 客户端 SDK(Lua 5.1+)
|
├── kenaito-conf-lua-client/ # Lua 客户端 SDK(Lua 5.1+)
|
||||||
└── kenaito-conf-lua-client-demo/ # Lua 客户端示例
|
├── kenaito-conf-lua-client-demo/ # Lua 客户端示例
|
||||||
|
├── kenaito-conf-cpp-client/ # C++ 客户端 SDK(C++11+)
|
||||||
|
├── kenaito-conf-cpp-client-demo/ # C++ 客户端示例
|
||||||
|
├── kenaito-conf-rust-client/ # Rust 客户端 SDK(Rust 2021+)
|
||||||
|
└── kenaito-conf-rust-client-demo/ # Rust 客户端示例
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|||||||
+72
-10
@@ -4,29 +4,37 @@
|
|||||||
|
|
||||||
所有命令均在项目根目录执行。
|
所有命令均在项目根目录执行。
|
||||||
|
|
||||||
## Go 客户端(测试通过)
|
## Go 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.go -t kenaito-conf-go-demo .
|
docker build -f Dockerfile.go -t kenaito-conf-go-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-go-demo
|
kenaito-conf-golang-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-golang-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Node.js 客户端(测试通过)
|
## Node.js 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.nodejs -t kenaito-conf-nodejs-demo .
|
docker build -f Dockerfile.nodejs -t kenaito-conf-nodejs-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -d --rm --name nodejs-demo \
|
docker run -d --rm --name nodejs-demo \
|
||||||
-p 8083:8083 \
|
-p 8083:8083 \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-nodejs-demo
|
kenaito-conf-nodejs-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-nodejs-demo
|
||||||
|
|
||||||
# 查看日志
|
# 查看日志
|
||||||
docker logs -f nodejs-demo
|
docker logs -f nodejs-demo
|
||||||
@@ -39,17 +47,21 @@ curl http://localhost:8083/health
|
|||||||
docker stop nodejs-demo
|
docker stop nodejs-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Python 客户端(测试通过)
|
## Python 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.python -t kenaito-conf-python-demo .
|
docker build -f Dockerfile.python -t kenaito-conf-python-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -d --rm --name python-demo \
|
docker run -d --rm --name python-demo \
|
||||||
-p 8082:8082 \
|
-p 8082:8082 \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-python-demo
|
kenaito-conf-python-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-python-demo
|
||||||
|
|
||||||
# 查看日志
|
# 查看日志
|
||||||
docker logs -f python-demo
|
docker logs -f python-demo
|
||||||
@@ -62,67 +74,85 @@ curl http://localhost:8082/health
|
|||||||
docker stop python-demo
|
docker stop python-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## .NET 客户端(测试通过)
|
## .NET 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
|
docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-dotnet-demo
|
kenaito-conf-dotnet-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-dotnet-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Perl 客户端(测试通过)
|
## Perl 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.perl -t kenaito-conf-perl-demo .
|
docker build -f Dockerfile.perl -t kenaito-conf-perl-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-perl-demo
|
kenaito-conf-perl-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-perl-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lua 客户端(测试通过)
|
## Lua 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.lua -t kenaito-conf-lua-demo .
|
docker build -f Dockerfile.lua -t kenaito-conf-lua-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-lua-demo
|
kenaito-conf-lua-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-lua-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## PHP 客户端(测试通过)
|
## PHP 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.php -t kenaito-conf-php-demo .
|
docker build -f Dockerfile.php -t kenaito-conf-php-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -it --rm \
|
docker run -it --rm \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-php-demo
|
kenaito-conf-php-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-php-demo
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-php-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Spring Boot 2 客户端(测试通过)
|
## Spring Boot 2 客户端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.springboot2 -t kenaito-conf-springboot2-demo .
|
docker build -f Dockerfile.springboot2 -t kenaito-conf-springboot2-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
docker run -d --rm --name springboot2-demo \
|
docker run -d --rm --name springboot2-demo \
|
||||||
-p 8081:8081 \
|
-p 8081:8081 \
|
||||||
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
-e KENAITO_ENV=daily \
|
-e KENAITO_ENV=daily \
|
||||||
kenaito-conf-springboot2-demo
|
kenaito-conf-springboot2-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-springboot2-demo
|
||||||
|
|
||||||
# 查看日志
|
# 查看日志
|
||||||
docker logs -f springboot2-demo
|
docker logs -f springboot2-demo
|
||||||
@@ -135,6 +165,38 @@ curl http://localhost:8081/health
|
|||||||
docker stop springboot2-demo
|
docker stop springboot2-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## C++ 客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.cpp -t kenaito-conf-cpp-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
|
docker run -it --rm \
|
||||||
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
|
-e KENAITO_ENV=daily \
|
||||||
|
kenaito-conf-cpp-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-cpp-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rust 客户端(待测试)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.rust -t kenaito-conf-rust-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
|
docker run -it --rm \
|
||||||
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
|
-e KENAITO_ENV=daily \
|
||||||
|
kenaito-conf-rust-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-rust-demo
|
||||||
|
```
|
||||||
|
|
||||||
## 环境变量说明
|
## 环境变量说明
|
||||||
|
|
||||||
| 变量名 | 说明 | 默认值 |
|
| 变量名 | 说明 | 默认值 |
|
||||||
|
|||||||
+5
-1
@@ -29,7 +29,11 @@ kenaito-conf/
|
|||||||
├── kenaito-conf-dotnet-client/ # .NET client SDK (.NET 6.0+)
|
├── kenaito-conf-dotnet-client/ # .NET client SDK (.NET 6.0+)
|
||||||
├── kenaito-conf-dotnet-client-demo/ # .NET client demo
|
├── kenaito-conf-dotnet-client-demo/ # .NET client demo
|
||||||
├── kenaito-conf-lua-client/ # Lua client SDK (Lua 5.1+)
|
├── kenaito-conf-lua-client/ # Lua client SDK (Lua 5.1+)
|
||||||
└── kenaito-conf-lua-client-demo/ # Lua client demo
|
├── kenaito-conf-lua-client-demo/ # Lua client demo
|
||||||
|
├── kenaito-conf-cpp-client/ # C++ client SDK (C++11+)
|
||||||
|
├── kenaito-conf-cpp-client-demo/ # C++ client demo
|
||||||
|
├── kenaito-conf-rust-client/ # Rust client SDK (Rust 2021+)
|
||||||
|
└── kenaito-conf-rust-client-demo/ # Rust client demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(kenaito-conf-cpp-client-demo LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# 引入客户端库
|
||||||
|
add_subdirectory(../kenaito-conf-cpp-client ${CMAKE_BINARY_DIR}/kenaito-conf-cpp-client)
|
||||||
|
|
||||||
|
add_executable(demo main.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(demo PRIVATE kenaito_conf_client)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#include <kenaito_conf/client.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <csignal>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
static kenaito::Client* g_client = nullptr;
|
||||||
|
|
||||||
|
static void signal_handler(int) {
|
||||||
|
std::cout << "\nShutting down..." << std::endl;
|
||||||
|
if (g_client) g_client->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string get_env(const char* key, const std::string& default_val) {
|
||||||
|
const char* val = std::getenv(key);
|
||||||
|
return (val && val[0] != '\0') ? val : default_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_env_int(const char* key, int default_val) {
|
||||||
|
const char* val = std::getenv(key);
|
||||||
|
if (val && val[0] != '\0') {
|
||||||
|
try { return std::stoi(val); } catch (...) {}
|
||||||
|
}
|
||||||
|
return default_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::map<std::string, std::string> config_values = {
|
||||||
|
{"database.host", "192.168.1.1"},
|
||||||
|
{"database.port", "8888"},
|
||||||
|
{"database.name", "xxxx"},
|
||||||
|
{"database.user", "xxxx"},
|
||||||
|
{"database.password", "xxxxxxxx"},
|
||||||
|
};
|
||||||
|
|
||||||
|
kenaito::Config config;
|
||||||
|
config.base_url = get_env("KENAITO_BASE_URL", "http://localhost:8080");
|
||||||
|
config.app_name = get_env("KENAITO_APP_NAME", "kenaito-springboot-demo");
|
||||||
|
config.env = get_env("KENAITO_ENV", "daily");
|
||||||
|
config.version = get_env("KENAITO_VERSION", "latest");
|
||||||
|
config.token = get_env("KENAITO_TOKEN", "");
|
||||||
|
config.sync_interval = get_env_int("KENAITO_SYNC_INTERVAL", 10);
|
||||||
|
config.cache_dir = get_env("KENAITO_CACHE_DIR", ".");
|
||||||
|
|
||||||
|
kenaito::Client client(config);
|
||||||
|
g_client = &client;
|
||||||
|
|
||||||
|
client.on_change([&config_values](const std::map<std::string, std::string>& changed) {
|
||||||
|
std::cout << "Config changed:" << std::endl;
|
||||||
|
for (const auto& kv : changed) {
|
||||||
|
std::cout << " " << kv.first << " = " << kv.second << std::endl;
|
||||||
|
auto it = config_values.find(kv.first);
|
||||||
|
if (it != config_values.end()) {
|
||||||
|
it->second = kv.second;
|
||||||
|
std::cout << " [updated] " << kv.first << " = " << kv.second << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!client.start()) {
|
||||||
|
std::cerr << "Failed to init config client, using local defaults" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto remote = client.get_all();
|
||||||
|
for (auto& kv : config_values) {
|
||||||
|
auto it = remote.find(kv.first);
|
||||||
|
if (it != remote.end()) {
|
||||||
|
kv.second = it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::cout << "loaded " << remote.size() << " configs from remote" << std::endl;
|
||||||
|
|
||||||
|
std::cout << "\nCurrent configs:" << std::endl;
|
||||||
|
for (const auto& kv : config_values) {
|
||||||
|
std::cout << " " << kv.first << " = " << kv.second << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\nListening for config changes... (press Ctrl+C to stop)" << std::endl;
|
||||||
|
|
||||||
|
std::signal(SIGINT, signal_handler);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
client.sync_once();
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(config.sync_interval));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(kenaito-conf-cpp-client VERSION 1.0.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 11)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
find_package(CURL REQUIRED)
|
||||||
|
|
||||||
|
# 自动下载 nlohmann/json(单头文件)
|
||||||
|
set(JSON_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/nlohmann)
|
||||||
|
if(NOT EXISTS ${JSON_INCLUDE_DIR}/json.hpp)
|
||||||
|
message(STATUS "Downloading nlohmann/json.hpp...")
|
||||||
|
file(MAKE_DIRECTORY ${JSON_INCLUDE_DIR})
|
||||||
|
file(DOWNLOAD
|
||||||
|
https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp
|
||||||
|
${JSON_INCLUDE_DIR}/json.hpp
|
||||||
|
SHOW_PROGRESS
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(kenaito_conf_client STATIC
|
||||||
|
src/client.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(kenaito_conf_client
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party
|
||||||
|
PRIVATE
|
||||||
|
${CURL_INCLUDE_DIRS}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(kenaito_conf_client
|
||||||
|
PRIVATE
|
||||||
|
${CURL_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Install
|
||||||
|
install(TARGETS kenaito_conf_client
|
||||||
|
ARCHIVE DESTINATION lib
|
||||||
|
)
|
||||||
|
|
||||||
|
install(DIRECTORY include/kenaito_conf
|
||||||
|
DESTINATION include
|
||||||
|
)
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Kenaito Config Center C++ Client
|
||||||
|
|
||||||
|
C++ 客户端 SDK,适用于 C++11 及以上项目。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 从配置中心拉取应用配置
|
||||||
|
- 定时同步配置变更
|
||||||
|
- 支持配置变更回调监听
|
||||||
|
- 本地文件缓存(配置中心不可用时降级)
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- C++11 或更高版本
|
||||||
|
- libcurl
|
||||||
|
- nlohmann/json(已包含在 third_party 目录)
|
||||||
|
|
||||||
|
## 编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd kenaito-conf-cpp-client
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake ..
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <kenaito_conf/client.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
kenaito::Config config;
|
||||||
|
config.base_url = "http://localhost:8080";
|
||||||
|
config.app_name = "my-app";
|
||||||
|
config.env = "daily";
|
||||||
|
config.sync_interval = 10;
|
||||||
|
config.cache_dir = ".";
|
||||||
|
|
||||||
|
kenaito::Client client(config);
|
||||||
|
|
||||||
|
// 监听配置变更
|
||||||
|
client.on_change([](const std::map<std::string, std::string>& changed) {
|
||||||
|
std::cout << "Config changed:" << std::endl;
|
||||||
|
for (const auto& [k, v] : changed) {
|
||||||
|
std::cout << " " << k << " = " << v << std::endl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动客户端
|
||||||
|
if (!client.start()) {
|
||||||
|
std::cerr << "Failed to start config client" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
std::string db_host = client.get("database.host");
|
||||||
|
std::cout << "database.host = " << db_host << std::endl;
|
||||||
|
|
||||||
|
// 获取所有配置
|
||||||
|
auto all = client.get_all();
|
||||||
|
for (const auto& [k, v] : all) {
|
||||||
|
std::cout << k << " = " << v << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动同步
|
||||||
|
client.sync_once();
|
||||||
|
|
||||||
|
// 停止
|
||||||
|
client.stop();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置项
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 | 默认值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `base_url` | string | 配置中心地址 | `http://localhost:8080` |
|
||||||
|
| `app_name` | string | 应用名称 | (必填) |
|
||||||
|
| `env` | string | 环境标识 | `dev` |
|
||||||
|
| `version` | string | 配置版本 | `latest` |
|
||||||
|
| `token` | string | 应用 Token | 空 |
|
||||||
|
| `sync_interval` | int | 同步间隔(秒) | `60` |
|
||||||
|
| `ignore_global` | bool | 忽略全局配置 | `false` |
|
||||||
|
| `cache_dir` | string | 缓存目录 | `.` |
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd kenaito-conf-cpp-client-demo
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake ..
|
||||||
|
make
|
||||||
|
./demo
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
Demo 支持以下环境变量:
|
||||||
|
|
||||||
|
| 变量名 | 说明 | 默认值 |
|
||||||
|
|--------|------|--------|
|
||||||
|
| `KENAITO_BASE_URL` | 配置中心地址 | `http://localhost:8080` |
|
||||||
|
| `KENAITO_APP_NAME` | 应用名称 | `kenaito-springboot-demo` |
|
||||||
|
| `KENAITO_ENV` | 环境标识 | `daily` |
|
||||||
|
| `KENAITO_VERSION` | 配置版本 | `latest` |
|
||||||
|
| `KENAITO_TOKEN` | 应用 Token | 空 |
|
||||||
|
| `KENAITO_SYNC_INTERVAL` | 同步间隔(秒) | `10` |
|
||||||
|
| `KENAITO_CACHE_DIR` | 缓存目录 | `.` |
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Kenaito Config Center C++ Client
|
||||||
|
|
||||||
|
C++ client SDK for the Kenaito Config Center, suitable for C++11 and above.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fetch application configs from the config center
|
||||||
|
- Periodic config sync
|
||||||
|
- Config change callback listener
|
||||||
|
- Local file cache (fallback when config center is unavailable)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- C++11 or higher
|
||||||
|
- libcurl
|
||||||
|
- nlohmann/json (included in third_party directory)
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd kenaito-conf-cpp-client
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake ..
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <kenaito_conf/client.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
kenaito::Config config;
|
||||||
|
config.base_url = "http://localhost:8080";
|
||||||
|
config.app_name = "my-app";
|
||||||
|
config.env = "daily";
|
||||||
|
config.sync_interval = 10;
|
||||||
|
config.cache_dir = ".";
|
||||||
|
|
||||||
|
kenaito::Client client(config);
|
||||||
|
|
||||||
|
// Listen for config changes
|
||||||
|
client.on_change([](const std::map<std::string, std::string>& changed) {
|
||||||
|
std::cout << "Config changed:" << std::endl;
|
||||||
|
for (const auto& [k, v] : changed) {
|
||||||
|
std::cout << " " << k << " = " << v << std::endl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start client
|
||||||
|
if (!client.start()) {
|
||||||
|
std::cerr << "Failed to start config client" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get config
|
||||||
|
std::string db_host = client.get("database.host");
|
||||||
|
std::cout << "database.host = " << db_host << std::endl;
|
||||||
|
|
||||||
|
// Get all configs
|
||||||
|
auto all = client.get_all();
|
||||||
|
for (const auto& [k, v] : all) {
|
||||||
|
std::cout << k << " = " << v << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual sync
|
||||||
|
client.sync_once();
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
client.stop();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Parameter | Type | Description | Default |
|
||||||
|
|-----------|------|-------------|---------|
|
||||||
|
| `base_url` | string | Config center URL | `http://localhost:8080` |
|
||||||
|
| `app_name` | string | Application name | (Required) |
|
||||||
|
| `env` | string | Environment | `dev` |
|
||||||
|
| `version` | string | Config version | `latest` |
|
||||||
|
| `token` | string | App token | Empty |
|
||||||
|
| `sync_interval` | int | Sync interval (seconds) | `60` |
|
||||||
|
| `ignore_global` | bool | Whether to ignore global configs | `false` |
|
||||||
|
| `cache_dir` | string | Cache directory | `.` |
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd kenaito-conf-cpp-client-demo
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake ..
|
||||||
|
make
|
||||||
|
./demo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The demo supports the following environment variables:
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `KENAITO_BASE_URL` | Config center URL | `http://localhost:8080` |
|
||||||
|
| `KENAITO_APP_NAME` | Application name | `kenaito-springboot-demo` |
|
||||||
|
| `KENAITO_ENV` | Environment | `daily` |
|
||||||
|
| `KENAITO_VERSION` | Config version | `latest` |
|
||||||
|
| `KENAITO_TOKEN` | App token | Empty |
|
||||||
|
| `KENAITO_SYNC_INTERVAL` | Sync interval (seconds) | `10` |
|
||||||
|
| `KENAITO_CACHE_DIR` | Cache directory | `.` |
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace kenaito {
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
std::string base_url = "http://localhost:8080";
|
||||||
|
std::string app_name;
|
||||||
|
std::string env = "dev";
|
||||||
|
std::string version = "latest";
|
||||||
|
std::string token;
|
||||||
|
int sync_interval = 60;
|
||||||
|
bool ignore_global = false;
|
||||||
|
std::string cache_dir = ".";
|
||||||
|
};
|
||||||
|
|
||||||
|
using ChangeHandler = std::function<void(const std::map<std::string, std::string>&)>;
|
||||||
|
|
||||||
|
class Cache {
|
||||||
|
public:
|
||||||
|
explicit Cache(const std::string& dir);
|
||||||
|
bool write(const std::string& json_str);
|
||||||
|
std::string read();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string dir_;
|
||||||
|
static const std::string CACHE_FILE;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Listener {
|
||||||
|
public:
|
||||||
|
void on_change(ChangeHandler handler);
|
||||||
|
void notify(const std::map<std::string, std::string>& changed);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mu_;
|
||||||
|
std::vector<ChangeHandler> handlers_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
public:
|
||||||
|
explicit Client(const Config& config);
|
||||||
|
~Client();
|
||||||
|
|
||||||
|
void on_change(ChangeHandler handler);
|
||||||
|
std::string get(const std::string& key);
|
||||||
|
std::map<std::string, std::string> get_all();
|
||||||
|
bool start();
|
||||||
|
void stop();
|
||||||
|
void sync_once();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool fetch_all();
|
||||||
|
bool fetch_changed();
|
||||||
|
void apply_configs(const std::map<std::string, std::string>& data);
|
||||||
|
std::string http_post(const std::string& url, const std::string& body);
|
||||||
|
|
||||||
|
Config config_;
|
||||||
|
Cache cache_;
|
||||||
|
Listener listener_;
|
||||||
|
std::map<std::string, std::string> configs_;
|
||||||
|
int64_t last_time_ = 0;
|
||||||
|
std::mutex mu_;
|
||||||
|
std::atomic<bool> started_{false};
|
||||||
|
std::atomic<bool> stop_flag_{false};
|
||||||
|
std::thread sync_thread_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kenaito
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
#include "kenaito_conf/client.h"
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// nlohmann/json single header is bundled
|
||||||
|
#include "nlohmann/json.hpp"
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace kenaito {
|
||||||
|
|
||||||
|
// ---- Cache ----
|
||||||
|
|
||||||
|
const std::string Cache::CACHE_FILE = "config-cache.json";
|
||||||
|
|
||||||
|
Cache::Cache(const std::string& dir) : dir_(dir) {}
|
||||||
|
|
||||||
|
bool Cache::write(const std::string& json_str) {
|
||||||
|
std::string path = dir_ + "/" + CACHE_FILE;
|
||||||
|
std::ofstream ofs(path);
|
||||||
|
if (!ofs.is_open()) return false;
|
||||||
|
ofs << json_str;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Cache::read() {
|
||||||
|
std::string path = dir_ + "/" + CACHE_FILE;
|
||||||
|
std::ifstream ifs(path);
|
||||||
|
if (!ifs.is_open()) return "";
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << ifs.rdbuf();
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Listener ----
|
||||||
|
|
||||||
|
void Listener::on_change(ChangeHandler handler) {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
handlers_.push_back(std::move(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Listener::notify(const std::map<std::string, std::string>& changed) {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
for (auto& h : handlers_) {
|
||||||
|
h(changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Client ----
|
||||||
|
|
||||||
|
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||||
|
auto* str = static_cast<std::string*>(userp);
|
||||||
|
str->append(static_cast<char*>(contents), size * nmemb);
|
||||||
|
return size * nmemb;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::Client(const Config& config)
|
||||||
|
: config_(config), cache_(config.cache_dir) {
|
||||||
|
if (config_.version.empty()) config_.version = "latest";
|
||||||
|
if (config_.sync_interval <= 0) config_.sync_interval = 60;
|
||||||
|
if (config_.cache_dir.empty()) config_.cache_dir = ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::~Client() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::on_change(ChangeHandler handler) {
|
||||||
|
listener_.on_change(std::move(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Client::get(const std::string& key) {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
auto it = configs_.find(key);
|
||||||
|
return it != configs_.end() ? it->second : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, std::string> Client::get_all() {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
return configs_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Client::start() {
|
||||||
|
if (started_.exchange(true)) return true;
|
||||||
|
|
||||||
|
if (!fetch_all()) {
|
||||||
|
std::cerr << "[ConfigClient] fetch all configs error, trying cache" << std::endl;
|
||||||
|
std::string cached = cache_.read();
|
||||||
|
if (!cached.empty()) {
|
||||||
|
try {
|
||||||
|
auto j = json::parse(cached);
|
||||||
|
std::map<std::string, std::string> data;
|
||||||
|
for (auto& [k, v] : j.items()) {
|
||||||
|
if (v.is_string()) data[k] = v.get<std::string>();
|
||||||
|
}
|
||||||
|
apply_configs(data);
|
||||||
|
std::cerr << "[ConfigClient] loaded " << configs_.size() << " configs from cache" << std::endl;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[ConfigClient] cache parse error: " << e.what() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cerr << "[ConfigClient] fetch configs failed and no cache" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::stop() {
|
||||||
|
stop_flag_ = true;
|
||||||
|
if (sync_thread_.joinable()) {
|
||||||
|
sync_thread_.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::sync_once() {
|
||||||
|
if (!fetch_changed()) {
|
||||||
|
std::cerr << "[ConfigClient] sync error" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Client::http_post(const std::string& url, const std::string& body) {
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) return "";
|
||||||
|
|
||||||
|
std::string response;
|
||||||
|
struct curl_slist* headers = nullptr;
|
||||||
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
if (res != CURLE_OK) return "";
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Client::fetch_all() {
|
||||||
|
std::string url = config_.base_url + "/api/fetchAllConfigs";
|
||||||
|
|
||||||
|
json payload;
|
||||||
|
payload["appName"] = config_.app_name;
|
||||||
|
payload["env"] = config_.env;
|
||||||
|
payload["version"] = config_.version;
|
||||||
|
payload["ignoreGlobal"] = config_.ignore_global;
|
||||||
|
payload["token"] = config_.token;
|
||||||
|
|
||||||
|
std::string resp = http_post(url, payload.dump());
|
||||||
|
if (resp.empty()) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto j = json::parse(resp);
|
||||||
|
|
||||||
|
if (j.contains("code") && j["code"].get<int>() != 0) {
|
||||||
|
std::string msg = j.value("msg", "unknown");
|
||||||
|
std::cerr << "[ConfigClient] server error: " << msg << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.contains("latest.fetch.timestamp")) {
|
||||||
|
last_time_ = std::stoll(j["latest.fetch.timestamp"].get<std::string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache raw response
|
||||||
|
cache_.write(resp);
|
||||||
|
|
||||||
|
// apply configs
|
||||||
|
std::map<std::string, std::string> data;
|
||||||
|
for (auto& [k, v] : j.items()) {
|
||||||
|
if (v.is_string()) data[k] = v.get<std::string>();
|
||||||
|
}
|
||||||
|
apply_configs(data);
|
||||||
|
|
||||||
|
std::cerr << "[ConfigClient] fetched " << configs_.size() << " configs" << std::endl;
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[ConfigClient] parse error: " << e.what() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Client::fetch_changed() {
|
||||||
|
std::string url = config_.base_url + "/api/fetchChangedConfigs";
|
||||||
|
|
||||||
|
json payload;
|
||||||
|
payload["appName"] = config_.app_name;
|
||||||
|
payload["env"] = config_.env;
|
||||||
|
payload["version"] = config_.version;
|
||||||
|
payload["lastTime"] = last_time_;
|
||||||
|
payload["ignoreGlobal"] = config_.ignore_global;
|
||||||
|
|
||||||
|
std::string resp = http_post(url, payload.dump());
|
||||||
|
if (resp.empty()) {
|
||||||
|
std::cerr << "[ConfigClient] fetch_changed: empty response (network error or timeout)" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto j = json::parse(resp);
|
||||||
|
|
||||||
|
if (j.contains("code") && j["code"].get<int>() != 0) {
|
||||||
|
std::string msg = j.value("msg", "unknown");
|
||||||
|
std::cerr << "[ConfigClient] fetch_changed: server error code=" << j["code"] << " msg=" << msg << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!j.contains("data")) {
|
||||||
|
std::cerr << "[ConfigClient] fetch_changed: missing 'data' field in response" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = j["data"];
|
||||||
|
|
||||||
|
if (data.contains("latest.fetch.timestamp")) {
|
||||||
|
last_time_ = std::stoll(data["latest.fetch.timestamp"].get<std::string>());
|
||||||
|
data.erase("latest.fetch.timestamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle deleted keys
|
||||||
|
if (data.contains("deletedKeys") && !data["deletedKeys"].get<std::string>().empty()) {
|
||||||
|
std::string deleted_str = data["deletedKeys"].get<std::string>();
|
||||||
|
std::istringstream iss(deleted_str);
|
||||||
|
std::string key;
|
||||||
|
std::vector<std::string> deleted_keys;
|
||||||
|
while (std::getline(iss, key, ',')) {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
configs_.erase(key);
|
||||||
|
deleted_keys.push_back(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string keys;
|
||||||
|
for (const auto& k : deleted_keys) {
|
||||||
|
if (!keys.empty()) keys += ", ";
|
||||||
|
keys += k;
|
||||||
|
}
|
||||||
|
std::cerr << "[ConfigClient] " << deleted_keys.size() << " configs deleted: " << keys << std::endl;
|
||||||
|
data.erase("deletedKeys");
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle changed configs
|
||||||
|
std::map<std::string, std::string> changed;
|
||||||
|
for (auto& [k, v] : data.items()) {
|
||||||
|
if (v.is_string()) {
|
||||||
|
changed[k] = v.get<std::string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed.empty()) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
for (auto& [k, v] : changed) {
|
||||||
|
configs_[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listener_.notify(changed);
|
||||||
|
|
||||||
|
std::string keys;
|
||||||
|
for (auto& [k, v] : changed) {
|
||||||
|
if (!keys.empty()) keys += ", ";
|
||||||
|
keys += k;
|
||||||
|
}
|
||||||
|
std::cerr << "[ConfigClient] " << changed.size() << " configs changed: " << keys << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[ConfigClient] fetch_changed: parse error: " << e.what() << std::endl;
|
||||||
|
std::cerr << "[ConfigClient] response was: " << resp.substr(0, 200) << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::apply_configs(const std::map<std::string, std::string>& data) {
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
for (auto& [k, v] : data) {
|
||||||
|
if (k.substr(0, 8) == "kenaito." || k == "latest.fetch.timestamp") continue;
|
||||||
|
configs_[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kenaito
|
||||||
+24765
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "kenaito-conf-rust-client-demo"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
kenaito-conf-client = { path = "../kenaito-conf-rust-client" }
|
||||||
|
ctrlc = "3.4"
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
use kenaito_conf_client::{Client, Config};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn get_env(key: &str, default: &str) -> String {
|
||||||
|
env::var(key).unwrap_or_else(|_| default.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_env_int(key: &str, default: u64) -> u64 {
|
||||||
|
env::var(key)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let config_values: HashMap<String, String> = [
|
||||||
|
("database.host".to_string(), "192.168.1.1".to_string()),
|
||||||
|
("database.port".to_string(), "8888".to_string()),
|
||||||
|
("database.name".to_string(), "xxxx".to_string()),
|
||||||
|
("database.user".to_string(), "xxxx".to_string()),
|
||||||
|
("database.password".to_string(), "xxxxxxxx".to_string()),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
base_url: get_env("KENAITO_BASE_URL", "http://localhost:8080"),
|
||||||
|
app_name: get_env("KENAITO_APP_NAME", "kenaito-springboot-demo"),
|
||||||
|
env: get_env("KENAITO_ENV", "daily"),
|
||||||
|
version: get_env("KENAITO_VERSION", "latest"),
|
||||||
|
token: get_env("KENAITO_TOKEN", ""),
|
||||||
|
sync_interval: get_env_int("KENAITO_SYNC_INTERVAL", 10),
|
||||||
|
cache_dir: get_env("KENAITO_CACHE_DIR", "."),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = Arc::new(Client::new(config.clone()));
|
||||||
|
|
||||||
|
client.on_change(|changed| {
|
||||||
|
println!("Config changed:");
|
||||||
|
for (k, v) in changed {
|
||||||
|
println!(" {} = {}", k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = client.start() {
|
||||||
|
eprintln!("Failed to init config client, using local defaults: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut config_values = config_values;
|
||||||
|
let remote = client.get_all();
|
||||||
|
for (k, v) in &remote {
|
||||||
|
if config_values.contains_key(k) {
|
||||||
|
config_values.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("loaded {} configs from remote", remote.len());
|
||||||
|
|
||||||
|
println!("\nCurrent configs:");
|
||||||
|
let mut keys: Vec<&String> = config_values.keys().collect();
|
||||||
|
keys.sort();
|
||||||
|
for k in keys {
|
||||||
|
println!(" {} = {}", k, config_values[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nListening for config changes... (press Ctrl+C to stop)");
|
||||||
|
|
||||||
|
let client_clone = client.clone();
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
println!("\nShutting down...");
|
||||||
|
client_clone.stop();
|
||||||
|
std::process::exit(0);
|
||||||
|
})
|
||||||
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
client.sync_once();
|
||||||
|
thread::sleep(Duration::from_secs(config.sync_interval));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "kenaito-conf-client"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Kenaito Config Center Rust client SDK"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
log = "0.4"
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Kenaito Config Center Rust Client
|
||||||
|
|
||||||
|
Rust 客户端 SDK,适用于 Rust 2021 edition 项目。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 从配置中心拉取应用配置
|
||||||
|
- 定时同步配置变更
|
||||||
|
- 支持配置变更回调监听
|
||||||
|
- 本地文件缓存(配置中心不可用时降级)
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
- Rust 2021 edition
|
||||||
|
- reqwest (HTTP 客户端)
|
||||||
|
- serde / serde_json (JSON 处理)
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
在你的 `Cargo.toml` 中添加:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
kenaito-conf-client = { path = "../kenaito-conf-rust-client" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use kenaito_conf_client::{Config, Client};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let config = Config {
|
||||||
|
base_url: "http://localhost:8080".to_string(),
|
||||||
|
app_name: "my-app".to_string(),
|
||||||
|
env: "daily".to_string(),
|
||||||
|
sync_interval: 10,
|
||||||
|
cache_dir: ".".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = Client::new(config);
|
||||||
|
|
||||||
|
// 监听配置变更
|
||||||
|
client.on_change(|changed| {
|
||||||
|
println!("Config changed:");
|
||||||
|
for (k, v) in changed {
|
||||||
|
println!(" {} = {}", k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动客户端
|
||||||
|
if let Err(e) = client.start() {
|
||||||
|
eprintln!("Failed to start: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
let db_host = client.get("database.host");
|
||||||
|
println!("database.host = {}", db_host);
|
||||||
|
|
||||||
|
// 获取所有配置
|
||||||
|
let all = client.get_all();
|
||||||
|
for (k, v) in &all {
|
||||||
|
println!("{} = {}", k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动同步
|
||||||
|
client.sync_once();
|
||||||
|
|
||||||
|
// 停止
|
||||||
|
client.stop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置项
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 | 默认值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `base_url` | String | 配置中心地址 | `http://localhost:8080` |
|
||||||
|
| `app_name` | String | 应用名称 | (必填) |
|
||||||
|
| `env` | String | 环境标识 | `dev` |
|
||||||
|
| `version` | String | 配置版本 | `latest` |
|
||||||
|
| `token` | String | 应用 Token | 空 |
|
||||||
|
| `sync_interval` | u64 | 同步间隔(秒) | `60` |
|
||||||
|
| `ignore_global` | bool | 忽略全局配置 | `false` |
|
||||||
|
| `cache_dir` | String | 缓存目录 | `.` |
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd kenaito-conf-rust-client-demo
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
Demo 支持以下环境变量:
|
||||||
|
|
||||||
|
| 变量名 | 说明 | 默认值 |
|
||||||
|
|--------|------|--------|
|
||||||
|
| `KENAITO_BASE_URL` | 配置中心地址 | `http://localhost:8080` |
|
||||||
|
| `KENAITO_APP_NAME` | 应用名称 | `kenaito-springboot-demo` |
|
||||||
|
| `KENAITO_ENV` | 环境标识 | `daily` |
|
||||||
|
| `KENAITO_VERSION` | 配置版本 | `latest` |
|
||||||
|
| `KENAITO_TOKEN` | 应用 Token | 空 |
|
||||||
|
| `KENAITO_SYNC_INTERVAL` | 同步间隔(秒) | `10` |
|
||||||
|
| `KENAITO_CACHE_DIR` | 缓存目录 | `.` |
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Kenaito Config Center Rust Client
|
||||||
|
|
||||||
|
Rust client SDK for the Kenaito Config Center, suitable for Rust 2021 edition projects.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fetch application configs from the config center
|
||||||
|
- Periodic config sync
|
||||||
|
- Config change callback listener
|
||||||
|
- Local file cache (fallback when config center is unavailable)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Rust 2021 edition
|
||||||
|
- reqwest (HTTP client)
|
||||||
|
- serde / serde_json (JSON processing)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Add to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
kenaito-conf-client = { path = "../kenaito-conf-rust-client" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use kenaito_conf_client::{Config, Client};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let config = Config {
|
||||||
|
base_url: "http://localhost:8080".to_string(),
|
||||||
|
app_name: "my-app".to_string(),
|
||||||
|
env: "daily".to_string(),
|
||||||
|
sync_interval: 10,
|
||||||
|
cache_dir: ".".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = Client::new(config);
|
||||||
|
|
||||||
|
// Listen for config changes
|
||||||
|
client.on_change(|changed| {
|
||||||
|
println!("Config changed:");
|
||||||
|
for (k, v) in changed {
|
||||||
|
println!(" {} = {}", k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start client
|
||||||
|
if let Err(e) = client.start() {
|
||||||
|
eprintln!("Failed to start: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get config
|
||||||
|
let db_host = client.get("database.host");
|
||||||
|
println!("database.host = {}", db_host);
|
||||||
|
|
||||||
|
// Get all configs
|
||||||
|
let all = client.get_all();
|
||||||
|
for (k, v) in &all {
|
||||||
|
println!("{} = {}", k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual sync
|
||||||
|
client.sync_once();
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
client.stop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Parameter | Type | Description | Default |
|
||||||
|
|-----------|------|-------------|---------|
|
||||||
|
| `base_url` | String | Config center URL | `http://localhost:8080` |
|
||||||
|
| `app_name` | String | Application name | (Required) |
|
||||||
|
| `env` | String | Environment | `dev` |
|
||||||
|
| `version` | String | Config version | `latest` |
|
||||||
|
| `token` | String | App token | Empty |
|
||||||
|
| `sync_interval` | u64 | Sync interval (seconds) | `60` |
|
||||||
|
| `ignore_global` | bool | Whether to ignore global configs | `false` |
|
||||||
|
| `cache_dir` | String | Cache directory | `.` |
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd kenaito-conf-rust-client-demo
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The demo supports the following environment variables:
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `KENAITO_BASE_URL` | Config center URL | `http://localhost:8080` |
|
||||||
|
| `KENAITO_APP_NAME` | Application name | `kenaito-springboot-demo` |
|
||||||
|
| `KENAITO_ENV` | Environment | `daily` |
|
||||||
|
| `KENAITO_VERSION` | Config version | `latest` |
|
||||||
|
| `KENAITO_TOKEN` | App token | Empty |
|
||||||
|
| `KENAITO_SYNC_INTERVAL` | Sync interval (seconds) | `10` |
|
||||||
|
| `KENAITO_CACHE_DIR` | Cache directory | `.` |
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const CACHE_FILE: &str = "config-cache.json";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub base_url: String,
|
||||||
|
pub app_name: String,
|
||||||
|
pub env: String,
|
||||||
|
pub version: String,
|
||||||
|
pub token: String,
|
||||||
|
pub sync_interval: u64,
|
||||||
|
pub ignore_global: bool,
|
||||||
|
pub cache_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
base_url: "http://localhost:8080".to_string(),
|
||||||
|
app_name: String::new(),
|
||||||
|
env: "dev".to_string(),
|
||||||
|
version: "latest".to_string(),
|
||||||
|
token: String::new(),
|
||||||
|
sync_interval: 60,
|
||||||
|
ignore_global: false,
|
||||||
|
cache_dir: ".".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeHandler = Box<dyn Fn(&HashMap<String, String>) + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
struct Cache {
|
||||||
|
dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cache {
|
||||||
|
fn new(dir: &str) -> Self {
|
||||||
|
Self { dir: dir.to_string() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, data: &str) -> Result<(), std::io::Error> {
|
||||||
|
let path = PathBuf::from(&self.dir).join(CACHE_FILE);
|
||||||
|
let mut file = fs::File::create(path)?;
|
||||||
|
file.write_all(data.as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(&self) -> Option<String> {
|
||||||
|
let path = PathBuf::from(&self.dir).join(CACHE_FILE);
|
||||||
|
let mut file = fs::File::open(path).ok()?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).ok()?;
|
||||||
|
Some(contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Client {
|
||||||
|
config: Config,
|
||||||
|
cache: Cache,
|
||||||
|
handlers: Arc<Mutex<Vec<ChangeHandler>>>,
|
||||||
|
configs: Arc<Mutex<HashMap<String, String>>>,
|
||||||
|
last_time: Arc<Mutex<i64>>,
|
||||||
|
stop_flag: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
let cache_dir = if config.cache_dir.is_empty() { ".".to_string() } else { config.cache_dir.clone() };
|
||||||
|
let version = if config.version.is_empty() { "latest".to_string() } else { config.version.clone() };
|
||||||
|
let sync_interval = if config.sync_interval == 0 { 60 } else { config.sync_interval };
|
||||||
|
|
||||||
|
let mut cfg = config;
|
||||||
|
cfg.cache_dir = cache_dir;
|
||||||
|
cfg.version = version;
|
||||||
|
cfg.sync_interval = sync_interval;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cache: Cache::new(&cfg.cache_dir),
|
||||||
|
config: cfg,
|
||||||
|
handlers: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
configs: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
last_time: Arc::new(Mutex::new(0)),
|
||||||
|
stop_flag: Arc::new(Mutex::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_change<F>(&self, handler: F)
|
||||||
|
where
|
||||||
|
F: Fn(&HashMap<String, String>) + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let mut handlers = self.handlers.lock().unwrap();
|
||||||
|
handlers.push(Box::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, key: &str) -> String {
|
||||||
|
let configs = self.configs.lock().unwrap();
|
||||||
|
configs.get(key).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all(&self) -> HashMap<String, String> {
|
||||||
|
self.configs.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) -> Result<(), String> {
|
||||||
|
match self.fetch_all() {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[ConfigClient] fetch all configs error: {}, trying cache", e);
|
||||||
|
match self.cache.read() {
|
||||||
|
Some(cached) => {
|
||||||
|
match serde_json::from_str::<serde_json::Value>(&cached) {
|
||||||
|
Ok(val) => {
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
if let Some(obj) = val.as_object() {
|
||||||
|
for (k, v) in obj {
|
||||||
|
if let Some(s) = v.as_str() {
|
||||||
|
data.insert(k.clone(), s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.apply_configs(&data);
|
||||||
|
eprintln!("[ConfigClient] loaded {} configs from cache", self.configs.lock().unwrap().len());
|
||||||
|
}
|
||||||
|
Err(_) => return Err("fetch configs failed and no cache".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err("fetch configs failed and no cache".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_loop(&self) {
|
||||||
|
let interval = Duration::from_secs(self.config.sync_interval);
|
||||||
|
loop {
|
||||||
|
thread::sleep(interval);
|
||||||
|
if *self.stop_flag.lock().unwrap() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(e) = self.fetch_changed() {
|
||||||
|
eprintln!("[ConfigClient] sync error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_once(&self) {
|
||||||
|
if let Err(e) = self.fetch_changed() {
|
||||||
|
eprintln!("[ConfigClient] sync error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) {
|
||||||
|
*self.stop_flag.lock().unwrap() = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http_post(&self, url: &str, body: &str) -> Result<String, String> {
|
||||||
|
let client = reqwest::blocking::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(body.to_string())
|
||||||
|
.send()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("HTTP {}", resp.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.text().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_all(&self) -> Result<(), String> {
|
||||||
|
let url = format!("{}/api/fetchAllConfigs", self.config.base_url);
|
||||||
|
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"appName": self.config.app_name,
|
||||||
|
"env": self.config.env,
|
||||||
|
"version": self.config.version,
|
||||||
|
"ignoreGlobal": self.config.ignore_global,
|
||||||
|
"token": self.config.token,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp = self.http_post(&url, &payload.to_string())?;
|
||||||
|
let val: serde_json::Value = serde_json::from_str(&resp).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if let Some(code) = val.get("code").and_then(|c| c.as_i64()) {
|
||||||
|
if code != 0 {
|
||||||
|
let msg = val.get("msg").and_then(|m| m.as_str()).unwrap_or("unknown");
|
||||||
|
return Err(format!("server error: {}", msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ts) = val.get("latest.fetch.timestamp").and_then(|t| t.as_str()) {
|
||||||
|
if let Ok(t) = ts.parse::<i64>() {
|
||||||
|
*self.last_time.lock().unwrap() = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache raw response
|
||||||
|
let _ = self.cache.write(&resp);
|
||||||
|
|
||||||
|
// apply configs
|
||||||
|
let mut data = HashMap::new();
|
||||||
|
if let Some(obj) = val.as_object() {
|
||||||
|
for (k, v) in obj {
|
||||||
|
if let Some(s) = v.as_str() {
|
||||||
|
data.insert(k.clone(), s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.apply_configs(&data);
|
||||||
|
|
||||||
|
eprintln!("[ConfigClient] fetched {} configs", self.configs.lock().unwrap().len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_changed(&self) -> Result<(), String> {
|
||||||
|
let url = format!("{}/api/fetchChangedConfigs", self.config.base_url);
|
||||||
|
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"appName": self.config.app_name,
|
||||||
|
"env": self.config.env,
|
||||||
|
"version": self.config.version,
|
||||||
|
"lastTime": *self.last_time.lock().unwrap(),
|
||||||
|
"ignoreGlobal": self.config.ignore_global,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp = self.http_post(&url, &payload.to_string())?;
|
||||||
|
let val: serde_json::Value = serde_json::from_str(&resp).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if let Some(code) = val.get("code").and_then(|c| c.as_i64()) {
|
||||||
|
if code != 0 {
|
||||||
|
return Err(format!("server error code: {}", code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = val.get("data").and_then(|d| d.as_object()).cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(ts) = data.get("latest.fetch.timestamp").and_then(|t| t.as_str()) {
|
||||||
|
if let Ok(t) = ts.parse::<i64>() {
|
||||||
|
*self.last_time.lock().unwrap() = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle deleted keys
|
||||||
|
if let Some(deleted_str) = data.get("deletedKeys").and_then(|d| d.as_str()) {
|
||||||
|
if !deleted_str.is_empty() {
|
||||||
|
let deleted_keys: Vec<&str> = deleted_str.split(',').collect();
|
||||||
|
let mut configs = self.configs.lock().unwrap();
|
||||||
|
for key in &deleted_keys {
|
||||||
|
configs.remove(*key);
|
||||||
|
}
|
||||||
|
eprintln!("[ConfigClient] {} configs deleted", deleted_keys.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle changed configs
|
||||||
|
let mut changed = HashMap::new();
|
||||||
|
for (k, v) in &data {
|
||||||
|
if k == "latest.fetch.timestamp" || k == "deletedKeys" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(s) = v.as_str() {
|
||||||
|
changed.insert(k.clone(), s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !changed.is_empty() {
|
||||||
|
{
|
||||||
|
let mut configs = self.configs.lock().unwrap();
|
||||||
|
for (k, v) in &changed {
|
||||||
|
configs.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let handlers = self.handlers.lock().unwrap();
|
||||||
|
for h in handlers.iter() {
|
||||||
|
h(&changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys: Vec<&str> = changed.keys().map(|s| s.as_str()).collect();
|
||||||
|
eprintln!("[ConfigClient] {} configs changed: {}", changed.len(), keys.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_configs(&self, data: &HashMap<String, String>) {
|
||||||
|
let mut configs = self.configs.lock().unwrap();
|
||||||
|
for (k, v) in data {
|
||||||
|
if k.starts_with("kenaito.") || k == "latest.fetch.timestamp" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
configs.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user