diff --git a/internal/exporter/collector/collector.go b/internal/exporter/collector/collector.go index 13ad60f4..f08f6331 100644 --- a/internal/exporter/collector/collector.go +++ b/internal/exporter/collector/collector.go @@ -279,15 +279,21 @@ func (c *collector) collectComponentData(data *HealthData) error { health := "Unknown" reason := "No health data" + errorMsg := "" var timeValue interface{} var extraInfo interface{} = map[string]interface{}{} // Default to empty map for JSON marshaling + var suggestedActions interface{} var incidents interface{} = []apiv1.HealthStateIncident{} if len(healthStates) > 0 { firstState := healthStates[0] health = string(firstState.Health) reason = firstState.Reason + errorMsg = firstState.Error timeValue = firstState.Time + if firstState.SuggestedActions != nil { + suggestedActions = firstState.SuggestedActions + } if len(firstState.Incidents) > 0 { incidents = firstState.Incidents } @@ -314,12 +320,14 @@ func (c *collector) collectComponentData(data *HealthData) error { } componentData[componentName] = map[string]interface{}{ - "component_name": componentName, - "health": health, - "reason": reason, - "time": timeValue, - "extra_info": extraInfo, - "incidents": incidents, + "component_name": componentName, + "health": health, + "reason": reason, + "error": errorMsg, + "time": timeValue, + "extra_info": extraInfo, + "suggested_actions": suggestedActions, + "incidents": incidents, } } diff --git a/internal/exporter/collector/collector_test.go b/internal/exporter/collector/collector_test.go index 65450d9a..a7c9b0bc 100644 --- a/internal/exporter/collector/collector_test.go +++ b/internal/exporter/collector/collector_test.go @@ -440,8 +440,13 @@ func TestCollector_CollectComponentData_WithComponents(t *testing.T) { Component: "test-component", Health: "Healthy", Reason: "All checks passed", + Error: "detailed error info", Time: metav1.Time{Time: time.Now()}, ExtraInfo: map[string]string{"key": "value"}, + SuggestedActions: &apiv1.SuggestedActions{ + Description: "reboot the node", + RepairActions: []apiv1.RepairActionType{apiv1.RepairActionTypeRebootSystem}, + }, Incidents: []apiv1.HealthStateIncident{ { EntityID: "GPU-1234", @@ -472,6 +477,11 @@ func TestCollector_CollectComponentData_WithComponents(t *testing.T) { assert.Equal(t, "test-component", dataMap["component_name"]) assert.Equal(t, "Healthy", dataMap["health"]) assert.Equal(t, "All checks passed", dataMap["reason"]) + assert.Equal(t, "detailed error info", dataMap["error"]) + suggestedActions, ok := dataMap["suggested_actions"].(*apiv1.SuggestedActions) + require.True(t, ok, "suggested_actions should preserve the typed SuggestedActions pointer") + require.NotNil(t, suggestedActions) + assert.Equal(t, "reboot the node", suggestedActions.Description) incidents, ok := dataMap["incidents"].([]apiv1.HealthStateIncident) require.True(t, ok, "incidents should preserve the typed health incidents slice") require.Len(t, incidents, 1) @@ -504,6 +514,8 @@ func TestCollector_CollectComponentData_NoHealthStates(t *testing.T) { compData := data.ComponentData["empty-component"].(map[string]interface{}) assert.Equal(t, "Unknown", compData["health"]) assert.Equal(t, "No health data", compData["reason"]) + assert.Equal(t, "", compData["error"]) + assert.Nil(t, compData["suggested_actions"]) } func TestCollector_AllFeaturesEnabled(t *testing.T) { diff --git a/internal/exporter/converter/otlp.go b/internal/exporter/converter/otlp.go index 30348c88..778952d3 100644 --- a/internal/exporter/converter/otlp.go +++ b/internal/exporter/converter/otlp.go @@ -396,8 +396,10 @@ func (c *otlpConverter) convertToOTLPLogs(data *collector.HealthData) []*logsv1. health := componentInfo["health"] reason := componentInfo["reason"] + errorMsg := componentInfo["error"] timeVal := componentInfo["time"] extraInfo := componentInfo["extra_info"] + suggestedActions := componentInfo["suggested_actions"] incidents := componentInfo["incidents"] attributes := []*commonv1.KeyValue{ @@ -428,6 +430,15 @@ func (c *otlpConverter) convertToOTLPLogs(data *collector.HealthData) []*logsv1. } // Add optional fields + if errStr, ok := errorMsg.(string); ok && errStr != "" { + attributes = append(attributes, &commonv1.KeyValue{ + Key: "error", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{StringValue: errStr}, + }, + }) + } + if timeVal != nil { attributes = append(attributes, &commonv1.KeyValue{ Key: "time", @@ -449,6 +460,18 @@ func (c *otlpConverter) convertToOTLPLogs(data *collector.HealthData) []*logsv1. } } + if suggestedActions != nil { + jsonBytes, err := json.Marshal(suggestedActions) + if err == nil && string(jsonBytes) != "null" { + attributes = append(attributes, &commonv1.KeyValue{ + Key: "suggested_actions", + Value: &commonv1.AnyValue{ + Value: &commonv1.AnyValue_StringValue{StringValue: string(jsonBytes)}, + }, + }) + } + } + if typedIncidents, ok := toHealthStateIncidents(incidents); ok && len(typedIncidents) > 0 { attributes = append(attributes, &commonv1.KeyValue{ Key: "incidents", diff --git a/internal/exporter/converter/otlp_test.go b/internal/exporter/converter/otlp_test.go index 68ba4ead..83d879b6 100644 --- a/internal/exporter/converter/otlp_test.go +++ b/internal/exporter/converter/otlp_test.go @@ -302,12 +302,17 @@ func TestOTLPConverter_Convert_WithComponentData(t *testing.T) { "gpu": map[string]any{ "time": metav1.Time{Time: time.Now()}, "component_name": "gpu", - "health": "healthy", - "reason": "All checks passed", + "health": "Unhealthy", + "reason": "failed to get recent events", + "error": "database is locked", "extra_info": map[string]any{ "device_uuid": "PCI:0000:04:00", "data": rawData, }, + "suggested_actions": &apiv1.SuggestedActions{ + Description: "reboot the node", + RepairActions: []apiv1.RepairActionType{apiv1.RepairActionTypeRebootSystem}, + }, "incidents": []apiv1.HealthStateIncident{ { EntityID: "GPU-1234", @@ -335,12 +340,19 @@ func TestOTLPConverter_Convert_WithComponentData(t *testing.T) { // Find component data log found := false for _, log := range logs { - if contains(log.Body.GetStringValue(), "gpu") && contains(log.Body.GetStringValue(), "healthy") { + if contains(log.Body.GetStringValue(), "gpu") && contains(log.Body.GetStringValue(), "Unhealthy") { + assert.Equal(t, "database is locked", findAttribute(t, log.Attributes, "error").GetStringValue()) + extraInfo := findAttribute(t, log.Attributes, "extra_info").GetStringValue() require.NotEmpty(t, extraInfo) assert.Contains(t, extraInfo, `"device_uuid":"PCI:0000:04:00"`) assert.Contains(t, extraInfo, `"data":"{\"time\":\"2026-02-20T23:22:44Z\",\"data_source\":\"kmsg\",\"xid\":149}"`) + suggestedActions := findAttribute(t, log.Attributes, "suggested_actions").GetStringValue() + require.NotEmpty(t, suggestedActions) + assert.Contains(t, suggestedActions, `"description":"reboot the node"`) + assert.Contains(t, suggestedActions, `"REBOOT_SYSTEM"`) + incidents := findAttribute(t, log.Attributes, "incidents").GetArrayValue() require.NotNil(t, incidents) require.Len(t, incidents.Values, 1)