diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2009-12-21 08:46:49 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2009-12-21 08:46:49 -0800 |
commit | db20f139cf8b050f3ccfb85cf4da4acceaabd6b7 (patch) | |
tree | 8b78e9d662686eb8d4e2abea543949c74a70ef35 /src | |
parent | 28c927de95d274cb54b10dea7d005c19963b3209 (diff) | |
download | DotNetOpenAuth-db20f139cf8b050f3ccfb85cf4da4acceaabd6b7.zip DotNetOpenAuth-db20f139cf8b050f3ccfb85cf4da4acceaabd6b7.tar.gz DotNetOpenAuth-db20f139cf8b050f3ccfb85cf4da4acceaabd6b7.tar.bz2 |
Feature use counting reporting.
Diffstat (limited to 'src')
4 files changed, 197 insertions, 9 deletions
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs index f899f03..e4dbc9a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs @@ -31,6 +31,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Contract.Requires<ArgumentNullException>(exception != null); this.Exception = exception; + Reporting.RecordEventOccurrence(this); } #region IAuthenticationResponse Members diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs index 02c5185..2669634 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs @@ -30,6 +30,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal NegativeAuthenticationResponse(NegativeAssertionResponse response) { Contract.Requires<ArgumentNullException>(response != null); this.response = response; + + Reporting.RecordEventOccurrence(this); } #region IAuthenticationResponse Properties diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs index 1bc306c..08d0350 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs @@ -39,6 +39,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { if (response.ProviderEndpoint != null && response.Version != null) { this.provider = new ProviderEndpointDescription(response.ProviderEndpoint, response.Version); } + + Reporting.RecordEventOccurrence(this); } #region IAuthenticationResponse Properties diff --git a/src/DotNetOpenAuth/Reporting.cs b/src/DotNetOpenAuth/Reporting.cs index 45ee6db..79451b8 100644 --- a/src/DotNetOpenAuth/Reporting.cs +++ b/src/DotNetOpenAuth/Reporting.cs @@ -36,6 +36,15 @@ namespace DotNetOpenAuth { private static readonly TimeSpan minimumReportingInterval = TimeSpan.FromDays(1); /// <summary> + /// The maximum frequency the set can be flushed to disk. + /// </summary> +#if DEBUG + private static readonly TimeSpan minimumFlushInterval = TimeSpan.Zero; +#else + private static readonly TimeSpan minimumFlushInterval = TimeSpan.FromMinutes(15); +#endif + + /// <summary> /// The isolated storage to use for collecting data in between published reports. /// </summary> private static IsolatedStorageFile file; @@ -76,6 +85,11 @@ namespace DotNetOpenAuth { private static List<PersistentHashSet> observations = new List<PersistentHashSet>(); /// <summary> + /// The named events that we have counters for. + /// </summary> + private static Dictionary<string, PersistentCounter> events = new Dictionary<string, PersistentCounter>(StringComparer.OrdinalIgnoreCase); + + /// <summary> /// The lock acquired while considering whether to publish a report. /// </summary> private static object publishingConsiderationLock = new object(); @@ -122,6 +136,32 @@ namespace DotNetOpenAuth { private static bool Enabled { get; set; } /// <summary> + /// Records an event occurrence. + /// </summary> + /// <param name="eventName">Name of the event.</param> + internal static void RecordEventOccurrence(string eventName) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(eventName)); + + PersistentCounter counter; + lock (events) { + if (!events.TryGetValue(eventName, out counter)) { + events[eventName] = counter = new PersistentCounter(file, "event-" + SanitizeFileName(eventName) + ".txt"); + } + } + + counter.Increment(); + } + + /// <summary> + /// Records an event occurence. + /// </summary> + /// <param name="eventNameByObjectType">The object whose type name is the event name to record.</param> + internal static void RecordEventOccurrence(object eventNameByObjectType) { + Contract.Requires<ArgumentNullException>(eventNameByObjectType != null); + RecordEventOccurrence(eventNameByObjectType.GetType().Name); + } + + /// <summary> /// Records the use of a feature by name. /// </summary> /// <param name="feature">The feature.</param> @@ -229,6 +269,7 @@ namespace DotNetOpenAuth { writer.WriteLine(Util.LibraryVersion); foreach (var observation in observations) { + observation.Flush(); writer.WriteLine("===================================="); writer.WriteLine(observation.FileName); try { @@ -241,6 +282,19 @@ namespace DotNetOpenAuth { } } + foreach (var counter in events.Values) { + counter.Flush(); + } + + foreach (string eventFile in file.GetFileNames("event-*.txt")) { + writer.WriteLine("===================================="); + writer.WriteLine(eventFile); + using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } + } + // Make sure the stream is positioned at the beginning. writer.Flush(); stream.Position = 0; @@ -321,6 +375,28 @@ namespace DotNetOpenAuth { } /// <summary> + /// Sanitizes the name of the file so it only includes valid filename characters. + /// </summary> + /// <param name="fileName">The filename to sanitize.</param> + /// <returns>The filename, with any and all invalid filename characters replaced with the hyphen (-) character.</returns> + private static string SanitizeFileName(string fileName) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(fileName)); + char[] invalidCharacters = Path.GetInvalidFileNameChars(); + if (fileName.IndexOfAny(invalidCharacters) < 0) { + return fileName; // nothing invalid about this filename. + } + + // Use a stringbuilder since we may be replacing several characters + // and we don't want to instantiate a new string buffer for each new version. + StringBuilder sanitized = new StringBuilder(fileName); + foreach (char invalidChar in invalidCharacters) { + sanitized.Replace(invalidChar, '-'); + } + + return sanitized.ToString(); + } + + /// <summary> /// A set of values that persist the set to disk. /// </summary> private class PersistentHashSet : IDisposable { @@ -345,15 +421,6 @@ namespace DotNetOpenAuth { private readonly HashSet<string> memorySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); /// <summary> - /// The maximum frequency the set can be flushed to disk. - /// </summary> -#if DEBUG - private static readonly TimeSpan minimumFlushInterval = TimeSpan.Zero; -#else - private static readonly TimeSpan minimumFlushInterval = TimeSpan.FromMinutes(15); -#endif - - /// <summary> /// The maximum number of elements to track before not storing new elements. /// </summary> private readonly int maximumElements; @@ -484,5 +551,121 @@ namespace DotNetOpenAuth { } } } + + /// <summary> + /// A feature usage counter. + /// </summary> + private class PersistentCounter : IDisposable { + /// <summary> + /// The isolated persistent storage. + /// </summary> + private readonly FileStream fileStream; + + /// <summary> + /// The persistent reader. + /// </summary> + private readonly StreamReader reader; + + /// <summary> + /// The persistent writer. + /// </summary> + private readonly StreamWriter writer; + + /// <summary> + /// The time the last flush occurred. + /// </summary> + private DateTime lastFlushed; + + /// <summary> + /// The in-memory copy of the counter. + /// </summary> + private int counter; + + /// <summary> + /// A flag indicating whether the set has changed since it was last flushed. + /// </summary> + private bool dirty; + + /// <summary> + /// Initializes a new instance of the <see cref="PersistentCounter"/> class. + /// </summary> + /// <param name="storage">The storage location.</param> + /// <param name="fileName">Name of the file.</param> + internal PersistentCounter(IsolatedStorageFile storage, string fileName) { + Contract.Requires<ArgumentNullException>(storage != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(fileName)); + this.FileName = fileName; + + // Load the file into memory. + bool fileCreated = storage.GetFileNames(fileName).Length == 0; + this.fileStream = new IsolatedStorageFileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, storage); + this.reader = new StreamReader(this.fileStream, Encoding.UTF8); + int.TryParse(this.reader.ReadLine(), out this.counter); + + this.writer = new StreamWriter(this.fileStream, Encoding.UTF8); + this.writer.AutoFlush = true; + this.lastFlushed = DateTime.Now; + + // Write a unique header to the file so the report collector can match duplicates. + if (fileCreated) { + this.writer.WriteLine(Guid.NewGuid().ToString("B")); + } + } + + /// <summary> + /// Gets the name of the file. + /// </summary> + /// <value>The name of the file.</value> + internal string FileName { get; private set; } + + #region IDisposable Members + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() { + this.Dispose(true); + } + + #endregion + + /// <summary> + /// Increments the counter. + /// </summary> + internal void Increment() { + lock (this) { + this.counter++; + this.dirty = true; + if (this.dirty && DateTime.Now - this.lastFlushed > minimumFlushInterval) { + this.Flush(); + } + } + } + + /// <summary> + /// Flushes any newly added values to disk. + /// </summary> + internal void Flush() { + lock (this) { + this.writer.BaseStream.Position = 0; + this.writer.BaseStream.SetLength(0); // truncate file + this.writer.Write(this.counter); + this.dirty = false; + this.lastFlushed = DateTime.Now; + } + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { + if (disposing) { + this.writer.Dispose(); + this.reader.Dispose(); + this.fileStream.Dispose(); + } + } + } } } |