Server Event Log Dashboard

Edit: Current Files → 2 Mar 2024
-------------------------------------------------------------------------------
KEV_EventLog.baq (26.6 KB)
KEV_EventLogList.baq (20.3 KB)

KEV_EventLog.dbd (220.6 KB)
App.KEV_EventLog.MainController_Customization_EventLogV1_CustomExport.xml (285.6 KB)
-------------------------------------------------------------------------------

So, I did a thing. Got to do a little bit of touch up, and I’ll share it with the community.

Here is a screenshot.


.

16 Likes

ooo… :eyes:

I didn’t know I’d ever get excited over logs

1 Like

I’m not sure how best to share it, so I’m going to do a write up and attach the files.

Here is the BAQ Portion
Create the below Calculated Fields in the BAQ.
Set the BAQ to Updatable, Advanced BPM Update Only.

Query Phrase:

/*
 * Disclaimer!!!
 * This is not a real query being executed, but a simplified version for general vision.
 * Executing it with any other tool may produce a different result.
 */
 
select 
	(null) as [Calculated_RowNumber],
	(null) as [Calculated_ProcessedRowNumber],
	(null) as [Calculated_Level],
	(null) as [Calculated_DateAndTime],
	(null) as [Calculated_Source],
	(null) as [Calculated_EventID],
	(null) as [Calculated_TaskCategory],
	(null) as [Calculated_General],
	(null) as [Calculated_Details],
	(null) as [Calculated_PreSelectXPath],
	(DateAndTime) as [Calculated_DateGTE],
	(DateAndTime) as [Calculated_DateLTE],
	(null) as [Calculated_EventLogName],
	(null) as [Calculated_MaxReturnRecords],
	(null) as [Calculated_TaskID],
	(null) as [Calculated_MaxProcessRecords],
	(null) as [Calculated_ReplaceXPath],
	(null) as [Calculated_PrettyXML]

EventLogFields

Open the Actions, Run BPM Designer, and add a new Base Processing method on GetList.
Mine is called “GetEventLogs”, because it… “Gets Event Logs” :rofl:

Enable it, and choose Design.

Add a custom code block, and open it.

In the “Usings & References…” area, add the following to “Usings”

using System.Reflection;
using System.Xml.Linq;
using System.Text.RegularExpressions;

Here is the code for the main section, add it, and save all the way down.

