diff --git a/ATxCommon/ATxCommon.csproj b/ATxCommon/ATxCommon.csproj index 7e3dffdf3aaf249295a18f42c9e956c71bd303ea..40c19c2f0fd96c3c5e4f53e0b9865daa7618a329 100644 --- a/ATxCommon/ATxCommon.csproj +++ b/ATxCommon/ATxCommon.csproj @@ -46,6 +46,7 @@ <Compile Include="ActiveDirectory.cs" /> <Compile Include="BuildDetails.cs" /> <Compile Include="Conv.cs" /> + <Compile Include="DirectoryDetails.cs" /> <Compile Include="FsUtils.cs" /> <Compile Include="Monitoring\Cpu.cs" /> <Compile Include="Monitoring\MonitorBase.cs" /> @@ -55,6 +56,7 @@ <Compile Include="Serializables\ServiceConfig.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Serializables\ServiceStatus.cs" /> + <Compile Include="StorageStatus.cs" /> <Compile Include="SystemChecks.cs" /> <Compile Include="TimeUtils.cs" /> </ItemGroup> diff --git a/ATxCommon/DirectoryDetails.cs b/ATxCommon/DirectoryDetails.cs new file mode 100644 index 0000000000000000000000000000000000000000..df0f58223342056f5e9c40cee9070989815afc96 --- /dev/null +++ b/ATxCommon/DirectoryDetails.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using NLog; + +namespace ATxCommon +{ + public class DirectoryDetails + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private long _size = -1; + private int _age = -1; + + /// <summary> + /// Initialize the DirectoryDetails object from the given DirectoryInfo. + /// </summary> + /// <param name="dirInfo">The DirectoryInfo object of the directory to investigate.</param> + public DirectoryDetails(DirectoryInfo dirInfo) { + Dir = dirInfo; + } + + /// <summary> + /// The underlying DirectoryInfo object. + /// </summary> + public DirectoryInfo Dir { get; } + + /// <summary> + /// The age defined by the directory's NAME in days. + /// </summary> + public int AgeFromName { + get { + if (_age < 0) + _age = FsUtils.DirNameToAge(Dir, DateTime.Now); + + return _age; + } + } + + /// <summary> + /// The full size of the directory tree in bytes. + /// </summary> + public long Size { + get { + if (_size >= 0) + return _size; + + try { + _size = FsUtils.GetDirectorySize(Dir.FullName); + } + catch (Exception ex) { + Log.Error("ERROR getting directory size of [{0}]: {1}", + Dir.FullName, ex.Message); + } + + return _size; + } + } + + /// <summary> + /// Human friendly description of the directory's age, derived from its NAME. + /// </summary> + public string HumanAgeFromName => TimeUtils.DaysToHuman(AgeFromName, false); + + /// <summary> + /// Human friendly description of the directory tree size (recursively). + /// </summary> + public string HumanSize => Conv.BytesToString(Size); + } +} diff --git a/ATxCommon/FsUtils.cs b/ATxCommon/FsUtils.cs index 1c530d88bcbc1fa55624dd97f0623b41cd932dff..fd1c10165415ba64f811f891ba2535036f3c9d3b 100644 --- a/ATxCommon/FsUtils.cs +++ b/ATxCommon/FsUtils.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -52,11 +51,11 @@ namespace ATxCommon Log.Error("ERROR: CheckForDirectory() parameter must not be empty!"); return false; } - return FsUtils.CreateNewDirectory(path, false) == path; + return CreateNewDirectory(path, false) == path; } /// <summary> - /// Recursively sum up size of all files under a given path. + /// Recursively sum up size (in bytes) of all files under a given path. /// </summary> /// <param name="path">Full path of the directory.</param> /// <returns>The total size in bytes.</returns> @@ -88,67 +87,6 @@ namespace ATxCommon return (baseTime - dirTimestamp).Days; } - /// <summary> - /// Assemble a dictionary with information about expired directories. - /// </summary> - /// <param name="baseDir">The base directory to scan for subdirectories.</param> - /// <param name="thresh">The number of days used as expiration threshold.</param> - /// <returns>A dictionary having usernames as keys (of those users that actually do have - /// expired directories), where the values are lists of tuples with the DirInfo objects, - /// size (in bytes) and age (in days) of the expired directories.</returns> - public static Dictionary<string, List<Tuple<DirectoryInfo, long, int>>> - ExpiredDirs(DirectoryInfo baseDir,int thresh) { - - var collection = new Dictionary<string, List<Tuple<DirectoryInfo, long, int>>>(); - var now = DateTime.Now; - foreach (var userdir in baseDir.GetDirectories()) { - var expired = new List<Tuple<DirectoryInfo, long, int>>(); - foreach (var subdir in userdir.GetDirectories()) { - var age = DirNameToAge(subdir, now); - if (age < thresh) - continue; - long size = -1; - try { - size = GetDirectorySize(subdir.FullName); - } - catch (Exception ex) { - Log.Error("ERROR getting directory size of [{0}]: {1}", - subdir.FullName, ex.Message); - } - expired.Add(new Tuple<DirectoryInfo, long, int>(subdir, size, age)); - } - if (expired.Count > 0) - collection.Add(userdir.Name, expired); - } - return collection; - } - - /// <summary> - /// Generate a report on expired folders in the grace location. - /// - /// Check all user-directories in the grace location for subdirectories whose timestamp - /// (the directory name) exceeds the configured grace period and generate a summary - /// containing the age and size of those directories. - /// </summary> - /// <param name="graceLocation">The location to scan for expired folders.</param> - /// <param name="threshold">The number of days used as expiration threshold.</param> - public static string GraceLocationSummary(DirectoryInfo graceLocation, int threshold) { - var expired = ExpiredDirs(graceLocation, threshold); - var report = ""; - foreach (var userdir in expired.Keys) { - report += "\n - user '" + userdir + "'\n"; - foreach (var subdir in expired[userdir]) { - report += string.Format(" - {0} [age: {2}, size: {1}]\n", - subdir.Item1, Conv.BytesToString(subdir.Item2), - TimeUtils.DaysToHuman(subdir.Item3)); - } - } - if (string.IsNullOrEmpty(report)) - return ""; - - return "Expired folders in grace location:\n" + report; - } - /// <summary> /// Check if a given directory is empty. If a marker file is set in the config a /// file with this name will be created inside the given directory and will be diff --git a/ATxCommon/Serializables/DriveToCheck.cs b/ATxCommon/Serializables/DriveToCheck.cs index 6411bdbc36d8354cdcca054cba9a69872ccc6ef3..632f9948fd069950a4f0ec88abfed114d54fad4a 100644 --- a/ATxCommon/Serializables/DriveToCheck.cs +++ b/ATxCommon/Serializables/DriveToCheck.cs @@ -1,4 +1,6 @@ -namespace ATxCommon.Serializables +using System.Xml.Serialization; + +namespace ATxCommon.Serializables { /// <summary> /// Helper class for the nested SpaceMonitoring sections. @@ -14,5 +16,19 @@ /// Limit (in GB) of free space, lower values will trigger a notification. /// </summary> public long SpaceThreshold { get; set; } + + /// <summary> + /// Free space of a drive in bytes, set to -1 if unknown or check resulted in an error. + /// </summary> + [XmlIgnore] + public long FreeSpace { get; set; } = -1; + + /// <summary> + /// Check if this drive's free space is below its threshold. + /// </summary> + /// <returns>True if free space is below the threshold, false otherwise.</returns> + public bool DiskSpaceLow() { + return FreeSpace < SpaceThreshold * Conv.GigaBytes; + } } } \ No newline at end of file diff --git a/ATxCommon/Serializables/ServiceConfig.cs b/ATxCommon/Serializables/ServiceConfig.cs index b6f1ead758e8fb322a2678cb0801e5d3d5ce71a4..82aace483a7282c77ba5fb7f5948064974d36adc 100644 --- a/ATxCommon/Serializables/ServiceConfig.cs +++ b/ATxCommon/Serializables/ServiceConfig.cs @@ -224,6 +224,12 @@ namespace ATxCommon.Serializables /// Minimum time in minutes between two low-space notifications. Default: 720 (12h). /// </summary> public int StorageNotificationDelta { get; set; } = 720; + + /// <summary> + /// Minimum time in minutes between two startup system health notifications. + /// Default: 2880 (2d). + /// </summary> + public int StartupNotificationDelta { get; set; } = 2880; #endregion @@ -291,6 +297,11 @@ namespace ATxCommon.Serializables [XmlIgnore] public static string ValidatorWarnings { get; set; } + /// <summary> + /// Convenience property converting the grace period to a human-friendly format. + /// </summary> + [XmlIgnore] + public string HumanGracePeriod => TimeUtils.DaysToHuman(GracePeriod, false); #endregion @@ -472,6 +483,7 @@ namespace ATxCommon.Serializables WarnOnHighValue(c.AdminNotificationDelta, nameof(c.AdminNotificationDelta), 1440); WarnOnHighValue(c.GraceNotificationDelta, nameof(c.GraceNotificationDelta), 10080); WarnOnHighValue(c.StorageNotificationDelta, nameof(c.StorageNotificationDelta), 10080); + WarnOnHighValue(c.StartupNotificationDelta, nameof(c.StartupNotificationDelta), 40320); WarnOnHighValue(c.GracePeriod, nameof(c.GracePeriod), 100); if (!c.DestinationDirectory.StartsWith(@"\\")) @@ -557,6 +569,8 @@ namespace ATxCommon.Serializables TimeUtils.MinutesToHuman(GraceNotificationDelta, false) + ")\n" + $"StorageNotificationDelta: {StorageNotificationDelta} min (" + TimeUtils.MinutesToHuman(StorageNotificationDelta, false) + ")\n" + + $"StartupNotificationDelta: {StartupNotificationDelta} min (" + + TimeUtils.MinutesToHuman(StartupNotificationDelta, false) + ")\n" + ""; } return msg; diff --git a/ATxCommon/Serializables/ServiceStatus.cs b/ATxCommon/Serializables/ServiceStatus.cs index 0d43860be4eeb14d8883fa0a6784fa471a43d64e..2c349162ce8c1b80e444400385cea613c4ca728a 100644 --- a/ATxCommon/Serializables/ServiceStatus.cs +++ b/ATxCommon/Serializables/ServiceStatus.cs @@ -19,6 +19,7 @@ namespace ATxCommon.Serializables private DateTime _lastStorageNotification; private DateTime _lastAdminNotification; private DateTime _lastGraceNotification; + private DateTime _lastStartupNotification; private string _statusDescription; private string _currentTransferSrc; @@ -159,6 +160,18 @@ namespace ATxCommon.Serializables } } + /// <summary> + /// Timestamp indicating when the last startup system health notification has been sent. + /// </summary> + [XmlElement("LastStartupNotification", DataType = "dateTime")] + public DateTime LastStartupNotification { + get => _lastStartupNotification; + set { + _lastStartupNotification = value; + Serialize(); + } + } + /// <summary> /// String indicating why the service is currently suspended (empty if not suspended). /// </summary> @@ -374,7 +387,10 @@ namespace ATxCommon.Serializables $"LastAdminNotification: {LastAdminNotification:yyyy-MM-dd HH:mm:ss}" + $" ({TimeUtils.HumanSince(LastAdminNotification)})\n" + $"LastGraceNotification: {LastGraceNotification:yyyy-MM-dd HH:mm:ss}" + - $" ({TimeUtils.HumanSince(LastGraceNotification)})\n"; + $" ({TimeUtils.HumanSince(LastGraceNotification)})\n" + + $"LastStartupNotification: {LastStartupNotification:yyyy-MM-dd HH:mm:ss}" + + $" ({TimeUtils.HumanSince(LastStartupNotification)})\n" + + ""; } #endregion validate and report diff --git a/ATxCommon/StorageStatus.cs b/ATxCommon/StorageStatus.cs new file mode 100644 index 0000000000000000000000000000000000000000..4a3078cdd9e192c8f2e5775434c8544b717d6eb0 --- /dev/null +++ b/ATxCommon/StorageStatus.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using ATxCommon.Serializables; +using NLog; + +namespace ATxCommon +{ + public class StorageStatus + { + /// <summary> + /// By default the statuses will only be updated if more than UpdateDelta seconds have + /// elapsed since the last update to prevent too many updates causing high system load. + /// </summary> + public int UpdateDelta = 20; + + private DateTime _lastUpdateFreeSpace = DateTime.MinValue; + private DateTime _lastUpdateGraceLocation = DateTime.MinValue; + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private readonly Dictionary<string, List<DirectoryDetails>> _expiredDirs; + + private readonly List<DriveToCheck> _drives; + private readonly int _gracePeriod; + private readonly string _gracePeriodHuman; + private readonly DirectoryInfo _graceLocation; + + /// <summary> + /// Initialize the StorageStatus object from the given ServiceConfig. + /// </summary> + /// <param name="config">The service configuration object.</param> + public StorageStatus(ServiceConfig config) { + _drives = config.SpaceMonitoring; + _gracePeriod = config.GracePeriod; + _gracePeriodHuman = config.HumanGracePeriod; + _graceLocation = new DirectoryInfo(config.DonePath); + _expiredDirs = new Dictionary<string, List<DirectoryDetails>>(); + Log.Debug("StorageStatus initialization complete, updating status..."); + Update(true); + } + + /// <summary> + /// Number of expired directories in the grace location. + /// </summary> + public int ExpiredDirsCount { + get { + UpdateGraceLocation(); + return _expiredDirs.Count; + } + } + + /// <summary> + /// Check if free space on all configured drives is above their threshold. + /// </summary> + /// <returns>False if any of the drives is below its threshold, true otherwise.</returns> + public bool AllDrivesAboveThreshold() { + UpdateFreeSpace(); + foreach (var drive in _drives) { + if (drive.DiskSpaceLow()) { + return false; + } + } + + return true; + } + + /// <summary> + /// Get a dictionary of expired directories from the grace location. + /// </summary> + public Dictionary<string, List<DirectoryDetails>> ExpiredDirs { + get { + UpdateGraceLocation(); + return _expiredDirs; + } + } + + /// <summary> + /// Human-friendly summary of expired directories in the grace location. + /// </summary> + /// <returns>A human-readable (i.e. formatted) string with details on the grace location + /// and all expired directories, grouped by the topmost level (i.e. user dirs).</returns> + public string GraceLocationSummary() { + UpdateGraceLocation(); + var summary = "------ Grace location status, " + + $"threshold: {_gracePeriod} days ({_gracePeriodHuman}) ------\n\n" + + $" - location: [{_graceLocation}]\n"; + + if (_expiredDirs.Count == 0) + return summary + " -- NO EXPIRED folders in grace location! --"; + + foreach (var dir in _expiredDirs.Keys) { + summary += "\n - directory '" + dir + "'\n"; + foreach (var subdir in _expiredDirs[dir]) { + summary += $" - {subdir.Dir.Name} " + + $"[age: {subdir.HumanAgeFromName}, " + + $"size: {subdir.HumanSize}]\n"; + } + } + + return summary; + } + + /// <summary> + /// Human-friendly summary of free disk space on all configured drives. + /// </summary> + /// <param name="onlyLowSpace"></param> + /// <returns>A human-readable (i.e. formatted) string with details on the free space on all + /// configured drives. If <paramref name="onlyLowSpace"/> is set to "true", space will only + /// be reported for drives that are below their corresponding threshold.</returns> + public string SpaceSummary(bool onlyLowSpace = false) { + UpdateFreeSpace(); + var summary = "------ Storage space status ------\n\n"; + foreach (var drive in _drives) { + var msg = $" - drive [{drive.DriveName}] " + + $"free space: {Conv.BytesToString(drive.FreeSpace)} " + + $"(threshold: {Conv.GigabytesToString(drive.SpaceThreshold)})\n"; + + if (onlyLowSpace && !drive.DiskSpaceLow()) { + Log.Trace(msg); + continue; + } + + summary += msg; + } + + return summary; + } + + /// <summary> + /// Update the storage status of free drive space if it's older than its threshold. + /// </summary> + /// <param name="force">Update, independently of the last update timestamp.</param> + public void UpdateFreeSpace(bool force = false) { + if (force) + _lastUpdateFreeSpace = DateTime.MinValue; + + if (TimeUtils.SecondsSince(_lastUpdateFreeSpace) < UpdateDelta) + return; + + Log.Trace("Updating storage status: checking free disk space..."); + foreach (var drive in _drives) { + try { + drive.FreeSpace = new DriveInfo(drive.DriveName).TotalFreeSpace; + } + catch (Exception ex) { + // log this as an error which then also gets sent via email (if configured) and + // let the rate-limiter take care of not flooding the admin with mails: + Log.Error("Error in GetFreeDriveSpace({0}): {1}", drive.DriveName, ex.Message); + } + } + + _lastUpdateFreeSpace = DateTime.Now; + } + + /// <summary> + /// Update the storage status of the grace location if it's older than its threshold. + /// </summary> + /// <param name="force">Update, independently of the last update timestamp.</param> + public void UpdateGraceLocation(bool force = false) { + if (force) + _lastUpdateGraceLocation = DateTime.MinValue; + + if (TimeUtils.SecondsSince(_lastUpdateGraceLocation) < UpdateDelta) + return; + + Log.Debug("Updating storage status: checking grace location..."); + _expiredDirs.Clear(); + foreach (var userdir in _graceLocation.GetDirectories()) { + Log.Trace("Scanning directory [{0}]", userdir.Name); + var expired = new List<DirectoryDetails>(); + foreach (var subdir in userdir.GetDirectories()) { + var dirDetails = new DirectoryDetails(subdir); + Log.Trace("Checking directory [{0}]: {1}", + dirDetails.Dir.Name, dirDetails.HumanAgeFromName); + if (dirDetails.AgeFromName < _gracePeriod) + continue; + + Log.Trace("Found expired directory [{0}]", dirDetails.Dir.Name); + expired.Add(dirDetails); + } + Log.Trace("Found {0} expired dirs.", expired.Count); + if (expired.Count > 0) + _expiredDirs.Add(userdir.Name, expired); + } + _lastUpdateGraceLocation = DateTime.Now; + + if (_expiredDirs.Count > 0) { + Log.Debug("Updated storage status: {0} expired directories in grace location.", + _expiredDirs.Count); + } + } + + /// <summary> + /// Update the current storage status in case the last update is already older than the + /// configured threshold <see cref="_lastUpdateFreeSpace"/>. + /// </summary> + /// <param name="force">Update, independently of the last update timestamp.</param> + public void Update(bool force = false) { + try { + UpdateFreeSpace(force); + UpdateGraceLocation(force); + } + catch (Exception ex) { + Log.Error("Updating storage status failed: {0}", ex.Message); + throw; + } + } + + /// <summary> + /// Create an overall storage summary (free space and grace location). + /// </summary> + /// <returns>Human-readable string with details on free space + grace location.</returns> + public string Summary() { + return $"{SpaceSummary()}\n{GraceLocationSummary()}"; + } + } +} diff --git a/ATxCommon/SystemChecks.cs b/ATxCommon/SystemChecks.cs index 7f8231ba8a425a51ef2b755b32d48e6abcb401ff..bddd1ae331c73f2f705c86e606bd1c08d7540f3e 100644 --- a/ATxCommon/SystemChecks.cs +++ b/ATxCommon/SystemChecks.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Management; +using ATxCommon.Serializables; using NLog; namespace ATxCommon @@ -55,7 +56,7 @@ namespace ATxCommon /// Check all configured disks for their free space and generate a /// summary with details to be used in a notification message. /// </summary> - public static string CheckFreeDiskSpace(List<Serializables.DriveToCheck> drives) { + public static string CheckFreeDiskSpace(List<DriveToCheck> drives) { var msg = ""; foreach (var driveToCheck in drives) { var freeSpace = GetFreeDriveSpace(driveToCheck.DriveName); @@ -123,7 +124,7 @@ namespace ATxCommon /// log, enclosed by square brackets (e.g. [explorer]). If "longFormat" is set to true, /// each process name will be printed on a separate line, followed by the title of the /// corresponding main window (if existing).</param> - public static void LogRunningProcesses(bool longFormat=false) { + public static void LogRunningProcesses(bool longFormat = false) { if (!Log.IsDebugEnabled) return; @@ -149,5 +150,33 @@ namespace ATxCommon Log.Debug("Currently running processes: {0}", procs.Substring(2)); } } + + /// <summary> + /// Generate an overall system health report with free space, grace location status, etc. + /// </summary> + /// <param name="storage">StorageStatus object used for space and grace reports.</param> + /// <returns>A multi-line string containing the details assembled in the report. These + /// comprise system uptime, free RAM, free storage space and current grace location status. + /// </returns> + public static string HealthReport(StorageStatus storage) { + var report = "------ System health report ------\n\n" + + $" - hostname: {Environment.MachineName}\n" + + $" - uptime: {TimeUtils.SecondsToHuman(Uptime(), false)}\n" + + $" - free system memory: {GetFreeMemory()} MB" + "\n\n"; + + report += storage.Summary(); + + return report; + } + + /// <summary> + /// Get the current system uptime in seconds. Note that this will miss all times where the + /// system had been suspended / hibernated, as it is based on the OS's ticks counter. + /// </summary> + /// <returns>The time since the last system boot in seconds.</returns> + public static long Uptime() { + var ticks = Stopwatch.GetTimestamp(); + return ticks / Stopwatch.Frequency; + } } } diff --git a/ATxService/AutoTx.cs b/ATxService/AutoTx.cs index 172ac3f112695c3f21f12b6a727cb7b76b67bc2c..396854067f76244360a1368a388776bdf1b2851f 100644 --- a/ATxService/AutoTx.cs +++ b/ATxService/AutoTx.cs @@ -69,6 +69,7 @@ namespace ATxService /// <summary> /// Counter on how many load monitoring properties are currently exceeding their limit(s). /// </summary> + // ReSharper disable once RedundantDefaultMemberInitializer private int _exceedingLoadLimit = 0; private DateTime _lastUserDirCheck = DateTime.MinValue; @@ -100,6 +101,7 @@ namespace ATxService private ServiceConfig _config; private ServiceStatus _status; + private StorageStatus _storage; private static Timer _mainTimer; @@ -124,6 +126,7 @@ namespace ATxService InitializePerformanceMonitors(); InitializeDirectories(); + SetupStorageStatus(); StartupSummary(); if (_config.DebugRoboSharp) { @@ -405,28 +408,9 @@ namespace ATxService "\n------ Loaded configuration settings ------\n" + _config.Summary(); - msg += "\n------ Current system parameters ------\n" + - "Hostname: " + Environment.MachineName + "\n" + - "Free system memory: " + SystemChecks.GetFreeMemory() + " MB" + "\n"; - foreach (var driveToCheck in _config.SpaceMonitoring) { - msg += "Free space on drive '" + driveToCheck.DriveName + "': " + - Conv.BytesToString(SystemChecks.GetFreeDriveSpace(driveToCheck.DriveName)) + "\n"; - } - - - msg += "\n------ Grace location status, threshold: " + _config.GracePeriod + " days " + - "(" + TimeUtils.DaysToHuman(_config.GracePeriod) + ") ------\n"; - try { - var tmp = SendGraceLocationSummary(_config.GracePeriod); - if (string.IsNullOrEmpty(tmp)) { - msg += " -- NO EXPIRED folders in grace location! --\n"; - } else { - msg += tmp; - } - } - catch (Exception ex) { - Log.Error("GraceLocationSummary() failed: {0}", ex.Message); - } + var health = SystemChecks.HealthReport(_storage); + SendHealthReport(health); + msg += "\n" + health; Log.Debug(msg); @@ -643,7 +627,7 @@ namespace ATxService // throw new Exception("just a test exception from RunMainTasks"); // mandatory tasks, run on every call: - SendLowSpaceMail(SystemChecks.CheckFreeDiskSpace(_config.SpaceMonitoring)); + SendLowSpaceMail(); UpdateServiceState(); _status.SerializeHeartbeat(); @@ -944,9 +928,8 @@ namespace ATxService sourceDirectory.Delete(); if (sourceDirectory.Parent != null) sourceDirectory.Parent.Delete(); - // check age and size of existing folders in the grace location after - // a transfer has completed, trigger a notification if necessary: - Log.Debug(SendGraceLocationSummary(_config.GracePeriod)); + // check grace location and trigger a notification if necessary: + SendGraceLocationSummary(); return; } errMsg = "unable to move " + sourceDirectory.FullName; @@ -970,6 +953,13 @@ namespace ATxService _lastUserDirCheck = FsUtils.CreateIncomingDirectories( _config.DestinationDirectory, _config.TmpTransferDir, _config.IncomingPath); } + + /// <summary> + /// Set up the StorageStatus object using the current configuration. + /// </summary> + private void SetupStorageStatus() { + _storage = new StorageStatus(_config); + } #endregion diff --git a/ATxService/Email.cs b/ATxService/Email.cs index dca17e4730f1e27a7271c5602574d60a40e28658..df59f298ecce6c1b020d5a27c7fc2fe49cf5f22d 100644 --- a/ATxService/Email.cs +++ b/ATxService/Email.cs @@ -20,7 +20,8 @@ namespace ATxService subject = $"{_config.EmailPrefix}{ServiceName} - {subject} - {_config.HostAlias}"; body += $"\n\n--\n[{_versionSummary}]\n"; if (string.IsNullOrEmpty(_config.SmtpHost)) { - Log.Debug("SendEmail: {0}\n{1}", subject, body); + Log.Debug("SendEmail: config option <SmtpHost> is unset, not sending mail - " + + "content shown below.\n[Subject] {0}\n[Body] {1}", subject, body); return; } if (!recipient.Contains(@"@")) { @@ -77,16 +78,17 @@ namespace ATxService /// </summary> /// <param name="body">The email text.</param> /// <param name="subject">Optional subject for the email.</param> - private void SendAdminEmail(string body, string subject = "") { + /// <returns>True in case an email was sent, false otherwise.</returns> + private bool SendAdminEmail(string body, string subject = "") { if (_config.SendAdminNotification == false) - return; + return false; var delta = TimeUtils.MinutesSince(_status.LastAdminNotification); if (delta < _config.AdminNotificationDelta) { Log.Warn("Suppressed admin email, interval too short ({0} vs. {1}):\n\n{2}\n{3}", TimeUtils.MinutesToHuman(delta), TimeUtils.MinutesToHuman(_config.AdminNotificationDelta), subject, body); - return; + return false; } if (string.IsNullOrWhiteSpace(subject)) @@ -96,16 +98,19 @@ namespace ATxService Log.Debug("Sending an admin notification email."); SendEmail(_config.AdminEmailAdress, subject, body); _status.LastAdminNotification = DateTime.Now; - + Log.Debug("{0} sent to AdminEmailAdress.", subject); + return true; } /// <summary> - /// Send a notification about low drive space to the admin. + /// Send a notification about low drive space to the admin if the time since the last + /// notification has elapsed the configured delta. The report will also contain a summary + /// of the grace location status. If none of the drives are low on space nothing will be + /// done (i.e. only a generic trace-level message will be logged). /// </summary> - /// <param name="spaceDetails">String describing the drives being low on space.</param> - private void SendLowSpaceMail(string spaceDetails) { - if (string.IsNullOrWhiteSpace(spaceDetails)) { - Log.Trace("SendLowSpaceMail(): spaceDetails emtpy!"); + private void SendLowSpaceMail() { + if (_storage.AllDrivesAboveThreshold()) { + Log.Trace("Free space on all drives above threshold."); return; } @@ -116,25 +121,24 @@ namespace ATxService return; } - // reaching this point means a notification will be sent to the admin, and in that - // case it makes sense to also include details about the grace location: - var graceReport = FsUtils.GraceLocationSummary( - new DirectoryInfo(_config.DonePath), _config.GracePeriod); + // reaching this point means a notification will be sent, so now we can ask for the + // full storage status report: + var report = _storage.Summary(); - - Log.Warn("WARNING: {0}", spaceDetails); + Log.Warn("WARNING: {0}", report); _status.LastStorageNotification = DateTime.Now; var substitutions = new List<Tuple<string, string>> { Tuple.Create("SERVICE_NAME", ServiceName), Tuple.Create("HOST_ALIAS", _config.HostAlias), Tuple.Create("HOST_NAME", Environment.MachineName), - Tuple.Create("LOW_SPACE_DRIVES", spaceDetails) + Tuple.Create("LOW_SPACE_DRIVES", report) }; try { var body = LoadMailTemplate("DiskSpace-Low.txt", substitutions); - if (graceReport.Length > 0) - body += $"\n\n--\n{graceReport}"; + // explicitly use SendEmail() instead of SendAdminEmail() here to circumvent the + // additional checks done in the latter one and make sure the low space email is + // sent out independently of that: SendEmail(_config.AdminEmailAdress, "low disk space", body); } catch (Exception ex) { @@ -205,27 +209,48 @@ namespace ATxService /// <summary> /// Send a report on expired folders in the grace location if applicable. - /// - /// Create a summary of expired folders and send it to the admin address - /// if the configured GraceNotificationDelta has passed since the last email. + /// + /// A summary of expired folders is created and sent to the admin address if the configured + /// GraceNotificationDelta has passed since the last email. The report will also contain a + /// summary of free disk space for all configured drives. /// </summary> - /// <param name="threshold">The number of days used as expiration threshold.</param> - /// <returns>The summary report, empty if no expired folders exist.</returns> - private string SendGraceLocationSummary(int threshold) { - var report = FsUtils.GraceLocationSummary( - new DirectoryInfo(_config.DonePath), threshold); - if (string.IsNullOrEmpty(report)) - return ""; - - report += $"\n{SystemChecks.CheckFreeDiskSpace(_config.SpaceMonitoring)}" + - "\nTime since last grace notification: " + - $"{TimeUtils.HumanSince(_status.LastGraceNotification)}\n"; - if (TimeUtils.MinutesSince(_status.LastGraceNotification) < _config.GraceNotificationDelta) - return report; + /// <returns>True if a report was sent, false otherwise (includes situations where there + /// are expired directories but the report has not been sent via email as the grace + /// notification delta hasn't expired yet, the report will still be logged then).</returns> + private bool SendGraceLocationSummary() { + if (_storage.ExpiredDirsCount == 0) + return false; + + var report = _storage.Summary() + + "\nTime since last grace notification: " + + $"{TimeUtils.HumanSince(_status.LastGraceNotification)}\n"; + + if (TimeUtils.MinutesSince(_status.LastGraceNotification) < _config.GraceNotificationDelta) { + Log.Debug(report); + return false; + } _status.LastGraceNotification = DateTime.Now; - SendAdminEmail(report, "grace location summary"); - return report + "\nNotification sent to AdminEmailAdress.\n"; + return SendAdminEmail(report, "grace location summary"); + } + + /// <summary> + /// Send a system health report if enough time has elapsed since the previous one. + /// </summary> + /// <param name="report">The health report.</param> + /// <returns>True in case the report was sent, false otherwise.</returns> + private bool SendHealthReport(string report) { + var elapsedHuman = TimeUtils.HumanSince(_status.LastStartupNotification); + + if (TimeUtils.MinutesSince(_status.LastStartupNotification) < _config.StartupNotificationDelta) { + Log.Trace("Not sending system health report now, last one has been sent {0}", + elapsedHuman); + return false; + } + + report += $"\nPrevious system health report notification was sent {elapsedHuman}.\n"; + _status.LastStartupNotification = DateTime.Now; + return SendAdminEmail(report, "system health report"); } } } \ No newline at end of file diff --git a/Resources/conf/config.common.xml b/Resources/conf/config.common.xml index 43845c1d0abd6dc340ec4b54b1fc464d38b4e988..56655583db873d5d337640aa4b2904932d82182d 100644 --- a/Resources/conf/config.common.xml +++ b/Resources/conf/config.common.xml @@ -118,6 +118,11 @@ in case one of the drives is below the threshold (in minutes) --> <StorageNotificationDelta>720</StorageNotificationDelta> + <!-- StartupNotificationDelta: minimum time (in minutes) between two service + startup system health notification emails (default: 2880 (2d). Set to 0 + to disable startup health reports. --> + <StartupNotificationDelta>2880</StartupNotificationDelta> + <!-- OPTIONAL NOTIFICATION / EMAIL SETTINGS --> </ServiceConfig>