package profile import ( "fmt" "io" "os" "path/filepath" "regexp" "strings" "time" "gopkg.in/yaml.v3" ) // 格式版本号,用于向后兼容检查 const CurrentVersion = 1 // 名称合法性正则:1-64 字符,仅允许字母、数字、下划线、连字符 var nameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`) // Profile 环境配置文件,控制哪些采集器启用及其过滤规则 type Profile struct { Name string `yaml:"name"` Description string `yaml:"description,omitempty"` Version int `yaml:"version"` Collectors CollectorConfig `yaml:"collectors"` Settings map[string]SettingMap `yaml:"settings,omitempty"` } // CollectorConfig 采集器启用/禁用配置 type CollectorConfig struct { Enabled []string `yaml:"enabled,omitempty"` // 启用列表(空=全部启用) Disabled []string `yaml:"disabled,omitempty"` // 显式禁用列表 } // SettingMap 采集器自定义选项(键值对) type SettingMap map[string]interface{} // ProfileMeta 列出 Profile 时的摘要信息 type ProfileMeta struct { Name string `json:"name"` Description string `json:"description"` Path string `json:"path"` ModTime time.Time `json:"mod_time"` Collectors int `json:"collectors"` // 启用的采集器数量 } // ---------------------------------------------------------------- // 验证 // ---------------------------------------------------------------- // Validate 校验 Profile 的完整性和合法性 func (p *Profile) Validate() error { // 名称非空且合法 if !nameRegex.MatchString(p.Name) { return fmt.Errorf("E1010: Profile 名称 %q 非法,仅允许 1-64 个字母、数字、下划线或连字符", p.Name) } // 版本兼容 if p.Version < 1 || p.Version > CurrentVersion { return fmt.Errorf("E1016: Profile 版本 %d 不兼容,当前支持最高版本 %d", p.Version, CurrentVersion) } // enabled 与 disabled 不能有交集 if len(p.Collectors.Enabled) > 0 && len(p.Collectors.Disabled) > 0 { enabledSet := make(map[string]struct{}, len(p.Collectors.Enabled)) for _, name := range p.Collectors.Enabled { enabledSet[name] = struct{}{} } for _, name := range p.Collectors.Disabled { if _, ok := enabledSet[name]; ok { return fmt.Errorf("E1013: 采集器 %q 同时出现在 enabled 和 disabled 列表中", name) } } } return nil } // IsCollectorEnabled 判断指定采集器在此 Profile 下是否启用 func (p *Profile) IsCollectorEnabled(name string) bool { // 检查是否显式禁用 for _, d := range p.Collectors.Disabled { if d == name { return false } } // 如果 enabled 列表非空,只有列表中的才启用 if len(p.Collectors.Enabled) > 0 { for _, e := range p.Collectors.Enabled { if e == name { return true } } return false } // enabled 为空表示全部启用 return true } // GetSetting 获取指定采集器的某个设置项 func (p *Profile) GetSetting(collector, key string) (interface{}, bool) { sm, ok := p.Settings[collector] if !ok { return nil, false } val, ok := sm[key] return val, ok } // GetSettingBool 获取布尔类型设置项,不存在时返回 defaultVal func (p *Profile) GetSettingBool(collector, key string, defaultVal bool) bool { val, ok := p.GetSetting(collector, key) if !ok { return defaultVal } if b, ok := val.(bool); ok { return b } return defaultVal } // GetSettingStrings 获取字符串数组类型设置项 func (p *Profile) GetSettingStrings(collector, key string) []string { val, ok := p.GetSetting(collector, key) if !ok { return nil } switch v := val.(type) { case []interface{}: result := make([]string, 0, len(v)) for _, item := range v { if s, ok := item.(string); ok { result = append(result, s) } } return result case []string: return v default: return nil } } // ---------------------------------------------------------------- // CRUD 操作 // ---------------------------------------------------------------- // Load 从指定路径加载 Profile 文件 func Load(path string) (*Profile, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("打开 Profile 失败: %w", err) } defer f.Close() p := &Profile{} if err := yaml.NewDecoder(f).Decode(p); err != nil { return nil, fmt.Errorf("解析 Profile 失败: %w", err) } if err := p.Validate(); err != nil { return nil, err } return p, nil } // LoadByName 根据名称从 profiles 目录加载 Profile func LoadByName(profilesDir, name string) (*Profile, error) { path := filepath.Join(profilesDir, name+".yaml") return Load(path) } // Save 将 Profile 保存到指定路径(原子写入:先写临时文件再重命名) func Save(path string, p *Profile) error { if err := p.Validate(); err != nil { return err } // 确保目录存在 dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("创建目录失败: %w", err) } // 写入临时文件 tmpPath := path + ".tmp" f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return fmt.Errorf("创建临时文件失败: %w", err) } enc := yaml.NewEncoder(f) enc.SetIndent(2) if err := enc.Encode(p); err != nil { f.Close() os.Remove(tmpPath) return fmt.Errorf("序列化 Profile 失败: %w", err) } if err := enc.Close(); err != nil { f.Close() os.Remove(tmpPath) return fmt.Errorf("关闭编码器失败: %w", err) } if err := f.Close(); err != nil { os.Remove(tmpPath) return fmt.Errorf("关闭文件失败: %w", err) } // 原子重命名 if err := os.Rename(tmpPath, path); err != nil { os.Remove(tmpPath) return fmt.Errorf("重命名文件失败: %w", err) } return nil } // SaveByName 根据名称保存到 profiles 目录 func SaveByName(profilesDir string, p *Profile) error { path := filepath.Join(profilesDir, p.Name+".yaml") return Save(path, p) } // Delete 删除指定名称的 Profile 文件 func Delete(profilesDir, name string) error { path := filepath.Join(profilesDir, name+".yaml") if _, err := os.Stat(path); os.IsNotExist(err) { return fmt.Errorf("Profile %q 不存在", name) } return os.Remove(path) } // Exists 检查指定名称的 Profile 是否存在 func Exists(profilesDir, name string) bool { path := filepath.Join(profilesDir, name+".yaml") _, err := os.Stat(path) return err == nil } // List 列出目录下所有 Profile 摘要信息 func List(profilesDir string) ([]ProfileMeta, error) { entries, err := os.ReadDir(profilesDir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("读取 profiles 目录失败: %w", err) } var metas []ProfileMeta for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") { continue } path := filepath.Join(profilesDir, entry.Name()) p, err := Load(path) if err != nil { continue // 跳过无效文件 } info, _ := entry.Info() collectorCount := len(p.Collectors.Enabled) if collectorCount == 0 { collectorCount = -1 // -1 表示全部启用 } metas = append(metas, ProfileMeta{ Name: p.Name, Description: p.Description, Path: path, ModTime: info.ModTime(), Collectors: collectorCount, }) } return metas, nil } // Export 将 Profile 以格式化 YAML 写入 Writer(用于 profile show 命令) func Export(w io.Writer, p *Profile) error { enc := yaml.NewEncoder(w) enc.SetIndent(2) if err := enc.Encode(p); err != nil { return fmt.Errorf("导出 Profile 失败: %w", err) } return enc.Close() }