/*******************************************************************************  
* Title:   GetEventLogs GetList UBAQ BPM
* Author:  Kevin Lincecum
* Contact: klincecum @ this "moc.smlifesm" backwards lol  
*
* License: Free (None) - (Mention me if you want to be cool.)
*******************************************************************************/

  //Place these usings in "Usings & References..."
  //using System.Reflection;
  //using System.Xml.Linq;
  //using System.Text.RegularExpressions;

  //Just Debug/Messaging and Shortcut Variables
  string retString = "";
  string nl = Environment.NewLine;
  
  //Damn Zulu Time
  Func<DateTime, string> GetZuluDateTimeString = (dateTimeToConvert) =>
  {
    return DateTime.Parse(dateTimeToConvert.ToString()).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'");
  };
  
  //Check Where Clause Function Dictionary to see if the row fields match the tracker fields from the dashboard. 
  Func<Dictionary<string, dynamic>, List<string>, ResultsUbaqRow, bool> DoesRowMeetCriteria = (funcDict, excludedFieldsList, baqRow) =>
  {
    bool retVal = true; //Assume row matches
  
    try
    {
      foreach(var col in result.Results.Columns)
      {
        string fieldName  = col.ColumnName;
        var fieldValue = baqRow[fieldName]; 

        bool fldExcluded  = false;
        
        if(excludedFieldsList.Contains(fieldName)) fldExcluded = true; //Exclude our tracker fields we are using for variables
        
        if( fldExcluded == false )
        {
            if(funcDict.Keys.Contains(fieldName))
            {
                //Run the compare function in the dictionary for the field we are on
                if (funcDict[fieldName].matchingFunction(fieldValue, funcDict[fieldName].storedCompareData) == false) 
                {
                    retVal = false; //Row does not match a condition
                    break; //Exit For Loop (no reason to continue)
                }
            }
        }
      }//foreach
    }//try
    catch (Exception ex)
    {
      retString += "DoesRowMeetCriteria: Main" + nl;
      retString += ex.Message + nl;
      //OMG, always check for null on InnerException if you like your sanity. Ever had an error in your error catcher.....not fun;
      retString += ex.InnerException.Message != null ? ex.InnerException.Message + nl : "";
    }//catch
  
    return retVal; //Return Match or No Match
  };


  //This monstrosity builds the Where Clause Function Dictionary. A dictionary object containing a key(fieldName), and a dynamic Func<T> delegate to compare matches from the filter(tracker) fields.
  Func<List<string>, Dictionary<string, dynamic>> BuildFunctionDictionaryForWhereClauses = (excludedFieldsList) =>
  {

    Dictionary<string, dynamic> funcDict = new Dictionary<string, dynamic>();

    var whereRows = (from whereRow in executionParams.ExecutionSetting
                        where whereRow.Name == "Where"
                        select whereRow);

    //We have whereRows! Yay!
    if(whereRows.Count() > 0)
    {
      try
      {
        var wRow = whereRows.FirstOrDefault();
        
        string[] whereClauseArray = wRow.Value.Split(new[] {"AND"}, StringSplitOptions.None);
    
        foreach (var wClause in whereClauseArray)
        {
          //Break out Field Names, Operands, and Values.
          //I had some regular expressions here, but this is easier to understand.
          string fieldName    = wClause.Trim().Split(' ')[0].Replace("(","");
          string fieldOperand = wClause.Trim().Split(' ')[1];
          string fieldValue   = wClause.Trim().Split(new[] {"N'"}, StringSplitOptions.None)[1].Replace("')", "");


          //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
          //retString += $"{wClause.Trim()}\n";
          //retString += $"{fieldName} {fieldOperand} {fieldValue}\n";
          //
          //Just found a crappy bug "undocumented feature?" that I cannot fix, as it's not mine.
          //If the where clause from the tracker has a period in it, like say you were searching for ".NET", the where clause comes in like '%NET' instead of '.NET', so it would search
          //for Ends With "NET".
          //Tried to escape the period in the tracker, but doesn't work.
          //Will use this as a workaround, could add others as found.
          if(fieldValue.ToLower().Contains("<period>") == true) fieldValue = Regex.Replace(fieldValue, "<period>", ".", RegexOptions.IgnoreCase);
          //
          //retString += $"{fieldValue}\n";
          //
          //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

          bool fldExcluded = false;
          if(excludedFieldsList.Contains(fieldName)) fldExcluded = true; //Exclude our tracker fields we are using for variables

          if( fldExcluded == false )
          {
            //OK, what this switch case does is determine what operand we are working with, LIKE, Greater Than, Equality, etc. It then adds a function to a dictionary
            //as a dynamic object with a custom comparator. We can call this dictionary function with the field name, and the data we want to validate.
            //Functions return true on validate.
            switch (fieldOperand)
            {
                case "LIKE":
                    //Strings Only
                    if (fieldValue.EndsWith("%") && !fieldValue.StartsWith("%")) //Starts With
                    {
                        string fieldValueTemp = fieldValue.Replace("%", "");  
                        
                        //Define function delegate comparator, "comp" is stored in the dictionary, "data" is the data we are validating
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is string) { retValue = ((string)data).StartsWith((string)comp); }
                          
                          return retValue;
                        };
                        
                        //retString += "StartsWith" + nl;
                        
                        //We actually store an object in the dictionary, with two properties, the "fieldValue" from our tracker field, and the matching function. 
                        dynamic compareObject = new { storedCompareData = fieldValueTemp, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                        continue;
                    }
    
                    if (fieldValue.StartsWith("%") && !fieldValue.EndsWith("%")) //Ends With
                    {
                        string fieldValueTemp = fieldValue.Replace("%", "");  
                          
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is string) { retValue = ((string)data).EndsWith((string)comp); }
                          
                          return retValue;
                        };
                        
                        //retString += "EndsWith" + nl;
                    
                        dynamic compareObject = new { storedCompareData = fieldValueTemp, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                        continue;
                    }
                
                    if ( (fieldValue.StartsWith("%") && fieldValue.EndsWith("%")) || !fieldValue.Contains("%"))  //Matches/Contains
                    {
                        string fieldValueTemp = fieldValue.Replace("%", "");
                        
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is string) { retValue = ((string)data).Contains((string)comp); }
                          
                          return retValue;
                        };
                        
                        //retString += "Matches" + nl;
                        
                        dynamic compareObject = new { storedCompareData = fieldValueTemp, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                        continue;
                    }
                  break;
                
                case "<>":  //Not Equal
                    {
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is Int32)   { retValue = data != Int32.Parse(comp); }
                          
                          if(data is DateTime){ retValue = data != DateTime.Parse(comp); }
                          
                          if(data is string)  { retValue = data != comp; }
                        
                          return retValue;
                        };
                        
                        dynamic compareObject = new { storedCompareData = fieldValue, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                    }
                  break;

                case "<":  //Less Than
                    {
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is Int32)   { retValue = data < Int32.Parse(comp); }
                          
                          if(data is DateTime){ retValue = data < DateTime.Parse(comp); }
                          
                          return retValue;
                        };
                        
                        dynamic compareObject = new { storedCompareData = fieldValue, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                    }
                  break;

                case "<=":  //Less Than or Equal To 
                    {
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is Int32)   { retValue = data <= Int32.Parse(comp); }
                          
                          if(data is DateTime){ retValue = data <= DateTime.Parse(comp); }
                          
                          return retValue;
                        };
                        
                        dynamic compareObject = new { storedCompareData = fieldValue, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                    }
                  break;

                case ">":  //Greater Than
                    {
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is Int32)   { retValue = data > Int32.Parse(comp); }
                          
                          if(data is DateTime){ retValue = data > DateTime.Parse(comp); }
                          
                          return retValue;
                        };
                        
                        dynamic compareObject = new { storedCompareData = fieldValue, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                    }
                  break;

                case ">=":  //Greater Than Or Equal To
                    {
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is Int32)   { retValue = data >= Int32.Parse(comp); }
                          
                          if(data is DateTime){ retValue = data >= DateTime.Parse(comp); }
                          
                          return retValue;
                        };
                        
                        dynamic compareObject = new { storedCompareData = fieldValue, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                    }
                  break;

                default: //Equals
                    {
                        Func<dynamic, dynamic, bool> matchFunc = (data, comp) =>
                        {
                          bool retValue = false;
                          
                          if(data is Int32)   { retValue = data == Int32.Parse(comp); }
                          
                          if(data is DateTime){ retValue = data == DateTime.Parse(comp); }
                          
                          if(data is string)  { retValue = data == comp; }
                          
                          return retValue;
                        };
                        
                        dynamic compareObject = new { storedCompareData = fieldValue, matchingFunction = matchFunc };
                        funcDict.Add(fieldName, compareObject);
                    }
                  break;
                  
            }//switch case
          }

        }//Main For Loop
        
      }
      catch (Exception ex)
      {
        retString += "BuildFunctionDictionaryForWhereClauses: Main" + nl;
        retString += ex.Message + nl;
        //OMG, always check for null on InnerException if you like your sanity. Ever had an error in your error catcher.....not fun;
        retString += ex.InnerException.Message != null ? ex.InnerException.Message + nl : "";
      }
    }
    
    return funcDict;
    
  };//BuildFunctionDictionaryForWhereClauses


  //Get our config variables from the tracker fields
  Func<string, string> GetConfigValueFromWhereClause = (configField) =>
  {
      string retVal = "";
  
      var whereRows = (from whereRow in executionParams.ExecutionSetting
                            where whereRow.Name == "Where"
                            select whereRow);

      if(whereRows.Count() > 0)
      {
          try
          {
              var wRow = whereRows.FirstOrDefault();
              
              string[] whereClauseArray = wRow.Value.Split(new[] {"AND"}, StringSplitOptions.None);
              
              foreach (var wClause in whereClauseArray)
              {
                  //Break out Field Names, Operands, and Values.
                  //I had some regular expressions here, but this is easier to understand.
                  string fieldName    = wClause.Trim().Split(' ')[0].Replace("(","");
                  //string fieldOperand = wClause.Trim().Split(' ')[1]; //Not needed here
                  string fieldValue   = wClause.Trim().Split(new[] {"N'"}, StringSplitOptions.None)[1].Replace("')", "");
                  
                  if(fieldName == configField)
                  {
                    return fieldValue;
                  }
              }
          }
          catch(Exception ex) 
          {
            retString += ex.Message + nl;
          }
      }
      
      return retVal;
  };//GetConfigValueFromWhereClauses


  //The meat and potatoes. Read the EventLogs from the Operating System. Could be expanded to read event log files with a little work.
  Func<string, string, bool, bool, DateTime, DateTime, Dictionary<string, dynamic>, List<string>, int, int, List<ResultsUbaqRow>> GetEventLog = (eventLogName, preSelectXPath, replaceXPath, prettyXML, startDateTime, endDateTime, wcFuncDict, excludedFieldsList, maxRecordsToProcess, maxRecordsToReturn) =>
  {
    List<ResultsUbaqRow> rowsToReturn = new List<ResultsUbaqRow>(); //Rows we will add to result.Results

    try
    {
      //Event Log Data is Zulu Time
      string zuluStartDateTime = GetZuluDateTimeString(startDateTime);
      string zuluEndDateTime   = GetZuluDateTimeString(endDateTime  );
    
      //No reference available in "Usings & References - Assemblies", so we will load by reflection.
      Assembly System_Diagnostics_EventLog = Assembly.Load("System.Diagnostics.EventLog");

      //The types we will need: Extracted from the Assembly
      Type typeEventLogReader = System_Diagnostics_EventLog.GetType("System.Diagnostics.Eventing.Reader.EventLogReader");
      Type typeEventLogQuery  = System_Diagnostics_EventLog.GetType("System.Diagnostics.Eventing.Reader.EventLogQuery");
      Type typeEventLogRecord = System_Diagnostics_EventLog.GetType("System.Diagnostics.Eventing.Reader.EventLogRecord");
      Type typePathTypeEnum   = System_Diagnostics_EventLog.GetType("System.Diagnostics.Eventing.Reader.PathType");

      //We need this Enum - Enums in reflection suck
      object PathTypeEnum_LogName = Enum.Parse(typePathTypeEnum, "LogName");

      //EventLogRecords are selected via XPath Queries, we will use the dates from the tracker fields as a base XPath Query
      string xPathQuery = $"*[System[TimeCreated[@SystemTime >= '{zuluStartDateTime}']]] and *[System[TimeCreated[@SystemTime <= '{zuluEndDateTime}']]]";

      //Append/Replace xPathQuery with one from tracker field
      if(preSelectXPath != null)
      {
        if(preSelectXPath.Trim() != String.Empty)
        {
          xPathQuery = replaceXPath == true ? preSelectXPath : $"{xPathQuery} and {preSelectXPath}"; 
        }
      }

      //retString += xPathQuery + nl;

      //EventLogQuery Object Creation
      dynamic eventLogQuery  = Activator.CreateInstance(typeEventLogQuery,  new object[]{eventLogName, PathTypeEnum_LogName, xPathQuery});
      eventLogQuery.ReverseDirection = true; //Lets read events in reverse order
      
      //EventLogReader - This puppy does our reading of the Event Logs
      dynamic eventLogReader = Activator.CreateInstance(typeEventLogReader, eventLogQuery);

      int recordsProcessed = 0;
      int recordsKept = 0;
      for(dynamic eventLogRecord = eventLogReader.ReadEvent(); eventLogRecord != null; eventLogRecord = eventLogReader.ReadEvent())
      {
        try
        {
          if(recordsProcessed >= maxRecordsToProcess) break;  //We're done
          if(recordsKept      >= maxRecordsToReturn ) break;  //We're done 

          string taskDisplayName = eventLogRecord.TaskDisplayName != null ? eventLogRecord.TaskDisplayName : "";
          string formatDescription = "";
          string xmlDetail = "";
          
          try{ formatDescription = eventLogRecord.FormatDescription(); }catch{} //These error out if unavailable (probably a more elegant way to ignore)
          try{ xmlDetail         = eventLogRecord.ToXml();             }catch{} //These error out if unavailable (probably a more elegant way to ignore)
         
          if(prettyXML == true && xmlDetail != String.Empty) xmlDetail = XDocument.Parse(xmlDetail).ToString();
         
          //Build a row with our Event Log Data
          ResultsUbaqRow recordRow = new ResultsUbaqRow
          {
            Calculated_Level = eventLogRecord.LevelDisplayName.ToString(),
            Calculated_DateAndTime = eventLogRecord.TimeCreated,
            Calculated_EventID = eventLogRecord.Id,
            Calculated_Source = eventLogRecord.ProviderName,
            Calculated_TaskID = Convert.ToInt32(eventLogRecord.Task),

            Calculated_TaskCategory = taskDisplayName,
            Calculated_General = formatDescription,
            Calculated_Details = xmlDetail,

            Calculated_EventLogName = eventLogName,
            Calculated_RowNumber = rowsToReturn.Count + 1,
            Calculated_ProcessedRowNumber = recordsProcessed + 1,
            
            RowIdent = DateTime.Now.Ticks.ToString(),
            RowMod = "A"            
          };
          
          recordsProcessed++; //In case you limited it...
          
          //Check if the record returned meets the criteria set in the tracker fields from the dashboard
          if(DoesRowMeetCriteria(wcFuncDict, excludedFieldsList, recordRow) == true)
          {
            rowsToReturn.Add(recordRow); //Keep the row if it meets criteria
            recordsKept++; //In case you limited it...
          }
          
          //eventLogRecord.Dispose(); //I don't think we need it and it causes side effects
        }
        catch (Exception ex)
        {
          retString += "GetEventLog: In Loop" + nl;
          retString += ex.Message + nl;
          //OMG, always check for null on InnerException if you like your sanity. Ever had an error in your error catcher.....not fun;
          retString += ex.InnerException.Message != null ? ex.InnerException.Message + nl : "";
          
          eventLogReader.Dispose();
          
          return rowsToReturn;
        }
      }//for loop eventLogRecords
      
      eventLogReader.Dispose();
      //retString += recordsProcessed.ToString() + nl;
      
      return rowsToReturn; //return our list of records
      
    }
    catch (Exception ex)
    {
      retString += "GetEventLog: Main" + nl;
      retString += ex.Message + nl;
      //OMG, always check for null on InnerException if you like your sanity. Ever had an error in your error catcher.....not fun;
      retString += ex.InnerException.Message != null ? ex.InnerException.Message + nl : "";
      return rowsToReturn;
    }
    
  };//Func GetEventLog
  
  
  
  //*****************************************************************************************************
  //Begin Processing Here:
    
  //This is the list of fields from the tracker we need to exclude from matching fields.
  //We are using these as config values, outside the where clause matching function dictionary  
  List<string> excludeFields = new List<string>();
      excludeFields.Add("Calculated_EventLogName");
      excludeFields.Add("Calculated_PreSelectXPath");
      excludeFields.Add("Calculated_MaxProcessRecords");
      excludeFields.Add("Calculated_MaxReturnRecords");
      excludeFields.Add("Calculated_ReplaceXPath");
      excludeFields.Add("Calculated_PrettyXML");

      //We exclude these as well, because they are used in the XPath query, not our where clause comparer
      excludeFields.Add("Calculated_DateGTE");
      excludeFields.Add("Calculated_DateLTE");
      

  //Define and Build the Where Clause Function Dictionary - It checks fields for matches with the criteria from the dashboard tracker
  Dictionary<string, dynamic> whereClauseFunctionDictionary = BuildFunctionDictionaryForWhereClauses(excludeFields); 
  
  //Get EventLogName (System, Application) "Security" will not work in cloud, no access 
  string EventLogName = GetConfigValueFromWhereClause("Calculated_EventLogName");
  EventLogName = EventLogName == "" ? "System" : EventLogName; 

  //Get Dates for events between  
  string logDateGTEString = GetConfigValueFromWhereClause("Calculated_DateGTE");
  string logDateLTEString = GetConfigValueFromWhereClause("Calculated_DateLTE");
     
  //Processed Dates
  //string TheBeginningOfTime = "1979-11-02T00:00:00.000000000Z"; //My Birthday
  //DateTime logDateGTE = logDateGTEString == "" ? DateTime.Parse(TheBeginningOfTime) : DateTime.Parse(logDateGTEString); 
  DateTime logDateGTE = logDateGTEString == "" ? DateTime.Now.AddDays(-1) : DateTime.Parse(logDateGTEString); //Default: Now - 1 Day
  DateTime logDateLTE = logDateLTEString == "" ? DateTime.Now             : DateTime.Parse(logDateLTEString);
  
  //How many records we will PROCESS in total, whether it matches criteria or not (MAX)
  int MaxRecordsToProcess = 0;
  try
  {
    MaxRecordsToProcess = Convert.ToInt32(GetConfigValueFromWhereClause("Calculated_MaxProcessRecords"));
  }
  catch
  {
    MaxRecordsToProcess = 0;
  }
  MaxRecordsToProcess = MaxRecordsToProcess <= 0 ? 2147483647 : MaxRecordsToProcess;  
  
  //How many records we will RETURN in total (MAX)
  int MaxRecordsToReturn  = 0;
  try
  {
    MaxRecordsToReturn = Convert.ToInt32(GetConfigValueFromWhereClause("Calculated_MaxReturnRecords"));
  }
  catch
  {
    MaxRecordsToReturn = 0;
  }
  MaxRecordsToReturn = MaxRecordsToReturn <= 0 ? 2147483647 : MaxRecordsToReturn;
  
  string xPathPreSelect = GetConfigValueFromWhereClause("Calculated_PreSelectXPath");
  
  bool ReplaceXPath = Convert.ToBoolean(GetConfigValueFromWhereClause("Calculated_ReplaceXPath"));
  bool PrettyXML = Convert.ToBoolean(GetConfigValueFromWhereClause("Calculated_PrettyXML"));
  
  //Define & Grab the list of events  
  List<ResultsUbaqRow> eventRecords = GetEventLog(EventLogName, xPathPreSelect, ReplaceXPath, PrettyXML, logDateGTE, logDateLTE, whereClauseFunctionDictionary, excludeFields, MaxRecordsToProcess, MaxRecordsToReturn);
  
  result.Results.Clear();//Make sure return data is empty
  
  if(eventRecords.Count > 0)
  {
    result.Results.AddRange(eventRecords); //Add records from List
  }
  
  //Display errors/information
  if(!String.IsNullOrEmpty(retString)) InfoMessage.Publish(retString);
  

