新增profile模块
This commit is contained in:
290
internal/profile/profile.go
Normal file
290
internal/profile/profile.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user