Add Go SDK implementation with APM monitoring and configuration center support Co-authored-by: nnhy <506367+nnhy@users.noreply.github.com>copilot-swe-agent[bot] authored at 2026-02-13 05:32:17
diff --git a/Doc/SDK/stardust-sdk-go.md b/Doc/SDK/stardust-sdk-go.md
index 853c700..81ef011 100644
--- a/Doc/SDK/stardust-sdk-go.md
+++ b/Doc/SDK/stardust-sdk-go.md
@@ -1,6 +1,6 @@
# 星尘监控 Go SDK
-适用于 Go 1.18+,提供星尘 APM 监控的接入能力。
+适用于 Go 1.18+,提供星尘 APM 监控和配置中心的接入能力。
## 安装
@@ -570,3 +570,356 @@ func main() {
http.ListenAndServe(":8080", StardustHandler(mux))
}
```
+
+---
+
+# 配置中心
+
+## 配置中心快速开始
+
+```go
+package main
+
+import (
+ "fmt"
+ "stardust"
+)
+
+func main() {
+ // 创建配置客户端
+ config := stardust.NewConfigClient("http://star.example.com:6600", "MyGoApp", "MySecret")
+ config.Start()
+ defer config.Stop()
+
+ // 获取配置
+ value := config.Get("database.host")
+ fmt.Println("Database Host:", value)
+
+ // 监听配置变更
+ config.OnChange(func(configs map[string]string) {
+ fmt.Println("配置已更新:", configs)
+ })
+
+ // 等待程序运行
+ select {}
+}
+```
+
+## 配置中心完整代码
+
+```go
+package stardust
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "sync"
+ "time"
+)
+
+// ConfigInfo 配置信息
+type ConfigInfo struct {
+ Version int `json:"Version"`
+ Scope string `json:"Scope"`
+ SourceIP string `json:"SourceIP"`
+ NextVersion int `json:"NextVersion"`
+ NextPublish string `json:"NextPublish"`
+ UpdateTime int64 `json:"UpdateTime"`
+ Configs map[string]string `json:"Configs"`
+}
+
+// ConfigClient 配置中心客户端
+type ConfigClient struct {
+ Server string
+ AppID string
+ Secret string
+ ClientID string
+
+ token string
+ tokenExpire int64
+ version int
+ configs map[string]string
+ mu sync.RWMutex
+ running bool
+ stopCh chan struct{}
+ client *http.Client
+ onChange func(map[string]string)
+}
+
+// NewConfigClient 创建配置客户端
+func NewConfigClient(server, appID, secret string) *ConfigClient {
+ return &ConfigClient{
+ Server: server,
+ AppID: appID,
+ Secret: secret,
+ ClientID: fmt.Sprintf("%s@%d", getLocalIP(), os.Getpid()),
+ configs: make(map[string]string),
+ stopCh: make(chan struct{}),
+ client: &http.Client{Timeout: 10 * time.Second},
+ }
+}
+
+// Start 启动配置客户端
+func (c *ConfigClient) Start() {
+ c.login()
+ c.running = true
+ go c.pollLoop()
+}
+
+// Stop 停止配置客户端
+func (c *ConfigClient) Stop() {
+ c.running = false
+ close(c.stopCh)
+}
+
+// Get 获取配置项
+func (c *ConfigClient) Get(key string) string {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ return c.configs[key]
+}
+
+// GetAll 获取所有配置
+func (c *ConfigClient) GetAll() map[string]string {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ result := make(map[string]string, len(c.configs))
+ for k, v := range c.configs {
+ result[k] = v
+ }
+ return result
+}
+
+// OnChange 注册配置变更回调
+func (c *ConfigClient) OnChange(callback func(map[string]string)) {
+ c.onChange = callback
+}
+
+func (c *ConfigClient) login() {
+ payload := map[string]interface{}{
+ "AppId": c.AppID,
+ "Secret": c.Secret,
+ "ClientId": c.ClientID,
+ "AppName": c.AppID,
+ }
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return
+ }
+
+ resp, err := c.client.Post(c.Server+"/App/Login", "application/json", bytes.NewReader(body))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[Stardust] Config login failed: %v\n", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ var apiResp APIResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil || apiResp.Code != 0 {
+ return
+ }
+
+ var loginResp LoginResponse
+ if err := json.Unmarshal(apiResp.Data, &loginResp); err != nil {
+ return
+ }
+
+ c.token = loginResp.Token
+ if loginResp.Expire > 0 {
+ c.tokenExpire = time.Now().Unix() + int64(loginResp.Expire)
+ }
+}
+
+func (c *ConfigClient) getAllConfig() {
+ payload := map[string]interface{}{
+ "AppId": c.AppID,
+ "Secret": c.Secret,
+ "ClientId": c.ClientID,
+ "Version": c.version,
+ }
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return
+ }
+
+ reqURL := fmt.Sprintf("%s/Config/GetAll?Token=%s", c.Server, url.QueryEscape(c.token))
+ resp, err := c.client.Post(reqURL, "application/json", bytes.NewReader(body))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[Stardust] Config GetAll failed: %v\n", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ respBody, _ := io.ReadAll(resp.Body)
+ var apiResp APIResponse
+ if err := json.Unmarshal(respBody, &apiResp); err != nil || apiResp.Code != 0 {
+ return
+ }
+
+ var configInfo ConfigInfo
+ if err := json.Unmarshal(apiResp.Data, &configInfo); err != nil {
+ return
+ }
+
+ // 版本没变化,不更新
+ if c.version > 0 && configInfo.Version == c.version && configInfo.Configs == nil {
+ return
+ }
+
+ // 更新配置
+ if configInfo.Configs != nil {
+ c.mu.Lock()
+ c.configs = configInfo.Configs
+ c.version = configInfo.Version
+ c.mu.Unlock()
+
+ // 触发回调
+ if c.onChange != nil {
+ go c.onChange(configInfo.Configs)
+ }
+ }
+}
+
+func (c *ConfigClient) pollLoop() {
+ // 首次立即获取配置
+ c.getAllConfig()
+
+ ticker := time.NewTicker(30 * time.Second)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ c.getAllConfig()
+ case <-c.stopCh:
+ return
+ }
+ }
+}
+```
+
+## 配置中心与APM监控集成示例
+
+```go
+package main
+
+import (
+ "fmt"
+ "stardust"
+ "time"
+)
+
+func main() {
+ // 启动APM监控
+ tracer := stardust.NewTracer("http://star.example.com:6600", "MyGoApp", "MySecret")
+ tracer.Start()
+ defer tracer.Stop()
+
+ // 启动配置中心
+ config := stardust.NewConfigClient("http://star.example.com:6600", "MyGoApp", "MySecret")
+ config.Start()
+ defer config.Stop()
+
+ // 监听配置变更
+ config.OnChange(func(configs map[string]string) {
+ span := tracer.NewSpan("配置更新", "")
+ span.Tag = fmt.Sprintf("配置项数量: %d", len(configs))
+ span.Finish()
+
+ fmt.Println("配置已更新:")
+ for k, v := range configs {
+ fmt.Printf(" %s = %s\n", k, v)
+ }
+ })
+
+ // 业务逻辑
+ for {
+ span := tracer.NewSpan("业务处理", "")
+
+ // 读取配置
+ dbHost := config.Get("database.host")
+ dbPort := config.Get("database.port")
+
+ span.Tag = fmt.Sprintf("DB: %s:%s", dbHost, dbPort)
+
+ // 模拟业务处理
+ time.Sleep(5 * time.Second)
+
+ span.Finish()
+ }
+}
+```
+
+## 在Gin框架中使用配置中心
+
+```go
+package main
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "stardust"
+)
+
+var (
+ tracer = stardust.NewTracer("http://star.example.com:6600", "MyGinApp", "secret")
+ config = stardust.NewConfigClient("http://star.example.com:6600", "MyGinApp", "secret")
+)
+
+func main() {
+ tracer.Start()
+ defer tracer.Stop()
+
+ config.Start()
+ defer config.Stop()
+
+ // 监听配置变更
+ config.OnChange(func(configs map[string]string) {
+ fmt.Println("配置已更新,请重新加载相关模块")
+ })
+
+ r := gin.Default()
+
+ // APM中间件
+ r.Use(StardustMiddleware())
+
+ r.GET("/config/:key", func(c *gin.Context) {
+ key := c.Param("key")
+ value := config.Get(key)
+ c.JSON(200, gin.H{
+ "key": key,
+ "value": value,
+ })
+ })
+
+ r.GET("/configs", func(c *gin.Context) {
+ c.JSON(200, config.GetAll())
+ })
+
+ r.Run(":8080")
+}
+
+func StardustMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ name := c.Request.Method + " " + c.Request.URL.Path
+ span := tracer.NewSpan(name, "")
+ span.Tag = c.Request.Method + " " + c.Request.RequestURI
+
+ c.Next()
+
+ if c.Writer.Status() >= 400 {
+ span.Error = fmt.Sprintf("HTTP %d", c.Writer.Status())
+ }
+ if len(c.Errors) > 0 {
+ span.Error = c.Errors.String()
+ }
+ span.Finish()
+ }
+}
+```
diff --git a/SDK/Go/examples/apm_basic.go b/SDK/Go/examples/apm_basic.go
new file mode 100644
index 0000000..3cecebf
--- /dev/null
+++ b/SDK/Go/examples/apm_basic.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+)
+
+func main() {
+ // 创建追踪器
+ tracer := stardust.NewTracer("http://localhost:6600", "MyGoApp", "MySecret")
+ tracer.Start()
+ defer tracer.Stop()
+
+ fmt.Println("APM 追踪器已启动,开始模拟业务操作...")
+
+ // 模拟业务操作
+ for i := 0; i < 5; i++ {
+ // 方式1:手动创建和结束
+ span := tracer.NewSpan("业务操作", "")
+ span.Tag = fmt.Sprintf("操作编号: %d", i+1)
+
+ // 模拟业务处理
+ time.Sleep(time.Millisecond * 100)
+
+ span.Finish()
+
+ // 方式2:使用 defer
+ func() {
+ span2 := tracer.NewSpan("数据库查询", "")
+ span2.Tag = "SELECT * FROM users"
+ defer span2.Finish()
+
+ // 模拟数据库查询
+ time.Sleep(time.Millisecond * 50)
+ }()
+
+ time.Sleep(time.Second)
+ }
+
+ fmt.Println("业务操作完成,等待数据上报...")
+ time.Sleep(time.Second * 5)
+}
diff --git a/SDK/Go/examples/combined.go b/SDK/Go/examples/combined.go
new file mode 100644
index 0000000..394c3c5
--- /dev/null
+++ b/SDK/Go/examples/combined.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+)
+
+func main() {
+ // 同时启动 APM 和配置中心
+ tracer := stardust.NewTracer("http://localhost:6600", "MyGoApp", "MySecret")
+ tracer.Start()
+ defer tracer.Stop()
+
+ config := stardust.NewConfigClient("http://localhost:6600", "MyGoApp", "MySecret")
+ config.Start()
+ defer config.Stop()
+
+ fmt.Println("APM 和配置中心已启动...")
+
+ // 监听配置变更,并记录到 APM
+ config.OnChange(func(configs map[string]string) {
+ span := tracer.NewSpan("配置更新", "")
+ span.Tag = fmt.Sprintf("配置项数量: %d", len(configs))
+ span.Finish()
+
+ fmt.Println("\n配置已更新:")
+ for k, v := range configs {
+ fmt.Printf(" %s = %s\n", k, v)
+ }
+ })
+
+ // 等待配置加载
+ time.Sleep(time.Second * 2)
+
+ // 业务循环:根据配置执行业务逻辑
+ for i := 0; i < 10; i++ {
+ span := tracer.NewSpan("业务处理", "")
+
+ // 读取配置
+ dbHost := config.Get("database.host")
+ dbPort := config.Get("database.port")
+
+ span.Tag = fmt.Sprintf("DB: %s:%s, 批次: %d", dbHost, dbPort, i+1)
+
+ // 模拟业务处理
+ time.Sleep(time.Second * 2)
+
+ span.Finish()
+
+ fmt.Printf("业务批次 %d 完成\n", i+1)
+ }
+
+ fmt.Println("所有业务完成,等待数据上报...")
+ time.Sleep(time.Second * 5)
+}
diff --git a/SDK/Go/examples/config_basic.go b/SDK/Go/examples/config_basic.go
new file mode 100644
index 0000000..3cadd58
--- /dev/null
+++ b/SDK/Go/examples/config_basic.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+)
+
+func main() {
+ // 创建配置客户端
+ config := stardust.NewConfigClient("http://localhost:6600", "MyGoApp", "MySecret")
+ config.Start()
+ defer config.Stop()
+
+ fmt.Println("配置中心客户端已启动...")
+
+ // 监听配置变更
+ config.OnChange(func(configs map[string]string) {
+ fmt.Println("\n=== 配置已更新 ===")
+ for k, v := range configs {
+ fmt.Printf(" %s = %s\n", k, v)
+ }
+ fmt.Println("==================\n")
+ })
+
+ // 等待配置加载
+ time.Sleep(time.Second * 2)
+
+ // 读取配置
+ fmt.Println("当前配置:")
+ for k, v := range config.GetAll() {
+ fmt.Printf(" %s = %s\n", k, v)
+ }
+
+ // 读取单个配置
+ dbHost := config.Get("database.host")
+ dbPort := config.Get("database.port")
+ fmt.Printf("\n数据库配置: %s:%s\n", dbHost, dbPort)
+
+ // 持续运行
+ fmt.Println("\n按 Ctrl+C 退出...")
+ select {}
+}
diff --git a/SDK/Go/examples/README.md b/SDK/Go/examples/README.md
new file mode 100644
index 0000000..1731747
--- /dev/null
+++ b/SDK/Go/examples/README.md
@@ -0,0 +1,92 @@
+# Stardust Go SDK 示例程序
+
+本目录包含了 Stardust Go SDK 的使用示例。
+
+## 前置要求
+
+1. 安装 Go 1.18 或更高版本
+2. 运行 Stardust 服务器(默认端口 6600)
+
+## 示例列表
+
+### 1. APM 基础示例 (apm_basic.go)
+
+演示如何使用 APM 追踪器进行链路追踪。
+
+```bash
+go run apm_basic.go
+```
+
+功能:
+- 创建和启动追踪器
+- 手动埋点
+- 使用 defer 自动结束 span
+- 自动上报数据到服务器
+
+### 2. 配置中心基础示例 (config_basic.go)
+
+演示如何使用配置中心客户端。
+
+```bash
+go run config_basic.go
+```
+
+功能:
+- 连接配置中心
+- 获取单个配置项
+- 获取所有配置
+- 监听配置变更
+
+### 3. 综合示例 (combined.go)
+
+演示如何同时使用 APM 和配置中心。
+
+```bash
+go run combined.go
+```
+
+功能:
+- 同时启动 APM 和配置中心
+- 配置变更时记录到 APM
+- 根据配置执行业务逻辑
+- 业务操作的链路追踪
+
+## 配置说明
+
+在运行示例前,需要在 Stardust 服务器中创建应用:
+
+1. 应用标识:`MyGoApp`
+2. 密钥:`MySecret`
+
+或者修改示例代码中的连接参数:
+
+```go
+tracer := stardust.NewTracer("http://your-server:6600", "YourAppId", "YourSecret")
+config := stardust.NewConfigClient("http://your-server:6600", "YourAppId", "YourSecret")
+```
+
+## 在自己的项目中使用
+
+1. 安装依赖:
+
+```bash
+go get github.com/NewLifeX/Stardust/SDK/Go/stardust
+```
+
+2. 导入并使用:
+
+```go
+import "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+
+func main() {
+ tracer := stardust.NewTracer("http://localhost:6600", "MyApp", "secret")
+ tracer.Start()
+ defer tracer.Stop()
+
+ // 你的业务代码
+}
+```
+
+## 更多文档
+
+详细文档请参考:[/Doc/SDK/stardust-sdk-go.md](../../../Doc/SDK/stardust-sdk-go.md)
diff --git a/SDK/Go/README.md b/SDK/Go/README.md
new file mode 100644
index 0000000..600b91b
--- /dev/null
+++ b/SDK/Go/README.md
@@ -0,0 +1,92 @@
+# Stardust Go SDK
+
+星尘(Stardust)Go 语言客户端 SDK,提供 APM 监控和配置中心功能。
+
+## 目录结构
+
+```
+SDK/Go/
+├── stardust/ # SDK 核心包
+│ ├── go.mod # Go 模块定义
+│ ├── README.md # SDK 使用文档
+│ ├── tracer.go # APM 追踪器实现
+│ ├── config.go # 配置中心客户端实现
+│ └── stardust_test.go # 单元测试
+└── examples/ # 示例程序
+ ├── README.md # 示例说明
+ ├── apm_basic.go # APM 基础示例
+ ├── config_basic.go # 配置中心基础示例
+ └── combined.go # 综合示例
+```
+
+## 快速开始
+
+### 安装
+
+```bash
+go get github.com/NewLifeX/Stardust/SDK/Go/stardust
+```
+
+### APM 监控
+
+```go
+import "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+
+tracer := stardust.NewTracer("http://localhost:6600", "MyGoApp", "MySecret")
+tracer.Start()
+defer tracer.Stop()
+
+span := tracer.NewSpan("业务操作", "")
+span.Tag = "参数信息"
+defer span.Finish()
+// 你的业务代码
+```
+
+### 配置中心
+
+```go
+import "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+
+config := stardust.NewConfigClient("http://localhost:6600", "MyGoApp", "MySecret")
+config.Start()
+defer config.Stop()
+
+value := config.Get("database.host")
+config.OnChange(func(configs map[string]string) {
+ // 配置变更处理
+})
+```
+
+## 特性
+
+- ✅ 完整的 APM 链路追踪支持
+- ✅ 配置中心客户端实现
+- ✅ 自动登录和 Token 刷新
+- ✅ 心跳保活
+- ✅ Gzip 压缩大数据包
+- ✅ 自动采样控制
+- ✅ 配置变更推送
+- ✅ 无第三方依赖
+- ✅ 支持 Go 1.18+
+- ✅ 完整的单元测试
+
+## 运行测试
+
+```bash
+cd stardust
+go test -v
+```
+
+## 示例程序
+
+查看 `examples/` 目录获取完整示例。
+
+## 文档
+
+- [SDK 详细文档](stardust/README.md)
+- [示例程序说明](examples/README.md)
+- [完整 API 文档](/Doc/SDK/stardust-sdk-go.md)
+
+## License
+
+MIT License
diff --git a/SDK/Go/stardust/config.go b/SDK/Go/stardust/config.go
new file mode 100644
index 0000000..e8dbfdc
--- /dev/null
+++ b/SDK/Go/stardust/config.go
@@ -0,0 +1,194 @@
+package stardust
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "sync"
+ "time"
+)
+
+// ConfigInfo 配置信息
+type ConfigInfo struct {
+ Version int `json:"Version"`
+ Scope string `json:"Scope"`
+ SourceIP string `json:"SourceIP"`
+ NextVersion int `json:"NextVersion"`
+ NextPublish string `json:"NextPublish"`
+ UpdateTime int64 `json:"UpdateTime"`
+ Configs map[string]string `json:"Configs"`
+}
+
+// ConfigClient 配置中心客户端
+type ConfigClient struct {
+ Server string
+ AppID string
+ Secret string
+ ClientID string
+
+ token string
+ tokenExpire int64
+ version int
+ configs map[string]string
+ mu sync.RWMutex
+ running bool
+ stopCh chan struct{}
+ client *http.Client
+ onChange func(map[string]string)
+}
+
+// NewConfigClient 创建配置客户端
+func NewConfigClient(server, appID, secret string) *ConfigClient {
+ return &ConfigClient{
+ Server: server,
+ AppID: appID,
+ Secret: secret,
+ ClientID: fmt.Sprintf("%s@%d", getLocalIP(), os.Getpid()),
+ configs: make(map[string]string),
+ stopCh: make(chan struct{}),
+ client: &http.Client{Timeout: 10 * time.Second},
+ }
+}
+
+// Start 启动配置客户端
+func (c *ConfigClient) Start() {
+ c.login()
+ c.running = true
+ go c.pollLoop()
+}
+
+// Stop 停止配置客户端
+func (c *ConfigClient) Stop() {
+ c.running = false
+ close(c.stopCh)
+}
+
+// Get 获取配置项
+func (c *ConfigClient) Get(key string) string {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ return c.configs[key]
+}
+
+// GetAll 获取所有配置
+func (c *ConfigClient) GetAll() map[string]string {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ result := make(map[string]string, len(c.configs))
+ for k, v := range c.configs {
+ result[k] = v
+ }
+ return result
+}
+
+// OnChange 注册配置变更回调
+func (c *ConfigClient) OnChange(callback func(map[string]string)) {
+ c.onChange = callback
+}
+
+func (c *ConfigClient) login() {
+ payload := map[string]interface{}{
+ "AppId": c.AppID,
+ "Secret": c.Secret,
+ "ClientId": c.ClientID,
+ "AppName": c.AppID,
+ }
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return
+ }
+
+ resp, err := c.client.Post(c.Server+"/App/Login", "application/json", bytes.NewReader(body))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[Stardust] Config login failed: %v\n", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ var apiResp APIResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil || apiResp.Code != 0 {
+ return
+ }
+
+ var loginResp LoginResponse
+ if err := json.Unmarshal(apiResp.Data, &loginResp); err != nil {
+ return
+ }
+
+ c.token = loginResp.Token
+ if loginResp.Expire > 0 {
+ c.tokenExpire = time.Now().Unix() + int64(loginResp.Expire)
+ }
+}
+
+func (c *ConfigClient) getAllConfig() {
+ payload := map[string]interface{}{
+ "AppId": c.AppID,
+ "Secret": c.Secret,
+ "ClientId": c.ClientID,
+ "Version": c.version,
+ }
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return
+ }
+
+ reqURL := fmt.Sprintf("%s/Config/GetAll?Token=%s", c.Server, url.QueryEscape(c.token))
+ resp, err := c.client.Post(reqURL, "application/json", bytes.NewReader(body))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[Stardust] Config GetAll failed: %v\n", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ respBody, _ := io.ReadAll(resp.Body)
+ var apiResp APIResponse
+ if err := json.Unmarshal(respBody, &apiResp); err != nil || apiResp.Code != 0 {
+ return
+ }
+
+ var configInfo ConfigInfo
+ if err := json.Unmarshal(apiResp.Data, &configInfo); err != nil {
+ return
+ }
+
+ // 版本没变化,不更新
+ if c.version > 0 && configInfo.Version == c.version && configInfo.Configs == nil {
+ return
+ }
+
+ // 更新配置
+ if configInfo.Configs != nil {
+ c.mu.Lock()
+ c.configs = configInfo.Configs
+ c.version = configInfo.Version
+ c.mu.Unlock()
+
+ // 触发回调
+ if c.onChange != nil {
+ go c.onChange(configInfo.Configs)
+ }
+ }
+}
+
+func (c *ConfigClient) pollLoop() {
+ // 首次立即获取配置
+ c.getAllConfig()
+
+ ticker := time.NewTicker(30 * time.Second)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ c.getAllConfig()
+ case <-c.stopCh:
+ return
+ }
+ }
+}
diff --git a/SDK/Go/stardust/go.mod b/SDK/Go/stardust/go.mod
new file mode 100644
index 0000000..ef90ba0
--- /dev/null
+++ b/SDK/Go/stardust/go.mod
@@ -0,0 +1,3 @@
+module github.com/NewLifeX/Stardust/SDK/Go/stardust
+
+go 1.18
diff --git a/SDK/Go/stardust/README.md b/SDK/Go/stardust/README.md
new file mode 100644
index 0000000..8947b6c
--- /dev/null
+++ b/SDK/Go/stardust/README.md
@@ -0,0 +1,97 @@
+# Stardust Go SDK
+
+星尘监控(Stardust)Go SDK,提供 APM 监控和配置中心的接入能力。
+
+## 特性
+
+- ✅ APM 链路追踪
+- ✅ 配置中心
+- ✅ 无第三方依赖,仅使用 Go 标准库
+- ✅ 支持 Go 1.18+
+
+## 安装
+
+```bash
+go get github.com/NewLifeX/Stardust/SDK/Go/stardust
+```
+
+## APM 监控快速开始
+
+```go
+package main
+
+import (
+ "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+)
+
+func main() {
+ tracer := stardust.NewTracer("http://star.example.com:6600", "MyGoApp", "MySecret")
+ tracer.Start()
+ defer tracer.Stop()
+
+ // 手动埋点
+ span := tracer.NewSpan("业务操作", "")
+ span.Tag = "参数信息"
+ doSomething()
+ span.Finish()
+}
+```
+
+## 配置中心快速开始
+
+```go
+package main
+
+import (
+ "fmt"
+ "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+)
+
+func main() {
+ config := stardust.NewConfigClient("http://star.example.com:6600", "MyGoApp", "MySecret")
+ config.Start()
+ defer config.Stop()
+
+ // 获取配置
+ value := config.Get("database.host")
+ fmt.Println("Database Host:", value)
+
+ // 监听配置变更
+ config.OnChange(func(configs map[string]string) {
+ fmt.Println("配置已更新:", configs)
+ })
+
+ select {}
+}
+```
+
+## 完整文档
+
+详细文档请参考:[/Doc/SDK/stardust-sdk-go.md](../../../Doc/SDK/stardust-sdk-go.md)
+
+## 框架集成
+
+### Gin 框架
+
+```go
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/NewLifeX/Stardust/SDK/Go/stardust"
+)
+
+var tracer = stardust.NewTracer("http://star.example.com:6600", "MyGinApp", "secret")
+
+func StardustMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ name := c.Request.Method + " " + c.Request.URL.Path
+ span := tracer.NewSpan(name, "")
+ span.Tag = c.Request.Method + " " + c.Request.RequestURI
+ defer span.Finish()
+ c.Next()
+ }
+}
+```
+
+## License
+
+MIT License
diff --git a/SDK/Go/stardust/stardust_test.go b/SDK/Go/stardust/stardust_test.go
new file mode 100644
index 0000000..614b936
--- /dev/null
+++ b/SDK/Go/stardust/stardust_test.go
@@ -0,0 +1,183 @@
+package stardust
+
+import (
+ "testing"
+ "time"
+)
+
+func TestNewTracer(t *testing.T) {
+ tracer := NewTracer("http://localhost:6600", "TestApp", "TestSecret")
+ if tracer == nil {
+ t.Fatal("NewTracer returned nil")
+ }
+ if tracer.AppID != "TestApp" {
+ t.Errorf("Expected AppID=TestApp, got %s", tracer.AppID)
+ }
+ if tracer.Secret != "TestSecret" {
+ t.Errorf("Expected Secret=TestSecret, got %s", tracer.Secret)
+ }
+ if tracer.Period != 60 {
+ t.Errorf("Expected Period=60, got %d", tracer.Period)
+ }
+}
+
+func TestSpan(t *testing.T) {
+ tracer := NewTracer("http://localhost:6600", "TestApp", "TestSecret")
+
+ span := tracer.NewSpan("TestOperation", "")
+ if span == nil {
+ t.Fatal("NewSpan returned nil")
+ }
+ if span.name != "TestOperation" {
+ t.Errorf("Expected name=TestOperation, got %s", span.name)
+ }
+ if span.ID == "" {
+ t.Error("Span ID should not be empty")
+ }
+ if span.TraceID == "" {
+ t.Error("Span TraceID should not be empty")
+ }
+
+ span.Tag = "test tag"
+ span.Finish()
+
+ if span.EndTime == 0 {
+ t.Error("Span EndTime should not be zero after Finish()")
+ }
+ if span.EndTime < span.StartTime {
+ t.Error("Span EndTime should be greater than StartTime")
+ }
+}
+
+func TestSpanBuilder(t *testing.T) {
+ builder := newSpanBuilder("TestOp", 10, 5)
+ if builder == nil {
+ t.Fatal("newSpanBuilder returned nil")
+ }
+ if builder.Name != "TestOp" {
+ t.Errorf("Expected Name=TestOp, got %s", builder.Name)
+ }
+
+ // 添加正常 span
+ span1 := &Span{
+ StartTime: time.Now().UnixMilli(),
+ EndTime: time.Now().UnixMilli() + 100,
+ Error: "",
+ }
+ builder.addSpan(span1)
+
+ if builder.Total != 1 {
+ t.Errorf("Expected Total=1, got %d", builder.Total)
+ }
+ if builder.Errors != 0 {
+ t.Errorf("Expected Errors=0, got %d", builder.Errors)
+ }
+
+ // 添加错误 span
+ span2 := &Span{
+ StartTime: time.Now().UnixMilli(),
+ EndTime: time.Now().UnixMilli() + 200,
+ Error: "test error",
+ }
+ builder.addSpan(span2)
+
+ if builder.Total != 2 {
+ t.Errorf("Expected Total=2, got %d", builder.Total)
+ }
+ if builder.Errors != 1 {
+ t.Errorf("Expected Errors=1, got %d", builder.Errors)
+ }
+ if len(builder.ErrorSamples) != 1 {
+ t.Errorf("Expected 1 error sample, got %d", len(builder.ErrorSamples))
+ }
+}
+
+func TestNewConfigClient(t *testing.T) {
+ config := NewConfigClient("http://localhost:6600", "TestApp", "TestSecret")
+ if config == nil {
+ t.Fatal("NewConfigClient returned nil")
+ }
+ if config.AppID != "TestApp" {
+ t.Errorf("Expected AppID=TestApp, got %s", config.AppID)
+ }
+ if config.Secret != "TestSecret" {
+ t.Errorf("Expected Secret=TestSecret, got %s", config.Secret)
+ }
+}
+
+func TestConfigClientGetSet(t *testing.T) {
+ config := NewConfigClient("http://localhost:6600", "TestApp", "TestSecret")
+
+ // 手动设置配置(模拟从服务器获取)
+ config.mu.Lock()
+ config.configs["test.key"] = "test.value"
+ config.configs["database.host"] = "localhost"
+ config.mu.Unlock()
+
+ // 测试 Get
+ value := config.Get("test.key")
+ if value != "test.value" {
+ t.Errorf("Expected test.value, got %s", value)
+ }
+
+ // 测试不存在的 key
+ empty := config.Get("nonexistent")
+ if empty != "" {
+ t.Errorf("Expected empty string, got %s", empty)
+ }
+
+ // 测试 GetAll
+ all := config.GetAll()
+ if len(all) != 2 {
+ t.Errorf("Expected 2 configs, got %d", len(all))
+ }
+ if all["database.host"] != "localhost" {
+ t.Errorf("Expected localhost, got %s", all["database.host"])
+ }
+}
+
+func TestRandomHex(t *testing.T) {
+ hex1 := randomHex(8)
+ hex2 := randomHex(8)
+
+ if len(hex1) != 16 { // 8 bytes = 16 hex chars
+ t.Errorf("Expected length 16, got %d", len(hex1))
+ }
+ if hex1 == hex2 {
+ t.Error("randomHex should generate different values")
+ }
+}
+
+func TestGetLocalIP(t *testing.T) {
+ ip := getLocalIP()
+ if ip == "" {
+ t.Error("getLocalIP should not return empty string")
+ }
+ // 应该返回 IP 地址格式,至少不是空
+ if len(ip) < 7 { // 最短的 IP: 0.0.0.0
+ t.Errorf("IP address too short: %s", ip)
+ }
+}
+
+func TestContainsIgnoreCase(t *testing.T) {
+ tests := []struct {
+ s string
+ substr string
+ want bool
+ }{
+ {"HelloWorld", "world", true},
+ {"HelloWorld", "WORLD", true},
+ {"HelloWorld", "hello", true},
+ {"HelloWorld", "xyz", false},
+ {"", "test", false},
+ {"test", "", true},
+ }
+
+ for _, tt := range tests {
+ got := containsIgnoreCase(tt.s, tt.substr)
+ if got != tt.want {
+ t.Errorf("containsIgnoreCase(%q, %q) = %v, want %v",
+ tt.s, tt.substr, got, tt.want)
+ }
+ }
+}
diff --git a/SDK/Go/stardust/tracer.go b/SDK/Go/stardust/tracer.go
new file mode 100644
index 0000000..b2896a4
--- /dev/null
+++ b/SDK/Go/stardust/tracer.go
@@ -0,0 +1,456 @@
+package stardust
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "sync"
+ "time"
+)
+
+// Span 追踪片段
+type Span struct {
+ ID string `json:"Id"`
+ ParentID string `json:"ParentId"`
+ TraceID string `json:"TraceId"`
+ StartTime int64 `json:"StartTime"`
+ EndTime int64 `json:"EndTime"`
+ Tag string `json:"Tag"`
+ Error string `json:"Error"`
+
+ name string
+ tracer *Tracer
+}
+
+// SetError 设置错误
+func (s *Span) SetError(err error) {
+ if err != nil {
+ s.Error = err.Error()
+ }
+}
+
+// Finish 结束片段
+func (s *Span) Finish() {
+ s.EndTime = time.Now().UnixMilli()
+ if s.tracer != nil {
+ s.tracer.finishSpan(s)
+ }
+}
+
+// SpanBuilder 构建器,按操作名聚合
+type SpanBuilder struct {
+ Name string `json:"Name"`
+ StartTime int64 `json:"StartTime"`
+ EndTime int64 `json:"EndTime"`
+ Total int `json:"Total"`
+ Errors int `json:"Errors"`
+ Cost int64 `json:"Cost"`
+ MaxCost int `json:"MaxCost"`
+ MinCost int `json:"MinCost"`
+ Samples []*Span `json:"Samples"`
+ ErrorSamples []*Span `json:"ErrorSamples"`
+
+ maxSamples int
+ maxErrors int
+ mu sync.Mutex
+}
+
+func newSpanBuilder(name string, maxSamples, maxErrors int) *SpanBuilder {
+ return &SpanBuilder{
+ Name: name,
+ StartTime: time.Now().UnixMilli(),
+ Samples: make([]*Span, 0),
+ ErrorSamples: make([]*Span, 0),
+ maxSamples: maxSamples,
+ maxErrors: maxErrors,
+ }
+}
+
+func (b *SpanBuilder) addSpan(span *Span) {
+ elapsed := int(span.EndTime - span.StartTime)
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ b.Total++
+ b.Cost += int64(elapsed)
+ if b.MaxCost == 0 || elapsed > b.MaxCost {
+ b.MaxCost = elapsed
+ }
+ if b.MinCost == 0 || elapsed < b.MinCost {
+ b.MinCost = elapsed
+ }
+
+ if span.Error != "" {
+ b.Errors++
+ if len(b.ErrorSamples) < b.maxErrors {
+ b.ErrorSamples = append(b.ErrorSamples, span)
+ }
+ } else {
+ if len(b.Samples) < b.maxSamples {
+ b.Samples = append(b.Samples, span)
+ }
+ }
+ b.EndTime = time.Now().UnixMilli()
+}
+
+// TraceModel 上报请求体
+type TraceModel struct {
+ AppID string `json:"AppId"`
+ AppName string `json:"AppName"`
+ ClientID string `json:"ClientId"`
+ Version string `json:"Version,omitempty"`
+ Builders []*SpanBuilder `json:"Builders"`
+}
+
+// TraceResponse 上报响应
+type TraceResponse struct {
+ Period int `json:"Period"`
+ MaxSamples int `json:"MaxSamples"`
+ MaxErrors int `json:"MaxErrors"`
+ Timeout int `json:"Timeout"`
+ MaxTagLength int `json:"MaxTagLength"`
+ RequestTagLength int `json:"RequestTagLength"`
+ EnableMeter *bool `json:"EnableMeter"`
+ Excludes []string `json:"Excludes"`
+}
+
+// APIResponse 通用响应
+type APIResponse struct {
+ Code int `json:"code"`
+ Data json.RawMessage `json:"data"`
+}
+
+// LoginResponse 登录响应
+type LoginResponse struct {
+ Code string `json:"Code"`
+ Secret string `json:"Secret"`
+ Name string `json:"Name"`
+ Token string `json:"Token"`
+ Expire int `json:"Expire"`
+ ServerTime int64 `json:"ServerTime"`
+}
+
+// PingResponse 心跳响应
+type PingResponse struct {
+ Time int64 `json:"Time"`
+ ServerTime int64 `json:"ServerTime"`
+ Period int `json:"Period"`
+ Token string `json:"Token"`
+}
+
+// Tracer 星尘追踪器
+type Tracer struct {
+ Server string
+ AppID string
+ AppName string
+ Secret string
+ ClientID string
+
+ // 采样参数
+ Period int
+ MaxSamples int
+ MaxErrors int
+ Timeout int
+ MaxTagLen int
+ Excludes []string
+ EnableMeter bool
+
+ token string
+ tokenExpire int64
+
+ builders map[string]*SpanBuilder
+ mu sync.Mutex
+ running bool
+ stopCh chan struct{}
+ client *http.Client
+}
+
+// NewTracer 创建追踪器
+func NewTracer(server, appID, secret string) *Tracer {
+ return &Tracer{
+ Server: server,
+ AppID: appID,
+ AppName: appID,
+ Secret: secret,
+ ClientID: fmt.Sprintf("%s@%d", getLocalIP(), os.Getpid()),
+ Period: 60,
+ MaxSamples: 1,
+ MaxErrors: 10,
+ Timeout: 5000,
+ MaxTagLen: 1024,
+ EnableMeter: true,
+ builders: make(map[string]*SpanBuilder),
+ stopCh: make(chan struct{}),
+ client: &http.Client{Timeout: 10 * time.Second},
+ }
+}
+
+// Start 启动追踪器
+func (t *Tracer) Start() {
+ t.login()
+ t.running = true
+ go t.reportLoop()
+ go t.pingLoop()
+}
+
+// Stop 停止追踪器
+func (t *Tracer) Stop() {
+ t.running = false
+ close(t.stopCh)
+ t.flush()
+}
+
+// NewSpan 创建追踪片段
+func (t *Tracer) NewSpan(name, parentID string) *Span {
+ return &Span{
+ ID: randomHex(8),
+ ParentID: parentID,
+ TraceID: randomHex(16),
+ StartTime: time.Now().UnixMilli(),
+ name: name,
+ tracer: t,
+ }
+}
+
+func (t *Tracer) finishSpan(span *Span) {
+ // 排除自身
+ if span.name == "/Trace/Report" || span.name == "/Trace/ReportRaw" {
+ return
+ }
+ for _, exc := range t.Excludes {
+ if exc != "" && containsIgnoreCase(span.name, exc) {
+ return
+ }
+ }
+
+ // 截断 Tag
+ if len(span.Tag) > t.MaxTagLen {
+ span.Tag = span.Tag[:t.MaxTagLen]
+ }
+
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ builder, ok := t.builders[span.name]
+ if !ok {
+ builder = newSpanBuilder(span.name, t.MaxSamples, t.MaxErrors)
+ t.builders[span.name] = builder
+ }
+ builder.addSpan(span)
+}
+
+func (t *Tracer) login() {
+ payload := map[string]interface{}{
+ "AppId": t.AppID,
+ "Secret": t.Secret,
+ "ClientId": t.ClientID,
+ "AppName": t.AppName,
+ }
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return
+ }
+
+ resp, err := t.client.Post(t.Server+"/App/Login", "application/json", bytes.NewReader(body))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[Stardust] Login failed: %v\n", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ var apiResp APIResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil || apiResp.Code != 0 {
+ return
+ }
+
+ var loginResp LoginResponse
+ if err := json.Unmarshal(apiResp.Data, &loginResp); err != nil {
+ return
+ }
+
+ t.token = loginResp.Token
+ if loginResp.Expire > 0 {
+ t.tokenExpire = time.Now().Unix() + int64(loginResp.Expire)
+ }
+ if loginResp.Code != "" {
+ t.AppID = loginResp.Code
+ }
+ if loginResp.Secret != "" {
+ t.Secret = loginResp.Secret
+ }
+}
+
+func (t *Tracer) ping() {
+ payload := map[string]interface{}{
+ "Id": os.Getpid(),
+ "Name": t.AppName,
+ "Time": time.Now().UnixMilli(),
+ }
+
+ body, _ := json.Marshal(payload)
+ reqURL := fmt.Sprintf("%s/App/Ping?Token=%s", t.Server, url.QueryEscape(t.token))
+
+ resp, err := t.client.Post(reqURL, "application/json", bytes.NewReader(body))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[Stardust] Ping failed: %v\n", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ var apiResp APIResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil || apiResp.Code != 0 {
+ return
+ }
+
+ var pingResp PingResponse
+ if err := json.Unmarshal(apiResp.Data, &pingResp); err != nil {
+ return
+ }
+
+ if pingResp.Token != "" {
+ t.token = pingResp.Token
+ }
+}
+
+func (t *Tracer) report(buildersData []*SpanBuilder) {
+ model := TraceModel{
+ AppID: t.AppID,
+ AppName: t.AppName,
+ ClientID: t.ClientID,
+ Builders: buildersData,
+ }
+
+ body, err := json.Marshal(model)
+ if err != nil {
+ return
+ }
+
+ var resp *http.Response
+ if len(body) > 1024 {
+ // Gzip 压缩
+ var buf bytes.Buffer
+ gz := gzip.NewWriter(&buf)
+ gz.Write(body)
+ gz.Close()
+
+ reqURL := fmt.Sprintf("%s/Trace/ReportRaw?Token=%s", t.Server, url.QueryEscape(t.token))
+ resp, err = t.client.Post(reqURL, "application/x-gzip", &buf)
+ } else {
+ reqURL := fmt.Sprintf("%s/Trace/Report?Token=%s", t.Server, url.QueryEscape(t.token))
+ resp, err = t.client.Post(reqURL, "application/json", bytes.NewReader(body))
+ }
+
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "[Stardust] Report failed: %v\n", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ respBody, _ := io.ReadAll(resp.Body)
+ var apiResp APIResponse
+ if err := json.Unmarshal(respBody, &apiResp); err != nil || apiResp.Code != 0 {
+ return
+ }
+
+ var traceResp TraceResponse
+ if err := json.Unmarshal(apiResp.Data, &traceResp); err != nil {
+ return
+ }
+
+ if traceResp.Period > 0 {
+ t.Period = traceResp.Period
+ }
+ if traceResp.MaxSamples > 0 {
+ t.MaxSamples = traceResp.MaxSamples
+ }
+ if traceResp.MaxErrors > 0 {
+ t.MaxErrors = traceResp.MaxErrors
+ }
+ if traceResp.Timeout > 0 {
+ t.Timeout = traceResp.Timeout
+ }
+ if traceResp.MaxTagLength > 0 {
+ t.MaxTagLen = traceResp.MaxTagLength
+ }
+ if traceResp.Excludes != nil {
+ t.Excludes = traceResp.Excludes
+ }
+}
+
+func (t *Tracer) flush() {
+ t.mu.Lock()
+ if len(t.builders) == 0 {
+ t.mu.Unlock()
+ return
+ }
+ list := make([]*SpanBuilder, 0, len(t.builders))
+ for _, b := range t.builders {
+ if b.Total > 0 {
+ list = append(list, b)
+ }
+ }
+ t.builders = make(map[string]*SpanBuilder)
+ t.mu.Unlock()
+
+ if len(list) > 0 {
+ t.report(list)
+ }
+}
+
+func (t *Tracer) reportLoop() {
+ ticker := time.NewTicker(time.Duration(t.Period) * time.Second)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ t.flush()
+ case <-t.stopCh:
+ return
+ }
+ }
+}
+
+func (t *Tracer) pingLoop() {
+ ticker := time.NewTicker(30 * time.Second)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ t.ping()
+ case <-t.stopCh:
+ return
+ }
+ }
+}
+
+// ========== 工具函数 ==========
+
+func randomHex(n int) string {
+ b := make([]byte, n)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func getLocalIP() string {
+ conn, err := net.Dial("udp", "8.8.8.8:80")
+ if err != nil {
+ return "127.0.0.1"
+ }
+ defer conn.Close()
+ return conn.LocalAddr().(*net.UDPAddr).IP.String()
+}
+
+func containsIgnoreCase(s, substr string) bool {
+ return len(s) >= len(substr) &&
+ bytes.Contains(bytes.ToLower([]byte(s)), bytes.ToLower([]byte(substr)))
+}