Your BAQ should be complete.

6 Likes

Now, on to the dashboard

I have the main grid, a filter section, and a section to display the details

Here are three pictures showing how I set up the dashboard:



I then customized the deployed dashboard to look like the following:

The following code is for the script section. If you are recreating this manually, you
will need to do some guid touch ups.

public void InitializeCustomCode()
{
	// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Variable Initialization' lines **
	// Begin Wizard Added Variable Initialization
	// End Wizard Added Variable Initialization
	// Begin Wizard Added Custom Method Calls
	// End Wizard Added Custom Method Calls

	EpiCheckBox chkCalculated_PrettyXML = (EpiCheckBox)csm.GetNativeControlReference("5047ac52-b83c-4b7b-bd26-a9b44be7973b");
	chkCalculated_PrettyXML.Checked = true;

	txtXPathWarning.Text += "Default XPath Query includes the dates above, or the default of one day back." + Environment.NewLine;
	txtXPathWarning.Text += Environment.NewLine;
	txtXPathWarning.Text += "If you replace it, you might want to include the dates in your query or else you will query all the records. (Very Long Wait)" + Environment.NewLine;
	txtXPathWarning.Text += Environment.NewLine;
	txtXPathWarning.Text += "If you do not replace it, your XPath Query will be appended to the default query." + Environment.NewLine;
	txtXPathWarning.Text += Environment.NewLine;
	txtXPathWarning.Text += "You can also choose to limit the records processed or returned." + Environment.NewLine;
}
1 Like

