Skip to content

Commit f1e31c7

Browse files
authored
Merge pull request #248 from exceptionless/feature/repos
Updates to the new Foundatio.Repositories
2 parents 41d2d06 + 9205b80 commit f1e31c7

File tree

216 files changed

+3616
-6327
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

216 files changed

+3616
-6327
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,5 @@ App_Data
166166

167167
*.DS_Store
168168
*.orig
169-
Source/Api/Logs/log.txt
169+
Source/Api/Logs/*.txt
170+
*.nugetreferenceswitcher

Exceptionless.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 14
4-
VisualStudioVersion = 14.0.25123.0
4+
VisualStudioVersion = 14.0.25420.1
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exceptionless.Core", "Source\Core\Exceptionless.Core.csproj", "{3E5B39D5-7ACD-486B-9F90-59116B67952D}"
77
EndProject

Libraries/Create-Release.ps1

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ $apiConfig = [xml](Get-Content $webConfig)
5454
$apiConfig.SelectSingleNode('//appSettings/add[@key="BaseURL"]/@value').'#text' = 'http://localhost:50000/#'
5555
$apiConfig.SelectSingleNode('//appSettings/add[@key="EnableDailySummary"]/@value').'#text' = 'true'
5656
$apiConfig.SelectSingleNode('//system.web/compilation/@debug').'#text' = 'false'
57-
$apiConfig.SelectSingleNode('//system.web/customErrors/@mode').'#text' = 'RemoteOnly'
5857

5958
# Copy settings from app web.config
6059
$appConfig = [xml](Get-Content "$releaseArtifactsDir\app\web.config")

Libraries/elasticsearch.prod.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# multi node configuration
2+
cluster.name: exceptionless
3+
script.disable_dynamic: false
4+
index.number_of_shards: 3
5+
index.number_of_replicas: 1
6+
action.destructive_requires_name: true
7+
action.auto_create_index: +.marvel-*
8+
bootstrap.mlockall: true
9+
10+
#path.data: /mnt/data/db
11+
#path.work: /path/to/work
12+
#path.logs: /mnt/data/log
13+
14+
gateway.expected_nodes: 3
15+
discovery.zen.ping.multicast.enabled: false
16+
discovery.zen.minimum_master_nodes: 2
17+
discovery.zen.ping.unicast.hosts: [ '10.2.0.4', '10.2.0.5', '10.2.0.6' ]
18+
19+
#transport.tcp.compress: true
20+
transport.tcp.port: 9300
21+
http.port: 9200
22+
http.jsonp.enable: true
23+
http.cors.enabled: true
24+
http.cors.allow-origin: http://dashboards.localhost.com
25+
26+
# non-ssd configuration
27+
#index.merge.scheduler.max_thread_count: 1
28+
#index.translog.flush_threshold_size: 1gb
29+
30+
#cloud:
31+
# azure:
32+
# storage:
33+
# account: your_azure_storage_account
34+
# key: your_azure_storage_key

Libraries/elasticsearch.yml

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1+
# single node configuration
12
cluster.name: exceptionless
2-
node.name: ex-es01
3-
#action.destructive_requires_name: true
4-
index.merge.scheduler.max_thread_count: 1
5-
index.translog.flush_threshold_size: 1gb
6-
#path.data: /mnt/data/db
7-
#path.work: /path/to/work
8-
#path.logs: /mnt/data/log
9-
bootstrap.mlockall: true
103
script.disable_dynamic: false
11-
#gateway.expected_nodes: 3
4+
index.number_of_shards: 1
5+
index.number_of_replicas: 0
6+
action.auto_create_index: +.marvel-*
7+
node.local: true
8+
http.port: 9200
9+
transport.tcp.port: 9300
10+
bootstrap.mlockall: true
1211
discovery.zen.ping.multicast.enabled: false
13-
#discovery.zen.minimum_master_nodes: 2
14-
#discovery.zen.ping.unicast.hosts: [ '10.2.0.4', '10.2.0.5', '10.2.0.6' ]
15-
16-
#transport.tcp.compress: true
17-
http.jsonp.enable: true
18-
http.cors.enabled: true
19-
http.cors.allow-origin: http://dashboards.localhost.com
2012

21-
#cloud:
22-
# azure:
23-
# storage:
24-
# account: your_azure_storage_account
25-
# key: your_azure_storage_key
13+
# non-ssd configuration
14+
#index.merge.scheduler.max_thread_count: 1
15+
#index.translog.flush_threshold_size: 1gb

Source/Api/AppBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public static void Build(IAppBuilder app, Container container = null) {
5050
config.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented;
5151

5252
SetupRouteConstraints(config);
53+
container.RegisterSingleton(config);
5354
container.RegisterWebApiControllers(config);
5455

5556
VerifyContainer(container);
@@ -76,8 +77,7 @@ public static void Build(IAppBuilder app, Container container = null) {
7677

7778
EnableCors(config, app);
7879

79-
container.Bootstrap(config);
80-
container.Bootstrap(app);
80+
container.RunStartupActionsAsync().GetAwaiter().GetResult();
8181

8282
app.UseWebApi(config);
8383
SetupSignalR(app, container, loggerFactory);
@@ -113,7 +113,7 @@ await workItemQueue.EnqueueAsync(new OrganizationNotificationWorkItem {
113113
logger.Info("Jobs running out of process.");
114114
return;
115115
}
116-
116+
117117
new JobRunner(container.GetInstance<EventPostsJob>(), loggerFactory, initialDelay: TimeSpan.FromSeconds(2)).RunInBackground(token);
118118
new JobRunner(container.GetInstance<EventUserDescriptionsJob>(), loggerFactory, initialDelay: TimeSpan.FromSeconds(3)).RunInBackground(token);
119119
new JobRunner(container.GetInstance<EventNotificationsJob>(), loggerFactory, initialDelay: TimeSpan.FromSeconds(5)).RunInBackground(token);
@@ -205,7 +205,7 @@ private static async Task CreateSampleDataAsync(Container container) {
205205
await dataHelper.CreateDataAsync();
206206
}
207207

208-
public static Container CreateContainer(LoggerFactory loggerFactory, ILogger logger, bool includeInsulation = true) {
208+
public static Container CreateContainer(ILoggerFactory loggerFactory, ILogger logger, bool includeInsulation = true) {
209209
var container = new Container();
210210
container.Options.AllowOverridingRegistrations = true;
211211
container.Options.DefaultScopedLifestyle = new WebApiRequestLifestyle();

Source/Api/Bootstrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static void RegisterServices(Container container, ILoggerFactory loggerFa
3838
}
3939

4040
public class ApiMappings : Profile {
41-
protected override void Configure() {
41+
public ApiMappings() {
4242
CreateMap<UserDescription, EventUserDescription>();
4343

4444
CreateMap<NewOrganization, Organization>();

Source/Api/Controllers/AdminController.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Exceptionless.Core.Models.WorkItems;
1111
using Exceptionless.Core.Queues.Models;
1212
using Exceptionless.Core.Repositories;
13+
using Exceptionless.Core.Repositories.Configuration;
1314
using Foundatio.Jobs;
1415
using Foundatio.Messaging;
1516
using Foundatio.Queues;
@@ -20,13 +21,15 @@ namespace Exceptionless.Api.Controllers {
2021
[Authorize(Roles = AuthorizationRoles.GlobalAdmin)]
2122
[ApiExplorerSettings(IgnoreApi = true)]
2223
public class AdminController : ExceptionlessApiController {
24+
private readonly ExceptionlessElasticConfiguration _configuration;
2325
private readonly IFileStorage _fileStorage;
2426
private readonly IMessagePublisher _messagePublisher;
2527
private readonly IOrganizationRepository _organizationRepository;
2628
private readonly IQueue<EventPost> _eventPostQueue;
2729
private readonly IQueue<WorkItemData> _workItemQueue;
2830

29-
public AdminController(IFileStorage fileStorage, IMessagePublisher messagePublisher, IOrganizationRepository organizationRepository, IQueue<EventPost> eventPostQueue, IQueue<WorkItemData> workItemQueue) {
31+
public AdminController(ExceptionlessElasticConfiguration configuration, IFileStorage fileStorage, IMessagePublisher messagePublisher, IOrganizationRepository organizationRepository, IQueue<EventPost> eventPostQueue, IQueue<WorkItemData> workItemQueue) {
32+
_configuration = configuration;
3033
_fileStorage = fileStorage;
3134
_messagePublisher = messagePublisher;
3235
_organizationRepository = organizationRepository;
@@ -93,6 +96,9 @@ public async Task<IHttpActionResult> RequeueAsync(string path = null, bool archi
9396
[Route("maintenance/{name:minlength(1)}")]
9497
public async Task<IHttpActionResult> RunJobAsync(string name) {
9598
switch (name.ToLower()) {
99+
case "indexes":
100+
await _configuration.ConfigureIndexesAsync(beginReindexingOutdated: false);
101+
break;
96102
case "update-organization-plans":
97103
await _workItemQueue.EnqueueAsync(new OrganizationMaintenanceWorkItem { UpgradePlans = true });
98104
break;

Source/Api/Controllers/AuthController.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using FluentValidation;
1919
using Foundatio.Caching;
2020
using Foundatio.Logging;
21+
using Foundatio.Utility;
2122
using Newtonsoft.Json.Linq;
2223
using OAuth2.Client;
2324
using OAuth2.Client.Impl;
@@ -32,7 +33,7 @@ public class AuthController : ExceptionlessApiController {
3233
private readonly IOrganizationRepository _organizationRepository;
3334
private readonly IUserRepository _userRepository;
3435
private readonly ITokenRepository _tokenRepository;
35-
private readonly ICacheClient _cacheClient;
36+
private readonly ICacheClient _cache;
3637
private readonly IMailer _mailer;
3738
private readonly ILogger _logger;
3839

@@ -43,7 +44,7 @@ public AuthController(IOrganizationRepository organizationRepository, IUserRepos
4344
_organizationRepository = organizationRepository;
4445
_userRepository = userRepository;
4546
_tokenRepository = tokenRepository;
46-
_cacheClient = new ScopedCacheClient(cacheClient, "auth");
47+
_cache = new ScopedCacheClient(cacheClient, "Auth");
4748
_mailer = mailer;
4849
_logger = logger;
4950
}
@@ -81,11 +82,11 @@ public async Task<IHttpActionResult> LoginAsync(LoginModel model) {
8182

8283
// Only allow 5 password attempts per 15 minute period.
8384
string userLoginAttemptsCacheKey = $"user:{model.Email}:attempts";
84-
long userLoginAttempts = await _cacheClient.IncrementAsync(userLoginAttemptsCacheKey, 1, DateTime.UtcNow.Ceiling(TimeSpan.FromMinutes(15)));
85+
long userLoginAttempts = await _cache.IncrementAsync(userLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15)));
8586

8687
// Only allow 15 login attempts per 15 minute period by a single ip.
8788
string ipLoginAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:attempts";
88-
long ipLoginAttempts = await _cacheClient.IncrementAsync(ipLoginAttemptsCacheKey, 1, DateTime.UtcNow.Ceiling(TimeSpan.FromMinutes(15)));
89+
long ipLoginAttempts = await _cache.IncrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15)));
8990

9091
if (userLoginAttempts > 5) {
9192
_logger.Error().Message("Login denied for \"{0}\" for the {1} time.", model.Email, userLoginAttempts).Tag("Login").Identity(model.Email).SetActionContext(ActionContext).Write();
@@ -136,7 +137,7 @@ public async Task<IHttpActionResult> LoginAsync(LoginModel model) {
136137
if (!String.IsNullOrEmpty(model.InviteToken))
137138
await AddInvitedUserToOrganizationAsync(model.InviteToken, user);
138139

139-
await _cacheClient.RemoveAsync(userLoginAttemptsCacheKey);
140+
await _cache.RemoveAsync(userLoginAttemptsCacheKey);
140141

141142
_logger.Info().Message("\"{0}\" logged in.", user.EmailAddress).Tag("Login").Identity(user.EmailAddress).Property("User", user).SetActionContext(ActionContext).Write();
142143
return Ok(new TokenResult { Token = await GetTokenAsync(user) });
@@ -186,7 +187,7 @@ public async Task<IHttpActionResult> SignupAsync(SignupModel model) {
186187
bool hasValidInviteToken = !String.IsNullOrWhiteSpace(model.InviteToken) && await _organizationRepository.GetByInviteTokenAsync(model.InviteToken) != null;
187188
if (!hasValidInviteToken) {
188189
// Only allow 10 signups per hour period by a single ip.
189-
long ipSignupAttempts = await _cacheClient.IncrementAsync(ipSignupAttemptsCacheKey, 1, DateTime.UtcNow.Ceiling(TimeSpan.FromHours(1)));
190+
long ipSignupAttempts = await _cache.IncrementAsync(ipSignupAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1)));
190191
if (ipSignupAttempts > 10) {
191192
_logger.Error().Message("Signup denied for \"{0}\" for the {1} time.", model.Email, ipSignupAttempts).Tag("Signup").Identity(model.Email).SetActionContext(ActionContext).Write();
192193
return BadRequest();
@@ -335,7 +336,7 @@ public async Task<IHttpActionResult> IsEmailAddressAvailableAsync(string email)
335336

336337
// Only allow 3 checks attempts per hour period by a single ip.
337338
string ipEmailAddressAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:email:attempts";
338-
long attempts = await _cacheClient.IncrementAsync(ipEmailAddressAttemptsCacheKey, 1, DateTime.UtcNow.Ceiling(TimeSpan.FromHours(1)));
339+
long attempts = await _cache.IncrementAsync(ipEmailAddressAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1)));
339340

340341
if (attempts > 3 || await _userRepository.GetByEmailAddressAsync(email) == null)
341342
return StatusCode(HttpStatusCode.NoContent);
@@ -594,15 +595,15 @@ private Task ChangePasswordAsync(User user, string password) {
594595

595596
private async Task<string> GetTokenAsync(User user) {
596597
var userTokens = await _tokenRepository.GetByUserIdAsync(user.Id);
597-
var validAccessToken = userTokens.Documents.FirstOrDefault(t => (!t.ExpiresUtc.HasValue || t.ExpiresUtc > DateTime.UtcNow) && t.Type == TokenType.Access);
598+
var validAccessToken = userTokens.Documents.FirstOrDefault(t => (!t.ExpiresUtc.HasValue || t.ExpiresUtc > SystemClock.UtcNow) && t.Type == TokenType.Access);
598599
if (validAccessToken != null)
599600
return validAccessToken.Id;
600601

601602
var token = await _tokenRepository.AddAsync(new Token {
602603
Id = Core.Extensions.StringExtensions.GetNewToken(),
603604
UserId = user.Id,
604-
CreatedUtc = DateTime.UtcNow,
605-
ModifiedUtc = DateTime.UtcNow,
605+
CreatedUtc = SystemClock.UtcNow,
606+
ModifiedUtc = SystemClock.UtcNow,
606607
CreatedBy = user.Id,
607608
Type = TokenType.Access
608609
});

Source/Api/Controllers/Base/ExceptionlessApiController.cs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Net;
5-
using System.Text;
65
using System.Threading.Tasks;
76
using System.Web.Http;
87
using System.Web.Http.Results;
98
using Exceptionless.Api.Extensions;
109
using Exceptionless.Api.Security;
1110
using Exceptionless.Api.Utility;
12-
using Exceptionless.Core.Extensions;
1311
using Exceptionless.Api.Utility.Results;
1412
using Exceptionless.Core.Repositories;
1513
using Exceptionless.Core.Models;
14+
using Exceptionless.Core.Repositories.Queries;
1615
using Exceptionless.DateTimeExtensions;
1716
using Foundatio.Repositories.Models;
17+
using Foundatio.Utility;
1818

1919
namespace Exceptionless.Api.Controllers {
2020
[RequireHttpsExceptLocal]
@@ -49,7 +49,7 @@ protected virtual TimeInfo GetTimeInfo(string time, string offset, DateTime? min
4949
var utcOffset = GetOffset(offset);
5050

5151
// range parsing needs to be based on the user's local time.
52-
var localRange = DateTimeRange.Parse(time, DateTime.UtcNow.Add(utcOffset));
52+
var localRange = DateTimeRange.Parse(time, SystemClock.UtcNow.Add(utcOffset));
5353
var utcRange = localRange != DateTimeRange.Empty ? localRange.Subtract(utcOffset) : localRange;
5454

5555
if (utcRange.UtcStart < minimumUtcStartDate.GetValueOrDefault())
@@ -130,29 +130,35 @@ public ICollection<string> GetAssociatedOrganizationIds() {
130130
return Request.GetAssociatedOrganizationIds();
131131
}
132132

133-
public async Task<ICollection<Organization>> GetAssociatedOrganizationsAsync(IOrganizationRepository repository) {
133+
private static readonly IReadOnlyCollection<Organization> EmptyOrganizations = new List<Organization>(0).AsReadOnly();
134+
public async Task<IReadOnlyCollection<Organization>> GetAssociatedActiveOrganizationsAsync(IOrganizationRepository repository) {
134135
if (repository == null)
135-
return null;
136+
throw new ArgumentNullException(nameof(repository));
136137

137-
return (await repository.GetByIdsAsync(GetAssociatedOrganizationIds(), true)).Documents;
138-
}
139-
140-
public string BuildSystemFilter(ICollection<Organization> organizations, string filter, bool usesPremiumFeatures, string retentionDateFieldName = "date") {
141-
if (HasOrganizationOrProjectOrStackFilter(filter) && Request.IsGlobalAdmin())
142-
return null;
143-
144-
var allowedOrganizations = organizations.Where(o => !o.IsSuspended && (o.HasPremiumFeatures || (!o.HasPremiumFeatures && !usesPremiumFeatures))).ToList();
145-
if (allowedOrganizations.Count == 0)
146-
return "organization:none";
138+
var ids = GetAssociatedOrganizationIds();
139+
if (ids.Count == 0)
140+
return EmptyOrganizations;
147141

148-
return allowedOrganizations.BuildRetentionFilter(retentionDateFieldName);
142+
var organizations = await repository.GetByIdsAsync(ids, true);
143+
return organizations.Where(o => !o.IsSuspended).ToList().AsReadOnly();
149144
}
150-
151-
private bool HasOrganizationOrProjectOrStackFilter(string filter) {
145+
146+
protected bool ShouldApplySystemFilter(IExceptionlessSystemFilterQuery sf, string filter) {
147+
// Apply filter to non admin user.
148+
if (!Request.IsGlobalAdmin())
149+
return true;
150+
151+
// Apply filter as it's scoped via a controller action.
152+
if (!sf.IsUserOrganizationsFilter)
153+
return true;
154+
155+
// Empty user filter
152156
if (String.IsNullOrEmpty(filter))
153-
return false;
157+
return true;
154158

155-
return filter.Contains("organization:") || filter.Contains("project:") || filter.Contains("stack:");
159+
// Used for impersonating a user. Only skip the filter if it contains an org, project or stack.
160+
bool hasOrganizationOrProjectOrStackFilter = filter.Contains("organization:") || filter.Contains("project:") || filter.Contains("stack:");
161+
return !hasOrganizationOrProjectOrStackFilter;
156162
}
157163

158164
protected StatusCodeActionResult StatusCodeWithMessage(HttpStatusCode statusCode, string message, string reason = null) {
@@ -195,11 +201,11 @@ public OkWithHeadersContentResult<T> OkWithHeaders<T>(T content, IEnumerable<Key
195201
return new OkWithHeadersContentResult<T>(content, this, headers);
196202
}
197203

198-
public OkWithResourceLinks<TEntity> OkWithResourceLinks<TEntity>(ICollection<TEntity> content, bool hasMore, Func<TEntity, string> pagePropertyAccessor = null, IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers = null, bool isDescending = false) where TEntity : class {
204+
public OkWithResourceLinks<TEntity> OkWithResourceLinks<TEntity>(IEnumerable<TEntity> content, bool hasMore, Func<TEntity, string> pagePropertyAccessor = null, IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers = null, bool isDescending = false) where TEntity : class {
199205
return new OkWithResourceLinks<TEntity>(content, this, hasMore, null, pagePropertyAccessor, headers, isDescending);
200206
}
201207

202-
public OkWithResourceLinks<TEntity> OkWithResourceLinks<TEntity>(ICollection<TEntity> content, bool hasMore, int page, long? total = null, IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers = null) where TEntity : class {
208+
public OkWithResourceLinks<TEntity> OkWithResourceLinks<TEntity>(IEnumerable<TEntity> content, bool hasMore, int page, long? total = null, IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers = null) where TEntity : class {
203209
return new OkWithResourceLinks<TEntity>(content, this, hasMore, page, total);
204210
}
205211

0 commit comments

Comments
 (0)