Skip to content

Commit

Permalink
Merge pull request #204 from exceptionless/feature/notifications
Browse files Browse the repository at this point in the history
Added the ability to send organization notifications
  • Loading branch information
niemyjski committed Mar 23, 2016
2 parents 0e247d5 + d90aeba commit 356e179
Show file tree
Hide file tree
Showing 18 changed files with 374 additions and 140 deletions.
39 changes: 29 additions & 10 deletions Source/Api/AppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,19 +85,34 @@ 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<CancellationToken>("host.OnAppDisposing");
RunMessageBusBroker(container, logger, token);
RunJobs(container, loggerFactory, logger, token);

logger.Info("Starting api...");
}

RunJobs(container, app, loggerFactory, logger);
logger.Info().Message("Starting api...").Write();
private static void RunMessageBusBroker(Container container, ILogger logger, CancellationToken token = default(CancellationToken)) {
var workItemQueue = container.GetInstance<IQueue<WorkItemData>>();
var subscriber = container.GetInstance<IMessageSubscriber>();

subscriber.Subscribe<PlanOverage>(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, IAppBuilder app, ILoggerFactory loggerFactory, ILogger logger) {

private static void RunJobs(Container container, LoggerFactory loggerFactory, ILogger logger, CancellationToken token = default(CancellationToken)) {
if (!Settings.Current.RunJobsInProcess) {
logger.Info().Message("Jobs running out of process.").Write();
logger.Info("Jobs running out of process.");
return;
}

var context = new OwinContext(app.Properties);
var token = context.Get<CancellationToken>("host.OnAppDisposing");

new JobRunner(container.GetInstance<EventPostsJob>(), loggerFactory, initialDelay: TimeSpan.FromSeconds(2)).RunInBackground(token);
new JobRunner(container.GetInstance<EventUserDescriptionsJob>(), loggerFactory, initialDelay: TimeSpan.FromSeconds(3)).RunInBackground(token);
Expand All @@ -106,7 +125,7 @@ private static void RunJobs(Container container, IAppBuilder app, ILoggerFactory
new JobRunner(container.GetInstance<RetentionLimitsJob>(), loggerFactory, initialDelay: TimeSpan.FromMinutes(15), interval: TimeSpan.FromDays(1)).RunInBackground(token);
new JobRunner(container.GetInstance<WorkItemJob>(), 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) {
Expand Down Expand Up @@ -201,7 +220,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) {
Expand Down
3 changes: 3 additions & 0 deletions Source/Api/Utility/Handlers/OverageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 1 addition & 2 deletions Source/Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -110,6 +108,7 @@ public static void RegisterServices(Container container, ILoggerFactory loggerFa
workItemHandlers.Register<StackWorkItem>(container.GetInstance<StackWorkItemHandler>);
workItemHandlers.Register<ThrottleBotsWorkItem>(container.GetInstance<ThrottleBotsWorkItemHandler>);
workItemHandlers.Register<OrganizationMaintenanceWorkItem>(container.GetInstance<OrganizationMaintenanceWorkItemHandler>);
workItemHandlers.Register<OrganizationNotificationWorkItem>(container.GetInstance<OrganizationNotificationWorkItemHandler>);
workItemHandlers.Register<ProjectMaintenanceWorkItem>(container.GetInstance<ProjectMaintenanceWorkItemHandler>);
container.RegisterSingleton<WorkItemHandlers>(workItemHandlers);
container.RegisterSingleton<IQueue<WorkItemData>>(() => new InMemoryQueue<WorkItemData>(behaviors: container.GetAllInstances<IQueueBehavior<WorkItemData>>(), workItemTimeout: TimeSpan.FromHours(1)));
Expand Down
12 changes: 12 additions & 0 deletions Source/Core/Exceptionless.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
<Compile Include="Geo\NullGeocodeService.cs" />
<Compile Include="Jobs\CloseInactiveSessionsJob.cs" />
<Compile Include="Jobs\DownloadGeoIPDatabaseJob.cs" />
<Compile Include="Jobs\WorkItemHandlers\OrganizationNotificationWorkItemHandler.cs" />
<Compile Include="Jobs\WorkItemHandlers\SetProjectIsConfiguredWorkItemHandler.cs" />
<Compile Include="Jobs\WorkItemHandlers\RemoveProjectWorkItemHandler.cs" />
<Compile Include="Jobs\WorkItemHandlers\RemoveOrganizationWorkItemHandler.cs" />
Expand All @@ -182,6 +183,7 @@
<Compile Include="Mail\Engine\RazorEmailGenerator.cs" />
<Compile Include="Mail\Engine\StringExtensions.cs" />
<Compile Include="Mail\Engine\TemplatedEmail.cs" />
<Compile Include="Mail\Models\OrganizationNotificationModel.cs" />
<Compile Include="Models\Application.cs" />
<Compile Include="Models\Billing\ChangePlanResult.cs" />
<Compile Include="Models\ClientConfiguration.cs" />
Expand Down Expand Up @@ -241,6 +243,7 @@
<Compile Include="Models\Token.cs" />
<Compile Include="Models\User.cs" />
<Compile Include="Models\WebHook.cs" />
<Compile Include="Models\WorkItems\OrganizationNotificationWorkItem.cs" />
<Compile Include="Models\WorkItems\RemoveOrganizationWorkItem.cs" />
<Compile Include="Models\WorkItems\RemoveProjectWorkItem.cs" />
<Compile Include="Models\WorkItems\SetLocationFromGeoWorkItem.cs" />
Expand Down Expand Up @@ -469,6 +472,15 @@
<Compile Include="Repositories\OrganizationRepository.cs" />
<Compile Include="Repositories\ProjectRepository.cs" />
<Compile Include="Repositories\UserRepository.cs" />
<Content Include="Mail\Templates\OrganizationNotice\Html.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Mail\Templates\OrganizationNotice\PlainText.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Include="Mail\Templates\OrganizationNotice\Subject.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/Jobs/EventNotificationsJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ protected override async Task<JobResult> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ILock> 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<OrganizationNotificationWorkItem>();
_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.");
}
}
}
3 changes: 2 additions & 1 deletion Source/Core/Mail/IMailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ 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);
}
}
2 changes: 1 addition & 1 deletion Source/Core/Mail/InMemoryMailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public InMemoryMailSender(int messagesToStore = 25) {

public long TotalSent => _totalSent;
public List<MailMessage> SentMessages => _recentMessages.ToList();
public MailMessage LastMessage => SentMessages.Last();
public MailMessage LastMessage => SentMessages.LastOrDefault();

public Task SendAsync(MailMessage model) {
_recentMessages.Enqueue(model);
Expand Down
Loading

0 comments on commit 356e179

Please sign in to comment.