Question

I'm trying to write a script to simplify the process of searching through a particular applications log files for specific information. So I thought maybe there's a way to convert them into an XML tree, and I'm off to a decent start....but The problem is, the application log files are an absolute mess if you ask me

Some entries are simple

2014/04/09 11:27:03 INFO  Some.code.function - Doing stuff

Ideally I'd like to turn the above into something like this

    <Message>
    <Date>2014/04/09</Date>
    <Time>11:48:38</Time>
    <Type>INFO</Type>
    <Source>Some.code.function</Source>
    <Sub>Doing stuff</Sub>
    </Message>

Other entries are something like this where there's additional information and line breaks

2014/04/09 11:27:04 INFO  Some.code.function - Something happens

changes: 
this stuff happened

I'd like to turn this last chunk into something like the above, but add the additional info into a section

    <Message>
    <Date>2014/04/09</Date>
    <Time>11:48:38</Time>
    <Type>INFO</Type>
    <Source>Some.code.function</Source>
    <Sub>Doing stuff</Sub>
    <details>changes: 
this stuff happened</details>
    </Message>

and then other messages, errors will be in the form of

2014/04/09 11:27:03 ERROR  Some.code.function - Something didn't work right
Log Entry: LONGARSEDGUID
Error Code: E3145
Application: Name
Details:
message information etc etc and more line breaks, this part of the message may add up to an unknown number of lines before the next entry

This last chunk I'd like to convert as the last to above examples, but adding XML nodes for log entry, error code, application, and again, details like so

    <Message>
    <Date>2014/04/09</Date>
    <Time>11:48:38</Time>
    <Type>ERROR  </Type>
    <Source>Some.code.function</Source>
    <Sub>Something didn't work right</Sub>
    <Entry>LONGARSEDGUID</Entry>
    <Code>E3145</Code>
    <Application>Name</Application>
    <details>message information etc etc and more line breaks, this part of the message may add up to an unknown number of lines before the next entry</details>
    </Message>

Now I know that Select-String has a context option which would let me select a number of lines after the line I've filtered, the problem is, this isn't a constant number.

I'm thinking a regular expression would also me to select the paragraph chunk before the date string, but regular expressions are not a strong point of mine, and I thought there might be a better way because the one constant is that new entries start with a date string

the idea though is to either break these up into xml or tables of sorts and then from there I'm hoping it might take the last or filtering non relevant or recurring messages a little easier

I have a sample I just tossed on pastebin after removing/replacing a few bits of information for privacy reasons

http://pastebin.com/raw.php?i=M9iShyT2

Was it helpful?

Solution

Sorry this is kind of late, I got tied up with work for a bit there (darn work expecting me to be productive while on their dime). I ended up with something similar to Ansgar Wiechers solution, but formatted things into objects and collected those into an array. It doesn't manage your XML that you added later, but this gives you a nice array of objects to work with for the other records. I'll explain the main RegEx line here, I'll comment in-line where it's practical.

'(^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) [\d+?] (\w+?) {1,2}(.+?) - (.+)$' is the Regex that detects the start of a new record. I started to explain it, but there are probably better resources for you to learn RegEx than me explaining it to me. See this RegEx101.com link for a full breakdown and examples.

