diff --git a/World Possible Dev.pbix b/World Possible Dev.pbix new file mode 100644 index 0000000..6d35665 Binary files /dev/null and b/World Possible Dev.pbix differ diff --git a/World Possible Master.pbix b/World Possible Master.pbix new file mode 100644 index 0000000..c8dc169 Binary files /dev/null and b/World Possible Master.pbix differ diff --git a/parserapp/access4.log b/parserapp/access4.log index 58ca7b5..c97b67a 100644 --- a/parserapp/access4.log +++ b/parserapp/access4.log @@ -1,8 +1,30 @@ -192.168.1.10 - - [24/Jul/2019:18:51:16 +0000] "GET /modules/en-teachertraining/index.html HTTP/1.1" 200 13480 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" -192.168.1.10 RACHEL 80 [24/Jul/2019:18:51:16 +0000] "GET /modules/en-teachertraining/teachertraining-logo.jpg HTTP/1.1" 200 13480 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" -192.168.1.10 - - [24/Jul/2019:18:52:16 +0000] "GET /modules/en-kolibri-index/kolibri-logo.svg HTTP/1.1" 200 10220 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" -192.168.1.10 RACHEL 80 [24/Jul/2019:18:52:16 +0000] "GET /modules/en-kolibri-index/kolibri-logo.svg HTTP/1.1" 200 10220 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" -192.168.1.10 - - [24/Jul/2019:18:53:16 +0000] "GET /modules/en-wikipedia/enwiki.png HTTP/1.1" 200 20616 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" -192.168.1.10 RACHEL 80 [24/Jul/2019:18:53:16 +0000] "GET /modules/en-wikipedia/enwiki.png HTTP/1.1" 200 20616 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" -192.168.1.10 - - [24/Jul/2019:18:53:16 +0000] "GET /modules/en-wikipedia_for_schools/wfs_logo_smooth.jpg HTTP/1.1" 200 31908 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" -192.168.1.10 RACHEL 80 [24/Jul/2019:18:53:16 +0000] "GET /modules/en-wikipedia_for_schools/wfs_logo_smooth.jpg HTTP/1.1" 200 31908 "http://192.168.1.150/" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET / HTTP/1.1","status": "200","body_bytes_sent":"4821","request_time":"0.394","http_referrer":"-","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET / HTTP/1.1" 200 4821 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-asst_medical/Logo-small.png HTTP/1.1","status": "200","body_bytes_sent":"9043","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /modules/en-asst_medical/Logo-small.png HTTP/1.1" 200 9043 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-educate/logo.png HTTP/1.1","status": "200","body_bytes_sent":"28560","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /modules/en-educate/logo.png HTTP/1.1" 200 28560 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-catdogbooks/logo.png HTTP/1.1","status": "200","body_bytes_sent":"72051","request_time":"0.028","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /modules/en-catdogbooks/logo.png HTTP/1.1" 200 72051 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /js/jquery-1.10.2.min.js HTTP/1.1","status": "200","body_bytes_sent":"93107","request_time":"0.015","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /js/jquery-1.10.2.min.js HTTP/1.1" 200 93107 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /css/normalize-1.1.3.css HTTP/1.1","status": "200","body_bytes_sent":"9559","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /css/normalize-1.1.3.css HTTP/1.1" 200 9559 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-edison/Edison-Logo.jpg HTTP/1.1","status": "200","body_bytes_sent":"10914","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /modules/en-edison/Edison-Logo.jpg HTTP/1.1" 200 10914 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-afristory/af.png HTTP/1.1","status": "200","body_bytes_sent":"17787","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /modules/en-afristory/af.png HTTP/1.1" 200 17787 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /css/ui-lightness/jquery-ui-1.10.4.custom.min.css HTTP/1.1","status": "200","body_bytes_sent":"17114","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /css/ui-lightness/jquery-ui-1.10.4.custom.min.css HTTP/1.1" 200 17114 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-blockly-games/logo.png HTTP/1.1","status": "200","body_bytes_sent":"8954","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /modules/en-blockly-games/logo.png HTTP/1.1" 200 8954 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /js/jquery-ui-1.10.4.custom.min.js HTTP/1.1","status": "200","body_bytes_sent":"60845","request_time":"0.011","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /js/jquery-ui-1.10.4.custom.min.js HTTP/1.1" 200 60845 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-bookdash/logo.png HTTP/1.1","status": "200","body_bytes_sent":"12730","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /modules/en-bookdash/logo.png HTTP/1.1" 200 12730 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:25 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /css/style.css HTTP/1.1","status": "200","body_bytes_sent":"4171","request_time":"0.000","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:25 +0000] "GET /css/style.css HTTP/1.1" 200 4171 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:26 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-causebooks/osu-logo.png HTTP/1.1","status": "200","body_bytes_sent":"65596","request_time":"0.062","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:26 +0000] "GET /modules/en-causebooks/osu-logo.png HTTP/1.1" 200 65596 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" +{"time_local":"23/Sep/2019:22:23:26 +0000","remote_addr":"192.168.88.238","remote_user":"-","request":"GET /modules/en-binofino/B&NFiles/Bino&Fino5.png HTTP/1.1","status": "200","body_bytes_sent":"136122","request_time":"0.062","http_referrer":"http://my.content/","http_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"} +192.168.88.238 RACHEL 80 [23/Sep/2019:22:23:26 +0000] "GET /modules/en-binofino/B&NFiles/Bino&Fino5.png HTTP/1.1" 200 136122 "http://my.content/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362" \ No newline at end of file diff --git a/parserapp/parserapp.csproj b/parserapp/parserapp.csproj index 22294a0..1ee5384 100644 --- a/parserapp/parserapp.csproj +++ b/parserapp/parserapp.csproj @@ -32,7 +32,7 @@ - + diff --git a/parserfn/AccessLogSummary.cs b/parserfn/AccessLogSummary.cs new file mode 100644 index 0000000..7776fcd --- /dev/null +++ b/parserfn/AccessLogSummary.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ParserFn +{ + class AccessLogSummary + { + public string DeviceId { get; set; } + public string AccessDate { get; set; } + public long Bandwidth { get; set; } + } +} diff --git a/parserfn/AccessSitesLog.cs b/parserfn/AccessSitesLog.cs new file mode 100644 index 0000000..be9a10e --- /dev/null +++ b/parserfn/AccessSitesLog.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ParserFn +{ + public class AccessedSitesLog + { + public string id { get; set; } + public string PartitionKey { get; set; } + public string Date { get; set; } + public string Url { get; set; } + public long Bandwidth { get; set; } + public long MainModuleCount { get; set; } + public long SubModuleCount { get; set; } + } +} diff --git a/parserfn/JsonToDB.cs b/parserfn/JsonToDB.cs index c0ad8b3..bb457e8 100644 --- a/parserfn/JsonToDB.cs +++ b/parserfn/JsonToDB.cs @@ -8,18 +8,10 @@ using Microsoft.Azure.Documents; using System.Collections.Generic; -namespace parserfn +namespace ParserFn { // Cosmos DB Entity - public class AccessedSitesLog - { - public string id { get; set; } - public string PartitionKey { get; set; } - public string Date { get; set; } - public string Url { get; set; } - public long MainModuleCount { get; set; } - public long SubModuleCount { get; set; } - } + public static class JsonToDB { @@ -42,7 +34,7 @@ public static void Run( result = serializer.Deserialize(jsonTextReader); } } - + //For each AccessData Object in the list - create a record in Cosmos DB foreach (AccessData accessedsite in result.AccessDetails) { @@ -54,13 +46,27 @@ public static void Run( Url = accessedsite.ModuleName, Date = accessedsite.UpLoadTime.ToString("MM/dd/yyyy"), MainModuleCount = accessedsite.MainModuleCount, - SubModuleCount = accessedsite.SubModuleCount + SubModuleCount = accessedsite.SubModuleCount, + Bandwidth = accessedsite.Bandwidth }; // upload accessedSiteRecord to cosmos db cosmosOutput.Add(accessedSiteRecord); log.LogInformation($"Added all to Cosmos DB"); } + + // Add general information + var summary = new AccessedSitesLog + { + PartitionKey = result.DeviceId + " " + result.AccessDate, + id = result.DeviceId + " TotalBandwidth", + Url = "Total Bandwidth", + MainModuleCount = -1, + SubModuleCount = -1, + Date = result.AccessDetails[0].UpLoadTime.ToString("MM/dd/yyy"), // should be same date anyways and forces using the correct format. Assumes at least one module was counted + Bandwidth = result.Bandwidth + }; + cosmosOutput.Add(summary); } } } diff --git a/parserfn/LogToJson.cs b/parserfn/LogToJson.cs index dd91f98..7ea4e74 100644 --- a/parserfn/LogToJson.cs +++ b/parserfn/LogToJson.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; using ParserLib; -namespace parserfn +namespace ParserFn { public static class LogToJson { diff --git a/parserfn/parserfn.csproj b/parserfn/parserfn.csproj index 331f46c..6256dc2 100644 --- a/parserfn/parserfn.csproj +++ b/parserfn/parserfn.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.2 v2 @@ -10,7 +10,7 @@ - + diff --git a/parserlib/AccessData.cs b/parserlib/AccessData.cs new file mode 100644 index 0000000..18498c3 --- /dev/null +++ b/parserlib/AccessData.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ParserLib +{ + //Represents a record in Access Log + public class AccessData + { + public string ModuleName { get; set; } + public long MainModuleCount { get; set; } + public long SubModuleCount { get; set; } + public DateTime UpLoadTime { get; set; } + public long Bandwidth { get; set; } + } +} diff --git a/parserlib/AccessDataDetail.cs b/parserlib/AccessDataDetail.cs new file mode 100644 index 0000000..d714d24 --- /dev/null +++ b/parserlib/AccessDataDetail.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ParserLib +{ + //AccessDataDetail Object to be serialized into JSON - One per day + public class AccessDataDetail + { + public string DeviceId { get; set; } + public string AccessDate { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public long Bandwidth { get; set; } + public List AccessDetails { get; set; } = new List(); + } +} diff --git a/parserlib/AzureBlobStorage.cs b/parserlib/AzureBlobStorage.cs new file mode 100644 index 0000000..4742569 --- /dev/null +++ b/parserlib/AzureBlobStorage.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.Storage; +using Microsoft.Azure.Storage.Blob; +using Newtonsoft.Json; +using System.Text.RegularExpressions; + +namespace ParserLib +{ + //Utility Class to Upload to Blob + public class AzureBlobStorage : IStorage + { + public async Task Create(Stream stream, string path, string containerName, string storageConnectionString) + { + CloudStorageAccount account = CloudStorageAccount.Parse(storageConnectionString); + + var blobClient = account.CreateCloudBlobClient(); + + var blobContainer = blobClient.GetContainerReference(containerName); + await blobContainer.CreateIfNotExistsAsync(); + + CloudBlockBlob blockBlob = blobContainer.GetBlockBlobReference(path); + await blockBlob.UploadFromStreamAsync(stream); + + } + } +} diff --git a/parserlib/DateRange.cs b/parserlib/DateRange.cs new file mode 100644 index 0000000..193fa35 --- /dev/null +++ b/parserlib/DateRange.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ParserLib +{ + public class DateRange + { + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + } +} diff --git a/parserlib/IStorage.cs b/parserlib/IStorage.cs new file mode 100644 index 0000000..c6ce179 --- /dev/null +++ b/parserlib/IStorage.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.Storage; +using Microsoft.Azure.Storage.Blob; +using Newtonsoft.Json; +using System.Text.RegularExpressions; + +namespace ParserLib +{ + public interface IStorage + { + Task Create(Stream stream, string path, string containerName, string storageConnectionString); + } +} diff --git a/parserlib/LineDataDetail.cs b/parserlib/LineDataDetail.cs new file mode 100644 index 0000000..8b2f941 --- /dev/null +++ b/parserlib/LineDataDetail.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ParserLib +{ + //contains information taken out of each line of the log + public class LineDataDetail + { + public string ClientIP { get; set; } + public string Date { get; set; } + public string Module { get; set; } + public long Bandwidth { get; set; } + + //note whether this particular line is of JSON type or of "tokenized" type + //these are the only types of log lines that we know of, for now + public string LineType { get; set; } + + //to account for some specific condition in "tokenized" lines that the lines must be skipped + public bool Skip { get; set; } + } +} diff --git a/parserlib/parserlib.cs b/parserlib/LogParser.cs similarity index 69% rename from parserlib/parserlib.cs rename to parserlib/LogParser.cs index cb012fc..390fa6d 100644 --- a/parserlib/parserlib.cs +++ b/parserlib/LogParser.cs @@ -8,58 +8,10 @@ using Microsoft.Azure.Storage; using Microsoft.Azure.Storage.Blob; using Newtonsoft.Json; -using System.Text.RegularExpressions; - +using System.Text.RegularExpressions; + namespace ParserLib { - public interface IStorage - { - Task Create(Stream stream, string path, string containerName, string storageConnectionString); - } - - //Utility Class to Upload to Blob - public class AzureBlobStorage : IStorage - { - public async Task Create(Stream stream, string path, string containerName, string storageConnectionString) - { - CloudStorageAccount account = CloudStorageAccount.Parse(storageConnectionString); - - var blobClient = account.CreateCloudBlobClient(); - - var blobContainer = blobClient.GetContainerReference(containerName); - await blobContainer.CreateIfNotExistsAsync(); - - CloudBlockBlob blockBlob = blobContainer.GetBlockBlobReference(path); - await blockBlob.UploadFromStreamAsync(stream); - - } - } - - //Represents a record in Access Log - public class AccessData - { - public string ModuleName { get; set; } - public long MainModuleCount { get; set; } - public long SubModuleCount { get; set; } - public DateTime UpLoadTime { get; set; } - } - - //AccessDataDetail Object to be serialized into JSON - One per day - public class AccessDataDetail - { - public string DeviceId { get; set; } - public string AccessDate { get; set; } - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public List AccessDetails { get; set; } = new List(); - } - - public class DateRange - { - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - } - public class LogParser { private IDictionary> accessDataList = new Dictionary>(); @@ -82,195 +34,299 @@ public LogParser(string connectionString, string containerName, bool inCloud, st //Blob Name is of the following format //abc@def.ghi.com_AccessDate //Device Id is "def" - that is anything between @ and the first "." - private string GetDeviceId(string blobName) - { - string[] blobSubStrings = blobName.Split('@', '.'); - if (blobSubStrings.Length >= 2) - return blobSubStrings[1]; - return "unknown"; + private string GetDeviceId(string blobName) + { + string[] blobSubStrings = blobName.Split('@', '.'); + if (blobSubStrings.Length >= 2) + return blobSubStrings[1]; + return "unknown"; } - private string ComputeJsonName(string deviceId, string accessDate) - { - string result = deviceId + "_" + accessDate + ".json"; - return result; + private string ComputeJsonName(string deviceId, string accessDate) + { + string result = deviceId + "_" + accessDate + ".json"; + return result; } - public AccessDataDetail DeserializeJsonDataInCloud(string deviceId, string accessDate) - { - string jsonName = ComputeJsonName(deviceId, accessDate); - - // connect to our storage account and create a blob client - var storageAccount = CloudStorageAccount.Parse(connectionString); - var blobClient = storageAccount.CreateCloudBlobClient(); - - // get a reference to the container - var blobcontainer = blobClient.GetContainerReference(containerName); - blobcontainer.CreateIfNotExists(); - - foreach(var blob in blobcontainer.ListBlobs()) - { - string flName = blob.Uri.Segments.Last().ToString(); - Console.WriteLine(flName); - - if(jsonName == flName) - { - CloudBlob cblob = blobcontainer.GetBlobReference(flName); - using (StreamReader reader = new StreamReader(cblob.OpenRead())) - { - string content = ""; - StringBuilder strjson=new StringBuilder(); - while ((content = reader.ReadLine())!=null) - { - strjson.Append(content); - } - AccessDataDetail overlapAddObj = JsonConvert.DeserializeObject(strjson.ToString()); - return overlapAddObj; - } - } - } - - return null; + public AccessDataDetail DeserializeJsonDataInCloud(string deviceId, string accessDate) + { + string jsonName = ComputeJsonName(deviceId, accessDate); + + // connect to our storage account and create a blob client + var storageAccount = CloudStorageAccount.Parse(connectionString); + var blobClient = storageAccount.CreateCloudBlobClient(); + + // get a reference to the container + var blobcontainer = blobClient.GetContainerReference(containerName); + blobcontainer.CreateIfNotExists(); + + foreach (var blob in blobcontainer.ListBlobs()) + { + string flName = blob.Uri.Segments.Last().ToString(); + Console.WriteLine(flName); + + if (jsonName == flName) + { + CloudBlob cblob = blobcontainer.GetBlobReference(flName); + using (StreamReader reader = new StreamReader(cblob.OpenRead())) + { + string content = ""; + StringBuilder strjson = new StringBuilder(); + while ((content = reader.ReadLine()) != null) + { + strjson.Append(content); + } + AccessDataDetail overlapAddObj = JsonConvert.DeserializeObject(strjson.ToString()); + return overlapAddObj; + } + } + } + + return null; } - private AccessDataDetail DeserializeJsonDataInLocal(string deviceId, string accessDate) - { - string jsonName = ComputeJsonName(deviceId, accessDate); - - if (!Directory.Exists(localPath)) - return null; - - string fullPath = Path.Combine(localPath, jsonName); - if (File.Exists(fullPath)) - { - var content = File.ReadAllText(fullPath); - AccessDataDetail deserializedObj = JsonConvert.DeserializeObject(content); - return deserializedObj; - } - - return null; + private AccessDataDetail DeserializeJsonDataInLocal(string deviceId, string accessDate) + { + string jsonName = ComputeJsonName(deviceId, accessDate); + + if (!Directory.Exists(localPath)) + return null; + + string fullPath = Path.Combine(localPath, jsonName); + if (File.Exists(fullPath)) + { + var content = File.ReadAllText(fullPath); + AccessDataDetail deserializedObj = JsonConvert.DeserializeObject(content); + return deserializedObj; + } + + return null; + } + + private AccessDataDetail DeserializeJsonData(string deviceId, string accessDate) + { + try + { + if (inCloud) + return DeserializeJsonDataInCloud(deviceId, accessDate); + + return DeserializeJsonDataInLocal(deviceId, accessDate); + } + catch (Exception) + { + return null; + } + } + + // Updates a bandwidth record for the given module + // Returns true if this module was already in the dictionary, + // Returns false otherwise + private void IncrementModuleBandwidth(IDictionary bandwidths, string module, long bandwidth) + { + if (!bandwidths.TryGetValue(module, out long current)) { + bandwidths.Add(module, bandwidth); + } else + { + bandwidths[module] = current + bandwidth; + } } - private AccessDataDetail DeserializeJsonData(string deviceId, string accessDate) - { - try - { - if (inCloud) - return DeserializeJsonDataInCloud(deviceId, accessDate); - - return DeserializeJsonDataInLocal(deviceId, accessDate); - } - catch(Exception) - { - return null; - } + private void IncrementModuleBandwidth(IDictionary> bandwidths, string date, string module, long bandwidth) + { + if (!bandwidths.TryGetValue(date, out IDictionary dateBandwidths)) + { + dateBandwidths = new Dictionary(); + bandwidths.Add(date, dateBandwidths); + } + IncrementModuleBandwidth(dateBandwidths, module, bandwidth); + } + + private LineDataDetail ExtractLineData(string lineEntry) + { + LineDataDetail info = new LineDataDetail(); + + try + { + dynamic line = JsonConvert.DeserializeObject(lineEntry); + info.ClientIP = line.remote_addr; + //info.Date = line.time_local.Substring(0, line.time_local.Length - 6); + info.Date = ((string)line.time_local).Substring(0, ((string)line.time_local).Length - 6); + info.Module = line.request; + info.Bandwidth = Convert.ToInt64(line.body_bytes_sent); + info.LineType = "json"; + info.Skip = false; + } + catch (JsonReaderException) + { + string[] tokens = lineEntry.Split(' '); + + info.ClientIP = string.Empty; + info.Date = string.Empty; + info.Module = string.Empty; + info.LineType = "tokenized"; + info.Bandwidth = 0; + + //If the first token (token[0]) is not of type IP address - skip processing - evaluate via Regex + Regex ipRegex = new Regex(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"); + MatchCollection regexResult = ipRegex.Matches(tokens[0]); + + if (regexResult.Count == 0 || tokens.Length <= 6) + { + //this line would be skipped anyway so no data is recorded because the line is probably broken + info.Skip = true; + } + else + { + //token[0] - ip address + info.ClientIP = tokens[0]; + //Ignore token[1] & token[2] - Irrelevant + //token[3] - Date information + info.Date = tokens[3].Substring(1, 20); + //Ignore token[4] & token[5] - Irrelevant + //token[6] - Contains Module Information. + info.Module = tokens[6]; + //token[7] and token[8] are not used for now + //token[9] - Contains bandwidth info + info.Bandwidth = Convert.ToInt64(tokens[9]); + info.Skip = false; + } + } + + return info; } public IList Parse(string blobName, Stream accessLogBlob) { string deviceId = GetDeviceId(blobName); + // Using another object that does its work before the skip logic. This should be refactored at some point. + IDictionary> bandwidths = new Dictionary>(); // any way to approximate final size to reduce rehashes? + + long totalBandwidth = 0L; + IList result = new List(); string lineEntry = string.Empty; //Initialize the prev record to some random date DateTime prevRecord = DateTime.Parse("01/01/1900"); + string prevLineType = string.Empty; + var file = new StreamReader(accessLogBlob); while ((lineEntry = file.ReadLine()) != null) { - string[] tokens = lineEntry.Split(' '); - bool isMainModule = false; + LineDataDetail lineInfo = ExtractLineData(lineEntry); - //TODO: We skip this for now - need to check if this is an error and log appropriately - if (tokens.Length <= 6) - continue; + //skip the next line if the next line is not of the same type as the previous type + //deciding type is the type of the very first line + //this assumes that the group of the lines of different type contain the same data so they can just be skipped without losing data + if (!prevLineType.Equals(string.Empty)) + { + if (!lineInfo.LineType.Equals(prevLineType)) + { + continue; + } + } + else + { + prevLineType = lineInfo.LineType; + } - //If the first token (token[0]) is not of type IP address - skip processing - evaluate via Regex - string clientIP = tokens[0]; - Regex ipRegex = new Regex(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"); - MatchCollection regexResult = ipRegex.Matches(clientIP); - if (regexResult.Count == 0) + if (lineInfo.Skip) + { continue; + } - //Ignore Token 2(token[1]) & Token 3(token[2]) - Irrelevant - - //Token 4(token[3]) - Date information - string dateValue = tokens[3].Substring(1); + string dateValue = lineInfo.Date; CultureInfo provider = CultureInfo.InvariantCulture; DateTime dtObj = DateTime.ParseExact(dateValue, "dd/MMM/yyyy:HH:mm:ss", provider); //TODO return if date is not valid string formattedDateValue = dtObj.ToString("yyyyMMdd"); - //Ignore Token 5(token[4]) & Token 6(token[5]) - Irrelevant - - //Token 7(token[6]) Contains Module Information. - string module = tokens[6]; + string module = lineInfo.Module; + + long bandwidth = lineInfo.Bandwidth; string[] moduleTokens = module.Split('/', '?'); + //TODO: We skip this for now - need to check if this is an error and log appropriately - if (moduleTokens.Length <= 2) - continue; + if (moduleTokens.Length <= 2) + { + //continue; + throw new Exception(); + } + + // Handle bandwidth before we skip + // Cleans hour:minute:second from the datetime string + IncrementModuleBandwidth(bandwidths, formattedDateValue, moduleTokens[2], bandwidth); + Console.WriteLine("{0} {1}", moduleTokens[2], bandwidth); + totalBandwidth += bandwidth; //Specification says, the module url should start with "modules" - if that's not the case skip - if (!moduleTokens.Contains("modules")) + if (!moduleTokens.Contains("modules")) + { continue; + } //Ignore all the Image based URLs - if (moduleTokens[moduleTokens.Length - 1].Contains(".png") || moduleTokens[moduleTokens.Length - 1].Contains(".jpg") || + if (moduleTokens[moduleTokens.Length - 1].Contains(".png") || moduleTokens[moduleTokens.Length - 1].Contains(".jpg") || moduleTokens[moduleTokens.Length - 1].Contains(".bmp") || moduleTokens[moduleTokens.Length - 1].Contains(".gif") || moduleTokens[moduleTokens.Length - 1].Contains(".js") || moduleTokens[moduleTokens.Length - 1].Contains(".css")) + { continue; - - //Ignore all the request that came for the same time - An html page has css, js, and lot of references - they dont count towards an article that was read - round off to 1 event for 1 second. - if (prevRecord == dtObj) + } + + //Ignore all the request that came for the same time - An html page has css, js, and lot of references - they dont count towards an article that was read - round off to 1 event for 1 second. + if (prevRecord == dtObj) continue; else prevRecord = dtObj; //If we reached here - we are certainly processing the record - Save any metadata information about the record. - if (!dateRangeList.ContainsKey(formattedDateValue)) - { - //If there is no record for a date - create a record and register start and end time as that of current log entry - dateRangeList.Add(formattedDateValue, new DateRange() { StartTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")), EndTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")) }); + if (!dateRangeList.ContainsKey(formattedDateValue)) + { + //If there is no record for a date - create a record and register start and end time as that of current log entry + dateRangeList.Add(formattedDateValue, new DateRange() { StartTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")), EndTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")) }); } else - { - //If there exist a record, update only the end time. - DateRange rangeObj = dateRangeList[formattedDateValue]; - rangeObj.EndTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")); + { + //If there exist a record, update only the end time. + DateRange rangeObj = dateRangeList[formattedDateValue]; + rangeObj.EndTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")); } - if (!existingRecords.ContainsKey(formattedDateValue)) - { - //Check if there is a JSON for this date - if exist load the JSON in memory - AccessDataDetail existingObj = DeserializeJsonData(deviceId, formattedDateValue); - if (existingObj != null) - { - existingRecords.Add(formattedDateValue, existingObj); - } - else - { - existingRecords.Add(formattedDateValue, null); + if (!existingRecords.ContainsKey(formattedDateValue)) + { + //Check if there is a JSON for this date - if exist load the JSON in memory + AccessDataDetail existingObj = DeserializeJsonData(deviceId, formattedDateValue); + if (existingObj != null) + { + existingRecords.Add(formattedDateValue, existingObj); + } + else + { + existingRecords.Add(formattedDateValue, null); } } - if (existingRecords[formattedDateValue] != null) - { - DateTime startTime = existingRecords[formattedDateValue].StartTime; - DateTime endTime = existingRecords[formattedDateValue].EndTime; - - DateTime currrentTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")); - - //If the current log entry is a overlapping with existing record - ignore - if (currrentTime >= startTime && currrentTime <= endTime) - continue; + if (existingRecords[formattedDateValue] != null) + { + DateTime startTime = existingRecords[formattedDateValue].StartTime; + DateTime endTime = existingRecords[formattedDateValue].EndTime; + + DateTime currrentTime = DateTime.Parse(dtObj.ToString("HH:mm:ss")); + + //TODO: need to ignore module count but still add to bandwidth count here + //If the current log entry is a overlapping with existing record - ignore + if (currrentTime >= startTime && currrentTime <= endTime) + { + //it seems that total bandwidth is still being added to totalBandwidth at this point + //because of continue, the module count below is not executed but the bandwidth count before the continue is still executed + //so far, it seems nothing needs to be done for this TODO + continue; + } } - - //Check if the request is for Main Module - if (moduleTokens[3].Contains(".htm") || moduleTokens[3].Contains("html")) - isMainModule = true; if (!accessDataList.ContainsKey(formattedDateValue)) { @@ -290,10 +346,15 @@ public IList Parse(string blobName, Stream accessLogBlob) } AccessData accessDataObj = dictionaryObj[moduleName]; - if (isMainModule) - accessDataObj.MainModuleCount = accessDataObj.MainModuleCount + 1; + //Check if the request is for Main Module + if (moduleTokens[3].Contains(".htm") || moduleTokens[3].Contains("html")) + { + accessDataObj.MainModuleCount += 1; + } else - accessDataObj.SubModuleCount = accessDataObj.SubModuleCount + 1; + { + accessDataObj.SubModuleCount += 1; + } } @@ -303,59 +364,63 @@ public IList Parse(string blobName, Stream accessLogBlob) AccessDataDetail addObj = new AccessDataDetail(); addObj.AccessDate = dateValue; addObj.DeviceId = deviceId; + addObj.Bandwidth = totalBandwidth; - if (dateRangeList.ContainsKey(dateValue)) - { - addObj.StartTime = dateRangeList[dateValue].StartTime; - addObj.EndTime = dateRangeList[dateValue].EndTime; + if (dateRangeList.ContainsKey(dateValue)) + { + addObj.StartTime = dateRangeList[dateValue].StartTime; + addObj.EndTime = dateRangeList[dateValue].EndTime; } IDictionary dictionaryObj = accessDataList[dateValue]; foreach (string moduleName in dictionaryObj.Keys) { - addObj.AccessDetails.Add(dictionaryObj[moduleName]); + var next = dictionaryObj[moduleName]; + next.Bandwidth = (bandwidths[dateValue])[moduleName]; + addObj.AccessDetails.Add(next); + Console.WriteLine(moduleName); } - //Merge existing record with current record - if (existingRecords.ContainsKey(dateValue)) - { - if (existingRecords[dateValue] != null) - { - if (existingRecords[dateValue].StartTime != null) - { - //Update Start Time with minimum of both - if (existingRecords[dateValue].StartTime <= addObj.StartTime) - addObj.StartTime = existingRecords[dateValue].StartTime; - } - - if (existingRecords[dateValue].EndTime != null) - { - //Update End Time with maximum of both - if (existingRecords[dateValue].EndTime >= addObj.EndTime) - addObj.EndTime = existingRecords[dateValue].EndTime; - } - - foreach (AccessData existingaddObj in existingRecords[dateValue].AccessDetails) - { - string moduleName = existingaddObj.ModuleName; - //Check if record exist for this module - bool matchFound = false; - foreach (AccessData newaddObj in addObj.AccessDetails) - { - if (moduleName == newaddObj.ModuleName) - { - matchFound = true; - newaddObj.MainModuleCount += existingaddObj.MainModuleCount; - newaddObj.SubModuleCount += existingaddObj.SubModuleCount; - break; - } - } - - if (matchFound == false) - { - addObj.AccessDetails.Add(existingaddObj); - } - } + // Merge existing record with current record + if (existingRecords.ContainsKey(dateValue)) + { + if (existingRecords[dateValue] != null) + { + if (existingRecords[dateValue].StartTime != null) + { + //Update Start Time with minimum of both + if (existingRecords[dateValue].StartTime <= addObj.StartTime) + addObj.StartTime = existingRecords[dateValue].StartTime; + } + + if (existingRecords[dateValue].EndTime != null) + { + //Update End Time with maximum of both + if (existingRecords[dateValue].EndTime >= addObj.EndTime) + addObj.EndTime = existingRecords[dateValue].EndTime; + } + + foreach (AccessData existingaddObj in existingRecords[dateValue].AccessDetails) + { + string moduleName = existingaddObj.ModuleName; + //Check if record exist for this module + bool matchFound = false; + foreach (AccessData newaddObj in addObj.AccessDetails) + { + if (moduleName == newaddObj.ModuleName) + { + matchFound = true; + newaddObj.MainModuleCount += existingaddObj.MainModuleCount; + newaddObj.SubModuleCount += existingaddObj.SubModuleCount; + break; + } + } + + if (matchFound == false) + { + addObj.AccessDetails.Add(existingaddObj); + } + } } } @@ -379,26 +444,25 @@ public IList Parse(string blobName, Stream accessLogBlob) return null; } } - return result; } //Upload JSON Files to Temp Path private bool UploadFile(IList fileList) - { - if (!Directory.Exists(localPath)) - return false; - - foreach (string jsonPath in fileList) - { - FileInfo fInfo = new FileInfo(jsonPath); - string destPath = Path.Combine(localPath, fInfo.Name); - File.Copy(jsonPath, destPath, true); - } - return true; - } - - //Upload JSON Files to blob container + { + if (!Directory.Exists(localPath)) + return false; + + foreach (string jsonPath in fileList) + { + FileInfo fInfo = new FileInfo(jsonPath); + string destPath = Path.Combine(localPath, fInfo.Name); + File.Copy(jsonPath, destPath, true); + } + return true; + } + + //Upload JSON Files to blob container private bool UploadBlob(IList fileList) { foreach (string filePath in fileList) diff --git a/parserlib/parserlib.csproj b/parserlib/parserlib.csproj index 7979c2b..4666274 100644 --- a/parserlib/parserlib.csproj +++ b/parserlib/parserlib.csproj @@ -5,9 +5,11 @@ - - + + + + diff --git a/parsertest/parsertest.csproj b/parsertest/parsertest.csproj index 8c33a3a..06f40cb 100644 --- a/parsertest/parsertest.csproj +++ b/parsertest/parsertest.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.2 @@ -40,7 +40,7 @@ - + diff --git a/websrvlogprocessor.sln b/websrvlogprocessor.sln index faaf663..d893378 100644 --- a/websrvlogprocessor.sln +++ b/websrvlogprocessor.sln @@ -1,15 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.779 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29324.140 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "parserfn", "parserfn\parserfn.csproj", "{B2A8AFA6-7B11-489E-BAF2-7947723755C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserFn", "parserfn\ParserFn.csproj", "{B2A8AFA6-7B11-489E-BAF2-7947723755C2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "parserapp", "parserapp\parserapp.csproj", "{DC45E53E-4E10-4EEB-AC0C-45330F8D58CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserApp", "parserapp\ParserApp.csproj", "{DC45E53E-4E10-4EEB-AC0C-45330F8D58CC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "parsertest", "parsertest\parsertest.csproj", "{C7FA2820-8051-4819-BEF8-D4A11B79A22C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserTest", "parsertest\ParserTest.csproj", "{C7FA2820-8051-4819-BEF8-D4A11B79A22C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "parserlib", "parserlib\parserlib.csproj", "{D4B553BC-AA2C-4966-A962-606CB2447471}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParserLib", "parserlib\ParserLib.csproj", "{D4B553BC-AA2C-4966-A962-606CB2447471}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,10 +17,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D4B553BC-AA2C-4966-A962-606CB2447471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4B553BC-AA2C-4966-A962-606CB2447471}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4B553BC-AA2C-4966-A962-606CB2447471}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4B553BC-AA2C-4966-A962-606CB2447471}.Release|Any CPU.Build.0 = Release|Any CPU + {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Release|Any CPU.Build.0 = Release|Any CPU {DC45E53E-4E10-4EEB-AC0C-45330F8D58CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC45E53E-4E10-4EEB-AC0C-45330F8D58CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC45E53E-4E10-4EEB-AC0C-45330F8D58CC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -29,10 +29,10 @@ Global {C7FA2820-8051-4819-BEF8-D4A11B79A22C}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7FA2820-8051-4819-BEF8-D4A11B79A22C}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7FA2820-8051-4819-BEF8-D4A11B79A22C}.Release|Any CPU.Build.0 = Release|Any CPU - {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2A8AFA6-7B11-489E-BAF2-7947723755C2}.Release|Any CPU.Build.0 = Release|Any CPU + {D4B553BC-AA2C-4966-A962-606CB2447471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4B553BC-AA2C-4966-A962-606CB2447471}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4B553BC-AA2C-4966-A962-606CB2447471}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4B553BC-AA2C-4966-A962-606CB2447471}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE