feat: add precise diagnostics and optional client copy logging controls

This commit is contained in:
2026-09-26 18:04:33 +08:00
parent 69585dab61
commit 3bbc6f60d7
44 changed files with 1529 additions and 84 deletions

View File

@@ -7,6 +7,8 @@ import (
"errors"
"reflect"
"time"
"git.yuntuo.hk/k2safe/go-omni/internal/configdiag"
)
// PoolConfig contains only driver-independent settings. Connection credentials,
@@ -25,15 +27,29 @@ func DefaultPoolConfig() PoolConfig {
}
func (c PoolConfig) Validate() error {
if c.MaxOpenConns < 0 || c.MaxIdleConns < 0 {
return errors.New("database pool limits cannot be negative")
for _, f := range []struct {
name string
value int
}{{"maxOpenConns", c.MaxOpenConns}, {"maxIdleConns", c.MaxIdleConns}} {
if f.value < 0 {
return configdiag.New(f.name, "must be nonnegative")
}
}
if c.MaxOpenConns > 0 && c.MaxIdleConns > c.MaxOpenConns {
return errors.New("max idle connections cannot exceed max open connections")
return configdiag.New("maxIdleConns", "must not exceed maxOpenConns")
}
if c.ConnMaxLifetime < 0 || c.ConnMaxIdleTime < 0 || c.PingTimeout <= 0 {
return errors.New("database timeouts are invalid")
for _, f := range []struct {
name string
value time.Duration
}{{"connMaxLifetime", c.ConnMaxLifetime}, {"connMaxIdleTime", c.ConnMaxIdleTime}} {
if f.value < 0 {
return configdiag.New(f.name, "must be nonnegative")
}
}
if c.PingTimeout <= 0 {
return configdiag.New("pingTimeout", "must be positive")
}
return nil
}

View File

@@ -10,6 +10,8 @@ import (
"fmt"
"strings"
"time"
"git.yuntuo.hk/k2safe/go-omni/internal/configdiag"
)
type Config struct {
@@ -43,10 +45,10 @@ func (c Config) PoolConfig() PoolConfig {
func (c Config) Validate() error {
if strings.TrimSpace(c.Driver) == "" {
return errors.New("database driver is required")
return configdiag.New("driver", "is required")
}
if strings.TrimSpace(c.DSN) == "" {
return errors.New("database DSN is required")
return configdiag.New("dsn", "is required")
}
return c.PoolConfig().Validate()
}

View File

@@ -70,3 +70,7 @@ func (c *ServiceContext) DatabaseOptions() []mysqladapter.Option {
`WithDriverConfig(func(*mysql.Config) error)` 支持高级拨号、驱动参数等,配置在回调后克隆;这是项目明确选择的底层扩展。`NewConnector` 返回标准 driver.Connector,供已有基础设施使用;它不接受启动检查选项。任意驱动仍可用核心 `database.OpenConnector`。
错误有安全摘要,errors.Is/As 保留 TLS/驱动/项目检查的原因。配置验证不读证书、不连接数据库;Open 才读取证书、创建池并握手。不要自行打印原始配置或展开可能包含内部信息的 driver 字段给用户。
## 字段级配置错误
Validate 返回独立字段与安全原因,例如 port(1–65535)、readTimeout、pool.maxIdleConns、tls.keyFile。项目调用 `config.AtField("database", err)` 后,统一加载器补齐 database 前缀、文件、环境名和行列,无需重复写端口/连接池校验。TLS 成对配置会指出缺失的那个字段。连接失败、驱动和证书文件的内部原因仍通过 errors.Is/As 保留,不把凭据放进公开提示。见 [配置诊断](../../CONFIG.md#框架模块的字段诊断)。

View File

@@ -19,6 +19,7 @@ import (
"time"
"git.yuntuo.hk/k2safe/go-omni/database"
"git.yuntuo.hk/k2safe/go-omni/internal/configdiag"
"git.yuntuo.hk/k2safe/go-omni/logx"
mysqldriver "github.com/go-sql-driver/mysql"
)
@@ -61,45 +62,60 @@ var identifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
// Validate does not read certificate files or open connections. All defaults
// come from DefaultConfig; explicit zero I/O timeouts mean no driver deadline.
func (c Config) Validate() error {
if c.Socket == "" && (c.Host == "" || strings.ContainsAny(c.Host, "/\r\n\t ") || c.Port < 1 || c.Port > 65535) {
return errors.New("mysql: invalid host or port")
if c.Socket == "" {
if c.Host == "" || strings.ContainsAny(c.Host, "/\r\n\t ") {
return configdiag.New("host", "must be a nonempty hostname or IP address")
}
if c.Port < 1 || c.Port > 65535 {
return configdiag.New("port", "must be between 1 and 65535")
}
}
if strings.ContainsRune(c.Socket, 0) || strings.ContainsRune(c.Database, 0) || strings.ContainsRune(c.User, 0) {
return errors.New("mysql: invalid connection field")
for _, f := range []struct{ name, value string }{{"socket", c.Socket}, {"database", c.Database}, {"user", c.User}} {
if strings.ContainsRune(f.value, 0) {
return configdiag.New(f.name, "must not contain a NUL character")
}
}
if c.ConnectTimeout < 0 || c.ReadTimeout < 0 || c.WriteTimeout < 0 {
return errors.New("mysql: timeouts cannot be negative")
for _, f := range []struct {
name string
value time.Duration
}{{"connectTimeout", c.ConnectTimeout}, {"readTimeout", c.ReadTimeout}, {"writeTimeout", c.WriteTimeout}} {
if f.value < 0 {
return configdiag.New(f.name, "must be nonnegative")
}
}
if err := c.Pool.Validate(); err != nil {
return err
return configdiag.At("pool", err)
}
if c.Location != "" {
if _, err := time.LoadLocation(c.Location); err != nil {
return errors.New("mysql: invalid time location")
return configdiag.Wrap("location", "must be an available IANA time zone or UTC", err)
}
}
if c.Charset != "" && !identifier.MatchString(c.Charset) {
return errors.New("mysql: invalid charset")
return configdiag.New("charset", "must be a valid MySQL identifier")
}
if c.Collation != "" && !identifier.MatchString(c.Collation) {
return errors.New("mysql: invalid collation")
return configdiag.New("collation", "must be a valid MySQL identifier")
}
for key := range c.Session {
if !identifier.MatchString(key) {
return errors.New("mysql: invalid session variable name")
return configdiag.New("session", "keys must be valid MySQL identifiers")
}
}
if c.TLS.Mode != "" && c.TLS.Mode != "required" && c.TLS.Mode != "disabled" {
return errors.New("mysql: TLS mode must be required or disabled")
return configdiag.New("tls.mode", "must be required or disabled")
}
if (c.TLS.CertFile == "") != (c.TLS.KeyFile == "") {
return errors.New("mysql: TLS client certificate and key must be configured together")
if c.TLS.CertFile == "" {
return configdiag.New("tls.certFile", "is required when keyFile is configured")
}
return configdiag.New("tls.keyFile", "is required when certFile is configured")
}
if c.TLS.Mode == "disabled" && (c.TLS.CAFile != "" || c.TLS.CertFile != "" || c.TLS.ServerName != "") {
return errors.New("mysql: TLS fields cannot be used when disabled")
return configdiag.New("tls.mode", "must be required when TLS fields are configured")
}
if c.Socket != "" && c.TLS.Mode != "disabled" && c.TLS.ServerName == "" {
return errors.New("mysql: TLS over a unix socket requires serverName")
return configdiag.New("tls.serverName", "is required for TLS over a unix socket")
}
return nil
}