package tracelog import ( "strconv" "strings" ) // writeVerbs are the operation-name prefixes that imply the request mutates // data (routes follow the /Namespace_VerbObject convention), so a healthy // trace must contain write statements and an audit entry. var writeVerbs = []string{"Create", "Update", "Delete", "Save", "Set"} // Anomalies inspects a completed trace for shapes that usually indicate // silent failures: successful responses whose side effects never happened. // It derives everything from the trace's entries, so detectors work on any // log stream, past or live, without app-side support. func Anomalies(entries []Entry) []string { var path string status, written := -1, -1 writes, audits, zeroRowMutations := 0, 0, 0 for _, e := range entries { switch e.Msg { case "http request": path = attrValue(e, "path") case "http response": status = atoi(attrValue(e, "status")) written = atoi(attrValue(e, "written")) case "db exec": writes++ sql := strings.ToUpper(attrValue(e, "sql")) zeroRow := attrValue(e, "rows") == "0" if zeroRow && (strings.HasPrefix(sql, "UPDATE") || strings.HasPrefix(sql, "DELETE")) { zeroRowMutations++ } case "audit recorded": audits++ } } var out []string if status == 200 && written == 0 { out = append(out, "empty response") } if zeroRowMutations > 0 { out = append(out, "zero-row write") } if isWriteOp(path) && status >= 200 && status < 400 { if writes == 0 { out = append(out, "no writes for mutating op") } if audits == 0 { out = append(out, "mutation without audit") } } return out } // isWriteOp reports whether a route path names a mutating operation, // e.g. /Domain_Delete or /Todos_CreateItem. func isWriteOp(path string) bool { path, _, _ = strings.Cut(path, "?") _, op, ok := strings.Cut(strings.TrimPrefix(path, "/"), "_") if !ok { return false } for _, verb := range writeVerbs { if strings.HasPrefix(op, verb) { return true } } return false } func attrValue(e Entry, key string) string { for _, a := range e.Attrs { if a.Key == key { return a.Value } } return "" } func atoi(s string) int { n, err := strconv.Atoi(s) if err != nil { return -1 } return n }