利用Mermaid產出例外錯誤流程圖
前言
最近研究繪製流程圖時,無意間發現了「Mermaid」,Mermaid是由JavaScript為基礎建構的開源軟體,其最主的特色是利用簡易的文字語法來繪製各種不同類型的圖示:Flowchart 流程圖、Sequence Diagram 循序圖、Class Diagram 類別圖、State Diagram 狀態圖、Entity Relationship Diagram 實體關係圖、User Journey 用戶旅程圖、Gantt 甘特圖、Pie Chart 圓餅圖、Git Graph 版控圖
網站也提供線上操作展示範例
下面文章環境是選擇使用C# asp.net core MVC,例外錯誤訊息利用Flowchart(流程圖)產出視覺化的圖示。
Flowchart 文字語法
flowchart TB
c1-->a2
subgraph one
a1-->a2
end
subgraph two
b1-->b2
end
subgraph three
c1-->c2
end
由文件說明及範例參考先觀察出語法如何撰寫
這邊先簡單寫一個會沒有檢查時間物件null而導致出現例外錯誤的範例
public class HelpObj
{
public static void TestTime()
{
DateTime? dt = null;
string testError = dt.ToStrForTime();
}
}
public static class DateTimeExtensions
{
public static string ToStrForTime(this DateTime? dateTime)
{
return dateTime.Value.ToString("yyyy MM dd");
}
}
其例外錯誤的StackTrace就會是
at System.Nullable`1.get_Value() at Common.Util.Extensions.DateTimeExtensions.ToStrForTime(Nullable`1 dateTime) in ...\Common\Util\Extensions\DateTimeExtensions.cs:line 38 at Common.Util.HelpObj.TestTime() in ..\CoreMVC\Common\Util\HelpObj.cs:line 21 at CoreMVC.Controllers.HomeController.Privacy() in ..\CoreMVC\CoreMVC\Controllers\HomeController.cs:line 34
這些錯誤訊息中
System.Nullable`1.get_Value()
Common.Util.Extensions.DateTimeExtensions.ToStrForTime(Nullable`1 dateTime)
Common.Util.HelpObj.TestTime()
CoreMVC.Controllers.HomeController.Privacy()
這四段就是我們要轉換為Flowchart語法的關鍵
將錯誤訊息分兩個部分
1.程式執行流程
從訊息中可得到 Privacy() > TestTime() > ToStrForTime() > get_Value()
2.物件所在位置
注意第二三行的有共同專案及目錄名稱
將錯誤訊息套到Flowchart語法中
Privacy(Privacy: 34):Privacy()是物件所在位置,()內為顯示文字,這邊顯示方法及程式行數
graph LR
ToStrForTime-->get_Value
TestTime-->ToStrForTime
Privacy-->TestTime
subgraph System
subgraph Nullable`1
get_Value
end
end
subgraph Common
subgraph Util
subgraph Extensions
subgraph DeTimeExtensions
ToStrForTime(ToStrForTime: 38)
end
end
end
subgraph HelpObj
TestTime(TestTime: 21)
end
end
subgraph CoreMVC
subgraph Controllers
subgraph HomeController
Privacy(Privacy: 34)
end
end
end
有了目標,有可以開發model、及轉換方法
轉換暫存model
public class ErrorDiagram
{
private List<ErrorDiagram> _child;
public List<ErrorDiagram> Child
{
get
{
if (_child == null)
{
_child = new List<ErrorDiagram>();
}
return _child;
}
}
/// <summary>
/// Sub name
/// </summary>
public string SubGraph { get; set; }
/// <summary>
/// function name
/// </summary>
public string FunctionName { get; set; }
/// <summary>
/// error line
/// </summary>
public string LineNum { get; set; }
/// <summary>
/// 是否為function
/// </summary>
/// <returns></returns>
public bool IsFunction()
{
return FunctionName != null;
}
/// <summary>
/// view function & error linenum
/// </summary>
/// <returns></returns>
public string FunctionView()
{
if (IsFunction() && string.IsNullOrEmpty(LineNum) != true)
{
return string.Format("{0}({0}:{1})", FunctionName, LineNum);
}
else if (IsFunction())
{
return FunctionName;
}
return string.Empty;
}
/// <summary>
/// 是否有Sub
/// </summary>
/// <returns></returns>
public bool HasSubChild()
{
if (Child != null)
{
return Child.Count > 0;
}
return false;
}
/// <summary>
/// get mermaid flowchart code
/// </summary>
/// <returns></returns>
public string GetDiagramCode()
{
//
if (IsFunction())
{
return FunctionView();
}
if (HasSubChild() && Child.Count == 1)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"subgraph {SubGraph}");
stringBuilder.AppendLine(Child.FirstOrDefault().GetDiagramCode());
stringBuilder.AppendLine("end");
return stringBuilder.ToString();
}
else if (HasSubChild())
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"subgraph {SubGraph}");
foreach (var item in Child)
{
stringBuilder.AppendLine(item.GetDiagramCode());
}
stringBuilder.AppendLine("end");
return stringBuilder.ToString();
}
return "";
}
}
轉換主要流程code
/// <summary>
/// 轉換為流程圖code
/// </summary>
/// <param name="StackTrace"></param>
/// <returns></returns>
public static string GetFlowChart(string stackTrace)
{
StringReader strReader = new StringReader(stackTrace);
string strline = strReader.ReadLine();
List<ErrorDiagram> errorDiagrams = new List<ErrorDiagram>();
List<string> functionName = new List<string>();
while (strline != null)
{
var strfirstLast = strline.Split(" in ");
string strfirst = strfirstLast.FirstOrDefault().Replace("at","").Trim();
var strLastLineNum = strfirstLast.Length > 1 ? strfirstLast.LastOrDefault().Split(":line").LastOrDefault() : "";
var subDiagrams = strfirst.Split(".").ToList();
functionName.Add(subDiagrams.LastOrDefault().Split("(").FirstOrDefault());
BuildErrorDiagrams(ref errorDiagrams, subDiagrams, strLastLineNum);
strline = strReader.ReadLine();
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("graph LR");
// flow
for (int i = 0; i < functionName.Count-1; i++)
{
int parentIndex = i + 1;
if (parentIndex < functionName.Count)
{
stringBuilder.AppendLine($"{functionName[parentIndex]}-->{functionName[i]}");
}
}
// object site
foreach (var item in errorDiagrams)
{
stringBuilder.AppendLine(item.GetDiagramCode());
}
string result = stringBuilder.ToString();
return result;
}
建置各層訊息流程方法
private static void BuildErrorDiagrams(ref List<ErrorDiagram> errorDiagrams, List<string> subDiagrams,string lineNum)
{
//建置 Diagrams物件
ErrorDiagram errorDiagramParent = null;
foreach (var item in subDiagrams)
{
if (errorDiagramParent == null)
{
// first
errorDiagramParent = errorDiagrams.Find(x => x.SubGraph == item);
// check has
if (errorDiagramParent == null)
{
errorDiagramParent = new ErrorDiagram()
{
SubGraph = item,
};
errorDiagrams.Add(errorDiagramParent);
}
}
else if (item == subDiagrams.LastOrDefault())
{
// function
errorDiagramParent.Child.Add(new ErrorDiagram()
{
FunctionName = item.Split("(").FirstOrDefault(),
LineNum = lineNum,
});
}
else
{
var errorDiagramChild = errorDiagramParent.Child.Find(x => x.SubGraph == item);
// check has
if (errorDiagramChild == null)
{
var errorDiagramNew = new ErrorDiagram()
{
SubGraph = item,
};
errorDiagramParent.Child.Add(errorDiagramNew);
errorDiagramParent = errorDiagramNew;
}
}
}
}
設置在執行Privacy頁面出錯時,將錯誤訊息導致DiagramError頁面
public class HomeController : Controller
{
...
public IActionResult Privacy()
{
try
{
//HelpObj.Test();
HelpObj.TestTime();
}
catch (Exception ex)
{
TempData["error"] = JsonConvert.SerializeObject(new DiagramErrorModel
{
Message = ex.Message,
StackTrace = ex.StackTrace
});
return RedirectToAction("DiagramError");
}
return View();
}
public IActionResult DiagramError()
{
DiagramErrorModel diagramErrorModel = new DiagramErrorModel();
object temp = TempData["error"];
if (temp != null)
{
diagramErrorModel = JsonConvert.DeserializeObject<DiagramErrorModel>(temp.ToString());
}
var diagramError = new DiagramErrorViewModel()
{
Message = diagramErrorModel.Message,
DiagramCode = HelpMermaid.GetFlowChart(diagramErrorModel.StackTrace),
StackTrace = diagramErrorModel.StackTrace
};
return View(diagramError);
}
}
Error Model
public class DiagramErrorModel
{
public string Message { get; set; }
public string StackTrace { get; set; }
}
public class DiagramErrorViewModel
{
public string Message { get; set; }
public string DiagramCode { get; set; }
public string StackTrace { get; set; }
}
參考文件加入script參考
顯示頁error view 加入script參考
@model DiagramErrorViewModel
<h1>錯誤流程圖DiagramError</h1>
<p>@Model.Message</p>
<div class="mermaid">
@Model.DiagramCode
</div>
<div>
@Model.StackTrace
</div>
@section Scripts{
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({ start"font-size: 12pt;">
}
...
點選Privacy
產出錯誤流程圖
結論
這次用例外錯誤範例來呈現Flowchart對於開發者來說,唯一比較明顯的幫助,大概程式是否有用錯方法或用錯誤物件,從Flowchart會比原來錯誤訊息來的更明確一些。
或許這個範例成效不是那麼明顯,但目前能想到的Mermaid可應用範圍,是那些不容易從文字訊息了解內容,如果可以透過轉換成簡單的圖示,是不是就可以幫助使用者更快速地了解系統產出的文字內容。