Here are the files for import if you want.
Edit: First post updated with current files.

Files - Original post

KEV_EventLog.baq (26.6 KB)
KEV_EventLog.dbd (220.6 KB)
App.KEV_EventLog.MainController_Customization_EventLogV1_CustomExport.xml (267.9 KB)

EventLogDB_Form_Customization_Script.cs (1.2 KB)
KEV_EventLog_UBAW_BPM_GetEventLogs.cs (25.3 KB)

Ok, some notes:

You can really only see Application, and System Event Logs.
Setup is blank, and Security is locked out because you won’t have permissions in our context.
System is default. If you leave EventLogName blank, it will query System.

MaxReturnRecords is the maximum amount of records you wish to return.

MaxProcessRecords is the maximum amount of records you wish to look at in aggregate.

Example, I want to look at the last 10,000 records to see if they match a certain criteria, but I only
want a sample size of 10 records back. You would set MaxProcessRecords to 10,000 & MaxReturnRecords to 10.
Use them if you want.

Date >= (GTE) & Date <= (LTE) fields are included in the XPath pre-select query. They will filter the records
before processing to only records in that range.
The default range is less than Now, and back 1 day.

The PreSelectXPath field is in case you want to write your own XPath queries. (XML filters based on the XML Details)

The ReplaceXPath checkbox is there in case you want to disregard the default XPath query and completely roll your own.
The default is to not Replace, but Append your XPath query onto the default (it’s literally an “and”).
The default is to query between the dates set above, or the default of back one day.
If you do a replace, it might be good to set date filters or max records filters, because Epicor is storing 3-4 months
of records. If no filters, prepare for a looooooooong wait…

