Files
DevPack/internal/profile/profile.go
2026-03-05 17:51:45 +08:00

291 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}