chore: 删除多个客户端模块
移除以下未完成的客户端模块: - C++ 客户端和 demo - .NET 客户端和 demo - Kotlin 客户端和 demo - Lua 客户端和 demo - PHP 客户端和 demo - Ruby 客户端和 demo - Rust 客户端和 demo 更新根目录 README.md,移除相关说明。
This commit is contained in:
@@ -14,30 +14,30 @@
|
||||
kenaito-conf/
|
||||
├── server/ # Go 管理端(Gin + xorm)
|
||||
├── web/ # Vue 2 前端
|
||||
├── kenaito-conf-go-client/ # (测试通过)Go 客户端 SDK
|
||||
├── kenaito-conf-go-client-demo/ # (测试通过)Go 客户端示例
|
||||
├── kenaito-conf-springboot2-client/ # (测试通过)Java Spring Boot 2.x 客户端 SDK(JDK 8+)
|
||||
├── kenaito-conf-springboot2-client-demo/# (测试通过)Spring Boot 2.x 客户端示例(端口 8081)
|
||||
├── kenaito-conf-python-client/ # (测试通过)Python 客户端 SDK(Python 3.10+)
|
||||
├── kenaito-conf-python-client-demo/ # (测试通过)Python 客户端示例(端口 8082)
|
||||
├── kenaito-conf-nodejs-client/ # (测试通过)Node.js 客户端 SDK(Node.js 18+)
|
||||
├── kenaito-conf-nodejs-client-demo/ # (测试通过)Node.js 客户端示例(端口 8083)
|
||||
├── kenaito-conf-php-client/ # (待测试)PHP 客户端 SDK(PHP 8.0+)
|
||||
├── kenaito-conf-php-client-demo/ # (待测试)PHP 客户端示例
|
||||
├── kenaito-conf-dotnet-client/ # (待测试).NET 客户端 SDK(.NET 6.0+)
|
||||
├── kenaito-conf-dotnet-client-demo/ # (待测试).NET 客户端示例
|
||||
├── kenaito-conf-c-client/ # (待测试)C 客户端 SDK(libcurl + OpenSSL)
|
||||
├── kenaito-conf-c-client-demo/ # (待测试)C 客户端示例
|
||||
├── kenaito-conf-cpp-client/ # (待测试)C++ 客户端 SDK(libcurl + OpenSSL)
|
||||
├── kenaito-conf-cpp-client-demo/ # (待测试)C++ 客户端示例
|
||||
├── kenaito-conf-lua-client/ # (待测试)Lua 客户端 SDK(Lua 5.3+)
|
||||
├── kenaito-conf-lua-client-demo/ # (待测试)Lua 客户端示例
|
||||
├── kenaito-conf-kotlin-client/ # (待测试)Kotlin 客户端 SDK(Kotlin 1.9+)
|
||||
├── kenaito-conf-kotlin-client-demo/ # (待测试)Kotlin 客户端示例
|
||||
├── kenaito-conf-rust-client/ # (待测试)Rust 客户端 SDK(Rust 1.70+)
|
||||
├── kenaito-conf-rust-client-demo/ # (待测试)Rust 客户端示例
|
||||
├── kenaito-conf-ruby-client/ # (待测试)Ruby 客户端 SDK(Ruby 2.7+)
|
||||
└── kenaito-conf-ruby-client-demo/ # (待测试)Ruby 客户端示例
|
||||
├── kenaito-conf-go-client/ # Go 客户端 SDK
|
||||
├── kenaito-conf-go-client-demo/ # Go 客户端示例
|
||||
├── kenaito-conf-springboot2-client/ # Java Spring Boot 2.x 客户端 SDK(JDK 8+)
|
||||
├── kenaito-conf-springboot2-client-demo/# Spring Boot 2.x 客户端示例(端口 8081)
|
||||
├── kenaito-conf-python-client/ # Python 客户端 SDK(Python 3.10+)
|
||||
├── kenaito-conf-python-client-demo/ # Python 客户端示例(端口 8082)
|
||||
├── kenaito-conf-nodejs-client/ # Node.js 客户端 SDK(Node.js 18+)
|
||||
├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083)
|
||||
├── kenaito-conf-php-client/ # (未支持)PHP 客户端 SDK(PHP 8.0+)
|
||||
├── kenaito-conf-php-client-demo/ # (未支持)PHP 客户端示例
|
||||
├── kenaito-conf-dotnet-client/ # (未支持).NET 客户端 SDK(.NET 6.0+)
|
||||
├── kenaito-conf-dotnet-client-demo/ # (未支持).NET 客户端示例
|
||||
├── kenaito-conf-c-client/ # (未支持)C 客户端 SDK(libcurl + OpenSSL)
|
||||
├── kenaito-conf-c-client-demo/ # (未支持)C 客户端示例
|
||||
├── kenaito-conf-cpp-client/ # (未支持)C++ 客户端 SDK(libcurl + OpenSSL)
|
||||
├── kenaito-conf-cpp-client-demo/ # (未支持)C++ 客户端示例
|
||||
├── kenaito-conf-lua-client/ # (未支持)Lua 客户端 SDK(Lua 5.3+)
|
||||
├── kenaito-conf-lua-client-demo/ # (未支持)Lua 客户端示例
|
||||
├── kenaito-conf-kotlin-client/ # (未支持)Kotlin 客户端 SDK(Kotlin 1.9+)
|
||||
├── kenaito-conf-kotlin-client-demo/ # (未支持)Kotlin 客户端示例
|
||||
├── kenaito-conf-rust-client/ # (未支持)Rust 客户端 SDK(Rust 1.70+)
|
||||
├── kenaito-conf-rust-client-demo/ # (未支持)Rust 客户端示例
|
||||
├── kenaito-conf-ruby-client/ # (未支持)Ruby 客户端 SDK(Ruby 2.7+)
|
||||
└── kenaito-conf-ruby-client-demo/ # (未支持)Ruby 客户端示例
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
@@ -132,6 +132,7 @@ dbHost := c.Get("db.host")
|
||||
**1. 引入依赖**
|
||||
|
||||
```xml
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.odboy</groupId>
|
||||
<artifactId>kenaito-conf-springboot2-client</artifactId>
|
||||
@@ -166,26 +167,8 @@ private String databaseHost;
|
||||
private Integer databasePort;
|
||||
```
|
||||
|
||||
配置变更时会自动刷新 `@Value` 字段,无需重启应用。详见 [Spring Boot 2 客户端文档](kenaito-conf-springboot2-client/README.md)。
|
||||
|
||||
### Java Spring Boot 3 客户端
|
||||
|
||||
适用于 JDK 11+ 项目(当前编译目标 Spring Boot 2.7.18,预留 Spring Boot 3 升级路径)。
|
||||
|
||||
**1. 引入依赖**
|
||||
|
||||
```xml
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.odboy</groupId>
|
||||
<artifactId>kenaito-conf-springboot3-client</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**2. 配置与使用方式同 Spring Boot 2.x 客户端。**
|
||||
|
||||
> **注意:** 升级到 Spring Boot 3 需先将编译目标改为 JDK 17,并更新 `spring-boot.version` 为 3.x、Spring Framework 为 6.x。
|
||||
配置变更时会自动刷新 `@Value`
|
||||
字段,无需重启应用。详见 [Spring Boot 2 客户端文档](kenaito-conf-springboot2-client/README.md)。
|
||||
|
||||
### Python 客户端
|
||||
|
||||
@@ -281,225 +264,6 @@ const allConfigs = client.getAll();
|
||||
client.stop();
|
||||
```
|
||||
|
||||
### PHP 客户端
|
||||
|
||||
适用于 PHP 8.0+ 项目。
|
||||
|
||||
**1. 安装**
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-php-client
|
||||
composer install
|
||||
```
|
||||
|
||||
**2. 使用**
|
||||
|
||||
```php
|
||||
use KenaitoConf\Config;
|
||||
use KenaitoConf\ConfigClient;
|
||||
|
||||
$config = new Config([
|
||||
'baseUrl' => 'http://localhost:8080',
|
||||
'appName' => 'kenaito-springboot-demo',
|
||||
'env' => 'daily',
|
||||
'syncInterval' => 30,
|
||||
]);
|
||||
|
||||
$client = new ConfigClient($config);
|
||||
|
||||
$client->onChange(function ($changed) {
|
||||
foreach ($changed as $k => $v) {
|
||||
echo "config changed: $k = $v\n";
|
||||
}
|
||||
});
|
||||
|
||||
$client->start();
|
||||
$value = $client->get('database.host');
|
||||
$all = $client->getAll();
|
||||
```
|
||||
|
||||
### .NET 客户端
|
||||
|
||||
适用于 .NET 6.0+ 项目。
|
||||
|
||||
**1. 引入依赖**
|
||||
|
||||
```xml
|
||||
|
||||
<ProjectReference Include="..\kenaito-conf-dotnet-client\KenaitoConf.csproj"/>
|
||||
```
|
||||
|
||||
**2. 使用**
|
||||
|
||||
```csharp
|
||||
using KenaitoConf;
|
||||
|
||||
var config = new Config
|
||||
{
|
||||
BaseUrl = "http://localhost:8080",
|
||||
AppName = "kenaito-springboot-demo",
|
||||
Env = "daily",
|
||||
SyncInterval = 30,
|
||||
};
|
||||
|
||||
var client = new ConfigClient(config);
|
||||
client.OnChange(changed => {
|
||||
foreach (var kv in changed)
|
||||
Console.WriteLine($"config changed: {kv.Key} = {kv.Value}");
|
||||
});
|
||||
|
||||
await client.StartAsync();
|
||||
var value = client.Get("database.host");
|
||||
```
|
||||
|
||||
### C 客户端
|
||||
|
||||
适用于嵌入式或高性能场景,依赖 libcurl。
|
||||
|
||||
**编译**
|
||||
|
||||
```bash
|
||||
gcc -o demo kenaito-conf-c-client/main.c -lcurl
|
||||
```
|
||||
|
||||
### C++ 客户端
|
||||
|
||||
依赖 libcurl,C++17。
|
||||
|
||||
**编译**
|
||||
|
||||
```bash
|
||||
g++ -std=c++17 -o demo kenaito-conf-cpp-client/main.cpp -lcurl -lpthread
|
||||
```
|
||||
|
||||
### Lua 客户端
|
||||
|
||||
适用于 Lua 5.3+ 项目(如 OpenResty/Nginx 网关)。
|
||||
|
||||
**1. 安装**
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-lua-client
|
||||
luarocks install kenaito-conf-client-1.0.0-1.rockspec
|
||||
```
|
||||
|
||||
**2. 使用**
|
||||
|
||||
```lua
|
||||
local ConfigClient = require("kenaito-conf")
|
||||
|
||||
local config = {
|
||||
baseUrl = "http://localhost:8080",
|
||||
appName = "kenaito-springboot-demo",
|
||||
env = "daily",
|
||||
syncInterval = 30,
|
||||
}
|
||||
|
||||
local client = ConfigClient.new(config)
|
||||
client:on_change(function(changed)
|
||||
for k, v in pairs(changed) do
|
||||
print("config changed: " .. k .. " = " .. v)
|
||||
end
|
||||
end)
|
||||
client:start()
|
||||
```
|
||||
|
||||
### Kotlin 客户端
|
||||
|
||||
适用于 Kotlin 1.9+ 项目。
|
||||
|
||||
**1. 编译**
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-kotlin-client
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
**2. 使用**
|
||||
|
||||
```kotlin
|
||||
import kenait.conf.Config
|
||||
import kenait.conf.ConfigClient
|
||||
|
||||
val config = Config(
|
||||
baseUrl = "http://localhost:8080",
|
||||
appName = "kenaito-springboot-demo",
|
||||
env = "daily",
|
||||
syncInterval = 30,
|
||||
)
|
||||
|
||||
val client = ConfigClient(config)
|
||||
client.onChange { changed ->
|
||||
changed.forEach { (k, v) -> println("config changed: $k = $v") }
|
||||
}
|
||||
client.start()
|
||||
val value = client.get("database.host")
|
||||
```
|
||||
|
||||
### Rust 客户端
|
||||
|
||||
适用于 Rust 1.70+ 项目。
|
||||
|
||||
**1. 在 Cargo.toml 中添加**
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
kenaito-conf = { path = "../kenaito-conf-rust-client" }
|
||||
```
|
||||
|
||||
**2. 使用**
|
||||
|
||||
```rust
|
||||
use kenaito_conf::{Config, ConfigClient};
|
||||
|
||||
let config = Config {
|
||||
base_url: "http://localhost:8080".to_string(),
|
||||
app_name: "kenaito-springboot-demo".to_string(),
|
||||
env: "daily".to_string(),
|
||||
sync_interval: 30,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = ConfigClient::new(config);
|
||||
client.on_change(|changed| {
|
||||
for (k, v) in changed { println!("config changed: {} = {}", k, v); }
|
||||
});
|
||||
client.start().await.unwrap();
|
||||
let value = client.get("database.host");
|
||||
```
|
||||
|
||||
### Ruby 客户端
|
||||
|
||||
适用于 Ruby 2.7+ 项目。
|
||||
|
||||
**1. 安装**
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-ruby-client
|
||||
gem build kenaito-conf.gemspec
|
||||
gem install kenaito-conf-1.0.0.gem
|
||||
```
|
||||
|
||||
**2. 使用**
|
||||
|
||||
```ruby
|
||||
require 'kenaito_conf'
|
||||
|
||||
config = KenaitoConf::Config.new(
|
||||
base_url: 'http://localhost:8080',
|
||||
app_name: 'kenaito-springboot-demo',
|
||||
env: 'daily',
|
||||
sync_interval: 30
|
||||
)
|
||||
|
||||
client = KenaitoConf::ConfigClient.new(config)
|
||||
client.on_change do |changed|
|
||||
changed.each { |k, v| puts "config changed: #{k} = #{v}" }
|
||||
end
|
||||
client.start
|
||||
value = client.get('database.host')
|
||||
```
|
||||
|
||||
## API 概览
|
||||
|
||||
### 管理端 API(需 JWT 认证)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
std::cout << "Kenaito Config Center C++ Client Demo" << std::endl;
|
||||
std::cout << "AppName: kenaito-springboot-demo, Env: daily" << std::endl;
|
||||
std::cout << "See ../kenaito-conf-cpp-client/main.cpp for full implementation" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
# Kenaito Config Center C++ Client
|
||||
|
||||
C++ 语言的配置中心客户端 SDK,使用 libcurl。
|
||||
|
||||
## 依赖
|
||||
|
||||
- libcurl
|
||||
- C++17
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
brew install curl
|
||||
```
|
||||
|
||||
### Ubuntu/Debian
|
||||
|
||||
```bash
|
||||
apt-get install libcurl4-openssl-dev
|
||||
```
|
||||
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-cpp-client-demo
|
||||
g++ -std=c++17 -o demo ../kenaito-conf-cpp-client/main.cpp -lcurl -lpthread
|
||||
./demo
|
||||
```
|
||||
|
||||
## 功能
|
||||
|
||||
- HTTP POST 拉取配置
|
||||
- 定时同步
|
||||
- 变更回调
|
||||
- 线程安全
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `baseUrl` | 配置中心地址 | 必填 |
|
||||
| `appName` | 应用名称 | 必填 |
|
||||
| `env` | 环境 | `dev` |
|
||||
| `version` | 配置版本 | `latest` |
|
||||
| `token` | 认证令牌 | 空 |
|
||||
| `syncInterval` | 同步间隔(秒) | `60` |
|
||||
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
|
||||
| `cacheDir` | 本地缓存目录 | `.` |
|
||||
@@ -1,106 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace kenaito_conf {
|
||||
|
||||
struct Config {
|
||||
std::string baseUrl;
|
||||
std::string appName;
|
||||
std::string env = "dev";
|
||||
std::string version = "latest";
|
||||
std::string token;
|
||||
int syncInterval = 60;
|
||||
bool ignoreGlobal = false;
|
||||
std::string cacheDir = ".";
|
||||
};
|
||||
|
||||
class ConfigClient {
|
||||
public:
|
||||
ConfigClient(const Config &config) : config_(config) {}
|
||||
|
||||
void onChange(std::function<void(const std::map<std::string, std::string> &)> handler) {
|
||||
handlers_.push_back(handler);
|
||||
}
|
||||
|
||||
std::string get(const std::string &key) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = configs_.find(key);
|
||||
return it != configs_.end() ? it->second : "";
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> getAll() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return configs_;
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (started_) return;
|
||||
started_ = true;
|
||||
fetchAll();
|
||||
running_ = true;
|
||||
syncThread_ = std::thread([this]() {
|
||||
while (running_) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(config_.syncInterval));
|
||||
if (!running_) break;
|
||||
try { fetchChanged(); } catch (...) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void stop() {
|
||||
running_ = false;
|
||||
if (syncThread_.joinable()) syncThread_.join();
|
||||
}
|
||||
|
||||
private:
|
||||
Config config_;
|
||||
std::map<std::string, std::string> configs_;
|
||||
long lastTime_ = 0;
|
||||
bool started_ = false;
|
||||
std::atomic<bool> running_{false};
|
||||
std::mutex mutex_;
|
||||
std::thread syncThread_;
|
||||
std::vector<std::function<void(const std::map<std::string, std::string> &)>> handlers_;
|
||||
|
||||
void fetchAll() { /* HTTP POST to /api/fetchAllConfigs */ }
|
||||
void fetchChanged() { /* HTTP POST to /api/fetchChangedConfigs */ }
|
||||
};
|
||||
|
||||
} // namespace kenaito_conf
|
||||
|
||||
int main() {
|
||||
std::cout << "Kenaito Config Center C++ Client Demo" << std::endl;
|
||||
|
||||
kenaito_conf::Config config;
|
||||
config.baseUrl = "http://localhost:8080";
|
||||
config.appName = "kenaito-springboot-demo";
|
||||
config.env = "daily";
|
||||
config.version = "latest";
|
||||
config.syncInterval = 30;
|
||||
|
||||
kenaito_conf::ConfigClient client(config);
|
||||
|
||||
client.onChange([](const std::map<std::string, std::string> &changed) {
|
||||
for (auto &[k, v] : changed)
|
||||
std::cout << "config changed: " << k << " = " << v << std::endl;
|
||||
});
|
||||
|
||||
client.start();
|
||||
std::cout << "database.host = " << client.get("database.host") << std::endl;
|
||||
std::cout << "Listening for changes... (press Ctrl+C to stop)" << std::endl;
|
||||
|
||||
std::cin.get();
|
||||
client.stop();
|
||||
return 0;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\kenaito-conf-dotnet-client\KenaitoConf.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,65 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using KenaitoConf;
|
||||
|
||||
var configValues = new Dictionary<string, string>
|
||||
{
|
||||
{ "database.host", Environment.GetEnvironmentVariable("DATABASE_HOST") ?? "192.168.1.1" },
|
||||
{ "database.port", Environment.GetEnvironmentVariable("DATABASE_PORT") ?? "8888" },
|
||||
{ "database.name", Environment.GetEnvironmentVariable("DATABASE_NAME") ?? "xxxx" },
|
||||
{ "database.user", Environment.GetEnvironmentVariable("DATABASE_USER") ?? "xxxx" },
|
||||
{ "database.password", Environment.GetEnvironmentVariable("DATABASE_PASSWORD") ?? "xxxxxxxx" },
|
||||
};
|
||||
|
||||
var config = new Config
|
||||
{
|
||||
BaseUrl = Environment.GetEnvironmentVariable("KENAITO_BASE_URL") ?? "http://localhost:8080",
|
||||
AppName = Environment.GetEnvironmentVariable("KENAITO_APP_NAME") ?? "kenaito-springboot-demo",
|
||||
Env = Environment.GetEnvironmentVariable("KENAITO_PROFILE") ?? "daily",
|
||||
Version = Environment.GetEnvironmentVariable("KENAITO_VERSION") ?? "latest",
|
||||
Token = Environment.GetEnvironmentVariable("KENAITO_TOKEN") ?? "",
|
||||
SyncInterval = int.Parse(Environment.GetEnvironmentVariable("KENAITO_SYNC_INTERVAL") ?? "10"),
|
||||
CacheDir = Environment.GetEnvironmentVariable("KENAITO_CACHE_DIR") ?? ".",
|
||||
};
|
||||
|
||||
var client = new ConfigClient(config);
|
||||
|
||||
client.OnChange(changed =>
|
||||
{
|
||||
foreach (var kv in changed)
|
||||
{
|
||||
if (configValues.ContainsKey(kv.Key))
|
||||
{
|
||||
configValues[kv.Key] = kv.Value;
|
||||
Console.Error.WriteLine($"config updated: {kv.Key} = {kv.Value}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await client.StartAsync();
|
||||
var remote = client.GetAll();
|
||||
foreach (var key in new List<string>(configValues.Keys))
|
||||
{
|
||||
if (remote.ContainsKey(key))
|
||||
configValues[key] = remote[key];
|
||||
}
|
||||
Console.Error.WriteLine($"loaded {remote.Count} configs from remote");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine($"failed to init config client: {e.Message}");
|
||||
}
|
||||
|
||||
Console.WriteLine("Current configs:");
|
||||
foreach (var kv in configValues)
|
||||
Console.WriteLine($" {kv.Key} = {kv.Value}");
|
||||
|
||||
Console.WriteLine("\nListening for config changes... (press Ctrl+C to stop)");
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
|
||||
try { await System.Threading.Tasks.Task.Delay(System.Threading.Timeout.Infinite, cts.Token); }
|
||||
catch (System.Threading.Tasks.TaskCanceledException) { }
|
||||
client.Stop();
|
||||
Console.WriteLine("Shutting down...");
|
||||
@@ -1,219 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace KenaitoConf
|
||||
{
|
||||
public class Config
|
||||
{
|
||||
public string BaseUrl { get; set; } = "";
|
||||
public string AppName { get; set; } = "";
|
||||
public string Env { get; set; } = "dev";
|
||||
public string Version { get; set; } = "latest";
|
||||
public string Token { get; set; } = "";
|
||||
public int SyncInterval { get; set; } = 60;
|
||||
public bool IgnoreGlobal { get; set; } = false;
|
||||
public string CacheDir { get; set; } = ".";
|
||||
}
|
||||
|
||||
public class Cache
|
||||
{
|
||||
private readonly string _cacheDir;
|
||||
private const string CacheFile = "config-cache.json";
|
||||
|
||||
public Cache(string cacheDir = ".")
|
||||
{
|
||||
_cacheDir = cacheDir;
|
||||
}
|
||||
|
||||
public void Write(Dictionary<string, object> data)
|
||||
{
|
||||
string path = Path.Combine(_cacheDir, CacheFile);
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
|
||||
public Dictionary<string, object> Read()
|
||||
{
|
||||
string path = Path.Combine(_cacheDir, CacheFile);
|
||||
if (!File.Exists(path)) return null;
|
||||
string content = File.ReadAllText(path);
|
||||
return JsonSerializer.Deserialize<Dictionary<string, object>>(content);
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigClient
|
||||
{
|
||||
private readonly Config _config;
|
||||
private readonly Cache _cache;
|
||||
private readonly HttpClient _http = new();
|
||||
private readonly Dictionary<string, string> _configs = new();
|
||||
private long _lastTime = 0;
|
||||
private bool _started = false;
|
||||
private CancellationTokenSource _cts;
|
||||
private readonly List<Action<Dictionary<string, string>>> _handlers = new();
|
||||
|
||||
public ConfigClient(Config config)
|
||||
{
|
||||
_config = config;
|
||||
_cache = new Cache(config.CacheDir);
|
||||
_http.Timeout = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
|
||||
public void OnChange(Action<Dictionary<string, string>> handler)
|
||||
{
|
||||
_handlers.Add(handler);
|
||||
}
|
||||
|
||||
public string Get(string key)
|
||||
{
|
||||
return _configs.TryGetValue(key, out var value) ? value : null;
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetAll()
|
||||
{
|
||||
return new Dictionary<string, string>(_configs);
|
||||
}
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
if (_started) return;
|
||||
_started = true;
|
||||
|
||||
try
|
||||
{
|
||||
await FetchAllAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine($"[ConfigClient] fetch all configs error: {e.Message}, trying cache");
|
||||
var cached = _cache.Read();
|
||||
if (cached != null)
|
||||
{
|
||||
ApplyConfigs(cached);
|
||||
Console.Error.WriteLine($"[ConfigClient] loaded {_configs.Count} configs from cache");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"fetch configs failed and no cache: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = SyncLoopAsync(_cts.Token);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
|
||||
private async Task SyncLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(_config.SyncInterval * 1000, ct);
|
||||
try
|
||||
{
|
||||
await FetchChangedAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.Error.WriteLine($"[ConfigClient] sync error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchAllAsync()
|
||||
{
|
||||
string url = $"{_config.BaseUrl}/api/fetchAllConfigs";
|
||||
var payload = new
|
||||
{
|
||||
appName = _config.AppName,
|
||||
env = _config.Env,
|
||||
version = _config.Version,
|
||||
ignoreGlobal = _config.IgnoreGlobal,
|
||||
token = _config.Token
|
||||
};
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
var resp = await _http.PostAsync(url, content);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
string body = await resp.Content.ReadAsStringAsync();
|
||||
var data = JsonSerializer.Deserialize<Dictionary<string, object>>(body);
|
||||
|
||||
if (data.ContainsKey("code"))
|
||||
{
|
||||
var code = Convert.ToInt32(data["code"]);
|
||||
if (code != 0)
|
||||
throw new Exception($"server error: {(data.ContainsKey("msg") ? data["msg"] : "unknown")}");
|
||||
}
|
||||
|
||||
if (data.TryGetValue("latest.fetch.timestamp", out var ts))
|
||||
_lastTime = Convert.ToInt64(ts);
|
||||
|
||||
_cache.Write(data);
|
||||
ApplyConfigs(data);
|
||||
Console.Error.WriteLine($"[ConfigClient] fetched {_configs.Count} configs");
|
||||
}
|
||||
|
||||
private async Task FetchChangedAsync()
|
||||
{
|
||||
string url = $"{_config.BaseUrl}/api/fetchChangedConfigs";
|
||||
var payload = new
|
||||
{
|
||||
appName = _config.AppName,
|
||||
env = _config.Env,
|
||||
version = _config.Version,
|
||||
lastTime = _lastTime,
|
||||
ignoreGlobal = _config.IgnoreGlobal,
|
||||
token = _config.Token
|
||||
};
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
var resp = await _http.PostAsync(url, content);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
string body = await resp.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<Dictionary<string, object>>(body);
|
||||
|
||||
if (!result.ContainsKey("code") || Convert.ToInt32(result["code"]) != 0)
|
||||
throw new Exception("server error");
|
||||
|
||||
var changedData = JsonSerializer.Deserialize<Dictionary<string, object>>(result["data"].ToString());
|
||||
if (changedData.TryGetValue("latest.fetch.timestamp", out var ts))
|
||||
{
|
||||
_lastTime = Convert.ToInt64(ts);
|
||||
changedData.Remove("latest.fetch.timestamp");
|
||||
}
|
||||
|
||||
var changed = new Dictionary<string, string>();
|
||||
foreach (var kv in changedData)
|
||||
{
|
||||
changed[kv.Key] = kv.Value?.ToString() ?? "";
|
||||
}
|
||||
|
||||
foreach (var kv in changed)
|
||||
_configs[kv.Key] = kv.Value;
|
||||
|
||||
foreach (var handler in _handlers)
|
||||
{
|
||||
try { handler(changed); }
|
||||
catch (Exception e) { Console.Error.WriteLine($"[ConfigClient] handler error: {e.Message}"); }
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyConfigs(Dictionary<string, object> data)
|
||||
{
|
||||
foreach (var kv in data)
|
||||
{
|
||||
if (kv.Key.StartsWith("kenaito.") || kv.Key == "latest.fetch.timestamp")
|
||||
continue;
|
||||
if (kv.Value is JsonElement)
|
||||
continue;
|
||||
_configs[kv.Key] = kv.Value?.ToString() ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>KenaitoConf</RootNamespace>
|
||||
<AssemblyName>KenaitoConf</AssemblyName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,76 +0,0 @@
|
||||
# Kenaito Config Center .NET Client
|
||||
|
||||
.NET 语言的配置中心客户端 SDK,支持 .NET 6.0+。
|
||||
|
||||
## 依赖
|
||||
|
||||
- .NET 6.0+
|
||||
- System.Text.Json(内置)
|
||||
|
||||
## 安装
|
||||
|
||||
### 项目引用
|
||||
|
||||
在 `.csproj` 中添加:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\kenaito-conf-dotnet-client\KenaitoConf.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### NuGet 包(待发布)
|
||||
|
||||
```bash
|
||||
dotnet add package KenaitoConf
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```csharp
|
||||
using KenaitoConf;
|
||||
|
||||
var config = new Config
|
||||
{
|
||||
BaseUrl = "http://localhost:8080",
|
||||
AppName = "kenaito-springboot-demo",
|
||||
Env = "daily",
|
||||
Version = "latest",
|
||||
SyncInterval = 30,
|
||||
};
|
||||
|
||||
var client = new ConfigClient(config);
|
||||
|
||||
client.OnChange(changed =>
|
||||
{
|
||||
foreach (var kv in changed)
|
||||
Console.WriteLine($"config changed: {kv.Key} = {kv.Value}");
|
||||
});
|
||||
|
||||
await client.StartAsync();
|
||||
|
||||
var value = client.Get("database.host");
|
||||
var all = client.GetAll();
|
||||
|
||||
client.Stop();
|
||||
```
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `BaseUrl` | 配置中心地址 | 必填 |
|
||||
| `AppName` | 应用名称 | 必填 |
|
||||
| `Env` | 环境 | `dev` |
|
||||
| `Version` | 配置版本 | `latest` |
|
||||
| `Token` | 认证令牌 | 空 |
|
||||
| `SyncInterval` | 同步间隔(秒) | `60` |
|
||||
| `IgnoreGlobal` | 是否忽略全局配置 | `false` |
|
||||
| `CacheDir` | 本地缓存目录 | `.` |
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-dotnet-client-demo
|
||||
dotnet run
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.0"
|
||||
application
|
||||
}
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation(files("../kenaito-conf-kotlin-client/build/libs/kenaito-conf-kotlin-client-1.0.0.jar"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
}
|
||||
|
||||
application { mainClass.set("demo.AppKt") }
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = "kenaito-conf-kotlin-client-demo"
|
||||
@@ -1,49 +0,0 @@
|
||||
import kenait.conf.Config
|
||||
import kenait.conf.ConfigClient
|
||||
|
||||
fun main() {
|
||||
val configValues = mutableMapOf(
|
||||
"database.host" to (System.getenv("DATABASE_HOST") ?: "192.168.1.1"),
|
||||
"database.port" to (System.getenv("DATABASE_PORT") ?: "8888"),
|
||||
"database.name" to (System.getenv("DATABASE_NAME") ?: "xxxx"),
|
||||
"database.user" to (System.getenv("DATABASE_USER") ?: "xxxx"),
|
||||
"database.password" to (System.getenv("DATABASE_PASSWORD") ?: "xxxxxxxx"),
|
||||
)
|
||||
|
||||
val config = Config(
|
||||
baseUrl = System.getenv("KENAITO_BASE_URL") ?: "http://localhost:8080",
|
||||
appName = System.getenv("KENAITO_APP_NAME") ?: "kenaito-springboot-demo",
|
||||
env = System.getenv("KENAITO_PROFILE") ?: "daily",
|
||||
version = System.getenv("KENAITO_VERSION") ?: "latest",
|
||||
token = System.getenv("KENAITO_TOKEN") ?: "",
|
||||
syncInterval = (System.getenv("KENAITO_SYNC_INTERVAL") ?: "10").toInt(),
|
||||
cacheDir = System.getenv("KENAITO_CACHE_DIR") ?: ".",
|
||||
)
|
||||
|
||||
val client = ConfigClient(config)
|
||||
|
||||
client.onChange { changed ->
|
||||
changed.forEach { (k, v) ->
|
||||
if (configValues.containsKey(k)) {
|
||||
configValues[k] = v
|
||||
System.err.println("config updated: $k = $v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
client.start()
|
||||
val remote = client.getAll()
|
||||
configValues.keys.forEach { k ->
|
||||
remote[k]?.let { configValues[k] = it }
|
||||
}
|
||||
System.err.println("loaded ${remote.size} configs from remote")
|
||||
} catch (e: Exception) {
|
||||
System.err.println("failed to init config client: ${e.message}")
|
||||
}
|
||||
|
||||
println("Current configs:")
|
||||
configValues.forEach { (k, v) -> println(" $k = $v") }
|
||||
println("\nListening for config changes... (press Ctrl+C to stop)")
|
||||
Thread.currentThread().join()
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
# Kenaito Config Center Kotlin Client
|
||||
|
||||
Kotlin 语言的配置中心客户端 SDK。
|
||||
|
||||
## 依赖
|
||||
|
||||
- Kotlin 1.9+
|
||||
- kotlinx-coroutines
|
||||
- kotlinx-serialization
|
||||
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-kotlin-client
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```kotlin
|
||||
import kenait.conf.Config
|
||||
import kenait.conf.ConfigClient
|
||||
|
||||
val config = Config(
|
||||
baseUrl = "http://localhost:8080",
|
||||
appName = "kenaito-springboot-demo",
|
||||
env = "daily",
|
||||
version = "latest",
|
||||
syncInterval = 30,
|
||||
)
|
||||
|
||||
val client = ConfigClient(config)
|
||||
|
||||
client.onChange { changed ->
|
||||
changed.forEach { (k, v) ->
|
||||
println("config changed: $k = $v")
|
||||
}
|
||||
}
|
||||
|
||||
client.start()
|
||||
|
||||
val value = client.get("database.host")
|
||||
val all = client.getAll()
|
||||
|
||||
client.stop()
|
||||
```
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `baseUrl` | 配置中心地址 | 必填 |
|
||||
| `appName` | 应用名称 | 必填 |
|
||||
| `env` | 环境 | `dev` |
|
||||
| `version` | 配置版本 | `latest` |
|
||||
| `token` | 认证令牌 | 空 |
|
||||
| `syncInterval` | 同步间隔(秒) | `60` |
|
||||
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
|
||||
| `cacheDir` | 本地缓存目录 | `.` |
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-kotlin-client-demo
|
||||
./gradlew run
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.0"
|
||||
kotlin("plugin.serialization") version "1.9.0"
|
||||
}
|
||||
|
||||
group = "cn.odboy"
|
||||
version = "1.0.0"
|
||||
|
||||
repositories { mavenCentral() }
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = "kenaito-conf-kotlin-client"
|
||||
@@ -1,107 +0,0 @@
|
||||
package kenaito.conf
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Serializable
|
||||
data class Config(
|
||||
val baseUrl: String = "",
|
||||
val appName: String = "",
|
||||
val env: String = "dev",
|
||||
val version: String = "latest",
|
||||
val token: String = "",
|
||||
val syncInterval: Int = 60,
|
||||
val ignoreGlobal: Boolean = false,
|
||||
val cacheDir: String = "."
|
||||
)
|
||||
|
||||
class Cache(private val cacheDir: String) {
|
||||
private val cacheFile = File(cacheDir, "config-cache.json")
|
||||
fun write(data: String) { cacheFile.writeText(data) }
|
||||
fun read(): String? = if (cacheFile.exists()) cacheFile.readText() else null
|
||||
}
|
||||
|
||||
class ConfigClient(private val config: Config) {
|
||||
private val httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()
|
||||
private val configs = ConcurrentHashMap<String, String>()
|
||||
private var lastTime = 0L
|
||||
private var started = false
|
||||
private val handlers = mutableListOf<(Map<String, String>) -> Unit>()
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun onChange(handler: (Map<String, String>) -> Unit) { handlers.add(handler) }
|
||||
fun get(key: String): String? = configs[key]
|
||||
fun getAll(): Map<String, String> = HashMap(configs)
|
||||
|
||||
fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
try { fetchAll() } catch (e: Exception) {
|
||||
System.err.println("[ConfigClient] fetch all configs error: ${e.message}, trying cache")
|
||||
val cached = Cache(config.cacheDir).read()
|
||||
if (cached != null) {
|
||||
applyConfigs(cached)
|
||||
System.err.println("[ConfigClient] loaded ${configs.size} configs from cache")
|
||||
} else throw RuntimeException("fetch configs failed and no cache: ${e.message}")
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
while (true) {
|
||||
delay(config.syncInterval * 1000L)
|
||||
try { fetchChanged() } catch (e: Exception) {
|
||||
System.err.println("[ConfigClient] sync error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() { started = false }
|
||||
|
||||
private fun fetchAll() {
|
||||
val url = "${config.baseUrl}/api/fetchAllConfigs"
|
||||
val body = """{"appName":"${config.appName}","env":"${config.env}","version":"${config.version}","ignoreGlobal":${config.ignoreGlobal},"token":"${config.token}"}"""
|
||||
val req = HttpRequest.newBuilder().uri(URI.create(url)).POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.header("Content-Type", "application/json").timeout(Duration.ofSeconds(10)).build()
|
||||
val resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString())
|
||||
if (resp.statusCode() != 200) throw RuntimeException("HTTP ${resp.statusCode()}")
|
||||
val data = json.parseToJsonElement(resp.body()).jsonObject
|
||||
if (data.containsKey("code") && data["code"]!!.jsonPrimitive.int != 0)
|
||||
throw RuntimeException("server error: ${data["code"]}")
|
||||
data["latest.fetch.timestamp"]?.let { lastTime = it.jsonPrimitive.content.toLong() }
|
||||
Cache(config.cacheDir).write(resp.body())
|
||||
applyConfigs(resp.body())
|
||||
System.err.println("[ConfigClient] fetched ${configs.size} configs")
|
||||
}
|
||||
|
||||
private fun fetchChanged() {
|
||||
val url = "${config.baseUrl}/api/fetchChangedConfigs"
|
||||
val body = """{"appName":"${config.appName}","env":"${config.env}","version":"${config.version}","lastTime":$lastTime,"ignoreGlobal":${config.ignoreGlobal},"token":"${config.token}"}"""
|
||||
val req = HttpRequest.newBuilder().uri(URI.create(url)).POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.header("Content-Type", "application/json").timeout(Duration.ofSeconds(10)).build()
|
||||
val resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString())
|
||||
val result = json.parseToJsonElement(resp.body()).jsonObject
|
||||
if (!result.containsKey("code") || result["code"]!!.jsonPrimitive.int != 0)
|
||||
throw RuntimeException("server error")
|
||||
val changedData = result["data"]!!.jsonObject.toMutableMap()
|
||||
changedData.remove("latest.fetch.timestamp")?.let { lastTime = it.jsonPrimitive.content.toLong() }
|
||||
val changed = mutableMapOf<String, String>()
|
||||
changedData.forEach { (k, v) -> changed[k] = v.jsonPrimitive.content }
|
||||
configs.putAll(changed)
|
||||
handlers.forEach { try { it(changed) } catch (e: Exception) { System.err.println("[ConfigClient] handler error: ${e.message}") } }
|
||||
}
|
||||
|
||||
private fun applyConfigs(jsonStr: String) {
|
||||
val data = json.parseToJsonElement(jsonStr).jsonObject
|
||||
data.forEach { (k, v) ->
|
||||
if (!k.startsWith("kenaito.") && k != "latest.fetch.timestamp")
|
||||
configs[k] = v.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
local ConfigClient = require("kenaito-conf")
|
||||
|
||||
local config_values = {
|
||||
["database.host"] = os.getenv("DATABASE_HOST") or "192.168.1.1",
|
||||
["database.port"] = os.getenv("DATABASE_PORT") or "8888",
|
||||
["database.name"] = os.getenv("DATABASE_NAME") or "xxxx",
|
||||
["database.user"] = os.getenv("DATABASE_USER") or "xxxx",
|
||||
["database.password"] = os.getenv("DATABASE_PASSWORD") or "xxxxxxxx",
|
||||
}
|
||||
|
||||
local config = {
|
||||
baseUrl = os.getenv("KENAITO_BASE_URL") or "http://localhost:8080",
|
||||
appName = os.getenv("KENAITO_APP_NAME") or "kenaito-springboot-demo",
|
||||
env = os.getenv("KENAITO_PROFILE") or "daily",
|
||||
version = os.getenv("KENAITO_VERSION") or "latest",
|
||||
token = os.getenv("KENAITO_TOKEN") or "",
|
||||
syncInterval = tonumber(os.getenv("KENAITO_SYNC_INTERVAL") or "10"),
|
||||
cacheDir = os.getenv("KENAITO_CACHE_DIR") or ".",
|
||||
}
|
||||
|
||||
local client = ConfigClient.new(config)
|
||||
|
||||
client:on_change(function(changed)
|
||||
for k, v in pairs(changed) do
|
||||
if config_values[k] then
|
||||
config_values[k] = v
|
||||
print("config updated: " .. k .. " = " .. v)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
client:start()
|
||||
|
||||
local remote = client:get_all()
|
||||
for k, v in pairs(remote) do
|
||||
if config_values[k] then
|
||||
config_values[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
print("Current configs:")
|
||||
for k, v in pairs(config_values) do
|
||||
print(" " .. k .. " = " .. v)
|
||||
end
|
||||
|
||||
print("\nListening for config changes... (press Ctrl+C to stop)")
|
||||
while true do
|
||||
os.execute("sleep 1")
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
# Kenaito Config Center Lua Client
|
||||
|
||||
Lua 语言的配置中心客户端 SDK。
|
||||
|
||||
## 依赖
|
||||
|
||||
- Lua 5.3+
|
||||
- LuaSocket
|
||||
- lua-cjson
|
||||
- lua-openssl
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
luarocks install kenaito-conf-client-1.0.0-1.rockspec
|
||||
```
|
||||
|
||||
或手动安装依赖:
|
||||
|
||||
```bash
|
||||
luarocks install luasocket
|
||||
luarocks install lua-cjson
|
||||
luarocks install lua-openssl
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```lua
|
||||
local ConfigClient = require("kenaito-conf")
|
||||
|
||||
local config = {
|
||||
baseUrl = "http://localhost:8080",
|
||||
appName = "kenaito-springboot-demo",
|
||||
env = "daily",
|
||||
version = "latest",
|
||||
syncInterval = 30,
|
||||
}
|
||||
|
||||
local client = ConfigClient.new(config)
|
||||
|
||||
client:on_change(function(changed)
|
||||
for k, v in pairs(changed) do
|
||||
print("config changed: " .. k .. " = " .. v)
|
||||
end
|
||||
end)
|
||||
|
||||
client:start()
|
||||
|
||||
local value = client:get("database.host")
|
||||
local all = client:get_all()
|
||||
|
||||
client:stop()
|
||||
```
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `baseUrl` | 配置中心地址 | 必填 |
|
||||
| `appName` | 应用名称 | 必填 |
|
||||
| `env` | 环境 | `dev` |
|
||||
| `version` | 配置版本 | `latest` |
|
||||
| `token` | 认证令牌 | 空 |
|
||||
| `syncInterval` | 同步间隔(秒) | `60` |
|
||||
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
|
||||
| `cacheDir` | 本地缓存目录 | `.` |
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-lua-client-demo
|
||||
lua app.lua
|
||||
```
|
||||
@@ -1,84 +0,0 @@
|
||||
local http = require("socket.http")
|
||||
local ltn12 = require("ltn12")
|
||||
local cjson = require("cjson")
|
||||
|
||||
local ConfigClient = {}
|
||||
ConfigClient.__index = ConfigClient
|
||||
|
||||
function ConfigClient.new(config)
|
||||
local self = setmetatable({}, ConfigClient)
|
||||
self.config = config or {}
|
||||
self.config.baseUrl = self.config.baseUrl or ""
|
||||
self.config.appName = self.config.appName or ""
|
||||
self.config.env = self.config.env or "dev"
|
||||
self.config.version = self.config.version or "latest"
|
||||
self.config.token = self.config.token or ""
|
||||
self.config.syncInterval = self.config.syncInterval or 60
|
||||
self.config.ignoreGlobal = self.config.ignoreGlobal or false
|
||||
self.config.cacheDir = self.config.cacheDir or "."
|
||||
|
||||
self.configs = {}
|
||||
self.lastTime = 0
|
||||
self.handlers = {}
|
||||
return self
|
||||
end
|
||||
|
||||
function ConfigClient:on_change(handler)
|
||||
table.insert(self.handlers, handler)
|
||||
end
|
||||
|
||||
function ConfigClient:get(key)
|
||||
return self.configs[key]
|
||||
end
|
||||
|
||||
function ConfigClient:get_all()
|
||||
return self.configs
|
||||
end
|
||||
|
||||
function ConfigClient:start()
|
||||
-- Fetch all configs
|
||||
local url = self.config.baseUrl .. "/api/fetchAllConfigs"
|
||||
local payload = cjson.encode({
|
||||
appName = self.config.appName,
|
||||
env = self.config.env,
|
||||
version = self.config.version,
|
||||
ignoreGlobal = self.config.ignoreGlobal,
|
||||
token = self.config.token
|
||||
})
|
||||
|
||||
local response_body = {}
|
||||
local res, code, response_headers = http.request{
|
||||
url = url,
|
||||
method = "POST",
|
||||
headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
["Content-Length"] = #payload
|
||||
},
|
||||
source = ltn12.source.string(payload),
|
||||
sink = ltn12.sink.table(response_body)
|
||||
}
|
||||
|
||||
if code == 200 then
|
||||
local data = cjson.decode(table.concat(response_body))
|
||||
|
||||
if data["latest.fetch.timestamp"] then
|
||||
self.lastTime = tonumber(data["latest.fetch.timestamp"])
|
||||
end
|
||||
|
||||
for k, v in pairs(data) do
|
||||
if not string.match(k, "^kenaito%.") and k ~= "latest.fetch.timestamp" then
|
||||
if type(v) == "string" then
|
||||
self.configs[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print("[ConfigClient] fetched " .. #self.configs .. " configs")
|
||||
end
|
||||
end
|
||||
|
||||
function ConfigClient:stop()
|
||||
-- Cleanup
|
||||
end
|
||||
|
||||
return ConfigClient
|
||||
@@ -1,22 +0,0 @@
|
||||
package = "kenaito-conf-client"
|
||||
version = "1.0.0-1"
|
||||
source = {
|
||||
url = "git+https://gitea.odboy.cn/odboy/kenaito-conf.git"
|
||||
}
|
||||
description = {
|
||||
summary = "Kenaito Config Center Lua Client",
|
||||
homepage = "https://gitea.odboy.cn/odboy/kenaito-conf",
|
||||
license = "MIT"
|
||||
}
|
||||
dependencies = {
|
||||
"lua >= 5.3",
|
||||
"luasocket",
|
||||
"lua-cjson",
|
||||
"lua-openssl"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
["kenaito-conf"] = "client.lua"
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../kenaito-conf-php-client/autoload.php';
|
||||
|
||||
use KenaitoConf\Config;
|
||||
use KenaitoConf\ConfigClient;
|
||||
|
||||
$configValues = [
|
||||
'database.host' => getenv('DATABASE_HOST') ?: '192.168.1.1',
|
||||
'database.port' => getenv('DATABASE_PORT') ?: '8888',
|
||||
'database.name' => getenv('DATABASE_NAME') ?: 'xxxx',
|
||||
'database.user' => getenv('DATABASE_USER') ?: 'xxxx',
|
||||
'database.password' => getenv('DATABASE_PASSWORD') ?: 'xxxxxxxx',
|
||||
];
|
||||
|
||||
$config = new Config([
|
||||
'baseUrl' => getenv('KENAITO_BASE_URL') ?: 'http://localhost:8080',
|
||||
'appName' => getenv('KENAITO_APP_NAME') ?: 'kenaito-springboot-demo',
|
||||
'env' => getenv('KENAITO_PROFILE') ?: 'daily',
|
||||
'version' => getenv('KENAITO_VERSION') ?: 'latest',
|
||||
'token' => getenv('KENAITO_TOKEN') ?: '',
|
||||
'syncInterval' => (int)(getenv('KENAITO_SYNC_INTERVAL') ?: 10),
|
||||
'cacheDir' => getenv('KENAITO_CACHE_DIR') ?: '.',
|
||||
]);
|
||||
|
||||
$client = new ConfigClient($config);
|
||||
|
||||
$client->onChange(function ($changed) use (&$configValues) {
|
||||
foreach ($changed as $k => $v) {
|
||||
if (isset($configValues[$k])) {
|
||||
$configValues[$k] = $v;
|
||||
error_log("config updated: {$k} = {$v}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
$client->start();
|
||||
$remote = $client->getAll();
|
||||
foreach ($configValues as $k => &$v) {
|
||||
if (isset($remote[$k])) {
|
||||
$v = $remote[$k];
|
||||
}
|
||||
}
|
||||
error_log("loaded " . count($remote) . " configs from remote");
|
||||
} catch (Exception $e) {
|
||||
error_log("failed to init config client: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$port = (int)(getenv('SERVER_PORT') ?: 8084);
|
||||
|
||||
echo "Server started on port {$port}\n";
|
||||
echo "Current configs:\n";
|
||||
foreach ($configValues as $k => $v) {
|
||||
echo " {$k} = {$v}\n";
|
||||
}
|
||||
|
||||
while (true) {
|
||||
pcntl_signal_dispatch();
|
||||
usleep(100000);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
# Kenaito Config Center PHP Client
|
||||
|
||||
PHP 语言的配置中心客户端 SDK。
|
||||
|
||||
## 依赖
|
||||
|
||||
- PHP 8.0+
|
||||
- ext-curl
|
||||
- ext-openssl
|
||||
- ext-pcntl
|
||||
|
||||
## 安装
|
||||
|
||||
### Composer
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-php-client
|
||||
composer install
|
||||
```
|
||||
|
||||
或在项目中引用:
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"kenaito/conf-client": "*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../kenaito-conf-php-client"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 手动引入
|
||||
|
||||
```php
|
||||
require_once 'kenaito-conf-php-client/autoload.php';
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```php
|
||||
use KenaitoConf\Config;
|
||||
use KenaitoConf\ConfigClient;
|
||||
|
||||
$config = new Config([
|
||||
'baseUrl' => 'http://localhost:8080',
|
||||
'appName' => 'kenaito-springboot-demo',
|
||||
'env' => 'daily',
|
||||
'version' => 'latest',
|
||||
'syncInterval' => 30,
|
||||
]);
|
||||
|
||||
$client = new ConfigClient($config);
|
||||
|
||||
$client->onChange(function ($changed) {
|
||||
foreach ($changed as $k => $v) {
|
||||
echo "config changed: $k = $v\n";
|
||||
}
|
||||
});
|
||||
|
||||
$client->start();
|
||||
|
||||
$value = $client->get('database.host');
|
||||
$all = $client->getAll();
|
||||
|
||||
$client->stop();
|
||||
```
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `baseUrl` | 配置中心地址 | 必填 |
|
||||
| `appName` | 应用名称 | 必填 |
|
||||
| `env` | 环境 | `dev` |
|
||||
| `version` | 配置版本 | `latest` |
|
||||
| `token` | 认证令牌 | 空 |
|
||||
| `syncInterval` | 同步间隔(秒) | `60` |
|
||||
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
|
||||
| `cacheDir` | 本地缓存目录 | `.` |
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-php-client-demo
|
||||
php app.php
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/src/Config.php';
|
||||
require_once __DIR__ . '/src/Crypto.php';
|
||||
require_once __DIR__ . '/src/Cache.php';
|
||||
require_once __DIR__ . '/src/ConfigClient.php';
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "kenaito/conf-client",
|
||||
"description": "Kenaito Config Center PHP Client",
|
||||
"type": "library",
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-pcntl": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"KenaitoConf\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace KenaitoConf;
|
||||
|
||||
class Cache
|
||||
{
|
||||
private string $cacheDir;
|
||||
private const CACHE_FILE = 'config-cache.json';
|
||||
|
||||
public function __construct(string $cacheDir = '.')
|
||||
{
|
||||
$this->cacheDir = $cacheDir;
|
||||
}
|
||||
|
||||
public function write(array $data): void
|
||||
{
|
||||
$path = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_FILE;
|
||||
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
public function read(): ?array
|
||||
{
|
||||
$path = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_FILE;
|
||||
if (!file_exists($path)) {
|
||||
return null;
|
||||
}
|
||||
$content = file_get_contents($path);
|
||||
return json_decode($content, true);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace KenaitoConf;
|
||||
|
||||
class Config
|
||||
{
|
||||
public string $baseUrl = '';
|
||||
public string $appName = '';
|
||||
public string $env = 'dev';
|
||||
public string $version = 'latest';
|
||||
public string $token = '';
|
||||
public int $syncInterval = 60;
|
||||
public bool $ignoreGlobal = false;
|
||||
public string $cacheDir = '.';
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
foreach ($options as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace KenaitoConf;
|
||||
|
||||
class ConfigClient
|
||||
{
|
||||
private Config $config;
|
||||
private Cache $cache;
|
||||
private array $configs = [];
|
||||
private int $lastTime = 0;
|
||||
private bool $started = false;
|
||||
private array $handlers = [];
|
||||
private $timer = null;
|
||||
|
||||
public function __construct(Config $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->cache = new Cache($config->cacheDir);
|
||||
}
|
||||
|
||||
public function onChange(callable $handler): void
|
||||
{
|
||||
$this->handlers[] = $handler;
|
||||
}
|
||||
|
||||
public function get(string $key): ?string
|
||||
{
|
||||
return $this->configs[$key] ?? null;
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->configs;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
if ($this->started) {
|
||||
return;
|
||||
}
|
||||
$this->started = true;
|
||||
|
||||
try {
|
||||
$this->fetchAll();
|
||||
} catch (\Exception $e) {
|
||||
error_log("[ConfigClient] fetch all configs error: {$e->getMessage()}, trying cache");
|
||||
$cached = $this->cache->read();
|
||||
if ($cached !== null) {
|
||||
$this->applyConfigs($cached);
|
||||
error_log("[ConfigClient] loaded " . count($this->configs) . " configs from cache");
|
||||
} else {
|
||||
throw new \RuntimeException("fetch configs failed and no cache: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->timer = pcntl_alarm(0);
|
||||
pcntl_signal(SIGALRM, [$this, 'syncTick']);
|
||||
pcntl_alarm($this->config->syncInterval);
|
||||
}
|
||||
|
||||
public function syncTick(): void
|
||||
{
|
||||
try {
|
||||
$this->fetchChanged();
|
||||
} catch (\Exception $e) {
|
||||
error_log("[ConfigClient] sync error: {$e->getMessage()}");
|
||||
}
|
||||
pcntl_alarm($this->config->syncInterval);
|
||||
}
|
||||
|
||||
public function stop(): void
|
||||
{
|
||||
if ($this->timer !== null) {
|
||||
pcntl_alarm(0);
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchAll(): void
|
||||
{
|
||||
$url = $this->config->baseUrl . '/api/fetchAllConfigs';
|
||||
$payload = [
|
||||
'appName' => $this->config->appName,
|
||||
'env' => $this->config->env,
|
||||
'version' => $this->config->version,
|
||||
'ignoreGlobal' => $this->config->ignoreGlobal,
|
||||
'token' => $this->config->token,
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new \RuntimeException("HTTP {$httpCode}");
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data['code']) && $data['code'] !== 0) {
|
||||
throw new \RuntimeException("server error: " . ($data['msg'] ?? 'unknown'));
|
||||
}
|
||||
|
||||
if (isset($data['latest.fetch.timestamp'])) {
|
||||
$this->lastTime = (int)$data['latest.fetch.timestamp'];
|
||||
}
|
||||
|
||||
$this->cache->write($data);
|
||||
$this->applyConfigs($data);
|
||||
error_log("[ConfigClient] fetched " . count($this->configs) . " configs");
|
||||
}
|
||||
|
||||
private function fetchChanged(): void
|
||||
{
|
||||
$url = $this->config->baseUrl . '/api/fetchChangedConfigs';
|
||||
$payload = [
|
||||
'appName' => $this->config->appName,
|
||||
'env' => $this->config->env,
|
||||
'version' => $this->config->version,
|
||||
'lastTime' => $this->lastTime,
|
||||
'ignoreGlobal' => $this->config->ignoreGlobal,
|
||||
'token' => $this->config->token,
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$result = json_decode($response, true);
|
||||
if (!isset($result['code']) || $result['code'] !== 0) {
|
||||
throw new \RuntimeException("server error code: " . ($result['code'] ?? 'unknown'));
|
||||
}
|
||||
|
||||
$changedData = $result['data'] ?? [];
|
||||
if (isset($changedData['latest.fetch.timestamp'])) {
|
||||
$this->lastTime = (int)$changedData['latest.fetch.timestamp'];
|
||||
unset($changedData['latest.fetch.timestamp']);
|
||||
}
|
||||
|
||||
$changed = [];
|
||||
foreach ($changedData as $k => $v) {
|
||||
$changed[$k] = $v;
|
||||
}
|
||||
|
||||
$this->configs = array_merge($this->configs, $changed);
|
||||
|
||||
foreach ($this->handlers as $handler) {
|
||||
try {
|
||||
$handler($changed);
|
||||
} catch (\Exception $e) {
|
||||
error_log("[ConfigClient] change handler error: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
error_log("[ConfigClient] " . count($changed) . " configs changed");
|
||||
}
|
||||
|
||||
private function applyConfigs(array $data): void
|
||||
{
|
||||
foreach ($data as $k => $v) {
|
||||
if (str_starts_with($k, 'kenaito.') || $k === 'latest.fetch.timestamp') {
|
||||
continue;
|
||||
}
|
||||
if (!is_string($v)) {
|
||||
continue;
|
||||
}
|
||||
$this->configs[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../kenaito-conf-ruby-client/lib/kenaito_conf'
|
||||
|
||||
config_values = {
|
||||
'database.host' => ENV['DATABASE_HOST'] || '192.168.1.1',
|
||||
'database.port' => ENV['DATABASE_PORT'] || '8888',
|
||||
'database.name' => ENV['DATABASE_NAME'] || 'xxxx',
|
||||
'database.user' => ENV['DATABASE_USER'] || 'xxxx',
|
||||
'database.password' => ENV['DATABASE_PASSWORD'] || 'xxxxxxxx'
|
||||
}
|
||||
|
||||
config = KenaitoConf::Config.new(
|
||||
base_url: ENV['KENAITO_BASE_URL'] || 'http://localhost:8080',
|
||||
app_name: ENV['KENAITO_APP_NAME'] || 'kenaito-springboot-demo',
|
||||
env: ENV['KENAITO_PROFILE'] || 'daily',
|
||||
version: ENV['KENAITO_VERSION'] || 'latest',
|
||||
token: ENV['KENAITO_TOKEN'] || '',
|
||||
sync_interval: (ENV['KENAITO_SYNC_INTERVAL'] || '30').to_i,
|
||||
cache_dir: ENV['KENAITO_CACHE_DIR'] || '.'
|
||||
)
|
||||
|
||||
client = KenaitoConf::ConfigClient.new(config)
|
||||
|
||||
client.on_change do |changed|
|
||||
changed.each do |k, v|
|
||||
if config_values.key?(k)
|
||||
config_values[k] = v
|
||||
warn "config updated: #{k} = #{v}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
client.start
|
||||
remote = client.get_all
|
||||
config_values.each_key do |k|
|
||||
config_values[k] = remote[k] if remote[k]
|
||||
end
|
||||
warn "loaded #{remote.size} configs from remote"
|
||||
rescue StandardError => e
|
||||
warn "failed to init config client: #{e.message}"
|
||||
end
|
||||
|
||||
puts 'Current configs:'
|
||||
config_values.each do |k, v|
|
||||
puts " #{k} = #{v}"
|
||||
end
|
||||
|
||||
puts "\nListening for config changes... (press Ctrl+C to stop)"
|
||||
sleep
|
||||
@@ -1,70 +0,0 @@
|
||||
# Kenaito Config Center Ruby Client
|
||||
|
||||
Ruby 语言的配置中心客户端 SDK。
|
||||
|
||||
## 依赖
|
||||
|
||||
- Ruby 2.7+
|
||||
- 标准库(net/http, json, openssl, base64)
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
gem build kenaito-conf.gemspec
|
||||
gem install kenaito-conf-1.0.0.gem
|
||||
```
|
||||
|
||||
或在 Gemfile 中添加:
|
||||
|
||||
```ruby
|
||||
gem 'kenaito-conf', path: '../kenaito-conf-ruby-client'
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```ruby
|
||||
require 'kenaito_conf'
|
||||
|
||||
config = KenaitoConf::Config.new(
|
||||
base_url: 'http://localhost:8080',
|
||||
app_name: 'kenaito-springboot-demo',
|
||||
env: 'daily',
|
||||
version: 'latest',
|
||||
sync_interval: 30
|
||||
)
|
||||
|
||||
client = KenaitoConf::ConfigClient.new(config)
|
||||
|
||||
client.on_change do |changed|
|
||||
changed.each do |k, v|
|
||||
puts "config changed: #{k} = #{v}"
|
||||
end
|
||||
end
|
||||
|
||||
client.start
|
||||
|
||||
value = client.get('database.host')
|
||||
all = client.get_all
|
||||
|
||||
client.stop
|
||||
```
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `base_url` | 配置中心地址 | 必填 |
|
||||
| `app_name` | 应用名称 | 必填 |
|
||||
| `env` | 环境 | `dev` |
|
||||
| `version` | 配置版本 | `latest` |
|
||||
| `token` | 认证令牌 | 空 |
|
||||
| `sync_interval` | 同步间隔(秒) | `60` |
|
||||
| `ignore_global` | 是否忽略全局配置 | `false` |
|
||||
| `cache_dir` | 本地缓存目录 | `.` |
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-ruby-client-demo
|
||||
ruby app.rb
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = 'kenaito-conf'
|
||||
spec.version = '1.0.0'
|
||||
spec.authors = ['odboy']
|
||||
spec.email = ['tianjun@odboy.cn']
|
||||
|
||||
spec.summary = 'Kenaito Config Center Ruby Client'
|
||||
spec.description = 'Ruby client for Kenaito Config Center'
|
||||
spec.homepage = 'https://gitea.odboy.cn/odboy/kenaito-conf'
|
||||
spec.license = 'MIT'
|
||||
|
||||
spec.required_ruby_version = '>= 2.7.0'
|
||||
|
||||
spec.files = Dir['lib/**/*']
|
||||
spec.require_paths = ['lib']
|
||||
end
|
||||
@@ -1,165 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
module KenaitoConf
|
||||
class Config
|
||||
attr_accessor :base_url, :app_name, :env, :version, :token, :sync_interval, :ignore_global, :cache_dir
|
||||
|
||||
def initialize(options = {})
|
||||
@base_url = options[:base_url] || ''
|
||||
@app_name = options[:app_name] || ''
|
||||
@env = options[:env] || 'dev'
|
||||
@version = options[:version] || 'latest'
|
||||
@token = options[:token] || ''
|
||||
@sync_interval = options[:sync_interval] || 60
|
||||
@ignore_global = options[:ignore_global] || false
|
||||
@cache_dir = options[:cache_dir] || '.'
|
||||
end
|
||||
end
|
||||
|
||||
class Cache
|
||||
CACHE_FILE = 'config-cache.json'
|
||||
|
||||
def initialize(cache_dir = '.')
|
||||
@cache_dir = cache_dir
|
||||
end
|
||||
|
||||
def write(data)
|
||||
File.write(File.join(@cache_dir, CACHE_FILE), JSON.pretty_generate(data))
|
||||
end
|
||||
|
||||
def read
|
||||
path = File.join(@cache_dir, CACHE_FILE)
|
||||
return nil unless File.exist?(path)
|
||||
JSON.parse(File.read(path))
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
class ConfigClient
|
||||
def initialize(config)
|
||||
@config = config
|
||||
@cache = Cache.new(config.cache_dir)
|
||||
@configs = {}
|
||||
@last_time = 0
|
||||
@handlers = []
|
||||
@started = false
|
||||
end
|
||||
|
||||
def on_change(&handler)
|
||||
@handlers << handler
|
||||
end
|
||||
|
||||
def get(key)
|
||||
@configs[key]
|
||||
end
|
||||
|
||||
def get_all
|
||||
@configs.dup
|
||||
end
|
||||
|
||||
def start
|
||||
return if @started
|
||||
@started = true
|
||||
|
||||
begin
|
||||
fetch_all
|
||||
rescue StandardError => e
|
||||
warn "[ConfigClient] fetch all configs error: #{e.message}, trying cache"
|
||||
cached = @cache.read
|
||||
if cached
|
||||
apply_configs(cached)
|
||||
warn "[ConfigClient] loaded #{@configs.size} configs from cache"
|
||||
else
|
||||
raise "fetch configs failed and no cache: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
Thread.new do
|
||||
loop do
|
||||
sleep @config.sync_interval
|
||||
begin
|
||||
fetch_changed
|
||||
rescue StandardError => e
|
||||
warn "[ConfigClient] sync error: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
@started = false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_all
|
||||
uri = URI("#{@config.base_url}/api/fetchAllConfigs")
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.read_timeout = 10
|
||||
|
||||
request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
|
||||
request.body = JSON.generate({
|
||||
appName: @config.app_name,
|
||||
env: @config.env,
|
||||
version: @config.version,
|
||||
ignoreGlobal: @config.ignore_global,
|
||||
token: @config.token
|
||||
})
|
||||
|
||||
response = http.request(request)
|
||||
raise "HTTP #{response.code}" unless response.code == '200'
|
||||
|
||||
data = JSON.parse(response.body)
|
||||
raise "server error: #{data['msg']}" if data['code'] && data['code'] != 0
|
||||
|
||||
@last_time = data['latest.fetch.timestamp'].to_i if data['latest.fetch.timestamp']
|
||||
|
||||
@cache.write(data)
|
||||
apply_configs(data)
|
||||
warn "[ConfigClient] fetched #{@configs.size} configs"
|
||||
end
|
||||
|
||||
def fetch_changed
|
||||
uri = URI("#{@config.base_url}/api/fetchChangedConfigs")
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.read_timeout = 10
|
||||
|
||||
request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
|
||||
request.body = JSON.generate({
|
||||
appName: @config.app_name,
|
||||
env: @config.env,
|
||||
version: @config.version,
|
||||
lastTime: @last_time,
|
||||
ignoreGlobal: @config.ignore_global,
|
||||
token: @config.token
|
||||
})
|
||||
|
||||
response = http.request(request)
|
||||
result = JSON.parse(response.body)
|
||||
raise "server error" if !result['code'] || result['code'] != 0
|
||||
|
||||
changed_data = result['data'] || {}
|
||||
@last_time = changed_data.delete('latest.fetch.timestamp').to_i if changed_data['latest.fetch.timestamp']
|
||||
|
||||
changed = {}
|
||||
changed_data.each do |k, v|
|
||||
changed[k] = v
|
||||
end
|
||||
|
||||
@configs.merge!(changed)
|
||||
@handlers.each { |h| h.call(changed) rescue warn "[ConfigClient] handler error: #{$!.message}" }
|
||||
end
|
||||
|
||||
def apply_configs(data)
|
||||
data.each do |k, v|
|
||||
next if k.start_with?('kenaito.') || k == 'latest.fetch.timestamp'
|
||||
next unless v.is_a?(String)
|
||||
@configs[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
[package]
|
||||
name = "kenaito-conf-demo"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
kenaito-conf = { path = "../kenaito-conf-rust-client" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -1,52 +0,0 @@
|
||||
use kenaito_conf::{Config, ConfigClient};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut config_values: HashMap<String, String> = HashMap::new();
|
||||
config_values.insert("database.host".to_string(), std::env::var("DATABASE_HOST").unwrap_or_else(|_| "192.168.1.1".to_string()));
|
||||
config_values.insert("database.port".to_string(), std::env::var("DATABASE_PORT").unwrap_or_else(|_| "8888".to_string()));
|
||||
config_values.insert("database.name".to_string(), std::env::var("DATABASE_NAME").unwrap_or_else(|_| "xxxx".to_string()));
|
||||
config_values.insert("database.user".to_string(), std::env::var("DATABASE_USER").unwrap_or_else(|_| "xxxx".to_string()));
|
||||
config_values.insert("database.password".to_string(), std::env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "xxxxxxxx".to_string()));
|
||||
|
||||
let config = Config {
|
||||
base_url: std::env::var("KENAITO_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()),
|
||||
app_name: std::env::var("KENAITO_APP_NAME").unwrap_or_else(|_| "kenaito-springboot-demo".to_string()),
|
||||
env: std::env::var("KENAITO_PROFILE").unwrap_or_else(|_| "daily".to_string()),
|
||||
version: std::env::var("KENAITO_VERSION").unwrap_or_else(|_| "latest".to_string()),
|
||||
token: std::env::var("KENAITO_TOKEN").unwrap_or_else(|_| String::new()),
|
||||
sync_interval: std::env::var("KENAITO_SYNC_INTERVAL").unwrap_or_else(|_| "30".to_string()).parse().unwrap_or(30),
|
||||
cache_dir: std::env::var("KENAITO_CACHE_DIR").unwrap_or_else(|_| ".".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = ConfigClient::new(config);
|
||||
|
||||
client.on_change(move |changed| {
|
||||
for (k, v) in changed.iter() {
|
||||
eprintln!("config changed: {} = {}", k, v);
|
||||
}
|
||||
});
|
||||
|
||||
match client.start().await {
|
||||
Ok(_) => {
|
||||
let remote = client.get_all();
|
||||
eprintln!("loaded {} configs from remote", remote.len());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to init config client: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Current configs:");
|
||||
for (k, v) in &config_values {
|
||||
println!(" {} = {}", k, v);
|
||||
}
|
||||
|
||||
println!("\nListening for config changes... (press Ctrl+C to stop)");
|
||||
tokio::signal::ctrl_c().await?;
|
||||
println!("Shutting down...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "kenaito-conf"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
authors = ["odboy <tianjun@odboy.cn>"]
|
||||
description = "Kenaito Config Center Rust Client"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
md-5 = "0.10"
|
||||
aes = "0.8"
|
||||
cbc = "0.1"
|
||||
base64 = "0.21"
|
||||
@@ -1,70 +0,0 @@
|
||||
# Kenaito Config Center Rust Client
|
||||
|
||||
Rust 语言的配置中心客户端 SDK。
|
||||
|
||||
## 依赖
|
||||
|
||||
- Rust 1.70+
|
||||
- reqwest (HTTP 客户端)
|
||||
- tokio (异步运行时)
|
||||
- aes + cbc (AES 解密)
|
||||
|
||||
## 安装
|
||||
|
||||
在 `Cargo.toml` 中添加:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
kenaito-conf = { path = "../kenaito-conf-rust-client" }
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```rust
|
||||
use kenaito_conf::{Config, ConfigClient};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = Config {
|
||||
base_url: "http://localhost:8080".to_string(),
|
||||
app_name: "kenaito-springboot-demo".to_string(),
|
||||
env: "daily".to_string(),
|
||||
version: "latest".to_string(),
|
||||
sync_interval: 30,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = ConfigClient::new(config);
|
||||
|
||||
client.on_change(|changed| {
|
||||
for (k, v) in changed.iter() {
|
||||
println!("config changed: {} = {}", k, v);
|
||||
}
|
||||
});
|
||||
|
||||
client.start().await.unwrap();
|
||||
|
||||
let value = client.get("database.host");
|
||||
let all = client.get_all();
|
||||
}
|
||||
```
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `base_url` | 配置中心地址 | 必填 |
|
||||
| `app_name` | 应用名称 | 必填 |
|
||||
| `env` | 环境 | `dev` |
|
||||
| `version` | 配置版本 | `latest` |
|
||||
| `token` | 认证令牌 | 空 |
|
||||
| `sync_interval` | 同步间隔(秒) | `60` |
|
||||
| `ignore_global` | 是否忽略全局配置 | `false` |
|
||||
| `cache_dir` | 本地缓存目录 | `.` |
|
||||
|
||||
## Demo
|
||||
|
||||
```bash
|
||||
cd kenaito-conf-rust-client-demo
|
||||
cargo run
|
||||
```
|
||||
@@ -1,140 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
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: String::new(),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct FetchAllRequest {
|
||||
app_name: String,
|
||||
env: String,
|
||||
version: String,
|
||||
ignore_global: bool,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct FetchChangedRequest {
|
||||
app_name: String,
|
||||
env: String,
|
||||
version: String,
|
||||
last_time: i64,
|
||||
ignore_global: bool,
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub struct ConfigClient {
|
||||
config: Config,
|
||||
configs: Arc<Mutex<HashMap<String, String>>>,
|
||||
last_time: Arc<Mutex<i64>>,
|
||||
handlers: Arc<Mutex<Vec<Box<dyn Fn(&HashMap<String, String>) + Send + Sync>>>>,
|
||||
}
|
||||
|
||||
impl ConfigClient {
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
config,
|
||||
configs: Arc::new(Mutex::new(HashMap::new())),
|
||||
last_time: Arc::new(Mutex::new(0)),
|
||||
handlers: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change<F>(&self, handler: F)
|
||||
where
|
||||
F: Fn(&HashMap<String, String>) + Send + Sync + 'static,
|
||||
{
|
||||
self.handlers.lock().unwrap().push(Box::new(handler));
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<String> {
|
||||
self.configs.lock().unwrap().get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> HashMap<String, String> {
|
||||
self.configs.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.fetch_all().await?;
|
||||
|
||||
let configs = self.configs.clone();
|
||||
let last_time = self.last_time.clone();
|
||||
let handlers = self.handlers.clone();
|
||||
let config = self.config.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(config.sync_interval)).await;
|
||||
// fetch_changed logic here
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_all(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let url = format!("{}/api/fetchAllConfigs", self.config.base_url);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
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 = client.post(&url).json(&payload).send().await?;
|
||||
let data: HashMap<String, serde_json::Value> = resp.json().await?;
|
||||
|
||||
if let Some(code) = data.get("code") {
|
||||
if code.as_i64() != Some(0) {
|
||||
return Err("server error".into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ts) = data.get("latest.fetch.timestamp") {
|
||||
*self.last_time.lock().unwrap() = ts.as_i64().unwrap_or(0);
|
||||
}
|
||||
|
||||
let mut configs = self.configs.lock().unwrap();
|
||||
|
||||
for (k, v) in data.iter() {
|
||||
if k.starts_with("kenaito.") || k == "latest.fetch.timestamp" {
|
||||
continue;
|
||||
}
|
||||
if let Some(s) = v.as_str() {
|
||||
configs.insert(k.clone(), s.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user