The PrettyXML checkbox turns the Details tab from this

<xml><hello>world</hello></xml>

to this

<xml>
    <hello>world</hello>
</xml>

I found an issue in the filters passed to the BAQ, like say I wanted to filter for “.NET”, Epicor would replace that period
with a percent sign, so I would receive “%NET” instead of “.NET” in my where clause. That obviously causes a problem, because my
condition went from Matches/Contains/LIKE .NET to %NET (Ends with NET), well that’s obviously a problem. I tried to escape it in the
filter, but that doesn’t work.
As a workaround, if you want to use a PERIOD (.), then use “<period>”, so “.NET” would be “<period>NET” in your filter.

And that’s about it. This could certainly be polished up a bit more, and will eventually need a Kinetic port, but hey, it’s here now.

I hope you like it!

4 Likes

Your tag says SaaS MT, so since you don’t have custom code, I wouldn’t get too excited. :crying_cat_face:

@klincecum Nicely done!! And thanks for sharing AND documenting :slight_smile:

This is new ground for me, but I was wondering if you can get to the “Application and Services Logs”? I was looking for a way to correlate Task Agent events with System Log entries to see if the server logged and error when nothing useful appears in the System Monitor. Sometime the error message you need is there - and sometimes it’s in both places.

Extending this Dashboard to include the Task Agent Log could be useful as well - everything in one place kind of thing.

