GitHub PullRequestReviewCommentEvent at 2014-01-22 01:41:57
Repository: kudu
Actor: amitapl
Payload: {"comment":{"url":"https://api.github.com/repos/projectkudu/kudu/pulls/comments/9062706","id":9062706,"diff_hunk":"@@ -0,0 +1,282 @@\n+﻿using System;\n+using System.Collections.Generic;\n+using System.Globalization;\n+using System.IO;\n+using System.IO.Abstractions;\n+using System.Linq;\n+using System.Text.RegularExpressions;\n+using Kudu.Contracts.Diagnostics;\n+using Kudu.Core;\n+\n+namespace Kudu.Services.Diagnostics\n+{\n+    public interface IApplicationLogsReader\n+    {\n+        IEnumerable<ApplicationLogEntry> GetRecentLogs(int top);\n+    }\n+\n+    public class ApplicationLogsReader : IApplicationLogsReader\n+    {\n+        public static readonly int ReadBatchSize = 5;\n+        public static readonly int FileOpenLimit = 50;\n+        public static readonly string LogFilenamePattern = \"*-*.txt\";\n+        public static readonly string LogErrorsSuffix = \"-logging-errors.txt\";\n+        public static readonly string LogEntryRegexPattern = @\"^(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d)\\s+PID\\[(\\d+)\\]\\s(Warning|Information|Error)\\s+(.*)\";\n+        \n+        private readonly string _logsFolder;\n+        private readonly LogFileFinder _logFinder;            \n+\n+        public ApplicationLogsReader(IFileSystem fileSystem, IEnvironment environment)\n+        {              \n+            _logsFolder = fileSystem.Path.Combine(environment.RootPath, Constants.ApplicationLogFilesPath);\n+            _logFinder = new LogFileFinder(fileSystem, this._logsFolder);\n+        }       \n+\n+        public IEnumerable<ApplicationLogEntry> GetRecentLogs(int top)\n+        {\n+            if (top <= 0)\n+                throw new ArgumentOutOfRangeException(\"top\", \"Number of logs to return must be a positive integer.\");\n+\n+            List<ResumableLogFileReader> logReaders = null;\n+            try\n+            {\n+                logReaders = _logFinder\n+                    .FindLogFiles()                    \n+                    .Select(f => new ResumableLogFileReader(f))                    \n+                    .ToList();\n+\n+                List<ApplicationLogEntry> logs = new List<ApplicationLogEntry>();\n+                while (logs.Count < top && logReaders.Count > 0)\n+                {\n+                    ResumableLogFileReader reader = logReaders.OrderByDescending(f => f.LastTime).First();\n+                    var logBatch = reader.ReadNextBatch(ReadBatchSize);\n+                    if (logBatch.Count > 0)\n+                    {\n+                        logs.AddRange(logBatch);\n+                    }\n+                    else\n+                    {\n+                        reader.Dispose();\n+                        logReaders.Remove(reader);\n+                    }\n+                }\n+\n+                return logs\n+                    .OrderByDescending(e => e.TimeStamp)\n+                    .Take(top);\n+            }\n+            finally\n+            {\n+                if (logReaders != null)\n+                {\n+                    foreach (var reader in logReaders)\n+                    {\n+                        reader.Dispose();\n+                    }\n+                }\n+            }\n+        }\n+        \n+\n+        internal class LogFileFinder\n+        {            \n+            private readonly DirectoryInfoBase _directory;\n+            private readonly LogFileAccessStats _stats;\n+\n+            internal HashSet<string> ExcludedFiles { get; private set; }\n+            internal HashSet<string> IncludedFiles { get; private set; }\n+\n+            public LogFileFinder(IFileSystem fileSystem, string logsFolder, LogFileAccessStats stats = null)\n+            {\n+                ExcludedFiles = new HashSet<string>();\n+                IncludedFiles = new HashSet<string>();                \n+                _directory = fileSystem.DirectoryInfo.FromDirectoryName(logsFolder);\n+                _stats = stats;                \n+            }\n+\n+            public IEnumerable<FileInfoBase> FindLogFiles()\n+            {\n+                if (!_directory.Exists)\n+                    return new List<FileInfoBase>();\n+\n+                var files = _directory.GetFiles(LogFilenamePattern, SearchOption.TopDirectoryOnly)\n+                    .Where(f => !f.FullName.EndsWith(LogErrorsSuffix, StringComparison.OrdinalIgnoreCase))\n+                    .OrderByDescending(f => f.LastWriteTimeUtc)\n+                    .Take(FileOpenLimit)\n+                    .ToList();\n+\n+                var fileNames = files.Select(f => f.FullName);\n+\n+                // Remove any deleted files\n+                var newIncludedFiles = new HashSet<string>(IncludedFiles.Intersect(fileNames));\n+                var newExcludedFiles = new HashSet<string>(ExcludedFiles.Intersect(fileNames));                \n+                \n+                foreach (var file in files)\n+                {\n+                    if(newIncludedFiles.Contains(file.FullName) || newExcludedFiles.Contains(file.FullName))\n+                        continue;\n+\n+                    var line = ReadFirstLine(file);\n+                    if (!string.IsNullOrWhiteSpace(line))\n+                    {\n+                        if (IsLineInExpectedFormat(line))\n+                            newIncludedFiles.Add(file.FullName);\n+                        else\n+                            newExcludedFiles.Add(file.FullName);\n+                    }\n+                }\n+\n+                var results = files.Where(f => newIncludedFiles.Contains(f.FullName));\n+                IncludedFiles = newIncludedFiles;\n+                ExcludedFiles = newExcludedFiles;\n+                return results;          \n+            }\n+\n+            private static bool IsLineInExpectedFormat(string line)\n+            {\n+                return Regex.Match(line, LogEntryRegexPattern, RegexOptions.IgnoreCase).Success;\n+            }\n+\n+            private string ReadFirstLine(FileInfoBase fileInfo)\n+            {\n+                using (var reader = fileInfo.OpenText())","path":"Kudu.Services/Diagnostics/ApplicationLogsReader.cs","position":142,"original_position":142,"commit_id":"245a8f67ea5647cc12e7f019257db1eea3cb3209","original_commit_id":"245a8f67ea5647cc12e7f019257db1eea3cb3209","user":{"login":"amitapl","id":1200598,"avatar_url":"https://gravatar.com/avatar/93b9bc8856c1359bcd30725539f55525?d=https%3A%2F%2Fidenticons.github.com%2F5ebad61256ce0f9ff057b6df796a8236.png&r=x","gravatar_id":"93b9bc8856c1359bcd30725539f55525","url":"https://api.github.com/users/amitapl","html_url":"https://github.com/amitapl","followers_url":"https://api.github.com/users/amitapl/followers","following_url":"https://api.github.com/users/amitapl/following{/other_user}","gists_url":"https://api.github.com/users/amitapl/gists{/gist_id}","starred_url":"https://api.github.com/users/amitapl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/amitapl/subscriptions","organizations_url":"https://api.github.com/users/amitapl/orgs","repos_url":"https://api.github.com/users/amitapl/repos","events_url":"https://api.github.com/users/amitapl/events{/privacy}","received_events_url":"https://api.github.com/users/amitapl/received_events","type":"User","site_admin":false},"body":"maybe iterate empty/whitespace lines over?","created_at":"2014-01-22T01:41:57Z","updated_at":"2014-01-22T01:41:57Z","html_url":"https://github.com/projectkudu/kudu/pull/981#discussion_r9062706","pull_request_url":"https://api.github.com/repos/projectkudu/kudu/pulls/981","_links":{"self":{"href":"https://api.github.com/repos/projectkudu/kudu/pulls/comments/9062706"},"html":{"href":"https://github.com/projectkudu/kudu/pull/981#discussion_r9062706"},"pull_request":{"href":"https://api.github.com/repos/projectkudu/kudu/pulls/981"}}}}