From f5a21d61eac4cce6197e5ab7f2ade557217821b2 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 22 Mar 2016 16:17:37 -0600 Subject: [PATCH 1/9] Added a messagebus broker that translates messages into work items --- Exceptionless.sln | 15 +++ Source/Api/AppBuilder.cs | 1 + Source/Core/Exceptionless.Core.csproj | 2 + Source/Core/Jobs/MessageBusBrokerJob.cs | 43 ++++++ .../OrganizationNotificationWorkItem.cs | 9 ++ .../MessageBusBrokerJob.csproj | 124 ++++++++++++++++++ Source/Jobs/MessageBusBroker/Program.cs | 19 +++ .../Properties/AssemblyInfo.cs | 6 + Source/Jobs/MessageBusBroker/packages.config | 7 + 9 files changed, 226 insertions(+) create mode 100644 Source/Core/Jobs/MessageBusBrokerJob.cs create mode 100644 Source/Core/Models/WorkItems/OrganizationNotificationWorkItem.cs create mode 100644 Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj create mode 100644 Source/Jobs/MessageBusBroker/Program.cs create mode 100644 Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs create mode 100644 Source/Jobs/MessageBusBroker/packages.config diff --git a/Exceptionless.sln b/Exceptionless.sln index f0d8565258..79abd03bdf 100644 --- a/Exceptionless.sln +++ b/Exceptionless.sln @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloseInactiveSessionsJob", {75D9D89F-09F2-43A6-8B53-E1518E9FA822} = {75D9D89F-09F2-43A6-8B53-E1518E9FA822} EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessageBusBrokerJob", "Source\Jobs\MessageBusBroker\MessageBusBrokerJob.csproj", "{FFA6E598-3820-4A89-B853-C9E60A5739AA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -247,6 +249,18 @@ Global {FA2A4275-9180-43A4-AAE6-12BF83DC1252}.Release|Mixed Platforms.Build.0 = Release|Any CPU {FA2A4275-9180-43A4-AAE6-12BF83DC1252}.Release|x86.ActiveCfg = Release|Any CPU {FA2A4275-9180-43A4-AAE6-12BF83DC1252}.Release|x86.Build.0 = Release|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|x86.Build.0 = Debug|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Any CPU.Build.0 = Release|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|x86.ActiveCfg = Release|Any CPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -262,5 +276,6 @@ Global {BE383D44-18A2-4145-9D04-95F715D5E500} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} {1A8C938F-D7F4-4E2F-8789-9053156E9077} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} {FA2A4275-9180-43A4-AAE6-12BF83DC1252} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} + {FFA6E598-3820-4A89-B853-C9E60A5739AA} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} EndGlobalSection EndGlobal diff --git a/Source/Api/AppBuilder.cs b/Source/Api/AppBuilder.cs index 2d1996e359..9f86dc2404 100644 --- a/Source/Api/AppBuilder.cs +++ b/Source/Api/AppBuilder.cs @@ -99,6 +99,7 @@ private static void RunJobs(Container container, IAppBuilder app, ILoggerFactory new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(3)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(5)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(5)).RunInBackground(token); + new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(2)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(5)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(30), interval: TimeSpan.FromMinutes(1)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromMinutes(1), interval: TimeSpan.FromHours(1)).RunInBackground(token); diff --git a/Source/Core/Exceptionless.Core.csproj b/Source/Core/Exceptionless.Core.csproj index 94d68750e8..8a154dd369 100644 --- a/Source/Core/Exceptionless.Core.csproj +++ b/Source/Core/Exceptionless.Core.csproj @@ -166,6 +166,7 @@ + @@ -241,6 +242,7 @@ + diff --git a/Source/Core/Jobs/MessageBusBrokerJob.cs b/Source/Core/Jobs/MessageBusBrokerJob.cs new file mode 100644 index 0000000000..ee0e3a4f34 --- /dev/null +++ b/Source/Core/Jobs/MessageBusBrokerJob.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Exceptionless.Core.Extensions; +using Exceptionless.Core.Messaging.Models; +using Exceptionless.Core.Models.WorkItems; +using Foundatio.Jobs; +using Foundatio.Logging; +using Foundatio.Messaging; +using Foundatio.Queues; + +namespace Exceptionless.Core.Jobs { + public class MessageBusBrokerJob : JobBase { + private readonly IQueue _workItemQueue; + private readonly IMessageSubscriber _subscriber; + + public MessageBusBrokerJob(IQueue workItemQueue, IMessageSubscriber subscriber, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _workItemQueue = workItemQueue; + _subscriber = subscriber; + } + + protected override async Task RunInternalAsync(JobContext context) { + _subscriber.Subscribe(OnPlanOverageAsync, context.CancellationToken); + + while (!context.CancellationToken.IsCancellationRequested) + await Task.Delay(TimeSpan.FromSeconds(5)); + + return JobResult.Success; + } + + private async Task OnPlanOverageAsync(PlanOverage overage, CancellationToken cancellationToken = default(CancellationToken)) { + if (overage == null) + return; + + _logger.Trace("Enqueueing plan overage work item for organization: {0} IsOverHourlyLimit: {1} IsOverMonthlyLimit: {2}", overage.OrganizationId, overage.IsHourly, !overage.IsHourly); + await _workItemQueue.EnqueueAsync(new OrganizationNotificationWorkItem { + OrganizationId = overage.OrganizationId, + IsOverHourlyLimit = overage.IsHourly, + IsOverMonthlyLimit = !overage.IsHourly + }).AnyContext(); + } + } +} diff --git a/Source/Core/Models/WorkItems/OrganizationNotificationWorkItem.cs b/Source/Core/Models/WorkItems/OrganizationNotificationWorkItem.cs new file mode 100644 index 0000000000..01727583db --- /dev/null +++ b/Source/Core/Models/WorkItems/OrganizationNotificationWorkItem.cs @@ -0,0 +1,9 @@ +using System; + +namespace Exceptionless.Core.Models.WorkItems { + public class OrganizationNotificationWorkItem { + public string OrganizationId { get; set; } + public bool IsOverHourlyLimit { get; set; } + public bool IsOverMonthlyLimit { get; set; } + } +} \ No newline at end of file diff --git a/Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj b/Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj new file mode 100644 index 0000000000..28426813dd --- /dev/null +++ b/Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj @@ -0,0 +1,124 @@ + + + + + Debug + AnyCPU + {FFA6E598-3820-4A89-B853-C9E60A5739AA} + Exe + Properties + MessageBusBrokerJob + MessageBusBrokerJob + v4.6.1 + 512 + true + + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\..\packages\Foundatio.4.0.836\lib\net451\Foundatio.dll + True + + + ..\..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll + True + + + ..\..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll + True + + + ..\..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll + True + + + + + + ..\..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll + True + + + ..\..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll + True + + + + + + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + App.config + + + NLog.config + Always + + + run.bat + Always + + + + + + {3E5B39D5-7ACD-486B-9F90-59116B67952D} + Exceptionless.Core + + + + + @echo off + +robocopy $(SolutionDir)Source\Insulation\bin\$(ConfigurationName) $(TargetDir)bin\ /S /NFL /NDL /NJH /NJS /nc /ns /np +IF %25ERRORLEVEL%25 GEQ 8 exit 1 + +robocopy $(TargetDir) $(TargetDir)bin\ /MOV /XD bin /XF MessageBusBrokerJob.* run.bat NLog.config /S /is /NFL /NDL /NJH /NJS /nc /ns /np +IF %25ERRORLEVEL%25 GEQ 8 exit 1 + +exit 0 + + + \ No newline at end of file diff --git a/Source/Jobs/MessageBusBroker/Program.cs b/Source/Jobs/MessageBusBroker/Program.cs new file mode 100644 index 0000000000..323cae498e --- /dev/null +++ b/Source/Jobs/MessageBusBroker/Program.cs @@ -0,0 +1,19 @@ +using System; +using Exceptionless.Core; +using Exceptionless.Core.Extensions; +using Foundatio.Extensions; +using Foundatio.Jobs; +using Foundatio.ServiceProviders; + +namespace MessageBusBrokerJob { + public class Program { + public static int Main() { + AppDomain.CurrentDomain.SetDataDirectory(); + + var loggerFactory = Settings.Current.GetLoggerFactory(); + var serviceProvider = ServiceProvider.GetServiceProvider(Settings.JobBootstrappedServiceProvider, loggerFactory); + var job = serviceProvider.GetService(); + return new JobRunner(job, loggerFactory, initialDelay: TimeSpan.FromSeconds(2), interval: TimeSpan.Zero).RunInConsole(); + } + } +} diff --git a/Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs b/Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..92e63df26a --- /dev/null +++ b/Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("MessageBusBrokerJob")] +[assembly: ComVisible(false)] +[assembly: Guid("ED3C69A7-DB35-4582-9AAF-66040BE3C975")] \ No newline at end of file diff --git a/Source/Jobs/MessageBusBroker/packages.config b/Source/Jobs/MessageBusBroker/packages.config new file mode 100644 index 0000000000..006883e8f9 --- /dev/null +++ b/Source/Jobs/MessageBusBroker/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From 78a582a8a15dfb572a13603b86c72a940204a67f Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 08:52:00 -0500 Subject: [PATCH 2/9] Added the ability to send an organization notice email. --- Source/Core/Bootstrapper.cs | 3 +- Source/Core/Exceptionless.Core.csproj | 11 +++ ...OrganizationNotificationWorkItemHandler.cs | 71 +++++++++++++++++++ Source/Core/Mail/IMailer.cs | 1 + Source/Core/Mail/Mailer.cs | 31 +++++--- .../Models/OrganizationNotificationModel.cs | 10 +++ .../Templates/OrganizationNotice/Html.cshtml | 49 +++++++++++++ .../OrganizationNotice/PlainText.cshtml | 14 ++++ .../OrganizationNotice/Subject.cshtml | 10 +++ .../Repositories/OrganizationRepository.cs | 12 ++-- Source/Tests/Mail/NullMailer.cs | 4 ++ 11 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 Source/Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs create mode 100644 Source/Core/Mail/Models/OrganizationNotificationModel.cs create mode 100644 Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml create mode 100644 Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml create mode 100644 Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml diff --git a/Source/Core/Bootstrapper.cs b/Source/Core/Bootstrapper.cs index 6e71b4efe9..2f76c5855f 100644 --- a/Source/Core/Bootstrapper.cs +++ b/Source/Core/Bootstrapper.cs @@ -33,14 +33,12 @@ using Foundatio.Metrics; using Foundatio.Queues; using Foundatio.Serializer; -using Foundatio.ServiceProviders; using Foundatio.Storage; using Nest; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using RazorSharpEmail; using SimpleInjector; -using SimpleInjector.Advanced; namespace Exceptionless.Core { public class Bootstrapper { @@ -110,6 +108,7 @@ public static void RegisterServices(Container container, ILoggerFactory loggerFa workItemHandlers.Register(container.GetInstance); workItemHandlers.Register(container.GetInstance); workItemHandlers.Register(container.GetInstance); + workItemHandlers.Register(container.GetInstance); workItemHandlers.Register(container.GetInstance); container.RegisterSingleton(workItemHandlers); container.RegisterSingleton>(() => new InMemoryQueue(behaviors: container.GetAllInstances>(), workItemTimeout: TimeSpan.FromHours(1))); diff --git a/Source/Core/Exceptionless.Core.csproj b/Source/Core/Exceptionless.Core.csproj index 8a154dd369..0597c4f441 100644 --- a/Source/Core/Exceptionless.Core.csproj +++ b/Source/Core/Exceptionless.Core.csproj @@ -167,6 +167,7 @@ + @@ -183,6 +184,7 @@ + @@ -471,6 +473,15 @@ + + Always + + + Always + + + Always + diff --git a/Source/Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs b/Source/Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs new file mode 100644 index 0000000000..05d58d4a1f --- /dev/null +++ b/Source/Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Exceptionless.Core.Extensions; +using Exceptionless.Core.Mail; +using Exceptionless.Core.Mail.Models; +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.WorkItems; +using Exceptionless.Core.Repositories; +using Foundatio.Caching; +using Foundatio.Jobs; +using Foundatio.Lock; +using Foundatio.Logging; +using Foundatio.Messaging; + +namespace Exceptionless.Core.Jobs.WorkItemHandlers { + public class OrganizationNotificationWorkItemHandler : WorkItemHandlerBase { + private readonly IOrganizationRepository _organizationRepository; + private readonly IUserRepository _userRepository; + private readonly IMailer _mailer; + private readonly ILockProvider _lockProvider; + + public OrganizationNotificationWorkItemHandler(IOrganizationRepository organizationRepository, IUserRepository userRepository, IMailer mailer, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _organizationRepository = organizationRepository; + _userRepository = userRepository; + _mailer = mailer; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } + + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + var cacheKey = $"{nameof(OrganizationNotificationWorkItemHandler)}:{((OrganizationNotificationWorkItem)workItem).OrganizationId}"; + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + } + + public override async Task HandleItemAsync(WorkItemContext context) { + var workItem = context.GetData(); + _logger.Info("Received organization notification work item for: {0} IsOverHourlyLimit: {1} IsOverMonthlyLimit: {2}", workItem.OrganizationId, workItem.IsOverHourlyLimit, workItem.IsOverMonthlyLimit); + + var organization = await _organizationRepository.GetByIdAsync(workItem.OrganizationId, true).AnyContext(); + if (organization == null) + return; + + if (workItem.IsOverMonthlyLimit) + await SendOverageNotificationsAsync(organization, workItem.IsOverHourlyLimit, workItem.IsOverMonthlyLimit).AnyContext(); + } + + private async Task SendOverageNotificationsAsync(Organization organization, bool isOverHourlyLimit, bool isOverMonthlyLimit) { + var results = await _userRepository.GetByOrganizationIdAsync(organization.Id).AnyContext(); + foreach (var user in results.Documents) { + if (!user.IsEmailAddressVerified) { + _logger.Info("User {0} with email address {1} has not been verified.", user.Id, user.EmailAddress); + continue; + } + + if (!user.EmailNotificationsEnabled) { + _logger.Info().Message("User {0} with email address {1} has email notifications disabled.", user.Id, user.EmailAddress); + continue; + } + + _logger.Trace("Sending email to {0}...", user.EmailAddress); + await _mailer.SendOrganizationNoticeAsync(user.EmailAddress, new OrganizationNotificationModel { + Organization = organization, + IsOverHourlyLimit = isOverHourlyLimit, + IsOverMonthlyLimit = isOverMonthlyLimit + }).AnyContext(); + } + + _logger.Trace().Message("Done sending email."); + } + } +} \ No newline at end of file diff --git a/Source/Core/Mail/IMailer.cs b/Source/Core/Mail/IMailer.cs index 79c129d925..6d3ca9c887 100644 --- a/Source/Core/Mail/IMailer.cs +++ b/Source/Core/Mail/IMailer.cs @@ -12,6 +12,7 @@ public interface IMailer { Task SendPaymentFailedAsync(User owner, Organization organization); Task SendAddedToOrganizationAsync(User sender, Organization organization, User user); Task SendNoticeAsync(string emailAddress, EventNotification model); + Task SendOrganizationNoticeAsync(string emailAddress, OrganizationNotificationModel organizationNotificationModel); Task SendDailySummaryAsync(string emailAddress, DailySummaryModel notification); } } \ No newline at end of file diff --git a/Source/Core/Mail/Mailer.cs b/Source/Core/Mail/Mailer.cs index 078ffed3a5..4949f2e418 100644 --- a/Source/Core/Mail/Mailer.cs +++ b/Source/Core/Mail/Mailer.cs @@ -85,14 +85,23 @@ public Task SendAddedToOrganizationAsync(User sender, Organization organization, } public Task SendNoticeAsync(string emailAddress, EventNotification model) { - var message = _pluginManager.GetEventNotificationMailMessage(model); - if (message == null) { - _logger.Warn().Message("Unable to create event notification mail message for event \"{0}\". User: \"{1}\"", model.EventId, emailAddress).Write(); + var msg = _pluginManager.GetEventNotificationMailMessage(model); + if (msg == null) { + _logger.Warn("Unable to create event notification mail message for event \"{0}\". User: \"{1}\"", model.EventId, emailAddress); return Task.CompletedTask; } - message.To = emailAddress; - return QueueMessageAsync(message.ToMailMessage()); + msg.To = emailAddress; + return QueueMessageAsync(msg.ToMailMessage()); + } + + public Task SendOrganizationNoticeAsync(string emailAddress, OrganizationNotificationModel model) { + model.BaseUrl = Settings.Current.BaseURL; + + System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(model, "OrganizationNotice"); + msg.To.Add(emailAddress); + + return QueueMessageAsync(msg); } public Task SendDailySummaryAsync(string emailAddress, DailySummaryModel notification) { @@ -108,22 +117,22 @@ private Task QueueMessageAsync(System.Net.Mail.MailMessage message) { return _queue.EnqueueAsync(message.ToMailMessage()); } - private static void CleanAddresses(System.Net.Mail.MailMessage msg) { + private static void CleanAddresses(System.Net.Mail.MailMessage message) { if (Settings.Current.WebsiteMode == WebsiteMode.Production) return; var invalid = new List(); - invalid.AddRange(CleanAddresses(msg.To)); - invalid.AddRange(CleanAddresses(msg.CC)); - invalid.AddRange(CleanAddresses(msg.Bcc)); + invalid.AddRange(CleanAddresses(message.To)); + invalid.AddRange(CleanAddresses(message.CC)); + invalid.AddRange(CleanAddresses(message.Bcc)); if (invalid.Count == 0) return; if (invalid.Count <= 3) - msg.Subject = String.Concat("[", invalid.ToDelimitedString(), "] ", msg.Subject).StripInvisible(); + message.Subject = String.Concat("[", invalid.ToDelimitedString(), "] ", message.Subject).StripInvisible(); - msg.To.Add(Settings.Current.TestEmailAddress); + message.To.Add(Settings.Current.TestEmailAddress); } private static IEnumerable CleanAddresses(MailAddressCollection mac) { diff --git a/Source/Core/Mail/Models/OrganizationNotificationModel.cs b/Source/Core/Mail/Models/OrganizationNotificationModel.cs new file mode 100644 index 0000000000..46970eb622 --- /dev/null +++ b/Source/Core/Mail/Models/OrganizationNotificationModel.cs @@ -0,0 +1,10 @@ +using System; +using Exceptionless.Core.Models; + +namespace Exceptionless.Core.Mail.Models { + public class OrganizationNotificationModel : MailModelBase { + public Organization Organization { get; set; } + public bool IsOverHourlyLimit { get; set; } + public bool IsOverMonthlyLimit { get; set; } + } +} \ No newline at end of file diff --git a/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml new file mode 100644 index 0000000000..afe12e7ce5 --- /dev/null +++ b/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml @@ -0,0 +1,49 @@ +@using System +@inherits RazorSharpEmail.EmailTemplate +@{ Layout = "_Layout.html.cshtml"; } + + + + + + + + +
+ +
+ + + + +
+

+ @if (Model.IsOverHourlyLimit) { + + Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. + + } else if (Model.IsOverMonthlyLimit) { + + @Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. + + } +

+ + +

+ Upgrade plan or view plan usage +

+ +
+
Other Actions
+ +
+ +
+
+ + +
+ diff --git a/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml new file mode 100644 index 0000000000..c87291d580 --- /dev/null +++ b/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml @@ -0,0 +1,14 @@ +@using System +@inherits RazorSharpEmail.EmailTemplate +@if (Model.IsOverHourlyLimit) { + +Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. + +} else if (Model.IsOverMonthlyLimit) { + +@Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. + +} + +Upgrade plan or view plan usage: +@Model.BaseUrl/organization/@Model.Organization.Id/manage diff --git a/Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml new file mode 100644 index 0000000000..fc1c30b44f --- /dev/null +++ b/Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml @@ -0,0 +1,10 @@ +@inherits RazorSharpEmail.EmailTemplate +@if (Model.IsOverHourlyLimit) { + +[@Model.Organization.Name] Events are currently being throttled. + +} else if (Model.aIsOverMonthlyLimit) { + +[@Model.Organization.Name] The monthly plan limit exceeded. + +} \ No newline at end of file diff --git a/Source/Core/Repositories/OrganizationRepository.cs b/Source/Core/Repositories/OrganizationRepository.cs index 5dd58a0ff0..8892fe16ec 100644 --- a/Source/Core/Repositories/OrganizationRepository.cs +++ b/Source/Core/Repositories/OrganizationRepository.cs @@ -192,12 +192,7 @@ public async Task IncrementUsageAsync(string organizationId, bool tooBig, bool justWentOverHourly = hourlyTotal > org.GetHourlyEventLimit() && hourlyTotal <= org.GetHourlyEventLimit() + count; bool justWentOverMonthly = monthlyTotal > org.GetMaxEventsPerMonthWithBonus() && monthlyTotal <= org.GetMaxEventsPerMonthWithBonus() + count; - - if (justWentOverMonthly) - await PublishMessageAsync(new PlanOverage { OrganizationId = org.Id }).AnyContext(); - else if (justWentOverHourly) - await PublishMessageAsync(new PlanOverage { OrganizationId = org.Id, IsHourly = true }).AnyContext(); - + bool shouldSaveUsage = false; var lastCounterSavedDate = await Cache.GetAsync(GetUsageSavedCacheKey(organizationId)).AnyContext(); @@ -223,6 +218,11 @@ public async Task IncrementUsageAsync(string organizationId, bool tooBig, await Cache.SetAsync(GetUsageSavedCacheKey(organizationId), DateTime.UtcNow, TimeSpan.FromDays(32)).AnyContext(); } + if (justWentOverMonthly) + await PublishMessageAsync(new PlanOverage { OrganizationId = org.Id }).AnyContext(); + else if (justWentOverHourly) + await PublishMessageAsync(new PlanOverage { OrganizationId = org.Id, IsHourly = true }).AnyContext(); + return overLimit; } diff --git a/Source/Tests/Mail/NullMailer.cs b/Source/Tests/Mail/NullMailer.cs index f1ac75e5c0..e3aaa7a8dc 100644 --- a/Source/Tests/Mail/NullMailer.cs +++ b/Source/Tests/Mail/NullMailer.cs @@ -31,6 +31,10 @@ public Task SendNoticeAsync(string emailAddress, EventNotification model) { return Task.CompletedTask; } + public Task SendOrganizationNoticeAsync(string emailAddress, OrganizationNotificationModel organizationNotificationModel) { + return Task.CompletedTask; + } + public Task SendDailySummaryAsync(string emailAddress, DailySummaryModel notification) { return Task.CompletedTask; } From ca506074ae17d02ee33c0b536cde4888d08ceb26 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 08:52:53 -0500 Subject: [PATCH 3/9] Improved logging --- Source/Api/AppBuilder.cs | 8 ++++---- Source/Core/Jobs/MessageBusBrokerJob.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Api/AppBuilder.cs b/Source/Api/AppBuilder.cs index 9f86dc2404..e5642f11ef 100644 --- a/Source/Api/AppBuilder.cs +++ b/Source/Api/AppBuilder.cs @@ -83,12 +83,12 @@ public static void Build(IAppBuilder app, Container container = null) { Task.Run(async () => await CreateSampleDataAsync(container)); RunJobs(container, app, loggerFactory, logger); - logger.Info().Message("Starting api...").Write(); + logger.Info("Starting api..."); } private static void RunJobs(Container container, IAppBuilder app, ILoggerFactory loggerFactory, ILogger logger) { if (!Settings.Current.RunJobsInProcess) { - logger.Info().Message("Jobs running out of process.").Write(); + logger.Info("Jobs running out of process."); return; } @@ -107,7 +107,7 @@ private static void RunJobs(Container container, IAppBuilder app, ILoggerFactory new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromMinutes(15), interval: TimeSpan.FromDays(1)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(2), instanceCount: 2).RunInBackground(token); - logger.Warn().Message("Jobs running in process.").Write(); + logger.Warn("Jobs running in process."); } private static void EnableCors(HttpConfiguration config, IAppBuilder app) { @@ -202,7 +202,7 @@ public static Container CreateContainer(LoggerFactory loggerFactory, ILogger log try { insulationAssembly = Assembly.Load("Exceptionless.Insulation"); } catch (Exception ex) { - logger.Error().Message("Unable to load the insulation assembly.").Exception(ex).Write(); + logger.Error(ex, "Unable to load the insulation assembly."); } if (insulationAssembly != null) { diff --git a/Source/Core/Jobs/MessageBusBrokerJob.cs b/Source/Core/Jobs/MessageBusBrokerJob.cs index ee0e3a4f34..b90d86282f 100644 --- a/Source/Core/Jobs/MessageBusBrokerJob.cs +++ b/Source/Core/Jobs/MessageBusBrokerJob.cs @@ -32,7 +32,7 @@ protected override async Task RunInternalAsync(JobContext context) { if (overage == null) return; - _logger.Trace("Enqueueing plan overage work item for organization: {0} IsOverHourlyLimit: {1} IsOverMonthlyLimit: {2}", overage.OrganizationId, overage.IsHourly, !overage.IsHourly); + _logger.Info("Enqueueing plan overage work item for organization: {0} IsOverHourlyLimit: {1} IsOverMonthlyLimit: {2}", overage.OrganizationId, overage.IsHourly, !overage.IsHourly); await _workItemQueue.EnqueueAsync(new OrganizationNotificationWorkItem { OrganizationId = overage.OrganizationId, IsOverHourlyLimit = overage.IsHourly, From b160dc31444eb5949b1005c4b47899a55336b742 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 09:16:00 -0500 Subject: [PATCH 4/9] Removed messagebus broker --- Exceptionless.sln | 15 --- Source/Api/AppBuilder.cs | 32 ++++- Source/Core/Exceptionless.Core.csproj | 1 - Source/Core/Jobs/MessageBusBrokerJob.cs | 43 ------ .../MessageBusBrokerJob.csproj | 124 ------------------ Source/Jobs/MessageBusBroker/Program.cs | 19 --- .../Properties/AssemblyInfo.cs | 6 - Source/Jobs/MessageBusBroker/packages.config | 7 - 8 files changed, 25 insertions(+), 222 deletions(-) delete mode 100644 Source/Core/Jobs/MessageBusBrokerJob.cs delete mode 100644 Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj delete mode 100644 Source/Jobs/MessageBusBroker/Program.cs delete mode 100644 Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs delete mode 100644 Source/Jobs/MessageBusBroker/packages.config diff --git a/Exceptionless.sln b/Exceptionless.sln index 79abd03bdf..f0d8565258 100644 --- a/Exceptionless.sln +++ b/Exceptionless.sln @@ -77,8 +77,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloseInactiveSessionsJob", {75D9D89F-09F2-43A6-8B53-E1518E9FA822} = {75D9D89F-09F2-43A6-8B53-E1518E9FA822} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessageBusBrokerJob", "Source\Jobs\MessageBusBroker\MessageBusBrokerJob.csproj", "{FFA6E598-3820-4A89-B853-C9E60A5739AA}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -249,18 +247,6 @@ Global {FA2A4275-9180-43A4-AAE6-12BF83DC1252}.Release|Mixed Platforms.Build.0 = Release|Any CPU {FA2A4275-9180-43A4-AAE6-12BF83DC1252}.Release|x86.ActiveCfg = Release|Any CPU {FA2A4275-9180-43A4-AAE6-12BF83DC1252}.Release|x86.Build.0 = Release|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|x86.ActiveCfg = Debug|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Debug|x86.Build.0 = Debug|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Any CPU.Build.0 = Release|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|x86.ActiveCfg = Release|Any CPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -276,6 +262,5 @@ Global {BE383D44-18A2-4145-9D04-95F715D5E500} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} {1A8C938F-D7F4-4E2F-8789-9053156E9077} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} {FA2A4275-9180-43A4-AAE6-12BF83DC1252} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} - {FFA6E598-3820-4A89-B853-C9E60A5739AA} = {4F070E46-F0D4-4134-A04E-BD1CB3B4AEA3} EndGlobalSection EndGlobal diff --git a/Source/Api/AppBuilder.cs b/Source/Api/AppBuilder.cs index e5642f11ef..8a7b0e4b9e 100644 --- a/Source/Api/AppBuilder.cs +++ b/Source/Api/AppBuilder.cs @@ -15,11 +15,15 @@ using Exceptionless.Core; using Exceptionless.Core.Extensions; using Exceptionless.Core.Jobs; +using Exceptionless.Core.Messaging.Models; +using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Repositories; using Exceptionless.Core.Utility; using Exceptionless.Serializer; using Foundatio.Jobs; using Foundatio.Logging; +using Foundatio.Messaging; +using Foundatio.Queues; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; using Microsoft.AspNet.SignalR.Infrastructure; @@ -81,25 +85,39 @@ public static void Build(IAppBuilder app, Container container = null) { if (Settings.Current.WebsiteMode == WebsiteMode.Dev) Task.Run(async () => await CreateSampleDataAsync(container)); + + var context = new OwinContext(app.Properties); + var token = context.Get("host.OnAppDisposing"); + RunMessageBusBroker(container, logger, token); + RunJobs(container, loggerFactory, logger, token); - RunJobs(container, app, loggerFactory, logger); logger.Info("Starting api..."); } - - private static void RunJobs(Container container, IAppBuilder app, ILoggerFactory loggerFactory, ILogger logger) { + + private static void RunMessageBusBroker(Container container, ILogger logger, CancellationToken token = default(CancellationToken)) { + var workItemQueue = container.GetInstance>(); + var subscriber = container.GetInstance(); + + subscriber.Subscribe(async overage => { + logger.Info("Enqueueing plan overage work item for organization: {0} IsOverHourlyLimit: {1} IsOverMonthlyLimit: {2}", overage.OrganizationId, overage.IsHourly, !overage.IsHourly); + await workItemQueue.EnqueueAsync(new OrganizationNotificationWorkItem { + OrganizationId = overage.OrganizationId, + IsOverHourlyLimit = overage.IsHourly, + IsOverMonthlyLimit = !overage.IsHourly + }); + }, token); + } + + private static void RunJobs(Container container, LoggerFactory loggerFactory, ILogger logger, CancellationToken token = default(CancellationToken)) { if (!Settings.Current.RunJobsInProcess) { logger.Info("Jobs running out of process."); return; } - - var context = new OwinContext(app.Properties); - var token = context.Get("host.OnAppDisposing"); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(2)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(3)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(5)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(5)).RunInBackground(token); - new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(2)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(5)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromSeconds(30), interval: TimeSpan.FromMinutes(1)).RunInBackground(token); new JobRunner(container.GetInstance(), loggerFactory, initialDelay: TimeSpan.FromMinutes(1), interval: TimeSpan.FromHours(1)).RunInBackground(token); diff --git a/Source/Core/Exceptionless.Core.csproj b/Source/Core/Exceptionless.Core.csproj index 0597c4f441..b494291d9f 100644 --- a/Source/Core/Exceptionless.Core.csproj +++ b/Source/Core/Exceptionless.Core.csproj @@ -166,7 +166,6 @@ - diff --git a/Source/Core/Jobs/MessageBusBrokerJob.cs b/Source/Core/Jobs/MessageBusBrokerJob.cs deleted file mode 100644 index b90d86282f..0000000000 --- a/Source/Core/Jobs/MessageBusBrokerJob.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; -using Exceptionless.Core.Messaging.Models; -using Exceptionless.Core.Models.WorkItems; -using Foundatio.Jobs; -using Foundatio.Logging; -using Foundatio.Messaging; -using Foundatio.Queues; - -namespace Exceptionless.Core.Jobs { - public class MessageBusBrokerJob : JobBase { - private readonly IQueue _workItemQueue; - private readonly IMessageSubscriber _subscriber; - - public MessageBusBrokerJob(IQueue workItemQueue, IMessageSubscriber subscriber, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _workItemQueue = workItemQueue; - _subscriber = subscriber; - } - - protected override async Task RunInternalAsync(JobContext context) { - _subscriber.Subscribe(OnPlanOverageAsync, context.CancellationToken); - - while (!context.CancellationToken.IsCancellationRequested) - await Task.Delay(TimeSpan.FromSeconds(5)); - - return JobResult.Success; - } - - private async Task OnPlanOverageAsync(PlanOverage overage, CancellationToken cancellationToken = default(CancellationToken)) { - if (overage == null) - return; - - _logger.Info("Enqueueing plan overage work item for organization: {0} IsOverHourlyLimit: {1} IsOverMonthlyLimit: {2}", overage.OrganizationId, overage.IsHourly, !overage.IsHourly); - await _workItemQueue.EnqueueAsync(new OrganizationNotificationWorkItem { - OrganizationId = overage.OrganizationId, - IsOverHourlyLimit = overage.IsHourly, - IsOverMonthlyLimit = !overage.IsHourly - }).AnyContext(); - } - } -} diff --git a/Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj b/Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj deleted file mode 100644 index 28426813dd..0000000000 --- a/Source/Jobs/MessageBusBroker/MessageBusBrokerJob.csproj +++ /dev/null @@ -1,124 +0,0 @@ - - - - - Debug - AnyCPU - {FFA6E598-3820-4A89-B853-C9E60A5739AA} - Exe - Properties - MessageBusBrokerJob - MessageBusBrokerJob - v4.6.1 - 512 - true - - - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\..\..\packages\Foundatio.4.0.836\lib\net451\Foundatio.dll - True - - - ..\..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True - - - ..\..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.dll - True - - - ..\..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Concurrent.dll - True - - - ..\..\..\packages\Nito.AsyncEx.3.0.1\lib\net45\Nito.AsyncEx.Enlightenment.dll - True - - - - - - ..\..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll - True - - - ..\..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll - True - - - - - - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - App.config - - - NLog.config - Always - - - run.bat - Always - - - - - - {3E5B39D5-7ACD-486B-9F90-59116B67952D} - Exceptionless.Core - - - - - @echo off - -robocopy $(SolutionDir)Source\Insulation\bin\$(ConfigurationName) $(TargetDir)bin\ /S /NFL /NDL /NJH /NJS /nc /ns /np -IF %25ERRORLEVEL%25 GEQ 8 exit 1 - -robocopy $(TargetDir) $(TargetDir)bin\ /MOV /XD bin /XF MessageBusBrokerJob.* run.bat NLog.config /S /is /NFL /NDL /NJH /NJS /nc /ns /np -IF %25ERRORLEVEL%25 GEQ 8 exit 1 - -exit 0 - - - \ No newline at end of file diff --git a/Source/Jobs/MessageBusBroker/Program.cs b/Source/Jobs/MessageBusBroker/Program.cs deleted file mode 100644 index 323cae498e..0000000000 --- a/Source/Jobs/MessageBusBroker/Program.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Exceptionless.Core; -using Exceptionless.Core.Extensions; -using Foundatio.Extensions; -using Foundatio.Jobs; -using Foundatio.ServiceProviders; - -namespace MessageBusBrokerJob { - public class Program { - public static int Main() { - AppDomain.CurrentDomain.SetDataDirectory(); - - var loggerFactory = Settings.Current.GetLoggerFactory(); - var serviceProvider = ServiceProvider.GetServiceProvider(Settings.JobBootstrappedServiceProvider, loggerFactory); - var job = serviceProvider.GetService(); - return new JobRunner(job, loggerFactory, initialDelay: TimeSpan.FromSeconds(2), interval: TimeSpan.Zero).RunInConsole(); - } - } -} diff --git a/Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs b/Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs deleted file mode 100644 index 92e63df26a..0000000000 --- a/Source/Jobs/MessageBusBroker/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("MessageBusBrokerJob")] -[assembly: ComVisible(false)] -[assembly: Guid("ED3C69A7-DB35-4582-9AAF-66040BE3C975")] \ No newline at end of file diff --git a/Source/Jobs/MessageBusBroker/packages.config b/Source/Jobs/MessageBusBroker/packages.config deleted file mode 100644 index 006883e8f9..0000000000 --- a/Source/Jobs/MessageBusBroker/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file From 921fdeacd8fb59c74dd0d0bffbba851819852014 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 09:25:52 -0500 Subject: [PATCH 5/9] Updated Tests: Ensure the published message has time to be delivered --- Source/Tests/Repositories/OrganizationRepositoryTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Tests/Repositories/OrganizationRepositoryTests.cs b/Source/Tests/Repositories/OrganizationRepositoryTests.cs index 8a6b87b0bc..0353b3cdf3 100644 --- a/Source/Tests/Repositories/OrganizationRepositoryTests.cs +++ b/Source/Tests/Repositories/OrganizationRepositoryTests.cs @@ -121,7 +121,7 @@ public async Task CanIncrementUsageAsync() { var messagePublisher = IoC.GetInstance() as InMemoryMessageBus; Assert.NotNull(messagePublisher); messagePublisher.Subscribe(po => { - _logger.Info($"Plan Overage for {po.OrganizationId} (Hourly: {po.IsHourly}"); + _logger.Info($"Plan Overage for {po.OrganizationId} (Hourly: {po.IsHourly})"); messages.Add(po); }); @@ -136,6 +136,7 @@ public async Task CanIncrementUsageAsync() { Assert.Equal(0, await cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); Assert.True(await _repository.IncrementUsageAsync(o.Id, false, 3)); + await Task.Delay(5); Assert.Equal(1, messages.Count); Assert.Equal(12, await cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); Assert.Equal(12, await cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); @@ -146,7 +147,8 @@ public async Task CanIncrementUsageAsync() { await _client.RefreshAsync(); Assert.True(await _repository.IncrementUsageAsync(o.Id, false, 751)); - //Assert.Equal(2, messages.Count); + await Task.Delay(5); + Assert.Equal(2, messages.Count); Assert.Equal(751, await cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); Assert.Equal(751, await cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); Assert.Equal(740, await cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); From 1e1ac6da1639ef000cd66b7509cc988451af3106 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 10:32:50 -0500 Subject: [PATCH 6/9] Updated stats --- Source/Core/Jobs/EventNotificationsJob.cs | 2 +- Source/Core/Mail/IMailer.cs | 2 +- Source/Core/Mail/Mailer.cs | 56 ++++++++++------------- Source/Tests/Mail/MailerTests.cs | 8 ++-- Source/Tests/Mail/NullMailer.cs | 2 +- 5 files changed, 32 insertions(+), 38 deletions(-) diff --git a/Source/Core/Jobs/EventNotificationsJob.cs b/Source/Core/Jobs/EventNotificationsJob.cs index d916efb0d4..167e1a3a99 100644 --- a/Source/Core/Jobs/EventNotificationsJob.cs +++ b/Source/Core/Jobs/EventNotificationsJob.cs @@ -173,7 +173,7 @@ protected override async Task ProcessQueueEntryAsync(QueueEntryContex } _logger.Trace("Sending email to {0}...", user.EmailAddress); - await _mailer.SendNoticeAsync(user.EmailAddress, model).AnyContext(); + await _mailer.SendEventNoticeAsync(user.EmailAddress, model).AnyContext(); emailsSent++; _logger.Trace().Message("Done sending email.").WriteIf(shouldLog); } diff --git a/Source/Core/Mail/IMailer.cs b/Source/Core/Mail/IMailer.cs index 6d3ca9c887..aee740b3eb 100644 --- a/Source/Core/Mail/IMailer.cs +++ b/Source/Core/Mail/IMailer.cs @@ -11,7 +11,7 @@ public interface IMailer { Task SendInviteAsync(User sender, Organization organization, Invite invite); Task SendPaymentFailedAsync(User owner, Organization organization); Task SendAddedToOrganizationAsync(User sender, Organization organization, User user); - Task SendNoticeAsync(string emailAddress, EventNotification model); + Task SendEventNoticeAsync(string emailAddress, EventNotification model); Task SendOrganizationNoticeAsync(string emailAddress, OrganizationNotificationModel organizationNotificationModel); Task SendDailySummaryAsync(string emailAddress, DailySummaryModel notification); } diff --git a/Source/Core/Mail/Mailer.cs b/Source/Core/Mail/Mailer.cs index fe80ae695a..a0b12b6a73 100644 --- a/Source/Core/Mail/Mailer.cs +++ b/Source/Core/Mail/Mailer.cs @@ -29,9 +29,9 @@ public Mailer(IEmailGenerator emailGenerator, IQueue queue, Formatt _logger = logger; } - public async Task SendPasswordResetAsync(User user) { + public Task SendPasswordResetAsync(User user) { if (String.IsNullOrEmpty(user?.PasswordResetToken)) - return; + return Task.CompletedTask; System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(new UserModel { User = user, @@ -39,22 +39,20 @@ public async Task SendPasswordResetAsync(User user) { }, "PasswordReset"); msg.To.Add(user.EmailAddress); - await _metrics.CounterAsync("mailer.passwordreset").AnyContext(); - await QueueMessageAsync(msg).AnyContext(); + return QueueMessageAsync(msg, "passwordreset"); } - public async Task SendVerifyEmailAsync(User user) { + public Task SendVerifyEmailAsync(User user) { System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(new UserModel { User = user, BaseUrl = Settings.Current.BaseURL }, "VerifyEmail"); msg.To.Add(user.EmailAddress); - await _metrics.CounterAsync("mailer.verifyemail").AnyContext(); - await QueueMessageAsync(msg).AnyContext(); + return QueueMessageAsync(msg, "verifyemail"); } - public async Task SendInviteAsync(User sender, Organization organization, Invite invite) { + public Task SendInviteAsync(User sender, Organization organization, Invite invite) { System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(new InviteModel { Sender = sender, Organization = organization, @@ -62,24 +60,22 @@ public async Task SendInviteAsync(User sender, Organization organization, Invite BaseUrl = Settings.Current.BaseURL }, "Invite"); msg.To.Add(invite.EmailAddress); - - await _metrics.CounterAsync("mailer.invite").AnyContext(); - await QueueMessageAsync(msg).AnyContext(); + + return QueueMessageAsync(msg, "invite"); } - public async Task SendPaymentFailedAsync(User owner, Organization organization) { + public Task SendPaymentFailedAsync(User owner, Organization organization) { System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(new PaymentModel { Owner = owner, Organization = organization, BaseUrl = Settings.Current.BaseURL }, "PaymentFailed"); msg.To.Add(owner.EmailAddress); - - await _metrics.CounterAsync("mailer.paymentfailed").AnyContext(); - await QueueMessageAsync(msg).AnyContext(); + + return QueueMessageAsync(msg, "paymentfailed"); } - public async Task SendAddedToOrganizationAsync(User sender, Organization organization, User user) { + public Task SendAddedToOrganizationAsync(User sender, Organization organization, User user) { System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(new AddedToOrganizationModel { Sender = sender, Organization = organization, @@ -87,22 +83,19 @@ public async Task SendAddedToOrganizationAsync(User sender, Organization organiz BaseUrl = Settings.Current.BaseURL }, "AddedToOrganization"); msg.To.Add(user.EmailAddress); - - await _metrics.CounterAsync("mailer.addedtoorganization").AnyContext(); - await QueueMessageAsync(msg).AnyContext(); + + return QueueMessageAsync(msg, "addedtoorganization"); } - public async Task SendNoticeAsync(string emailAddress, EventNotification model) { + public Task SendEventNoticeAsync(string emailAddress, EventNotification model) { var msg = _pluginManager.GetEventNotificationMailMessage(model); if (msg == null) { _logger.Warn("Unable to create event notification mail message for event \"{0}\". User: \"{1}\"", model.EventId, emailAddress); - return; + return Task.CompletedTask; } msg.To = emailAddress; - - await _metrics.CounterAsync("mailer.eventnotification").AnyContext(); - return QueueMessageAsync(message.ToMailMessage()); + return QueueMessageAsync(msg.ToMailMessage(), "eventnotice"); } public Task SendOrganizationNoticeAsync(string emailAddress, OrganizationNotificationModel model) { @@ -111,21 +104,22 @@ public Task SendOrganizationNoticeAsync(string emailAddress, OrganizationNotific System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(model, "OrganizationNotice"); msg.To.Add(emailAddress); - return QueueMessageAsync(msg); + return QueueMessageAsync(msg, "organizationnotice"); } - public async Task SendDailySummaryAsync(string emailAddress, DailySummaryModel notification) { + public Task SendDailySummaryAsync(string emailAddress, DailySummaryModel notification) { notification.BaseUrl = Settings.Current.BaseURL; System.Net.Mail.MailMessage msg = _emailGenerator.GenerateMessage(notification, "DailySummary"); msg.To.Add(emailAddress); - - await _metrics.CounterAsync("mailer.dailysummary").AnyContext(); - await QueueMessageAsync(msg).AnyContext(); + + return QueueMessageAsync(msg, "dailysummary"); } - private Task QueueMessageAsync(System.Net.Mail.MailMessage message) { + private async Task QueueMessageAsync(System.Net.Mail.MailMessage message, string metricsName) { + await _metrics.CounterAsync($"mailer.{metricsName}").AnyContext(); + CleanAddresses(message); - return _queue.EnqueueAsync(message.ToMailMessage()); + await _queue.EnqueueAsync(message.ToMailMessage()).AnyContext(); } private static void CleanAddresses(System.Net.Mail.MailMessage message) { diff --git a/Source/Tests/Mail/MailerTests.cs b/Source/Tests/Mail/MailerTests.cs index 4136737e96..df1a8237f4 100644 --- a/Source/Tests/Mail/MailerTests.cs +++ b/Source/Tests/Mail/MailerTests.cs @@ -17,7 +17,7 @@ public class MailerTests { [Fact(Skip = "Used for testing html formatting.")] public Task SendLogNotificationAsync() { var mailer = IoC.GetInstance(); - return mailer.SendNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { Event = new PersistentEvent { Id = "1", OrganizationId = "1", @@ -37,7 +37,7 @@ public Task SendLogNotificationAsync() { [Fact(Skip = "Used for testing html formatting.")] public Task SendNotFoundNotificationAsync() { var mailer = IoC.GetInstance(); - return mailer.SendNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { Event = new PersistentEvent { Id = "1", OrganizationId = "1", @@ -72,7 +72,7 @@ public Task SendSimpleErrorNotificationAsync() { ev.StackId = "1"; var mailer = IoC.GetInstance(); - return mailer.SendNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { Event = ev, IsNew = true, IsCritical = true, @@ -103,7 +103,7 @@ public Task SendErrorNotificationAsync() { ev.StackId = "1"; var mailer = IoC.GetInstance(); - return mailer.SendNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { Event = ev, IsNew = true, IsCritical = true, diff --git a/Source/Tests/Mail/NullMailer.cs b/Source/Tests/Mail/NullMailer.cs index e3aaa7a8dc..d612278029 100644 --- a/Source/Tests/Mail/NullMailer.cs +++ b/Source/Tests/Mail/NullMailer.cs @@ -27,7 +27,7 @@ public Task SendAddedToOrganizationAsync(User sender, Organization organization, return Task.CompletedTask; } - public Task SendNoticeAsync(string emailAddress, EventNotification model) { + public Task SendEventNoticeAsync(string emailAddress, EventNotification model) { return Task.CompletedTask; } From 8f4e4afbc92c8d935ba4639ea83769ff67032123 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 12:00:26 -0500 Subject: [PATCH 7/9] Improved the mailer unit tests --- Source/Core/Mail/InMemoryMailSender.cs | 2 +- .../Templates/OrganizationNotice/Html.cshtml | 8 +- .../OrganizationNotice/PlainText.cshtml | 8 +- .../OrganizationNotice/Subject.cshtml | 8 +- Source/Tests/Mail/MailerTests.cs | 178 ++++++++++-------- 5 files changed, 114 insertions(+), 90 deletions(-) diff --git a/Source/Core/Mail/InMemoryMailSender.cs b/Source/Core/Mail/InMemoryMailSender.cs index a16ddeffb0..9c1381f8c3 100644 --- a/Source/Core/Mail/InMemoryMailSender.cs +++ b/Source/Core/Mail/InMemoryMailSender.cs @@ -17,7 +17,7 @@ public InMemoryMailSender(int messagesToStore = 25) { public long TotalSent => _totalSent; public List SentMessages => _recentMessages.ToList(); - public MailMessage LastMessage => SentMessages.Last(); + public MailMessage LastMessage => SentMessages.LastOrDefault(); public Task SendAsync(MailMessage model) { _recentMessages.Enqueue(model); diff --git a/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml index afe12e7ce5..50c61cd1e9 100644 --- a/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml +++ b/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml @@ -13,13 +13,13 @@

- @if (Model.IsOverHourlyLimit) { + @if (Model.IsOverMonthlyLimit) { - Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. + @Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. - } else if (Model.IsOverMonthlyLimit) { + } else if (Model.IsOverHourlyLimit) { - @Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. + Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. }

diff --git a/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml index c87291d580..ff1596319d 100644 --- a/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml +++ b/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml @@ -1,12 +1,12 @@ @using System @inherits RazorSharpEmail.EmailTemplate -@if (Model.IsOverHourlyLimit) { +@if (Model.IsOverMonthlyLimit) { -Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. +@Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. -} else if (Model.IsOverMonthlyLimit) { +} else if (Model.IsOverHourlyLimit) { -@Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. +Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. } diff --git a/Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml index fc1c30b44f..02fdf767f3 100644 --- a/Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml +++ b/Source/Core/Mail/Templates/OrganizationNotice/Subject.cshtml @@ -1,10 +1,10 @@ @inherits RazorSharpEmail.EmailTemplate -@if (Model.IsOverHourlyLimit) { +@if (Model.IsOverMonthlyLimit) { -[@Model.Organization.Name] Events are currently being throttled. +[@Model.Organization.Name] Monthly plan limit exceeded. -} else if (Model.aIsOverMonthlyLimit) { +} else if (Model.IsOverHourlyLimit) { -[@Model.Organization.Name] The monthly plan limit exceeded. +[@Model.Organization.Name] Events are currently being throttled. } \ No newline at end of file diff --git a/Source/Tests/Mail/MailerTests.cs b/Source/Tests/Mail/MailerTests.cs index df1a8237f4..df41d78eb9 100644 --- a/Source/Tests/Mail/MailerTests.cs +++ b/Source/Tests/Mail/MailerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Exceptionless.Api.Tests.Utility; using Exceptionless.Core; @@ -9,15 +8,31 @@ using Exceptionless.Core.Queues.Models; using Exceptionless.DateTimeExtensions; using Exceptionless.Core.Models; +using Exceptionless.Core.Plugins.Formatting; using Exceptionless.Tests.Utility; +using Foundatio.Logging; +using Foundatio.Logging.Xunit; +using Foundatio.Metrics; +using Foundatio.Queues; +using RazorSharpEmail; using Xunit; +using Xunit.Abstractions; namespace Exceptionless.Api.Tests.Mail { - public class MailerTests { + public class MailerTests : TestWithLoggingBase { + private readonly IMailer _mailer; + private readonly InMemoryMailSender _mailSender = IoC.GetInstance() as InMemoryMailSender; + private readonly MailMessageJob _mailJob = IoC.GetInstance(); + + public MailerTests(ITestOutputHelper output) : base(output) { + _mailer = IoC.GetInstance(); + if (_mailer is NullMailer) + _mailer = new Mailer(IoC.GetInstance(), IoC.GetInstance>(), IoC.GetInstance(), IoC.GetInstance(), Log.CreateLogger()); + } + [Fact(Skip = "Used for testing html formatting.")] - public Task SendLogNotificationAsync() { - var mailer = IoC.GetInstance(); - return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + public async Task SendLogNotificationAsync() { + await _mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { Event = new PersistentEvent { Id = "1", OrganizationId = "1", @@ -32,12 +47,13 @@ public Task SendLogNotificationAsync() { TotalOccurrences = 1, ProjectName = "Testing" }); + + await RunMailJobAsync(); } [Fact(Skip = "Used for testing html formatting.")] - public Task SendNotFoundNotificationAsync() { - var mailer = IoC.GetInstance(); - return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + public async Task SendNotFoundNotificationAsync() { + await _mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { Event = new PersistentEvent { Id = "1", OrganizationId = "1", @@ -52,114 +68,111 @@ public Task SendNotFoundNotificationAsync() { TotalOccurrences = 1, ProjectName = "Testing" }); + + await RunMailJobAsync(); + } + + [Fact(Skip = "Used for testing html formatting.")] + public async Task SendOrganizationHourlyOverageNotificationAsync() { + await _mailer.SendOrganizationNoticeAsync(Settings.Current.TestEmailAddress, new OrganizationNotificationModel { + Organization = OrganizationData.GenerateSampleOrganization(), + IsOverHourlyLimit = true + }); + + await RunMailJobAsync(); } [Fact(Skip = "Used for testing html formatting.")] - public Task SendSimpleErrorNotificationAsync() { - PersistentEvent ev = null; - //var client = new ExceptionlessClient("123456789"); - //try { - // throw new Exception("Happy days are here again..."); - //} catch (Exception ex) { - // var builder = ex.ToExceptionless(client: client); - // EventEnrichmentManager.Enrich(new EventEnrichmentContext(client, builder.EnrichmentContextData), builder.Target); - // ev = Mapper.Map(builder.Target); - //} - - ev.Id = "1"; - ev.OrganizationId = "1"; - ev.ProjectId = "1"; - ev.StackId = "1"; - - var mailer = IoC.GetInstance(); - return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { - Event = ev, + public async Task SendOrganizationMonthlyOverageNotificationAsync() { + await _mailer.SendOrganizationNoticeAsync(Settings.Current.TestEmailAddress, new OrganizationNotificationModel { + Organization = OrganizationData.GenerateSampleOrganization(), + IsOverMonthlyLimit = true + }); + + await RunMailJobAsync(); + } + + [Fact(Skip = "Used for testing html formatting.")] + public async Task SendSimpleErrorNotificationAsync() { + await _mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + Event = new PersistentEvent { + Id = "1", + OrganizationId = "1", + ProjectId = "1", + StackId = "1" + }, IsNew = true, IsCritical = true, IsRegression = false, TotalOccurrences = 1, ProjectName = "Testing" }); + + await RunMailJobAsync(); } [Fact(Skip = "Used for testing html formatting.")] - public Task SendErrorNotificationAsync() { - PersistentEvent ev = null; - //var client = new ExceptionlessClient(c => { - // c.ApiKey = "123456789"; - // c.UseErrorEnrichment(); - //}); - //try { - // throw new Exception("Happy days are here again..."); - //} catch (Exception ex) { - // var builder = ex.ToExceptionless(client: client); - // EventEnrichmentManager.Enrich(new EventEnrichmentContext(client, builder.EnrichmentContextData), builder.Target); - // ev = Mapper.Map(builder.Target); - //} - - ev.Id = "1"; - ev.OrganizationId = "1"; - ev.ProjectId = "1"; - ev.StackId = "1"; - - var mailer = IoC.GetInstance(); - return mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { - Event = ev, + public async Task SendErrorNotificationAsync() { + await _mailer.SendEventNoticeAsync(Settings.Current.TestEmailAddress, new EventNotification { + Event = new PersistentEvent { + Id = "1", + OrganizationId = "1", + ProjectId = "1", + StackId = "1" + }, IsNew = true, IsCritical = true, IsRegression = false, TotalOccurrences = 1, ProjectName = "Testing" }); - } - [Fact] + await RunMailJobAsync(); + } + + [Fact(Skip = "Used for testing html formatting.")] public async Task SendInviteAsync() { - var mailer = IoC.GetInstance(); - var mailerSender = IoC.GetInstance() as InMemoryMailSender; - var mailJob = IoC.GetInstance(); - Assert.NotNull(mailerSender); - User user = UserData.GenerateSampleUser(); Organization organization = OrganizationData.GenerateSampleOrganization(); - await mailer.SendInviteAsync(user, organization, new Invite { + await _mailer.SendInviteAsync(user, organization, new Invite { DateAdded = DateTime.Now, EmailAddress = Settings.Current.TestEmailAddress, Token = "1" }); - await mailJob.RunAsync(); - - Assert.Equal(1, mailerSender.TotalSent); - Assert.Equal(Settings.Current.TestEmailAddress, mailerSender.LastMessage.To); - Assert.Contains("Join Organization", mailerSender.LastMessage.HtmlBody); + + await RunMailJobAsync(); + if (_mailSender != null) { + Assert.Equal(Settings.Current.TestEmailAddress, _mailSender.LastMessage.To); + Assert.Contains("Join Organization", _mailSender.LastMessage.HtmlBody); + } } [Fact(Skip = "Used for testing html formatting.")] - public Task SendAddedToOrganizationAsync() { - var mailer = IoC.GetInstance(); + public async Task SendAddedToOrganizationAsync() { User user = UserData.GenerateSampleUser(); Organization organization = OrganizationData.GenerateSampleOrganization(); - return mailer.SendAddedToOrganizationAsync(user, organization, user); + + await _mailer.SendAddedToOrganizationAsync(user, organization, user); + await RunMailJobAsync(); } [Fact(Skip = "Used for testing html formatting.")] - public Task SendPasswordResetAsync() { - var mailer = IoC.GetInstance(); + public async Task SendPasswordResetAsync() { User user = UserData.GenerateSampleUser(); - return mailer.SendPasswordResetAsync(user); + await _mailer.SendPasswordResetAsync(user); + await RunMailJobAsync(); } [Fact(Skip = "Used for testing html formatting.")] - public Task SendVerifyEmailAsync() { - var mailer = IoC.GetInstance(); + public async Task SendVerifyEmailAsync() { User user = UserData.GenerateSampleUser(); - return mailer.SendVerifyEmailAsync(user); + await _mailer.SendVerifyEmailAsync(user); + await RunMailJobAsync(); } [Fact(Skip = "Used for testing html formatting.")] - public Task SendSummaryNotificationAsync() { - var mailer = IoC.GetInstance(); - return mailer.SendDailySummaryAsync(Settings.Current.TestEmailAddress, new DailySummaryModel { + public async Task SendSummaryNotificationAsync() { + await _mailer.SendDailySummaryAsync(Settings.Current.TestEmailAddress, new DailySummaryModel { ProjectId = "1", BaseUrl = "http://be.exceptionless.io", StartDate = DateTime.Now.Date, @@ -173,14 +186,25 @@ public Task SendSummaryNotificationAsync() { HasSubmittedEvents = true, IsFreePlan = false }); + await RunMailJobAsync(); } [Fact(Skip = "Used for testing html formatting.")] - public Task SendPaymentFailedAsync() { - var mailer = IoC.GetInstance(); + public async Task SendPaymentFailedAsync() { User user = UserData.GenerateSampleUser(); Organization organization = OrganizationData.GenerateSampleOrganization(); - return mailer.SendPaymentFailedAsync(user, organization); + await _mailer.SendPaymentFailedAsync(user, organization); + await RunMailJobAsync(); + } + + private async Task RunMailJobAsync() { + await _mailJob.RunAsync(); + if (_mailSender == null) + return; + + _logger.Info($"To: {_mailSender.LastMessage.To}"); + _logger.Info($"Subject: {_mailSender.LastMessage.Subject}"); + _logger.Info($"TextBody:\n{_mailSender.LastMessage.TextBody}"); } } } \ No newline at end of file From 6600440c20a0013bc468d2b9ee10aa662153fe5f Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 13:42:11 -0500 Subject: [PATCH 8/9] Updated the text on the notification emails --- .../Core/Mail/Templates/OrganizationNotice/Html.cshtml | 7 ++++--- .../Mail/Templates/OrganizationNotice/PlainText.cshtml | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml index 50c61cd1e9..3d1b2f9b67 100644 --- a/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml +++ b/Source/Core/Mail/Templates/OrganizationNotice/Html.cshtml @@ -15,23 +15,24 @@

@if (Model.IsOverMonthlyLimit) { - @Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. + @Model.Organization.Name has reached its monthly plan limit. } else if (Model.IsOverHourlyLimit) { - Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. + Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. }

- Upgrade plan or view plan usage + View most frequent events

diff --git a/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml b/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml index ff1596319d..d245450c69 100644 --- a/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml +++ b/Source/Core/Mail/Templates/OrganizationNotice/PlainText.cshtml @@ -2,13 +2,16 @@ @inherits RazorSharpEmail.EmailTemplate @if (Model.IsOverMonthlyLimit) { -@Model.Organization.Name has reached its monthly plan limit. Upgrade now to continue receiving events. +@Model.Organization.Name has reached its monthly plan limit. } else if (Model.IsOverHourlyLimit) { -Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. Upgrade now to increase your limits. +Events are currently being throttled for @Model.Organization.Name to prevent using up your plan limit in a small window of time. } -Upgrade plan or view plan usage: +View usage: @Model.BaseUrl/organization/@Model.Organization.Id/manage + +View most frequent events: +@Model.BaseUrl/organization/@Model.Organization.Id/frequent \ No newline at end of file From d90aebaf37052efdddeda79e46d389ab13396b4c Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 23 Mar 2016 13:50:46 -0500 Subject: [PATCH 9/9] Fixed a bug where the overage handler wasn't incrementing usage count. --- Source/Api/Utility/Handlers/OverageHandler.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Api/Utility/Handlers/OverageHandler.cs b/Source/Api/Utility/Handlers/OverageHandler.cs index 1f8874958b..f23f6f17da 100644 --- a/Source/Api/Utility/Handlers/OverageHandler.cs +++ b/Source/Api/Utility/Handlers/OverageHandler.cs @@ -23,6 +23,9 @@ public OverageHandler(IOrganizationRepository organizationRepository, IMetricsCl } private bool IsEventPost(HttpRequestMessage request) { + if (request.Method == HttpMethod.Get) + return request.RequestUri.AbsolutePath.Contains("/events/submit"); + if (request.Method != HttpMethod.Post) return false;