Kompilační log v Dynamics AX

Dynamics AX umožňuje exportovat výsledky kompilace do souboru (.html). Tento soubor je pak možné naimportovat zpět a pracovat s výsledky kompilace v prostředí Dynamics AX stejným způsobem jako po běžné kompilaci. To se hodí pro pozdější analýzu bez nutnosti rekompilace, automatizované buildy a tak podobně.

Občas by ale bylo vhodné výsledky kompilace analyzovat mimo Dynamics AX a je třeba dostat data do vhodného formátu. Pokud se podíváte dovnitř vygenerovaného .html souboru, zjistíte, že jde vlastně o XML s HTML hlavičkou a transformací do HTML. Tudíž import a export ve skutečnosti pracuje jednoduše s XML.

Jenže XML vhodné pro import do a export z Dynamics AX nemusí být právě vhodné pro zpracování jinými způsoby. Vygenerované XML vlastně obsahuje jen dva typy elementů – Table a Field, což dost komplikuje dotazy, a některé hodnoty nejsou reprezentovány právě intuitivně:

<Table:Record name="TmpCompilerOutput" xmlns:Table='urn:www.microsoft.com/Formats/Table'>
    <Table:Field name="TreeNodePath">\Classes\Class1\classDeclaration</Table:Field>
    <Table:Field name="Column">5</Table:Field>
    <Table:Field name="Line">4</Table:Field>
    <Table:Field name="CompileErrorCode">-1</Table:Field>
    <Table:Field name="CompileErrorString"></Table:Field>
    <Table:Field name="UtilElementType">45</Table:Field>
    <Table:Field name="SysUtilElementName">Class1</Table:Field>
    <Table:Field name="SysCompilerSeverity">0</Table:Field>
    <Table:Field name="CompileErrorImage">0</Table:Field>
    <Table:Field name="SysPropertyName"></Table:Field>
    <Table:Field name="SysCompileErrorMessage">Syntax error.</Table:Field>
    <Table:Field name="SysAotMethodName">classDeclaration</Table:Field>
    <Table:Field name="UtilElementImage">0</Table:Field>
    <Table:Field name="LatestCompile">1</Table:Field>
    <Table:Field name="SysCompilerOutputTab">2</Table:Field>
    <Table:Field name="createdDateTime">10/1/2012 03:08:20 pm</Table:Field>
    <Table:Field name="recVersion">0</Table:Field>
</Table:Record>

Je to dobré řešení pro svůj účel, tzn. import a export z tabulky TmpCompilerOutput, ale pro nic moc jiného se nehodí.

Pokud se divíte, o jakém dalším zpracování to mluvím, tak si představte, že mám kompilační logy z nočních buildů za několik měsíců a chci z nich vydolovat nějaké informace – kde často dochází k chybám, jak se vyvíjí počet chyb čase a tak podobně. A vůbec se mi nechtělo pracovat s takovouto strukturou XML.

Nakonec jsem se nevydal cestou přímé transformace XML, ale napsal jsem jednoduchou .NET knihovnu pro parsování kompilačního výstupu do objektů. Nebudu ji tu detailně popisovat (odkaz na kompletní projekt najdete na konci článku), je opravdu velmi jednoduchá – celé parsování zajišťuje třída LogParser a pro začátek si vystačíte s metodou ParseFile().

Jádrem celé implementace je následující dotaz:

XDocument loaded = XDocument.Parse(xml);
XNamespace tableNs = "urn:www.microsoft.com/Formats/Table";
 
var faults = from record in loaded.Descendants(tableNs + "Record")
             let fields = record.Descendants()
             select new Fault
             {
                 Column = Int32.Parse(FindValue(fields, "Column")),
                 CompilationTime = DateTime.Parse(FindValue(fields, "createdDateTime")),
                 ElementName = FindValue(fields, "SysUtilElementName"),
                 ElementType = (UtilElementType)Int32.Parse(FindValue(fields, "UtilElementType")),
                 FaultCode = Int32.Parse(FindValue(fields, "CompileErrorCode")),
                 FaultMessage = FindValue(fields, "SysCompileErrorMessage"),
                 FaultString = FindValue(fields, "CompileErrorString"),
                 FaultType = ParseFaultType(fields),
                 Line = Int32.Parse(FindValue(fields, "Line")),
                 MethodName = FindValue(fields, "SysAotMethodName"),
                 PropertyName = FindValue(fields, "SysPropertyName"),
                 TreeNodePath = FindValue(fields, "TreeNodePath")
             };

