Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions server/controller/common/metadata/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (m *Platform) SetDomain(domain metadbmodel.Domain) {
m.teamID = domain.TeamID
m.LogPrefixes = append(m.LogPrefixes, logger.NewTeamPrefix(domain.TeamID))
}
m.LogPrefixes = append(m.LogPrefixes, NewDomainPrefix(domain.Name))
m.LogPrefixes = append(m.LogPrefixes, NewDomainPrefix(domain.Name, domain.Lcuuid))
}

func (m *Platform) SetSubDomain(subDomain metadbmodel.SubDomain) {
Expand All @@ -126,7 +126,7 @@ func (m *Platform) SetSubDomain(subDomain metadbmodel.SubDomain) {
m.teamID = subDomain.TeamID
m.LogPrefixes = append(m.LogPrefixes, logger.NewTeamPrefix(subDomain.TeamID))
}
m.LogPrefixes = append(m.LogPrefixes, NewSubDomainPrefix(subDomain.Name))
m.LogPrefixes = append(m.LogPrefixes, NewSubDomainPrefix(subDomain.Name, subDomain.Lcuuid))
}

func MetadataDomain(domain metadbmodel.Domain) func(*Platform) {
Expand All @@ -149,11 +149,11 @@ type SubDomainInfo struct {
metadbmodel.SubDomain
}

func NewDomainPrefix(name string) logger.Prefix {
func NewDomainPrefix(name, lcuuid string) logger.Prefix {
if name == "" {
return &DomainIDPrefix{0}
}
return &DomainNameLogPrefix{name}
return &DomainNameLogPrefix{name, lcuuid}
}

type DomainIDPrefix struct {
Expand All @@ -165,21 +165,23 @@ func (p *DomainIDPrefix) Prefix() string {
}

type DomainNameLogPrefix struct {
Name string
Name string
Lcuuid string
}

func (p *DomainNameLogPrefix) Prefix() string {
return fmt.Sprintf("[DomainName-%s]", p.Name)
return fmt.Sprintf("[DomainName-%s-%s]", p.Name, p.Lcuuid)
}

func NewSubDomainPrefix(name string) logger.Prefix {
return &SubDomainNameLogPrefix{name}
func NewSubDomainPrefix(name, lcuuid string) logger.Prefix {
return &SubDomainNameLogPrefix{name, lcuuid}
}

type SubDomainNameLogPrefix struct {
Name string
Name string
Lcuuid string
}

func (p *SubDomainNameLogPrefix) Prefix() string {
return fmt.Sprintf("[SubDomainName-%s]", p.Name)
return fmt.Sprintf("[SubDomainName-%s-%s]", p.Name, p.Lcuuid)
}
6 changes: 6 additions & 0 deletions server/controller/recorder/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type RecorderConfig struct {
EventCfg eventConfig.Config
SelfHealCfg SelfHealConfig `yaml:"self_heal"`
TagRecorderSelfHealCfg TagRecorderSelfHealConfig `yaml:"tagrecorder_self_heal"`
SkipSyncIfEmptyCfg SkipSyncIfEmptyConfig `yaml:"skip_sync_if_empty"`
}

func Get() *RecorderConfig {
Expand All @@ -51,6 +52,11 @@ type LogDebugConfig struct {
ResourceTypes []string `default:"" yaml:"resource_type"`
}

type SkipSyncIfEmptyConfig struct {
Enabled bool `default:"false" yaml:"enabled"`
Resources []string `default:"" yaml:"resources"`
}

type SelfHealConfig struct {
Enabled bool `default:"true" yaml:"enabled"`
Resources []string `default:"" yaml:"resources"`
Expand Down
42 changes: 42 additions & 0 deletions server/controller/recorder/debugger.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
package recorder

import (
"fmt"
"reflect"
"strings"

"github.com/deepflowio/deepflow/server/controller/recorder/cache"
)
Expand Down Expand Up @@ -66,3 +68,43 @@ func (r *Recorder) GetToolMap(domainLcuuid, subDomainLcuuid, field string) map[i
}
return dataSet.(map[interface{}]interface{})
}

// GetResourceFieldCountsString returns a string representation of slice and map field counts in the resource object
// This is a more descriptive name than CountString
func GetResourceFieldCountsString(obj interface{}) string {
var parts []string
v := reflect.ValueOf(obj)
t := v.Type()

for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
name := t.Field(i).Name

switch f.Kind() {
case reflect.Slice:
parts = append(parts, fmt.Sprintf("%s=%d", name, f.Len()))
case reflect.Map:
parts = append(parts, fmt.Sprintf("%s=%d", name, f.Len()))
for _, key := range f.MapKeys() {
sub := joinSliceFields(f.MapIndex(key))
parts = append(parts, fmt.Sprintf("%s[%v]=%s", name, key.Interface(), sub))
}
}
}
return strings.Join(parts, ", ")
}

// 提取结构体中所有 slice 字段的 Name=Len 拼接
func joinSliceFields(v reflect.Value) string {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
var parts []string
t := v.Type()
for i := 0; i < v.NumField(); i++ {
if f := v.Field(i); f.Kind() == reflect.Slice {
parts = append(parts, fmt.Sprintf("%s=%d", t.Field(i).Name, f.Len()))
}
}
return strings.Join(parts, ", ")
}
165 changes: 165 additions & 0 deletions server/controller/recorder/debugger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package recorder

import (
"strings"
"testing"
"time"

"github.com/deepflowio/deepflow/server/controller/cloud/model"
)

// TestGetResourceFieldCountsString tests the GetResourceFieldCountsString function with various scenarios
func TestGetResourceFieldCountsString(t *testing.T) {
// Test case 1: Empty lists and maps (0 values)
t.Run("EmptyValues", func(t *testing.T) {
resource := model.Resource{
SubDomains: []model.SubDomain{},
VMs: []model.VM{},
VPCs: []model.VPC{},
SubDomainResources: map[string]model.SubDomainResource{},
}

result := GetResourceFieldCountsString(resource)

// Expected result should contain all fields with 0 counts
expectedParts := []string{
"SubDomains=0",
"VMs=0",
"VPCs=0",
"SubDomainResources=0",
}

for _, part := range expectedParts {
if !strings.Contains(result, part) {
t.Errorf("Expected to find '%s' in result: %s", part, result)
}
}

// Ensure no map entries are shown since all maps are empty
if strings.Contains(result, "[") && strings.Contains(result, "]") {
t.Errorf("No map entries should be present for empty maps, got: %s", result)
}
})

// Test case 2: Non-empty lists and maps
t.Run("WithValues", func(t *testing.T) {
resource := model.Resource{
SubDomains: []model.SubDomain{{Lcuuid: "sd1", Name: "subdomain1"}},
VMs: []model.VM{{Name: "test-vm-1", Lcuuid: "vm1"}, {Name: "test-vm-2", Lcuuid: "vm2"}},
VPCs: []model.VPC{{Name: "test-vpc-1", Lcuuid: "vpc1"}},
SubDomainResources: map[string]model.SubDomainResource{"sub1": {}},
}

result := GetResourceFieldCountsString(resource)

// Check that we have the expected counts
expectedContains := []string{
"SubDomains=1",
"VMs=2",
"VPCs=1",
"SubDomainResources=1",
}

for _, exp := range expectedContains {
if !strings.Contains(result, exp) {
t.Errorf("Expected to find '%s' in result: %s", exp, result)
}
}

// Check that the map entry is properly formatted
if !strings.Contains(result, "SubDomainResources[sub1]=") {
t.Errorf("Expected map entry SubDomainResources[sub1]= to be present, got: %s", result)
}
})

// Test case 3: Map with flat structure having 0 values and non-zero values
t.Run("MapWithFlatStructure", func(t *testing.T) {
// Test empty SubDomainResource (all slice fields are zero values)
resourceEmpty := model.Resource{
SubDomainResources: map[string]model.SubDomainResource{
"empty": {}, // All slice fields in SubDomainResource are zero values (empty slices)
},
}

resultEmpty := GetResourceFieldCountsString(resourceEmpty)

// Should contain the map count
if !strings.Contains(resultEmpty, "SubDomainResources=1") {
t.Errorf("Expected SubDomainResources=1 in result: %s", resultEmpty)
}

// The 'empty' entry should show all slice counts as 0
if !strings.Contains(resultEmpty, "SubDomainResources[empty]=") {
t.Errorf("Expected SubDomainResources[empty]= entry in result: %s", resultEmpty)
}

// Even though SubDomainResource is empty, it should still show all slice fields as 0
// Since SubDomainResource has many slice fields, the result should contain multiple "=0" entries
zeroCountFound := strings.Contains(resultEmpty, "=0, ") || strings.HasSuffix(resultEmpty, "=0")
if !zeroCountFound {
t.Errorf("Expected to find zero counts in SubDomainResources[empty] entry, got: %s", resultEmpty)
}

// Test SubDomainResource with non-zero values
resourceWithData := model.Resource{
SubDomainResources: map[string]model.SubDomainResource{
"with-data": { // SubDomainResource with some data in slices
Networks: []model.Network{{Lcuuid: "net1", Name: "network1"}, {Lcuuid: "net2", Name: "network2"}},
Subnets: []model.Subnet{{Lcuuid: "subnet1", Name: "subnet1"}},
Pods: []model.Pod{{Lcuuid: "pod1", Name: "pod1"}, {Lcuuid: "pod2", Name: "pod2"}, {Lcuuid: "pod3", Name: "pod3"}},
},
},
}

resultWithData := GetResourceFieldCountsString(resourceWithData)

// Should contain the map count
if !strings.Contains(resultWithData, "SubDomainResources=1") {
t.Errorf("Expected SubDomainResources=1 in result: %s", resultWithData)
}

// The 'with-data' entry should show the count of slices inside the SubDomainResource
if !strings.Contains(resultWithData, "SubDomainResources[with-data]=") {
t.Errorf("Expected SubDomainResources[with-data]= entry in result: %s", resultWithData)
}

// Verify specific counts for the slices
if !strings.Contains(resultWithData, "Networks=2") {
t.Errorf("Expected Networks=2 in the with-data entry, got: %s", resultWithData)
}
if !strings.Contains(resultWithData, "Subnets=1") {
t.Errorf("Expected Subnets=1 in the with-data entry, got: %s", resultWithData)
}
if !strings.Contains(resultWithData, "Pods=3") {
t.Errorf("Expected Pods=3 in the with-data entry, got: %s", resultWithData)
}
})

// Test case 4: Resource with only basic fields (no slices or maps)
t.Run("BasicFieldsOnly", func(t *testing.T) {
// Create a resource with basic fields that don't contribute to the count
resource := model.Resource{
Verified: true,
ErrorState: 0,
ErrorMessage: "",
SyncAt: time.Now(),
SubDomains: []model.SubDomain{}, // Empty slice
SubDomainResources: map[string]model.SubDomainResource{}, // Empty map
}

result := GetResourceFieldCountsString(resource)

// Should only show slice and map fields, not basic fields like Verified, ErrorState, etc.
if !strings.Contains(result, "SubDomains=0") {
t.Errorf("Expected SubDomains=0 in result: %s", result)
}
if !strings.Contains(result, "SubDomainResources=0") {
t.Errorf("Expected SubDomainResources=0 in result: %s", result)
}

// Basic fields shouldn't appear in the result
if strings.Contains(result, "Verified=") {
t.Errorf("Basic fields like Verified should not appear in result: %s", result)
}
})
}
19 changes: 13 additions & 6 deletions server/controller/recorder/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

cloudmodel "github.com/deepflowio/deepflow/server/controller/cloud/model"
"github.com/deepflowio/deepflow/server/controller/common"
mysqlmodel "github.com/deepflowio/deepflow/server/controller/db/metadb/model"
metadbmodel "github.com/deepflowio/deepflow/server/controller/db/metadb/model"
"github.com/deepflowio/deepflow/server/controller/recorder/cache"
rcommon "github.com/deepflowio/deepflow/server/controller/recorder/common"
"github.com/deepflowio/deepflow/server/controller/recorder/config"
Expand Down Expand Up @@ -74,14 +74,14 @@ func (d *domain) CloseStatsd() {
}

func (d *domain) Refresh(target string, cloudData cloudmodel.Resource) error {
log.Infof("refresh target: %s", target, d.metadata.LogPrefixes)
log.Infof("refresh target: %s, cloudData count: %s", target, GetResourceFieldCountsString(cloudData), d.metadata.LogPrefixes)
switch target {
case RefreshTargetDomain:
log.Info("refresher started, triggered by ticker/hand", d.metadata.LogPrefixes)
if err := d.refreshDomainExcludeSubDomain(cloudData); err != nil {
return err
}
return d.subDomains.RefreshAll(cloudData.SubDomainResources)
return d.subDomains.RefreshAll(cloudData.SubDomains, cloudData.SubDomainResources)
case RefreshTargetSubDomain:
log.Info("refresher started, triggered by hand", d.metadata.LogPrefixes)
return d.subDomains.RefreshOne(cloudData.SubDomainResources)
Expand Down Expand Up @@ -132,6 +132,13 @@ func (d *domain) shouldRefresh(cloudData cloudmodel.Resource) error {
log.Info("domain has no vms and pods, does nothing", d.metadata.LogPrefixes)
return DataMissingError
}
// 检查当 SubDomains 为空时,是否需要跳过同步
if d.metadata.Config.SkipSyncIfEmptyCfg.Enabled &&
slices.Contains(d.metadata.Config.SkipSyncIfEmptyCfg.Resources, common.RESOURCE_TYPE_SUB_DOMAIN_EN) &&
len(cloudData.SubDomains) == 0 {
log.Info("domain has no SubDomains, does nothing", d.metadata.LogPrefixes)
return DataMissingError
}
} else {
log.Info("domain is not verified, does nothing", d.metadata.LogPrefixes)
return DataNotVerifiedError
Expand Down Expand Up @@ -285,7 +292,7 @@ func (d *domain) updateSyncedAt(syncAt time.Time) {
log.Infof("update domain synced_at: %s", syncAt.Format(common.GO_BIRTHDAY), d.metadata.LogPrefixes)
d.fillStatsd(syncAt)

var domain mysqlmodel.Domain
var domain metadbmodel.Domain
err := d.metadata.DB.Where("lcuuid = ?", d.metadata.GetDomainLcuuid()).First(&domain).Error
if err != nil {
log.Errorf("get domain from db failed: %s", err, d.metadata.LogPrefixes)
Expand All @@ -303,7 +310,7 @@ func (d *domain) fillStatsd(syncAt time.Time) {
}

func (d *domain) updateStateInfo(cloudData cloudmodel.Resource) {
var domain mysqlmodel.Domain
var domain metadbmodel.Domain
err := d.metadata.DB.Where("lcuuid = ?", d.metadata.GetDomainLcuuid()).First(&domain).Error
if err != nil {
log.Errorf("get domain from db failed: %s", err, d.metadata.LogPrefixes)
Expand All @@ -314,7 +321,7 @@ func (d *domain) updateStateInfo(cloudData cloudmodel.Resource) {
log.Debugf("update domain (%+v)", domain, d.metadata.LogPrefixes)

for subDomainLcuuid, subDomainResource := range cloudData.SubDomainResources {
var subDomain mysqlmodel.SubDomain
var subDomain metadbmodel.SubDomain
err := d.metadata.DB.Where("lcuuid = ?", subDomainLcuuid).First(&subDomain).Error
if err != nil {
log.Errorf("get sub_domain (lcuuid: %s) from db failed: %s", subDomainLcuuid, err, d.metadata.LogPrefixes)
Expand Down
Loading
Loading