What’s the Task Agent Log Path or name? I can’t find it in Computer Management.
I just tried this one in “Applications and Services Logs”

Oh - sorry - the Task Agent Log is an ICE table inside the Kinetic database. Since you are in a Dashboard/BAQ combination, adding a BAQ for an ICE Table is easy, then you can also create a Publish/Subscribe view of the data to link the two together in some fashion like Date/Time. That’s the direction I was going.

Well you’d have to write the data down to a ud table or something which I am not doing.
It could be done by moving this to a pre-processing on the getlist instead, and writing data to a table, so you could do some sql against it.

Or do it in code with Linq.

My apologies - I didn’t really go through all the code to see exactly how you were manipulating the data. Got caught up in the similarity to my other ‘project’ that I jumped right in with my response.

Using one of the UD tables as a temp table during the use of this dashboard could have its advantages like linking to other log data (including those who write their own log/debug data in BPMs and Functions).

XML/JSON/LINQ manipulation in code is not my strong suit so it’s something I need to learn & play with in the new Kinetic version as I move our customizations.

1 Like

My strong suit is whatever I’m actively working on and interested in at the moment.
I forget it all next week and have to relearn it :rofl:

3 Likes

Exactly. So true for me as well! When you have to do everything surrounding ERP, you have to be this way. There’s no time to be really good at any given topic.

But the intermittent exploration and learning of new topics is fun and offsets the aggravation for me.

2 Likes

Working on moving to public cloud… should be finished by the end of the month

2 Likes

@klincecum @MikeGross On my app servers these are the paths for the App Server and Task Agent.

image

image

image

What is the “Full Name:” in the properties?

The name above the path. Epicor ICE Task Agent Service or Epicor App Server.

image

@klincecum I got excited when I saw the name that you were digesting the ServerLog Trace Logs from the app server. Those are a beast to wade thru all of the print requests to get just the bpm calls and errors. I am going to look at possibly adapting this to do that.


Works as written. No changes.

We need to discuss if this is a security risk, and if it’s a real problem.

3 Likes

Dude. This, this is, well,… let’s have Joe tell us.

Joe Biden Congratulations GIF by GIPHY News

3 Likes