新增profile模块

This commit is contained in:
zyj
2026-03-05 17:51:45 +08:00
parent ac9315c0a1
commit 97e34c02ac
3 changed files with 923 additions and 0 deletions

290
internal/profile/profile.go Normal file
View File

@@ -0,0 +1,290 @@
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()
}