291 lines
7.5 KiB
Go
291 lines
7.5 KiB
Go
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()
|
||
}
|