feat(tracing): Implement Jaeger/OpenTracing provider with configuration options

- Added Punycode encoding implementation for cookie handling.
- Introduced serialization for cookie jar with JSON support.
- Created a comprehensive README for the tracing provider, detailing configuration and usage.
- Developed a configuration structure for tracing, including sampler and reporter settings.
- Implemented the provider logic to initialize Jaeger tracer with logging capabilities.
- Ensured graceful shutdown of the tracer on application exit.
This commit is contained in:
Rogee
2025-09-12 17:28:25 +08:00
parent 202239795b
commit 342f205b5e
37 changed files with 89 additions and 352 deletions

View File

@@ -57,13 +57,18 @@ atomctl new project # 在已有 go.mod 的项目中就地初始化
- 功能:创建 Provider 脚手架到 `providers/<name>` - 功能:创建 Provider 脚手架到 `providers/<name>`
- 参数: - 参数:
- `<name>`必填provider 名称(例如 `cache``email` - `<name>`可选。省略名称时会列出可用的内置预置 providers 供选择
- 选项:继承 `--dry-run``--dir` - 选项:继承 `--dry-run``--dir`
- 行为:
-`<name>` 与内置 `templates/providers/<name>` 目录同名,则渲染该目录
- 否则回退渲染 `templates/providers/default``providers/<name>`
示例: 示例:
``` ```
atomctl new provider email atomctl new provider # 列出可用预置 providers
atomctl new provider redis # 渲染 providers/redis 预置
atomctl new provider email # 未命中预置,回退 default 渲染到 providers/email
atomctl new --dry-run --dir ./demo provider cache atomctl new --dry-run --dir ./demo provider cache
``` ```
@@ -78,7 +83,7 @@ atomctl new --dry-run --dir ./demo provider cache
- 行为说明: - 行为说明:
- 生成 publisher 到 `app/events/publishers/<snake>.go` - 生成 publisher 到 `app/events/publishers/<snake>.go`
- 生成 subscriber 到 `app/events/subscribers/<snake>.go` - 生成 subscriber 到 `app/events/subscribers/<snake>.go`
- 追加常量到 `app/events/topics.go``const Topic{Name} = "<snake>"`(避免重复) - 追加常量到 `app/events/topics.go``const Topic{Name} = "event:<snake>"`(避免重复)
示例: 示例:
@@ -89,15 +94,18 @@ atomctl new event UserCreated --only=publisher
### new job ### new job
- 功能:生成任务模板`app/jobs/<snake>.go` - 功能:生成任务模板文件;支持可选的定时任务模板
- 参数: - 参数:
- `<name>`:必填,任务名 - `<name>`:必填,任务名
- 选项:继承 `--dry-run``--dir` - 选项:
- 继承 `--dry-run``--dir`
- `--cron`:除生成 `app/jobs/<snake>.go` 外,额外生成 `app/jobs/cron_<snake>.go`
示例: 示例:
``` ```
atomctl new job SendDailyReport atomctl new job SendDailyReport # 生成 app/jobs/send_daily_report.go
atomctl new job SendDailyReport --cron # 同时生成 cron 版本 app/jobs/cron_send_daily_report.go
``` ```
> 说明:代码中已存在 `new module` 实现,但当前未注册到 `new` 子命令中(处于弃用状态)。 > 说明:代码中已存在 `new module` 实现,但当前未注册到 `new` 子命令中(处于弃用状态)。

View File

@@ -1,43 +1,67 @@
package cmd package cmd
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "sort"
"text/template" "strings"
"text/template"
"github.com/iancoleman/strcase" "github.com/iancoleman/strcase"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.ipao.vip/atomctl/v2/templates" "go.ipao.vip/atomctl/v2/templates"
) )
// CommandNewProvider 注册 new_provider 命令 // CommandNewProvider 注册 new_provider 命令
func CommandNewProvider(root *cobra.Command) { func CommandNewProvider(root *cobra.Command) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "provider", Use: "provider",
Short: "创建新的 provider", Short: "创建新的 provider",
Long: `在 providers/<name> 目录下渲染创建 Provider 模板。 Long: `在 providers/<name> 目录下渲染创建 Provider 模板。
行为: 行为:
- 从内置模板 templates/provider 渲染相关文件 - 当 name 与内置预置目录同名时,渲染该目录;否则回退渲染 providers/default
- 从内置模板 templates/providers 渲染相关文件
- 使用 name 的 CamelCase 作为导出名 - 使用 name 的 CamelCase 作为导出名
- --dry-run 仅打印渲染与写入动作;--dir 指定输出基目录(默认 . - --dry-run 仅打印渲染与写入动作;--dir 指定输出基目录(默认 .
不带名称直接运行时,将列出可用预置 provider 列表供选择。
示例: 示例:
atomctl new provider email atomctl new provider email
atomctl new --dry-run --dir ./demo provider cache`, atomctl new --dry-run --dir ./demo provider cache`,
Args: cobra.ExactArgs(1), Args: cobra.MaximumNArgs(1),
RunE: commandNewProviderE, RunE: commandNewProviderE,
} }
root.AddCommand(cmd) root.AddCommand(cmd)
} }
func commandNewProviderE(cmd *cobra.Command, args []string) error { func commandNewProviderE(cmd *cobra.Command, args []string) error {
providerName := args[0] // no-arg: list available preset providers
if len(args) == 0 {
entries, err := templates.Providers.ReadDir("providers")
if err != nil {
return err
}
var names []string
for _, e := range entries {
if e.IsDir() {
names = append(names, e.Name())
}
}
sort.Strings(names)
fmt.Println("可用预置 providers:")
for _, n := range names {
fmt.Printf(" - %s\n", n)
}
return nil
}
providerName := args[0]
// shared flags // shared flags
dryRun, _ := cmd.Flags().GetBool("dry-run") dryRun, _ := cmd.Flags().GetBool("dry-run")
baseDir, _ := cmd.Flags().GetString("dir") baseDir, _ := cmd.Flags().GetString("dir")
@@ -56,32 +80,38 @@ func commandNewProviderE(cmd *cobra.Command, args []string) error {
} }
} }
err := fs.WalkDir(templates.Provider, "provider", func(path string, d fs.DirEntry, err error) error { // choose template source: providers/<name> or providers/default
if err != nil { srcDir := filepath.Join("providers", providerName)
return err if _, err := templates.Providers.ReadDir(srcDir); err != nil {
} srcDir = filepath.Join("providers", "default")
if d.IsDir() { }
return nil
}
relPath, err := filepath.Rel("provider", path) err := fs.WalkDir(templates.Providers, srcDir, func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err
} }
if d.IsDir() {
return nil
}
destPath := filepath.Join(targetPath, strings.TrimSuffix(relPath, ".tpl")) relPath, err := filepath.Rel(srcDir, path)
if dryRun { if err != nil {
fmt.Printf("[dry-run] mkdir -p %s\n", filepath.Dir(destPath)) return err
} else { }
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
}
tmpl, err := template.ParseFS(templates.Provider, path) destPath := filepath.Join(targetPath, strings.TrimSuffix(relPath, ".tpl"))
if err != nil { if dryRun {
return err fmt.Printf("[dry-run] mkdir -p %s\n", filepath.Dir(destPath))
} } else {
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
}
tmpl, err := template.ParseFS(templates.Providers, path)
if err != nil {
return err
}
if dryRun { if dryRun {
fmt.Printf("[dry-run] render > %s\n", destPath) fmt.Printf("[dry-run] render > %s\n", destPath)

View File

@@ -1,301 +0,0 @@
# 全局指令
我的主语言是简体中文,所以请用简体中文回答我,与我交流。
# 角色定义
您是一名高级 Go 程序员,具有丰富的后端开发经验,偏好干净的编程和设计模式。
# 基本原则
- 所有代码和文档使用中文。
- 遵循 Go 的官方规范和最佳实践。
- 使用 `gofumpt -w -l -extra .` 格式化代码。
- 错误处理优先使用 errors.New 和 fmt.Errorf。
- 业务返回的错误需要在 `app/errorx` 包中定义。
- 在错误处理时,使用适当的上下文信息提供更多错误细节。
# 命名规范
- 包名使用小写单词。
- 文件名使用小写下划线。
- 环境变量使用大写。
- 常量使用驼峰命名。
- 导出的标识符必须以大写字母开头。
- 缩写规则:
- i、j 用于循环
- err 用于错误
- ctx 用于上下文
- req、res 用于请求响应
# 函数设计
- 函数应该短小精悍,单一职责。
- 参数数量控制在 5 个以内。
- 使用多值返回处理错误。
- 优先使用命名返回值。
- 避免嵌套超过 3 层。
- 使用 defer 处理资源清理。
# 错误处理
- 总是检查错误返回。
- 使用自定义错误类型。
- 错误应该携带上下文信息。
- 使用 errors.Is 和 errors.As 进行错误比较。
# 并发处理
- 使用 channel 通信而非共享内存。
- 谨慎使用 goroutine。
- 使用 context 控制超时和取消。
- 使用 sync 包进行同步。
# 测试规范
- 编写单元测试和基准测试。
- 使用表驱动测试。
- 测试文件以 _test.go 结尾。
- 使用 `stretchr/testify` `github.com/agiledragon/gomonkey/v2` 测试框架。
# 项目技术栈
- github.com/uber-go/dig 依赖注入
- github.com/go-jet/jet 数据库查询构建器
- github.com/ThreeDotsLabs/watermill 即时Event消息队列
- github.com/riverqueue/river Job队列
- github.com/gofiber/fiber/v3 HTTP框架
- github.com/swaggo/swag 自动生成API文档, 在controller的方法上使用注解即可
# Atomctl 工具使用
## 生成命令
- gen model从数据库生成模型
- gen provider生成依赖注入提供者
- gen route生成路由定义
## 数据库命令
- migrate执行数据库迁移
- migrate up/down迁移或回滚up 命令执行成功即表示数据库操作完成,无需其它确认操作。
- migrate status查看迁移状态
- migrate create创建迁移文件迁移文件的命名需要使用动词名词的结合方式如 create_users_table, 创建完成后文件会存在于 `database/migrations` 目录下
## 最佳实践
- migration 创建后需要执行 `atomctl migrate up` 执行数据库表迁移
- 使用 gen model 前确保已migrate完成并配置好 database/transform.yaml
- 对model中需要转换的数据结构声明在目录 `database/fields` 中文件名与model名一致
- provider 生成时使用适当的注解标记
- 遵循目录结构约定
# 项目结构
## 标准目录
- main.go主程序入口
- providers/:依赖注入提供者, 通过 atomctl gen provider 生成, 但是你不可以对其中的内容进行修改
- database/fields数据库模型字段定义
- database/schemas数据库自动生成的模型文件不可以进行任何修改
- database/migrations: 数据库迁移文件,通过 atomctl migrate create 创建,你不可以手工创建,只可以使用脚手架工具进行创建
- configs.toml配置文件
- proto/ gRPC proto 定义
- pkg/atom: 为依赖注入框架的核心代码,你不可以进行修改
- fixtures/:测试文件
- app/errorx: 业务错误定义
- app/http: HTTP 服务
- app/grpc: gRPC 服务
- app/jobs: 后台任务定义
- app/middlewares: HTTP 中间件
- app/services: 服务启动逻辑,不可以进行任何修改
# 开发示例
## migration 定义
migration 文件示例.
```
-- +goose Up
-- +goose StatementBegin
CREATE TABLE tenants (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
) ;
COMMENT ON COLUMN tenants.created_at IS '创建时间';
COMMENT ON COLUMN tenants.updated_at IS '更新时间';
COMMENT ON COLUMN tenants.deleted_at IS '删除时间';
-- +goose StatementEnd
------------------------------------------------------------------------------------------------------
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS tenants ;
-- +goose StatementEnd
```
## http module
1. 创建一个新的 http module `atomctl new module [users]`
2. 在 `app/http` 目录下创建相关的处理程序。
3. 定义用户相关的路由。
4. 实现相关逻辑操作
5. module 名称需要使用复数形式,支持多层级目录,如 `atomctl new module [users.orders]`
## controller
- controller 的定义
```go
// @provider
type PayController struct {
svc *Service
log *log.Entry `inject:"false"`
}
func (c *PayController) Prepare() error {
c.log = log.WithField("module", "orders.Controller")
return nil
}
// actions ...
}
```
- controller 文件定义完成后运行 `atomctl gen provider` 来生成 provider
- 一个 action 方法的定义, **@Router**不再使用swago的定义方式替换为下面的定义方式参数做用@Bind来进行声明会自动注入不需要业务内获取参数
```go
// Orders show user orders
// @swagger definitions
// @Router /api/v1/orders/:channel [get]
// @Bind channel path
// @Bind claim local
// @Bind pagination query
// @Bind filter query
func (c *OrderController) List(ctx fiber.Ctx, claim *jwt.Claims,channel string, pagination *requests.Pagination, filter *UserOrderFilter) (*requests.Pager, error) {
pagination.Format()
pager := &requests.Pager{
Pagination: *pagination,
}
filter.UserID = claim.UserID
orders, total, err := c.svc.GetOrders(ctx.Context(), pagination, filter)
if err != nil {
return nil, err
}
pager.Total = total
pager.Items = lo.FilterMap(orders, func(item model.Orders, _ int) (UserOrder, bool) {
var o UserOrder
if err := copier.Copy(&o, item) ; err != nil {
return o, false
}
return o, true
})
return pager, nil
}
```
- 你需要把第二行的 `@swagger definitions` 替换成你的swagger定义
- @Bind 参数会有几个位置 path/query/body/header/cookie/local/file 会分别从 url/get query/post body/header/cookie/fiber.Local/file/中取出所需要的数据绑定到方法的请求参数中去。
- controller 只负责数据的接收返回及相关数据装饰具体的复杂逻辑实现需要在service文件中定义。
- action 文件内容完成运行 `atomctl gen route` 来生成路由
## service
- service 的定义
```go
// @provider
type Service struct {
db *sql.DB
log *log.Entry `inject:"false"`
}
func (svc *Service) Prepare() error {
svc.log = log.WithField("module", "orders.service")
_ = Int(1)
return nil
}
```
- service 文件定义完成后运行 `atomctl gen provider` 来生成 provider
- service 中 model 数据查询的示例需要注意table需要定义为一个短小的tblXXX以便代码展示简洁
```go
// GetUserOrderByOrderID
func (svc *Service) Get(ctx context.Context, orderID string, userID int64) (*model.Orders, error) {
_, span := otel.Start(ctx, "users.service.GetUserOrderByOrderID")
defer span.End()
span.SetAttributes(
attribute.String("order.id", orderID),
attribute.Int64("user.id", userID),
)
tbl := table.Orders
stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.OrderSerial.EQ(String(orderID)).AND(tbl.UserID.EQ(Int64(userID))))
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
var order model.Orders
if err := stmt.QueryContext(ctx, svc.db, &order) ; err != nil {
span.RecordError(err)
return nil, err
}
return &order, nil
}
// UpdateStage
func (svc *Service) Update(ctx context.Context, tenantID, userID, postID int64, stage fields.PostStage) error {
_, span := otel.Start(ctx, "users.service.UpdateStage")
defer span.End()
span.SetAttributes(
attribute.Int64("tenant.id", tenantID),
attribute.Int64("user.id", userID),
attribute.Int64("post.id", postID),
)
tbl := table.Posts
stmt := tbl.
UPDATE(tbl.UpdatedAt, tbl.Stage).
SET(
tbl.UpdatedAt.SET(TimestampT(time.Now())),
tbl.Stage.SET(Int16(int16(stage))),
).
WHERE(
tbl.ID.EQ(Int64(postID)).AND(
tbl.TenantID.EQ(Int64(tenantID)).AND(
tbl.UserID.EQ(Int64(userID)),
),
),
)
span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql()))
if _, err := stmt.ExecContext(ctx, svc.db) ; err != nil {
span.RecordError(err)
return err
}
return svc.Update(ctx, tenantID, userID, postID, post)
}
```
# 本项目说明
- 设计一个支持多租户的用户系统,一个用户可以同时属于多个租户
- 每一个租户有一个租户管理员角色,这个角色可以在后台由系统管理员指定,或者用户在申请创建租户申请时自动指定。
- 除系统管理员外,一个普通用户只可以是一个租户的管理员,不能同时管理多个租户。
**重要提示:**
- `database/schemas` 目录下所有为件为 `atomctl gen model` 自动生成,不能进行任何修改!
- migration SQL 中不要使用 `FOREIGN KEY` 约束,而是在业务中使用代码逻辑进行约束。
- 数据库表需要按需要添加 `created_at` `updated_at` `deleted_at` 字段,并且这三个时间字段(`created_at` `updated_at` `deleted_at`)需要**直接**位于 id 字段后面, **中间不可以包含其它任何字段声明**。
- ID 使用 `bigserial` 类型,数字类的使用 `int8`类型
- 所有表不使用 `FOREIGN KEY` 约束,而是在业务中使用代码逻辑进行约束。
- 所有字段需要添加中文字段 `comment`
- 执行 `migrate up` 命令完成后你不需要再使用 `psql` 来验证是否创建成功

View File

@@ -8,9 +8,6 @@ var Project embed.FS
//go:embed module //go:embed module
var Module embed.FS var Module embed.FS
//go:embed provider
var Provider embed.FS
//go:embed events //go:embed events
var Events embed.FS var Events embed.FS
@@ -19,3 +16,6 @@ var Jobs embed.FS
//go:embed services //go:embed services
var Services embed.FS var Services embed.FS
//go:embed providers/*
var Providers embed.FS