Add parse errors tab and improve error diagnostics UI
This commit is contained in:
@@ -110,6 +110,12 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
||||
if recoveredN := c.recoverCriticalRedfishDocsPlanB(ctx, criticalClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, emit); recoveredN > 0 {
|
||||
c.debugSnapshotf("critical plan-b recovered docs=%d", recoveredN)
|
||||
}
|
||||
// Hide transient fetch errors for endpoints that were eventually recovered into rawTree.
|
||||
for p := range fetchErrMap {
|
||||
if _, ok := rawTree[p]; ok {
|
||||
delete(fetchErrMap, p)
|
||||
}
|
||||
}
|
||||
if emit != nil {
|
||||
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: анализ raw snapshot..."})
|
||||
}
|
||||
|
||||
@@ -590,6 +590,203 @@ func (s *Server) handleGetFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
jsonResponse(w, buildFirmwareEntries(result.Hardware))
|
||||
}
|
||||
|
||||
type parseErrorEntry struct {
|
||||
Source string `json:"source"` // redfish | parser | file | collect_log
|
||||
Category string `json:"category"` // fetch | partial_inventory | collect_log
|
||||
Severity string `json:"severity,omitempty"` // error | warning | info
|
||||
Path string `json:"path,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) handleGetParseErrors(w http.ResponseWriter, r *http.Request) {
|
||||
result := s.GetResult()
|
||||
rawPkg := s.GetRawExport()
|
||||
|
||||
items := make([]parseErrorEntry, 0)
|
||||
seen := make(map[string]struct{})
|
||||
add := func(e parseErrorEntry) {
|
||||
key := strings.TrimSpace(e.Source) + "|" + strings.TrimSpace(e.Category) + "|" + strings.TrimSpace(e.Path) + "|" + strings.TrimSpace(e.Message)
|
||||
if _, ok := seen[key]; ok {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
items = append(items, e)
|
||||
}
|
||||
|
||||
var fetchErrMap map[string]string
|
||||
if result != nil && result.RawPayloads != nil {
|
||||
fetchErrMap = extractRedfishFetchErrors(result.RawPayloads["redfish_fetch_errors"])
|
||||
for path, msg := range fetchErrMap {
|
||||
add(parseErrorEntry{
|
||||
Source: "redfish",
|
||||
Category: "fetch",
|
||||
Severity: parseErrorSeverityFromMessage(msg),
|
||||
Path: path,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if rawPkg != nil && len(rawPkg.Source.CollectLogs) > 0 {
|
||||
for _, line := range rawPkg.Source.CollectLogs {
|
||||
if !looksLikeErrorLogLine(line) {
|
||||
continue
|
||||
}
|
||||
add(parseErrorEntry{
|
||||
Source: "collect_log",
|
||||
Category: "collect_log",
|
||||
Severity: parseErrorSeverityFromMessage(line),
|
||||
Message: strings.TrimSpace(line),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if result != nil && result.Protocol == "redfish" && result.Hardware != nil {
|
||||
hw := result.Hardware
|
||||
if len(hw.Memory) == 0 && hasFetchErrorSuffix(fetchErrMap, "/Memory") {
|
||||
add(parseErrorEntry{
|
||||
Source: "parser",
|
||||
Category: "partial_inventory",
|
||||
Severity: "warning",
|
||||
Path: "/redfish/v1/Systems/*/Memory",
|
||||
Message: "Memory inventory is empty because Redfish Memory endpoint failed during collection",
|
||||
})
|
||||
}
|
||||
if len(hw.CPUs) == 0 && hasFetchErrorSuffix(fetchErrMap, "/Processors") {
|
||||
add(parseErrorEntry{
|
||||
Source: "parser",
|
||||
Category: "partial_inventory",
|
||||
Severity: "warning",
|
||||
Path: "/redfish/v1/Systems/*/Processors",
|
||||
Message: "CPU inventory is empty because Redfish Processors endpoint failed during collection",
|
||||
})
|
||||
}
|
||||
if len(hw.Firmware) == 0 && (hasFetchErrorSuffix(fetchErrMap, "/Managers/1") || hasFetchErrorSuffix(fetchErrMap, "/UpdateService")) {
|
||||
add(parseErrorEntry{
|
||||
Source: "parser",
|
||||
Category: "partial_inventory",
|
||||
Severity: "warning",
|
||||
Message: "Firmware inventory may be incomplete due to Redfish Manager/UpdateService endpoint failures",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].Severity != items[j].Severity {
|
||||
// error > warning > info
|
||||
return parseErrorSeverityRank(items[i].Severity) < parseErrorSeverityRank(items[j].Severity)
|
||||
}
|
||||
if items[i].Source != items[j].Source {
|
||||
return items[i].Source < items[j].Source
|
||||
}
|
||||
if items[i].Category != items[j].Category {
|
||||
return items[i].Category < items[j].Category
|
||||
}
|
||||
return items[i].Path < items[j].Path
|
||||
})
|
||||
|
||||
jsonResponse(w, map[string]any{
|
||||
"items": items,
|
||||
"summary": map[string]any{
|
||||
"total": len(items),
|
||||
"source_kind": func() string {
|
||||
if rawPkg != nil {
|
||||
return rawPkg.Source.Kind
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func extractRedfishFetchErrors(raw any) map[string]string {
|
||||
out := make(map[string]string)
|
||||
list, ok := raw.([]map[string]interface{})
|
||||
if ok {
|
||||
for _, item := range list {
|
||||
p := strings.TrimSpace(fmt.Sprintf("%v", item["path"]))
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
out[p] = strings.TrimSpace(fmt.Sprintf("%v", item["error"]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
if generic, ok := raw.([]interface{}); ok {
|
||||
for _, itemAny := range generic {
|
||||
item, ok := itemAny.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
p := strings.TrimSpace(fmt.Sprintf("%v", item["path"]))
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
out[p] = strings.TrimSpace(fmt.Sprintf("%v", item["error"]))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func looksLikeErrorLogLine(line string) bool {
|
||||
s := strings.ToLower(strings.TrimSpace(line))
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(s, "ошибка") ||
|
||||
strings.Contains(s, "error") ||
|
||||
strings.Contains(s, "failed") ||
|
||||
strings.Contains(s, "timeout") ||
|
||||
strings.Contains(s, "deadline exceeded")
|
||||
}
|
||||
|
||||
func hasFetchErrorSuffix(fetchErrs map[string]string, suffix string) bool {
|
||||
if len(fetchErrs) == 0 {
|
||||
return false
|
||||
}
|
||||
for p := range fetchErrs {
|
||||
if strings.HasSuffix(p, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseErrorSeverityFromMessage(msg string) string {
|
||||
s := strings.ToLower(strings.TrimSpace(msg))
|
||||
if s == "" {
|
||||
return "info"
|
||||
}
|
||||
if strings.Contains(s, "timeout") || strings.Contains(s, "deadline exceeded") {
|
||||
return "error"
|
||||
}
|
||||
if strings.HasPrefix(s, "status 500 ") || strings.HasPrefix(s, "status 502 ") || strings.HasPrefix(s, "status 503 ") || strings.HasPrefix(s, "status 504 ") {
|
||||
return "error"
|
||||
}
|
||||
if strings.HasPrefix(s, "status 401 ") || strings.HasPrefix(s, "status 403 ") {
|
||||
return "error"
|
||||
}
|
||||
if strings.HasPrefix(s, "status 404 ") || strings.HasPrefix(s, "status 405 ") || strings.HasPrefix(s, "status 410 ") || strings.HasPrefix(s, "status 501 ") {
|
||||
return "info"
|
||||
}
|
||||
if strings.Contains(s, "ошибка") || strings.Contains(s, "error") || strings.Contains(s, "failed") {
|
||||
return "warning"
|
||||
}
|
||||
return "info"
|
||||
}
|
||||
|
||||
func parseErrorSeverityRank(severity string) int {
|
||||
switch strings.ToLower(strings.TrimSpace(severity)) {
|
||||
case "error":
|
||||
return 0
|
||||
case "warning":
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
type firmwareEntry struct {
|
||||
Component string `json:"component"`
|
||||
Model string `json:"model"`
|
||||
|
||||
@@ -68,6 +68,7 @@ func (s *Server) setupRoutes() {
|
||||
s.mux.HandleFunc("GET /api/config", s.handleGetConfig)
|
||||
s.mux.HandleFunc("GET /api/serials", s.handleGetSerials)
|
||||
s.mux.HandleFunc("GET /api/firmware", s.handleGetFirmware)
|
||||
s.mux.HandleFunc("GET /api/parse-errors", s.handleGetParseErrors)
|
||||
s.mux.HandleFunc("GET /api/export/csv", s.handleExportCSV)
|
||||
s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON)
|
||||
s.mux.HandleFunc("GET /api/export/reanimator", s.handleExportReanimator)
|
||||
|
||||
Reference in New Issue
Block a user