$Records=@() #Create empty array that we will populate with custom objects later
$Event = $Null #make sure nothing in $Event to give script a clean start
Get-Content 'C:\temp\test1.txt' | #Load file, and start looping through it line-by-line.
?{![string]::IsNullOrEmpty($_)}|% { #Filter out blank lines, and then perform the following on each line
  if ($_ -match '(^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[\d+?] (\w+?) {1,2}(.+?) - (.+)$') { #New Record Detector line! If it finds this RegEx match, it means we're starting a new record.
    if ($Event) { #If there's already a record in progress, add it to the Array
      $Records+=$Event
    }
    $Event = New-Object PSObject -Property @{ #Create a custom PSObject object with these properties that we just got from that RegEx match
DateStamp = [datetime](get-date $Matches[1]) #We convert the date/time stamp into an actual DateTime object. That way sorting works better, and you can compare it to real dates if needed.
Type = $Matches[2]
Source = $Matches[3]
Message = $Matches[4]}

Ok, little pause for the cause here. $Matches isn't defined by me, why am I referencing it? . When PowerShell gets matches from a RegEx expression it automagically stores the resulting matches in $Matches. So all the groups that we just matched in parenthesis become $Matches[1], $Matches[2], and so on. Yes, it's an array, and there is a $Matches[0], but that is the entire string that was matched against, not just the groups that matched. We now return you to your regularly scheduled script...

  } else { #End of the 'New Record' section. If it's not a new record if does the following
    if($_ -match "^((?:[^ ^\[])(?:\w| |\.)+?):(.*)$"){ 

RegEx match again. It starts off by stating that this has to be the beginning of the string with the carat character (^). Then it says (in a non-capturing group noted by the (?:<stuff>) format, which really for my purposes just means it won't show up in $Matches) [^ \[]; that means that the next character can not be a space or opening bracket (escaped with a ), just to speed things up and skip those lines for this check. If you have things in brackets [] and the first character is a carat it means 'don't match anything in these brackets'.

I actually just changed this next part to include periods, and used \w instead of [a-zA-Z0-9] because it's essentially the same thing but shorter. \w is a "word character" in RegEx, and includes letters, numbers, and the underscore. I'm not sure why the underscore is considered part of a word, but I don't make the rules I just play the game. I was using [a-zA-Z0-9] which matches anything between 'a' and 'z' (lowercase), anything between 'A' and 'Z' (uppercase), and anything between '0' and '9'. At the risk of including the underscore character \w is a lot shorter and simpler.

Then the actual capturing part of this RegEx. This has 2 groups, the first is letters, numbers, underscores, spaces, and periods (escaped with a \ because '.' on it's own matches any character). Then a colon. Then a second group that is everything else until the end of the line.

        $Field = $Matches[1] #Everything before the colon is the name of the field
        $Value = $Matches[2].trim() #everything after the colon is the data in that field
        $Event | Add-Member $Field $Value #Add the Field to $Event as a NoteProperty, with a value of $Value. Those two are actually positional parameters for Add-Member, so we don't have to go and specify what kind of member, specify what the name is, and what the value is. Just Add-Member <[string]name> <value can be a string, array, yeti, whatever... it's not picky>
        } #End of New Field for current record
    else{$Value = $_} #If it didn't find the regex to determine if it is a new field then this is just more data from the last field, so don't change the field, just set it all as data.

    } else { #If it didn't find the regex then this is just more data from the last field, so don't change the field, just set it all as data.the field does not 'not exist') do this:
            $Event.$Field += if(![string]::isNullOrEmpty($Event.$Field)){"`r`n$_"}else{$_}} 

This is a long explanation for a fairly short bit of code. Really all it does is add data to the field! This has an inverted (prefixed with !) If check to see if the current field has any data, if it, or if it is currently Null or Empty. If it is empty it adds a new line, and then adds the $Value data. If it doesn't have any data it skips the new line bit, and just adds the data.

    }
  }
}
$Records+=$Event #Adds the last event to the array of records.

Sorry, I'm not very good with XML. But at least this gets you clean records.

Edit: Ok, code is notated now, hopefully everything is explained well enough. If something is still confusing perhaps I can refer you to a site that explains better than I can. I ran the above against your sample input in PasteBin.

OTHER TIPS

One possible way to deal with such files is to process them line by line. Each log entry starts with a timestamp and ends when the next line starting with a timestamp appears, so you could do something like this:

Get-Content 'C:\path\to\your.log' | % {
  if ($_ -match '^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}') {
    if ($logRecord) {
      # If a current log record exists, it is complete now, so it can be added
      # to your XML or whatever, e.g.:

      $logRecord -match '^(\d{4}/\d{2}/\d{2}) (\d{2}:\d{2}:\d{2}) (\S+) ...'

      $message = $xml.CreateElement('Message')

      $date = $xml.CreateElement('Date')
      $date.InnerText = $matches[1]
      $message.AppendChild($date)

      $time = $xml.CreateElement('Time')
      $time.InnerText = $matches[2]
      $message.AppendChild($time)

      $type = $xml.CreateElement('Type')
      $type.InnerText = $matches[3]
      $message.AppendChild($type)

      ...

      $xml.SelectSingleNode('...').AppendChild($message)
    }
    $logRecord = $_          # start new record
  } else {
    $logRecord += "`r`n$_"   # append to current record
  }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top