Jak je vidět, XML je převedeno do kolekce instancí typu Fault, používá některé další typy (např. enum UtilElementType) a vynechává nebo transformuje některá z polí.

Objekty typu Fault mohou být přímo serializovány do XML – chyba uvedená výše jako XML z AX by teď vypadala takto:

<Fault>
    <Column>5</Column>
    <CompilationTime>2012-01-10T15:08:20</CompilationTime>
    <ElementName>Class1</ElementName>
    <ElementType>Class</ElementType>
    <FaultCode>-1</FaultCode>
    <FaultMessage>Syntax error.</FaultMessage>
    <FaultString />
    <FaultType>Error</FaultType>
    <Line>4</Line>
    <MethodName>classDeclaration</MethodName>
    <PropertyName />
    <TreeNodePath>\Classes\Class1\classDeclaration</TreeNodePath>
</Fault>

To už je podstatně snáze zpracovatelný formát, ale není nutné se omezovat jen na prostou transformaci jednoho souboru – můžete třeba rovnou vyfiltrovat pouze relevantní data nebo pracovat s více soubory naráz.

Pojďme se podívat na příklad s mými nočními buildu. Nejprve načteme informace o kompilačních chybách (ne varováních apod.) ve všech buildech a uložíme je do jednoho souboru.

string path = @"c:\AX\NightBuild\";
LogParser parser = new LogParser();
 
var faults = from file in Directory.EnumerateFiles(path, "AxCompileAll.html", SearchOption.AllDirectories)
             from fault in parser.ParseFile(file)
             where fault.FaultType == FaultType.Error
             select fault;
 
using (FileStream fs = new FileStream(@"c:\AX\errors.xml", FileMode.CreateNew))
{
    new XmlSerializer(typeof(List<Fault>)).Serialize(fs, faults.ToList());
}

Nutno podotknout, že tato implementace není zcela optimální z hlediska výkonu, ale je dostačující pro mé potřeby – takže není důvod příliš optimalizovat. Uložení do souboru má to výhodu, že tento krok není třeba opakovat pro další dotazy nad stejnými daty.

Se získaným seznamem chyb za delší období se dá dělat spousta věcí. Například kód níže vypíše objekty seřazené podle toho, v kolika buildech se vyskytovaly, a pro každý zobrazí seznam daných dnů. Z toho je zhruba vidět, kde se chybuje buď často nebo kde se chyby opravují pomalu.

List<Fault> faults = null;
using (FileStream fs = new FileStream(@"c:\AX\errors.xml", FileMode.Open))
{
    faults = (List<Fault>)new XmlSerializer(typeof(List<Fault>)).Deserialize(fs);
}
 
var transformed = from fault in faults
                  group fault.CompilationTime.Date by new { fault.ElementType, fault.ElementName } into grouped
                  let output = new
                  {
                      grouped.Key.ElementType,
                      grouped.Key.ElementName,
                      Dates = grouped.AsEnumerable().Distinct().OrderByDescending(=> d)
                  }                   
                  orderby output.Dates.Count() descending, output.ElementType, output.ElementName
                  select output;
 
foreach (var fault in transformed)
{
    Console.WriteLine("{0} {1}", fault.ElementType, fault.ElementName);
    foreach (DateTime date in fault.Dates)
    {
        Console.WriteLine("    {0:d}", date);
    }
}

Podstatné je, že místo jakéhosi .html souboru máte v ruce kolekci silně typových objektů. Jak je použijete je už zcela na vás.

Soubory ke stažení

CompilationLogParser.zip (vytvořeno ve Visual Studiu 2010 pro .NET 4.0)