PageRenderTime 3362ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/Source/Lokad.Cloud.Framework/Diagnostics/CloudLogger.cs

http://github.com/cdrnet/Lokad.Cloud
C# | 318 lines | 220 code | 49 blank | 49 comment | 16 complexity | 9945ba4ecb43e4415aae9dba339250e3 MD5 | raw file
  1. #region Copyright (c) Lokad 2009
  2. // This code is released under the terms of the new BSD licence.
  3. // URL: http://www.lokad.com/
  4. #endregion
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Globalization;
  8. using System.IO;
  9. using System.Security;
  10. using System.Text;
  11. using System.Xml.XPath;
  12. using Lokad.Cloud.Storage;
  13. using Microsoft.WindowsAzure.StorageClient;
  14. namespace Lokad.Cloud.Diagnostics
  15. {
  16. /// <summary>Log entry (when retrieving logs with the <see cref="CloudLogger"/>.
  17. /// </summary>
  18. public class LogEntry
  19. {
  20. public DateTime DateTime { get; set; }
  21. public string Level { get; set; }
  22. public string Message { get; set; }
  23. public string Error { get; set; }
  24. public string Source { get; set; }
  25. }
  26. /// <summary>Logger built on top of the Blob Storage.</summary>
  27. /// <remarks>
  28. /// Logs are formatted in XML with
  29. /// <code>
  30. /// &lt;log&gt;
  31. /// &lt;message&gt; {0} &lt;/message&gt;
  32. /// &lt;error&gt; {1} &lt;/error&gt;
  33. /// &lt;/log&gt;
  34. /// </code>
  35. ///
  36. /// Also, the logger is relying on date prefix in order to facilitate large
  37. /// scale enumeration of the logs. Yet, in order to facilitate fast enumeration
  38. /// of recent logs, an prefix inversion trick is used.
  39. /// </remarks>
  40. public class CloudLogger : ILog
  41. {
  42. public const string ContainerName = "lokad-cloud-logs";
  43. public const string Delimiter = "/";
  44. public const int DeleteBatchSize = 50;
  45. private static readonly char[] DelimiterCharArray = Delimiter.ToCharArray();
  46. readonly IBlobStorageProvider _provider;
  47. readonly string _source;
  48. LogLevel _logLevelThreshold;
  49. /// <summary>Minimal log level (inclusive), below this level,
  50. /// notifications are ignored.</summary>
  51. public LogLevel LogLevelThreshold
  52. {
  53. get { return _logLevelThreshold; }
  54. set { _logLevelThreshold = value; }
  55. }
  56. public CloudLogger(IBlobStorageProvider provider, string source)
  57. {
  58. _provider = provider;
  59. _source = source;
  60. _logLevelThreshold = LogLevel.Min;
  61. }
  62. public void Log(LogLevel level, object message)
  63. {
  64. Log(level, null, message);
  65. }
  66. public void Log(LogLevel level, Exception ex, object message)
  67. {
  68. if (!IsEnabled(level)) return;
  69. var blobName = GetNewLogBlobName(level);
  70. var log = string.Format(
  71. @"
  72. <log>
  73. <message>{0}</message>
  74. <error>{1}</error>
  75. <source>{2}</source>
  76. </log>
  77. ",
  78. SecurityElement.Escape(message.ToString()),
  79. ex != null ? SecurityElement.Escape(ex.ToString()) : string.Empty,
  80. string.IsNullOrEmpty(_source) ? "" : SecurityElement.Escape(_source));
  81. // on first execution, container needs to be created.
  82. var policy = ActionPolicy.With(e =>
  83. {
  84. var storageException = e as StorageClientException;
  85. if(storageException == null) return false;
  86. return storageException.ErrorCode == StorageErrorCode.ContainerNotFound;
  87. })
  88. .Retry(2, (e, i) => _provider.CreateContainer(ContainerName));
  89. policy.Do(() =>
  90. {
  91. var attempt = 0;
  92. while (!_provider.PutBlob(ContainerName, blobName + attempt, log, false))
  93. {
  94. attempt++;
  95. }
  96. });
  97. }
  98. public bool IsEnabled(LogLevel level)
  99. {
  100. return level >= _logLevelThreshold;
  101. }
  102. private static string GetNamePrefix(string blobName)
  103. {
  104. return blobName.Substring(0, 23); // prefix is always 23 char long
  105. }
  106. private static LogEntry DecodeLogEntry(string blobName, string blobContent)
  107. {
  108. var prefix = GetNamePrefix(blobName);
  109. var dateTime = ToDateTime(prefix);
  110. var level = blobName.Substring(23).Split(DelimiterCharArray, StringSplitOptions.RemoveEmptyEntries)[0];
  111. using(var stream = new StringReader(blobContent))
  112. {
  113. var xpath = new XPathDocument(stream);
  114. var nav = xpath.CreateNavigator();
  115. return new LogEntry
  116. {
  117. DateTime = dateTime,
  118. Level = level,
  119. Message = nav.SelectSingleNode("/log/message").InnerXml,
  120. Error = nav.SelectSingleNode("/log/error").InnerXml,
  121. Source = nav.SelectSingleNode("/log/source").InnerXml,
  122. };
  123. }
  124. }
  125. /// <summary>Lazily enumerates over the entire logs.</summary>
  126. /// <returns></returns>
  127. public IEnumerable<LogEntry> GetRecentLogs()
  128. {
  129. foreach(var blobName in _provider.List(ContainerName, string.Empty))
  130. {
  131. var rawlog = _provider.GetBlob<string>(ContainerName, blobName);
  132. if (!rawlog.HasValue)
  133. {
  134. continue;
  135. }
  136. yield return DecodeLogEntry(blobName, rawlog.Value);
  137. }
  138. }
  139. /// <summary>Lazily loads a page of logs.</summary>
  140. /// <param name="pageIndex">The zero-based index of the page.</param>
  141. /// <param name="pageSize">The size of the page.</param>
  142. /// <returns>The logs (silently fails if the page is empty).</returns>
  143. public IEnumerable<LogEntry> GetPagedLogs(int pageIndex, int pageSize)
  144. {
  145. return GetPagedLogs(pageIndex, pageSize, LogLevel.Min);
  146. }
  147. /// <summary>Lazily loads a page of logs.</summary>
  148. /// <param name="pageIndex">The zero-based index of the page.</param>
  149. /// <param name="pageSize">The size of the page.</param>
  150. /// <param name="levelThreshold">Minimal log level (inclusive) for entries to be included.</param>
  151. /// <returns>The logs (silently fails if the page is empty).</returns>
  152. public IEnumerable<LogEntry> GetPagedLogs(int pageIndex, int pageSize, LogLevel levelThreshold)
  153. {
  154. Enforce.Argument(() => pageIndex, Rules.Is.AtLeast(0));
  155. Enforce.Argument(() => pageSize, Rules.Is.AtLeast(2), Rules.Is.AtMost(100));
  156. int skipItems = pageIndex * pageSize;
  157. int count = 0;
  158. foreach (var blobName in _provider.List(ContainerName, String.Empty))
  159. {
  160. if (count >= skipItems)
  161. {
  162. if (count - skipItems >= pageSize)
  163. {
  164. yield break;
  165. }
  166. var content = _provider.GetBlob<string>(ContainerName, blobName);
  167. if (!content.HasValue)
  168. {
  169. continue;
  170. }
  171. var entry = DecodeLogEntry(blobName, content.Value);
  172. if(EnumUtil.Parse<LogLevel>(entry.Level) < levelThreshold)
  173. {
  174. continue;
  175. }
  176. yield return entry;
  177. }
  178. count++;
  179. }
  180. }
  181. /// <summary>Deletes all the logs older than <paramref name="maxWeeks"/> weeks.</summary>
  182. /// <param name="maxWeeks">The max number of weeks of logs to preserve.</param>
  183. /// <remarks>The implementation is far from being efficient, but it is expected to be used sparingly.</remarks>
  184. public void DeleteOldLogs(int maxWeeks)
  185. {
  186. Enforce.Argument(() => maxWeeks, Rules.Is.AtLeast(1));
  187. DeleteOldLogs(DateTime.UtcNow.AddDays(-7 * maxWeeks));
  188. }
  189. // This is used for testing only
  190. // limit should be universal time
  191. internal void DeleteOldLogs(DateTime limit)
  192. {
  193. // Algorithm:
  194. // Iterate over the logs, queuing deletions up to 50 items at a time,
  195. // then restart; continue until no deletions are queued
  196. var deleteQueue = new List<string>(DeleteBatchSize);
  197. do
  198. {
  199. deleteQueue.Clear();
  200. foreach(var blobName in _provider.List(ContainerName, string.Empty))
  201. {
  202. var prefix = GetNamePrefix(blobName);
  203. var dateTime = ToDateTime(prefix);
  204. if(dateTime < limit) deleteQueue.Add(blobName);
  205. if(deleteQueue.Count == DeleteBatchSize) break;
  206. }
  207. foreach(var blobName in deleteQueue)
  208. {
  209. _provider.DeleteBlob(ContainerName, blobName);
  210. }
  211. } while(deleteQueue.Count > 0);
  212. }
  213. static string GetNewLogBlobName(LogLevel level)
  214. {
  215. var builder = new StringBuilder();
  216. builder.Append(ToPrefix(DateTime.UtcNow));
  217. builder.Append(Delimiter);
  218. builder.Append(level.ToString());
  219. builder.Append(Delimiter);
  220. return builder.ToString();
  221. }
  222. /// <summary>Time prefix with inversion in order to enumerate
  223. /// starting from the most recent.</summary>
  224. /// <remarks>This method is the symmetric of <see cref="ToDateTime"/>.</remarks>
  225. public static string ToPrefix(DateTime dateTime)
  226. {
  227. dateTime = dateTime.ToUniversalTime();
  228. // yyyy/MM/dd/hh/mm/ss/fff
  229. return string.Format("{0}/{1}/{2}/{3}/{4}/{5}/{6}",
  230. (10000 - dateTime.Year).ToString(CultureInfo.InvariantCulture),
  231. (12 - dateTime.Month).ToString("00"),
  232. (31 - dateTime.Day).ToString("00"),
  233. (24 - dateTime.Hour).ToString("00"),
  234. (60 - dateTime.Minute).ToString("00"),
  235. (60 - dateTime.Second).ToString("00"),
  236. (999 - dateTime.Millisecond).ToString("000"));
  237. }
  238. /// <summary>Convert a prefix with inversion into a <c>DateTime</c>.</summary>
  239. /// <remarks>This method is the symmetric of <see cref="ToPrefix"/>.</remarks>
  240. public static DateTime ToDateTime(string prefix)
  241. {
  242. var tokens = prefix.Split('/');
  243. if(tokens.Length != 7) throw new ArgumentException("Incorrect prefix.", "prefix");
  244. var year = 10000 - int.Parse(tokens[0], CultureInfo.InvariantCulture);
  245. var month = 12 - int.Parse(tokens[1], CultureInfo.InvariantCulture);
  246. var day = 31 - int.Parse(tokens[2], CultureInfo.InvariantCulture);
  247. var hour = 24 - int.Parse(tokens[3], CultureInfo.InvariantCulture);
  248. var minute = 60 - int.Parse(tokens[4], CultureInfo.InvariantCulture);
  249. var second = 60 - int.Parse(tokens[5], CultureInfo.InvariantCulture);
  250. var millisecond = 999 - int.Parse(tokens[6], CultureInfo.InvariantCulture);
  251. return new DateTime(year, month, day, hour, minute, second, millisecond, DateTimeKind.Utc);
  252. }
  253. }
  254. ///<summary>
  255. /// Log provider for the cloud logger
  256. ///</summary>
  257. public class CloudLogProvider : ILogProvider
  258. {
  259. readonly IBlobStorageProvider _provider;
  260. public CloudLogProvider(IBlobStorageProvider provider)
  261. {
  262. _provider = provider;
  263. }
  264. ILog IProvider<string, ILog>.Get(string key)
  265. {
  266. return new CloudLogger(_provider, key);
  267. }
  268. }
  269. }