From 7b641b0a5b52fad7190d32cfef04b32d0cff9b52 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Mon, 8 Nov 2021 22:05:10 -0600 Subject: [PATCH] Use C# 10 implicit usings and file scoped namespaces --- .editorconfig | 25 +- build/common.props | 1 + .../ActiveDirectoryLoginProvider.cs | 86 +- .../Authentication/IDomainLoginProvider.cs | 16 +- .../Authorization/AuthorizationRoles.cs | 22 +- .../Billing/BillingManager.cs | 217 +- .../Billing/BillingPlans.cs | 359 ++- .../Billing/StripeEventHandler.cs | 188 +- src/Exceptionless.Core/Bootstrapper.cs | 477 ++-- .../Configuration/AppOptions.cs | 202 +- .../Configuration/AuthOptions.cs | 60 +- .../Configuration/CacheOptions.cs | 46 +- .../Configuration/ElasticsearchOptions.cs | 101 +- .../Configuration/EmailOptions.cs | 105 +- .../Configuration/IntercomOptions.cs | 25 +- .../Configuration/MessageBusOptions.cs | 50 +- .../Configuration/MetricOptions.cs | 38 +- .../Configuration/QueueOptions.cs | 46 +- .../Configuration/SlackOptions.cs | 29 +- .../Configuration/StorageOptions.cs | 46 +- .../Configuration/StripeOptions.cs | 31 +- .../Extensions/AppDomainExtensions.cs | 15 +- .../Extensions/ByteArrayExtensions.cs | 62 +- .../Extensions/CacheClientExtensions.cs | 54 +- .../Extensions/ConfigurationExtensions.cs | 119 +- .../Extensions/DataDictionaryExtensions.cs | 61 +- .../Extensions/DictionaryExtensions.cs | 211 +- .../Extensions/EnumHelper.cs | 85 +- .../Extensions/EnumerableExtensions.cs | 161 +- .../Extensions/ErrorExtensions.cs | 108 +- .../Extensions/EventExtensions.cs | 534 ++-- .../Extensions/HashExtensions.cs | 110 +- .../Extensions/HttpClientExtensions.cs | 28 +- .../Extensions/ILGeneratorExtensions.cs | 65 +- .../Extensions/IdentityUtils.cs | 235 +- .../Extensions/JsonExtensions.cs | 362 ++- .../Extensions/LogBuilderExtensions.cs | 177 +- .../Extensions/LoggerExtensions.cs | 353 ++- .../Extensions/MethodExtensions.cs | 111 +- .../NameValueCollectionExtensions.cs | 135 +- .../Extensions/NumericExtensions.cs | 12 +- .../Extensions/OrganizationExtensions.cs | 282 +- .../Extensions/PersistentEventExtensions.cs | 395 +-- .../Extensions/ProjectExtensions.cs | 155 +- .../Extensions/QueryNodeExtensions.cs | 31 +- .../Extensions/RequestInfoExtensions.cs | 150 +- .../Extensions/ServiceCollectionExtensions.cs | 94 +- .../Extensions/StackExtensions.cs | 85 +- .../Extensions/StringExtensions.cs | 2057 +++++++-------- .../Extensions/TaskExtensions.cs | 21 +- .../Extensions/TypeExtensions.cs | 120 +- .../Extensions/UriExtensions.cs | 41 +- .../Extensions/UsageExtensions.cs | 57 +- .../Extensions/UserExtensions.cs | 140 +- .../Extensions/ValidationExtensions.cs | 21 +- src/Exceptionless.Core/Geo/GeoResult.cs | 125 +- src/Exceptionless.Core/Geo/IGeoIpService.cs | 9 +- src/Exceptionless.Core/Geo/IGeocodeService.cs | 11 +- .../Geo/NullGeoIpService.cs | 11 +- .../Geo/NullGeocodeService.cs | 13 +- src/Exceptionless.Core/Jobs/CleanupDataJob.cs | 398 +-- .../Jobs/CleanupOrphanedDataJob.cs | 554 ++-- .../Jobs/CloseInactiveSessionsJob.cs | 214 +- .../Jobs/DailySummaryJob.cs | 283 +- .../Jobs/DownloadGeoIPDatabaseJob.cs | 113 +- .../Jobs/Elastic/DataMigrationJob.cs | 394 ++- .../Jobs/Elastic/MaintainIndexesJob.cs | 41 +- .../Jobs/Elastic/MigrationJob.cs | 44 +- .../Jobs/EventNotificationsJob.cs | 259 +- src/Exceptionless.Core/Jobs/EventPostsJob.cs | 492 ++-- .../Jobs/EventUserDescriptionsJob.cs | 81 +- src/Exceptionless.Core/Jobs/MailMessageJob.cs | 37 +- .../Jobs/StackEventCountJob.cs | 71 +- src/Exceptionless.Core/Jobs/StackStatusJob.cs | 89 +- src/Exceptionless.Core/Jobs/WebHooksJob.cs | 278 +- .../OrganizationMaintenanceWorkItemHandler.cs | 96 +- ...OrganizationNotificationWorkItemHandler.cs | 121 +- .../ProjectMaintenanceWorkItemHandler.cs | 80 +- .../RemoveBotEventsWorkItemHandler.cs | 50 +- .../RemoveStacksWorkItemHandler.cs | 53 +- .../SetLocationFromGeoWorkItemHandler.cs | 92 +- .../SetProjectIsConfiguredWorkItemHandler.cs | 55 +- .../UserMaintenanceWorkItemHandler.cs | 77 +- src/Exceptionless.Core/Mail/IMailSender.cs | 13 +- src/Exceptionless.Core/Mail/IMailer.cs | 27 +- .../Mail/InMemoryMailSender.cs | 42 +- src/Exceptionless.Core/Mail/Mailer.cs | 371 ++- .../Migrations/001_UpdateIndexMappings.cs | 129 +- .../Migrations/002_SetStackStatus.cs | 104 +- .../Migrations/FixDuplicateStacks.cs | 311 ++- .../Migrations/SetStackDuplicateSignature.cs | 114 +- .../Models/Billing/BillingPlan.cs | 30 +- .../Models/Billing/BillingPlanStats.cs | 36 +- .../Models/Billing/ChangePlanResult.cs | 24 +- .../Models/ClientConfiguration.cs | 22 +- .../Models/Collections/DataDictionary.cs | 67 +- .../Models/Collections/GenericArguments.cs | 6 +- .../Models/Collections/ModuleCollection.cs | 6 +- .../Collections/ObservableDictionary.cs | 174 +- .../Models/Collections/ParameterCollection.cs | 6 +- .../Models/Collections/SettingsDictionary.cs | 219 +- .../Collections/StackFrameCollection.cs | 6 +- .../Models/Collections/TagSet.cs | 47 +- src/Exceptionless.Core/Models/CoreMappings.cs | 6 +- .../Models/Data/EnvironmentInfo.cs | 259 +- src/Exceptionless.Core/Models/Data/Error.cs | 58 +- .../Models/Data/InnerError.cs | 119 +- .../Models/Data/Location.cs | 40 +- .../Models/Data/ManualStackingInfo.cs | 52 +- src/Exceptionless.Core/Models/Data/Method.cs | 83 +- src/Exceptionless.Core/Models/Data/Module.cs | 97 +- .../Models/Data/Parameter.cs | 69 +- .../Models/Data/RequestInfo.cs | 220 +- .../Models/Data/SimpleError.cs | 111 +- .../Models/Data/StackFrame.cs | 48 +- .../Models/Data/SubmissionClient.cs | 42 +- .../Models/Data/UserDescription.cs | 77 +- .../Models/Data/UserInfo.cs | 95 +- .../Models/Enums/NotificationMode.cs | 14 +- .../Models/Enums/ResponseStatusType.cs | 18 +- .../Models/Enums/SuspensionCode.cs | 16 +- src/Exceptionless.Core/Models/Event.cs | 228 +- .../Models/EventPreviousAndNextIdResult.cs | 10 +- .../Models/EventSummaryModel.cs | 12 +- .../Models/Exceptions/RateLimitException.cs | 8 +- .../Models/Exceptions/WebHookException.cs | 14 +- .../Models/Interfaces/IData.cs | 10 +- .../Models/Interfaces/IOwnedByOrganization.cs | 16 +- .../IOwnedByOrganizationAndProject.cs | 6 +- .../IOwnedByOrganizationAndProjectAndStack.cs | 6 +- ...anizationAndProjectAndStackWithIdentity.cs | 8 +- ...nedByOrganizationAndProjectWithIdentity.cs | 6 +- .../IOwnedByOrganizationWithIdentity.cs | 6 +- .../Models/Interfaces/IOwnedByProject.cs | 16 +- .../Models/Interfaces/IOwnedByStack.cs | 16 +- src/Exceptionless.Core/Models/Invite.cs | 14 +- .../Models/MailMessageData.cs | 12 +- .../Models/Messaging/ExtendedEntityChanged.cs | 86 +- .../Models/Messaging/PlanChanged.cs | 8 +- .../Models/Messaging/PlanOverage.cs | 10 +- .../Models/Messaging/ReleaseNotification.cs | 14 +- .../Models/Messaging/SystemNotification.cs | 12 +- .../Models/Messaging/UserMembershipChanged.cs | 12 +- .../Models/NotificationSettings.cs | 20 +- src/Exceptionless.Core/Models/OAuthAccount.cs | 99 +- src/Exceptionless.Core/Models/Organization.cs | 332 ++- .../Models/PersistentEvent.cs | 85 +- src/Exceptionless.Core/Models/Project.cs | 112 +- .../Models/Queues/EventNotification.cs | 16 +- .../Models/Queues/EventPostInfo.cs | 34 +- .../Models/Queues/EventUserDescription.cs | 10 +- .../Models/Queues/MailMessage.cs | 16 +- .../Models/Queues/SummaryNotification.cs | 14 +- .../Models/Queues/WebHookNotification.cs | 28 +- src/Exceptionless.Core/Models/SessionInfo.cs | 46 +- src/Exceptionless.Core/Models/SlackToken.cs | 146 +- src/Exceptionless.Core/Models/Stack.cs | 260 +- .../Models/StackSummaryModel.cs | 29 +- src/Exceptionless.Core/Models/SummaryData.cs | 12 +- src/Exceptionless.Core/Models/Token.cs | 50 +- src/Exceptionless.Core/Models/UsageInfo.cs | 18 +- src/Exceptionless.Core/Models/User.cs | 96 +- src/Exceptionless.Core/Models/WebHook.cs | 41 +- src/Exceptionless.Core/Models/WebHookEvent.cs | 62 +- src/Exceptionless.Core/Models/WebHookStack.cs | 56 +- .../OrganizationMaintenanceWorkItem.cs | 12 +- .../OrganizationNotificationWorkItem.cs | 14 +- .../WorkItems/ProjectMaintenanceWorkItem.cs | 14 +- .../WorkItems/RemoveBotEventsWorkItem.cs | 19 +- .../Models/WorkItems/RemoveStacksWorkItem.cs | 13 +- .../WorkItems/SetLocationFromGeoWorkItem.cs | 12 +- .../SetProjectIsConfiguredWorkItem.cs | 12 +- .../WorkItems/UserMaintenanceWorkItem.cs | 10 +- .../Pipeline/001_CheckEventDateAction.cs | 43 +- .../005_RunEventProcessingPluginsAction.cs | 28 +- .../Pipeline/006_TruncateFieldsAction.cs | 46 +- .../Pipeline/010_AssignToStackAction.cs | 268 +- .../Pipeline/020_MarkAsCriticalAction.cs | 31 +- .../Pipeline/030_CheckForRegressionAction.cs | 113 +- .../Pipeline/035_CopySimpleDataToIdxAction.cs | 46 +- .../Pipeline/040_SaveEventAction.cs | 54 +- .../050_MarkProjectConfiguredAction.cs | 70 +- .../Pipeline/060_UpdateStatsAction.cs | 88 +- .../Pipeline/070_QueueNotificationAction.cs | 172 +- .../Pipeline/090_IncrementCountersAction.cs | 70 +- .../100_RunEventProcessedPluginsAction.cs | 28 +- .../Pipeline/Base/PipelineActionBase.cs | 146 +- .../Pipeline/Base/PipelineBase.cs | 189 +- .../Pipeline/Base/PipelineContextBase.cs | 163 +- .../Pipeline/Base/PriorityAttribute.cs | 20 +- .../Pipeline/EventPipeline.cs | 101 +- .../Pipeline/EventPipelineActionBase.cs | 36 +- .../Default/FallbackEventParserPlugin.cs | 31 +- .../Default/JsonEventParserPlugin.cs | 37 +- .../Default/LegacyErrorParserPlugin.cs | 47 +- .../EventParser/EventParserPluginManager.cs | 72 +- .../Plugins/EventParser/IEventParserPlugin.cs | 13 +- .../Default/03_ManualStackingPlugin.cs | 27 +- .../05_CheckForDuplicateReferenceIdPlugin.cs | 58 +- .../Default/07_SubmissionClientPlugin.cs | 71 +- .../Default/0_ThrottleBotsPlugin.cs | 104 +- .../Default/10_NotFoundPlugin.cs | 46 +- .../EventProcessor/Default/20_ErrorPlugin.cs | 70 +- .../Default/30_SimpleErrorPlugin.cs | 50 +- .../Default/40_RequestInfoPlugin.cs | 129 +- .../Default/45_EnvironmentInfoPlugin.cs | 66 +- .../EventProcessor/Default/50_GeoPlugin.cs | 122 +- .../Default/60_LocationPlugin.cs | 68 +- .../Default/70_SessionPlugin.cs | 414 +-- .../Default/80_AngularPlugin.cs | 56 +- .../90_RemovePrivateInformationPlugin.cs | 38 +- .../Plugins/EventProcessor/EventContext.cs | 70 +- .../EventProcessor/EventPluginManager.cs | 107 +- .../EventProcessorPluginBase.cs | 85 +- .../EventProcessor/IEventProcessorPlugin.cs | 13 +- .../EventUpgrader/Default/GetVersion.cs | 74 +- .../Default/V1R500_EventUpgrade.cs | 46 +- .../Default/V1R844_EventUpgrade.cs | 49 +- .../Default/V1R850_EventUpgrade.cs | 44 +- .../EventUpgrader/Default/V2_EventUpgrade.cs | 221 +- .../EventUpgrader/EventUpgraderContext.cs | 61 +- .../EventUpgraderPluginManager.cs | 40 +- .../EventUpgrader/IEventUpgraderPlugin.cs | 8 +- .../05_ManualStackingFormattingPlugin.cs | 21 +- .../Default/10_SimpleErrorFormattingPlugin.cs | 211 +- .../Default/20_ErrorFormattingPlugin.cs | 234 +- .../Default/30_NotFoundFormattingPlugin.cs | 133 +- .../Default/40_UsageFormattingPlugin.cs | 92 +- .../Default/50_SessionFormattingPlugin.cs | 67 +- .../Default/60_LogFormattingPlugin.cs | 231 +- .../Default/99_DefaultFormattingPlugin.cs | 134 +- .../Formatting/FormattingPluginBase.cs | 108 +- .../Formatting/FormattingPluginManager.cs | 157 +- .../Plugins/Formatting/IFormattingPlugin.cs | 18 +- src/Exceptionless.Core/Plugins/IPlugin.cs | 40 +- .../Plugins/PluginManagerBase.cs | 90 +- .../Plugins/WebHook/Default/LoadDefaults.cs | 88 +- .../WebHook/Default/V1_WebHookDataPlugin.cs | 254 +- .../WebHook/Default/V2_WebHookDataPlugin.cs | 114 +- .../Plugins/WebHook/IWebHookDataPlugin.cs | 10 +- .../Plugins/WebHook/WebHookDataContext.cs | 59 +- .../Plugins/WebHook/WebHookDataPluginBase.cs | 15 +- .../WebHook/WebHookDataPluginManager.cs | 92 +- .../Repositories/Base/RepositoryBase.cs | 156 +- .../Base/RepositoryOwnedByOrganization.cs | 42 +- ...RepositoryOwnedByOrganizationAndProject.cs | 42 +- .../ExceptionlessElasticConfiguration.cs | 146 +- .../Configuration/Indexes/EventIndex.cs | 667 +++-- .../Indexes/OrganizationIndex.cs | 107 +- .../Configuration/Indexes/ProjectIndex.cs | 89 +- .../Configuration/Indexes/StackIndex.cs | 191 +- .../Configuration/Indexes/TokenIndex.cs | 62 +- .../Configuration/Indexes/UserIndex.cs | 71 +- .../Configuration/Indexes/WebHookIndex.cs | 50 +- .../Repositories/EventRepository.cs | 345 ++- .../DocumentLimitExceededException.cs | 11 +- .../Exceptions/DocumentNotFoundException.cs | 26 +- .../Interfaces/IEventRepository.cs | 34 +- .../Interfaces/IOrganizationRepository.cs | 17 +- .../Interfaces/IProjectRepository.cs | 20 +- .../IRepositoryOwnedByOrganization.cs | 13 +- ...RepositoryOwnedByOrganizationAndProject.cs | 6 +- .../Interfaces/IRepositoryOwnedByProject.cs | 13 +- .../Interfaces/IStackRepository.cs | 26 +- .../Interfaces/ITokenRepository.cs | 19 +- .../Interfaces/IUserRepository.cs | 21 +- .../Interfaces/IWebHookRepository.cs | 17 +- .../Repositories/OrganizationRepository.cs | 209 +- .../Repositories/ProjectRepository.cs | 92 +- .../Repositories/Queries/AppFilterQuery.cs | 6 +- .../Queries/EventStackFilterQuery.cs | 3 - .../Repositories/Queries/OrganizationQuery.cs | 5 +- .../Repositories/Queries/ProjectQuery.cs | 5 +- .../Repositories/Queries/QueryExtensions.cs | 21 +- .../Repositories/Queries/StackQuery.cs | 5 +- .../Queries/Validation/AppQueryValidator.cs | 118 +- .../PersistentEventQueryValidator.cs | 75 +- .../Queries/Validation/StackQueryValidator.cs | 75 +- .../Visitors/EventFieldsQueryVisitor.cs | 173 +- .../Visitors/EventStackFilterQueryVisitor.cs | 279 +- .../Visitors/StackDateFixedQueryVisitor.cs | 40 +- .../Repositories/StackRepository.cs | 170 +- .../Repositories/TokenRepository.cs | 65 +- .../Repositories/UserRepository.cs | 152 +- .../Repositories/WebHookRepository.cs | 104 +- .../Serialization/DataObjectConverter.cs | 240 +- .../DynamicTypeContractResolver.cs | 47 +- ...ConnectionSettingsAwareContractResolver.cs | 35 +- .../Serialization/ElasticJsonNetSerializer.cs | 55 +- ...UnderscorePropertyNamesContractResolver.cs | 37 +- .../Services/EventPostService.cs | 145 +- .../Services/MessageService.cs | 97 +- .../Services/OrganizationService.cs | 138 +- .../Services/SlackService.cs | 226 +- .../Services/StackService.cs | 183 +- .../Services/UsageService.cs | 391 ++- .../Utility/Apm/EventSourceEventFormatter.cs | 98 +- .../Apm/SelfDiagnosticsEventLogForwarder.cs | 227 +- .../SelfDiagnosticsLoggingHostedService.cs | 53 +- .../Utility/Apm/StringBuilderPool.cs | 94 +- .../Utility/AssemblyDetail.cs | 82 +- .../Utility/ErrorSignature.cs | 253 +- .../Utility/ExtensibleObject.cs | 106 +- .../Utility/IConnectionMapping.cs | 152 +- .../Utility/IConnectionString.cs | 32 +- .../Utility/LastReferenceIdManager.cs | 16 +- src/Exceptionless.Core/Utility/MetricNames.cs | 62 +- src/Exceptionless.Core/Utility/PathHelper.cs | 89 +- .../Utility/Reflection/DelegateFactory.cs | 247 +- .../Utility/Reflection/FieldAccessor.cs | 192 +- .../Utility/Reflection/IMemberAccessor.cs | 97 +- .../Utility/Reflection/LateBinder.cs | 472 ++-- .../Utility/Reflection/MemberAccessor.cs | 172 +- .../Utility/Reflection/PropertyAccessor.cs | 194 +- .../Utility/Reflection/TypeAccessor.cs | 278 +- .../Utility/SampleDataService.cs | 384 ++- .../Utility/SemanticVersionParser.cs | 85 +- src/Exceptionless.Core/Utility/SmtpUri.cs | 67 +- src/Exceptionless.Core/Utility/TypeHelper.cs | 209 +- .../Utility/UserAgentParser.cs | 53 +- .../Validation/IsObjectIdValidator.cs | 29 +- .../Validation/OrganizationValidator.cs | 41 +- .../Validation/PersistentEventValidator.cs | 95 +- .../Validation/ProjectValidator.cs | 16 +- .../Validation/StackValidator.cs | 81 +- .../Validation/TokenValidator.cs | 31 +- .../Validation/UserDescriptionValidator.cs | 27 +- .../Validation/UserValidator.cs | 23 +- .../Validation/WebHookValidator.cs | 21 +- src/Exceptionless.Insulation/Bootstrapper.cs | 535 ++-- .../YamlConfigurationExtensions.cs | 231 +- .../YamlConfigurationFileParser.cs | 140 +- .../YamlConfigurationProvider.cs | 74 +- .../Configuration/YamlConfigurationSource.cs | 14 +- ...ceptionlessClientLastReferenceIdManager.cs | 18 +- .../Geo/GoogleGeocodeService.cs | 50 +- .../Geo/MaxMindGeoIpService.cs | 170 +- .../HealthChecks/CacheHealthCheck.cs | 51 +- .../HealthChecks/ElasticsearchHealthCheck.cs | 57 +- .../HealthChecks/HealthCheckExtensions.cs | 34 +- .../HealthChecks/QueueHealthCheck.cs | 59 +- .../HealthChecks/StorageHealthCheck.cs | 49 +- .../Mail/ExtensionsProtocolLogger.cs | 95 +- .../Mail/MailKitMailSender.cs | 195 +- .../Redis/RedisConnectionMap.cs | 64 +- src/Exceptionless.Job/JobRunnerOptions.cs | 179 +- src/Exceptionless.Job/Program.cs | 316 ++- src/Exceptionless.Web/ApmExtensions.cs | 166 +- src/Exceptionless.Web/Bootstrapper.cs | 91 +- .../Controllers/AdminController.cs | 251 +- .../Controllers/AuthController.cs | 1264 ++++----- .../Base/ExceptionlessApiController.cs | 323 ++- .../Controllers/Base/ModelActionResults.cs | 25 +- .../Controllers/Base/PermissionResult.cs | 100 +- .../Base/ReadOnlyRepositoryApiController.cs | 137 +- .../Base/RepositoryApiController.cs | 334 ++- .../Controllers/Base/TimeInfo.cs | 53 +- .../Controllers/Base/WorkInProgressResult.cs | 22 +- .../Controllers/EventController.cs | 2320 ++++++++--------- .../Controllers/OrganizationController.cs | 1279 ++++----- .../Controllers/ProjectController.cs | 1289 +++++---- .../Controllers/StackController.cs | 1008 ++++--- .../Controllers/StatusController.cs | 234 +- .../Controllers/StripeController.cs | 81 +- .../Controllers/TokenController.cs | 504 ++-- .../Controllers/UserController.cs | 520 ++-- .../Controllers/UtilityController.cs | 61 +- .../Controllers/WebHookController.cs | 414 ++- .../Extensions/DeltaExtensions.cs | 22 +- .../ExceptionlessStateExtensions.cs | 18 +- .../Extensions/HttpExtensions.cs | 218 +- .../Extensions/LoggerExtensions.cs | 339 ++- .../Extensions/OAuth2Extensions.cs | 22 +- .../Extensions/SerilogExtensions.cs | 10 +- .../Hubs/MessageBusBroker.cs | 260 +- .../Hubs/MessageBusBrokerMiddleware.cs | 200 +- .../Hubs/WebSocketConnectionManager.cs | 241 +- .../Models/Auth/ChangePasswordModel.cs | 12 +- .../Models/Auth/ExternalAuthInfo.cs | 16 +- .../Models/Auth/LoginModel.cs | 14 +- .../Models/Auth/ResetPasswordModel.cs | 12 +- .../Models/Auth/SignupModel.cs | 10 +- .../Models/Auth/StatusResult.cs | 10 +- .../Models/Event/UpdateEvent.cs | 10 +- .../Models/Organization/Invoice.cs | 29 +- .../Models/Organization/InvoiceGridModel.cs | 14 +- .../Models/Organization/InvoiceLineItem.cs | 14 +- .../Models/Organization/NewOrganization.cs | 10 +- .../Models/Organization/ViewOrganization.cs | 78 +- .../Models/Project/NewProject.cs | 10 +- .../Models/Project/UpdateProject.cs | 10 +- .../Models/Project/ViewProject.cs | 42 +- .../Models/Token/NewToken.cs | 28 +- .../Models/Token/UpdateToken.cs | 12 +- .../Models/Token/ViewToken.cs | 32 +- .../Models/User/ChangePlanResult.cs | 10 +- .../Models/User/UpdateUser.cs | 12 +- .../Models/User/ViewCurrentUser.cs | 70 +- src/Exceptionless.Web/Models/User/ViewUser.cs | 29 +- src/Exceptionless.Web/Models/ValueFromBody.cs | 20 +- .../Models/WebHook/NewWebHook.cs | 27 +- .../Models/WebHook/UpdateWebHook.cs | 10 +- src/Exceptionless.Web/Program.cs | 190 +- .../Security/ApiKeyAuthenticationHandler.cs | 221 +- src/Exceptionless.Web/Startup.cs | 428 ++- .../Bindings/CustomAttributesModelBinder.cs | 141 +- .../DelimitedQueryStringValueProvider.cs | 136 +- .../ConfigurationResponseFilterAttribute.cs | 40 +- .../Constraints/IdentifierRouteConstraint.cs | 10 +- .../Constraints/IdentifiersRouteConstraint.cs | 10 +- .../Constraints/ObjectIdRouteConstraint.cs | 10 +- .../Constraints/ObjectIdsRouteConstraint.cs | 10 +- .../Constraints/TokenRouteConstraint.cs | 10 +- .../Constraints/TokensRouteConstraint.cs | 10 +- src/Exceptionless.Web/Utility/Delta/Delta.cs | 575 ++-- .../Utility/GetFilterScopeVisitor.cs | 80 +- .../Handlers/AllowSynchronousIOMiddleware.cs | 28 +- .../Utility/Handlers/ApiError.cs | 68 +- .../Utility/Handlers/ApiException.cs | 28 +- .../Utility/Handlers/ApiExceptionFilter.cs | 99 +- .../Utility/Handlers/OverageMiddleware.cs | 168 +- .../Handlers/ProjectConfigMiddleware.cs | 101 +- .../RecordSessionHeartbeatMiddleware.cs | 105 +- .../Utility/Handlers/ThrottlingMiddleware.cs | 164 +- src/Exceptionless.Web/Utility/Headers.cs | 24 +- .../Utility/RawRequestBodyFormatter.cs | 71 +- .../Utility/RequestBodyOperationFilter.cs | 2 - .../Utility/Results/MessageContent.cs | 20 +- .../Results/ObjectWithHeadersResult.cs | 29 +- .../Utility/Results/OkPaginatedResult.cs | 75 +- .../Results/OkWithHeadersContentResult.cs | 223 +- .../Utility/Results/PermissionResult.cs | 116 +- .../Exceptionless.Tests/AppWebHostFactory.cs | 30 +- .../Authentication/TestDomainLoginProvider.cs | 34 +- .../Billing/BillingManagerTests.cs | 70 +- .../Controllers/AuthControllerTests.cs | 1517 ++++++----- .../Controllers/EventControllerTests.cs | 1447 +++++----- .../Controllers/ProjectControllerTests.cs | 194 +- .../Controllers/StackControllerTests.cs | 186 +- .../Controllers/StatusControllerTests.cs | 82 +- .../Controllers/TokenControllerTests.cs | 112 +- .../Exceptionless.Tests.csproj | 3 +- .../Extensions/RequestExtensions.cs | 62 +- .../Extensions/StringExtensionsTests.cs | 74 +- .../Extensions/TaskExtensions.cs | 16 +- .../Extensions/TestServerExtensions.cs | 47 +- .../IntegrationTestsBase.cs | 325 ++- .../Jobs/CleanupDataJobTests.cs | 242 +- .../Jobs/CloseInactiveSessionsJobTests.cs | 346 ++- .../Jobs/EventPostJobTests.cs | 261 +- tests/Exceptionless.Tests/Mail/MailerTests.cs | 500 ++-- tests/Exceptionless.Tests/Mail/NullMailer.cs | 59 +- .../FixDuplicateStacksMigrationTests.cs | 343 ++- ...etStackDuplicateSignatureMigrationTests.cs | 77 +- .../Miscellaneous/DeltaTests.cs | 50 +- .../Miscellaneous/EfficientPagingTests.cs | 117 +- .../Pipeline/EventPipelineTests.cs | 1599 ++++++------ .../Plugins/EventParserTests.cs | 98 +- .../Plugins/EventUpgraderTests.cs | 59 +- tests/Exceptionless.Tests/Plugins/GeoTests.cs | 470 ++-- .../Plugins/ManualStackingTests.cs | 35 +- .../Plugins/SummaryDataTests.cs | 120 +- .../Plugins/WebHookDataTests.cs | 115 +- .../Repositories/EventRepositoryTests.cs | 375 ++- .../Repositories/IndexTests.cs | 43 +- .../OrganizationRepositoryTests.cs | 92 +- .../Repositories/ProjectRepositoryTests.cs | 252 +- .../Repositories/StackRepositoryTests.cs | 348 ++- .../Repositories/TokenRepositoryTests.cs | 84 +- .../Repositories/WebHookRepositoryTests.cs | 56 +- .../Search/EventIndexTests.cs | 800 +++--- .../Search/EventStackFilterQueryTests.cs | 200 +- .../EventStackFilterQueryVisitorTests.cs | 131 +- .../Search/EventStackFilterTests.cs | 109 +- .../Search/MoreEventIndexTests.cs | 343 ++- .../PersistentEventQueryValidatorTests.cs | 214 +- .../Search/StackIndexTests.cs | 325 ++- .../SemanticVersionTests.cs | 101 +- tests/Exceptionless.Tests/SerializerTests.cs | 210 +- .../Services/SlackServiceTests.cs | 320 ++- .../Services/StackServiceTests.cs | 231 +- .../Services/UsageServiceTests.cs | 457 ++-- .../Stats/AggregationTests.cs | 364 ++- tests/Exceptionless.Tests/TestWithServices.cs | 109 +- .../Utility/AppSendBuilder.cs | 168 +- .../Utility/DataBuilder.cs | 803 +++--- .../Exceptionless.Tests/Utility/EventData.cs | 283 +- .../Utility/OrganizationData.cs | 70 +- .../Utility/ProjectData.cs | 69 +- .../Utility/RandomEventGenerator.cs | 288 +- tests/Exceptionless.Tests/Utility/Run.cs | 14 +- .../Exceptionless.Tests/Utility/StackData.cs | 120 +- .../Utility/TestConstants.cs | 91 +- .../Exceptionless.Tests/Utility/TokenData.cs | 46 +- tests/Exceptionless.Tests/Utility/UserData.cs | 66 +- .../Validation/EventValidatorTests.cs | 163 +- .../Validation/TokenValidatorTests.cs | 57 +- 497 files changed, 33524 insertions(+), 34469 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6d6a4ea2c1..783e687fc1 100755 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,7 @@ indent_size = 4 # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 +dotnet_style_allow_multiple_blank_lines_experimental=false:silent # Xml config files [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] @@ -45,10 +46,21 @@ dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion +# Naming style +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + # CSharp code style settings: [*.cs] # Prefer "var" everywhere -csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_for_built_in_types = false:silent csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = true:suggestion @@ -68,6 +80,8 @@ csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion +csharp_style_namespace_declarations=file_scoped:silent +csharp_style_prefer_range_operator=false:suggestion # Newline settings csharp_new_line_before_open_brace = false:error @@ -75,4 +89,11 @@ csharp_new_line_before_else = false:error csharp_new_line_before_catch = false:error csharp_new_line_before_finally = false:error csharp_new_line_before_members_in_object_initializers = false:error -csharp_new_line_before_members_in_anonymous_types = false:error \ No newline at end of file +csharp_new_line_before_members_in_anonymous_types = false:error + +# CS1701: Assuming assembly reference matches identity +dotnet_diagnostic.CS1701.severity = none + +# CS1702: Assuming assembly reference matches identity +dotnet_diagnostic.CS1702.severity = none +csharp_prefer_braces=when_multiline:silent diff --git a/build/common.props b/build/common.props index 1981774dbb..286906c27e 100644 --- a/build/common.props +++ b/build/common.props @@ -2,6 +2,7 @@ net6.0 + enable Exceptionless true v diff --git a/src/Exceptionless.Core/Authentication/ActiveDirectoryLoginProvider.cs b/src/Exceptionless.Core/Authentication/ActiveDirectoryLoginProvider.cs index e5a92a29f7..658d5be3db 100644 --- a/src/Exceptionless.Core/Authentication/ActiveDirectoryLoginProvider.cs +++ b/src/Exceptionless.Core/Authentication/ActiveDirectoryLoginProvider.cs @@ -2,62 +2,62 @@ using System.DirectoryServices; using Exceptionless.Core.Configuration; -namespace Exceptionless.Core.Authentication { - public class ActiveDirectoryLoginProvider : IDomainLoginProvider { - private const string AD_EMAIL = "mail"; - private const string AD_FIRSTNAME = "givenName"; - private const string AD_LASTNAME = "sn"; - private const string AD_DISTINGUISHEDNAME = "distinguishedName"; - private const string AD_USERNAME = "sAMAccountName"; +namespace Exceptionless.Core.Authentication; - private readonly AuthOptions _authOptions; +public class ActiveDirectoryLoginProvider : IDomainLoginProvider { + private const string AD_EMAIL = "mail"; + private const string AD_FIRSTNAME = "givenName"; + private const string AD_LASTNAME = "sn"; + private const string AD_DISTINGUISHEDNAME = "distinguishedName"; + private const string AD_USERNAME = "sAMAccountName"; - public ActiveDirectoryLoginProvider(AuthOptions authOptions) { - _authOptions = authOptions; - } + private readonly AuthOptions _authOptions; - public bool Login(string username, string password) { - using (var de = new DirectoryEntry(_authOptions.LdapConnectionString, username, password, AuthenticationTypes.Secure)) { - using (var ds = new DirectorySearcher(de, $"(&({AD_USERNAME}={username}))", new[] { AD_DISTINGUISHEDNAME })) { - try { - var result = ds.FindOne(); - return result != null; - } - // Catch "username and password are invalid" - catch (DirectoryServicesCOMException ex) when (ex.ErrorCode == -2147023570) { - return false; - } + public ActiveDirectoryLoginProvider(AuthOptions authOptions) { + _authOptions = authOptions; + } + + public bool Login(string username, string password) { + using (var de = new DirectoryEntry(_authOptions.LdapConnectionString, username, password, AuthenticationTypes.Secure)) { + using (var ds = new DirectorySearcher(de, $"(&({AD_USERNAME}={username}))", new[] { AD_DISTINGUISHEDNAME })) { + try { + var result = ds.FindOne(); + return result != null; + } + // Catch "username and password are invalid" + catch (DirectoryServicesCOMException ex) when (ex.ErrorCode == -2147023570) { + return false; } } } + } - public string GetUsernameFromEmailAddress(string email) { - using (var entry = new DirectoryEntry(_authOptions.LdapConnectionString)) { - using (var searcher = new DirectorySearcher(entry, $"(&({AD_EMAIL}={email}))", new[] { AD_USERNAME })) { - var result = searcher.FindOne(); - return result?.Properties[AD_USERNAME][0].ToString(); - } + public string GetUsernameFromEmailAddress(string email) { + using (var entry = new DirectoryEntry(_authOptions.LdapConnectionString)) { + using (var searcher = new DirectorySearcher(entry, $"(&({AD_EMAIL}={email}))", new[] { AD_USERNAME })) { + var result = searcher.FindOne(); + return result?.Properties[AD_USERNAME][0].ToString(); } } + } - public string GetEmailAddressFromUsername(string username) { - var result = FindUser(username); - return result?.Properties[AD_EMAIL][0].ToString(); - } + public string GetEmailAddressFromUsername(string username) { + var result = FindUser(username); + return result?.Properties[AD_EMAIL][0].ToString(); + } - public string GetUserFullName(string username) { - var result = FindUser(username); - if (result == null) - return null; + public string GetUserFullName(string username) { + var result = FindUser(username); + if (result == null) + return null; - return $"{result.Properties[AD_FIRSTNAME][0]} {result.Properties[AD_LASTNAME]}"; - } + return $"{result.Properties[AD_FIRSTNAME][0]} {result.Properties[AD_LASTNAME]}"; + } - private SearchResult FindUser(string username) { - using (var entry = new DirectoryEntry(_authOptions.LdapConnectionString)) { - using (var searcher = new DirectorySearcher(entry, $"(&({AD_USERNAME}={username}))", new[] { AD_FIRSTNAME, AD_LASTNAME, AD_EMAIL })) { - return searcher.FindOne(); - } + private SearchResult FindUser(string username) { + using (var entry = new DirectoryEntry(_authOptions.LdapConnectionString)) { + using (var searcher = new DirectorySearcher(entry, $"(&({AD_USERNAME}={username}))", new[] { AD_FIRSTNAME, AD_LASTNAME, AD_EMAIL })) { + return searcher.FindOne(); } } } diff --git a/src/Exceptionless.Core/Authentication/IDomainLoginProvider.cs b/src/Exceptionless.Core/Authentication/IDomainLoginProvider.cs index 32e0518035..ab74f4f3f2 100644 --- a/src/Exceptionless.Core/Authentication/IDomainLoginProvider.cs +++ b/src/Exceptionless.Core/Authentication/IDomainLoginProvider.cs @@ -1,11 +1,11 @@ -namespace Exceptionless.Core.Authentication { - public interface IDomainLoginProvider { - bool Login(string username, string password); +namespace Exceptionless.Core.Authentication; - string GetEmailAddressFromUsername(string username); +public interface IDomainLoginProvider { + bool Login(string username, string password); - string GetUserFullName(string username); + string GetEmailAddressFromUsername(string username); - string GetUsernameFromEmailAddress(string email); - } -} \ No newline at end of file + string GetUserFullName(string username); + + string GetUsernameFromEmailAddress(string email); +} diff --git a/src/Exceptionless.Core/Authorization/AuthorizationRoles.cs b/src/Exceptionless.Core/Authorization/AuthorizationRoles.cs index c3a2d711c0..72dc4a359e 100644 --- a/src/Exceptionless.Core/Authorization/AuthorizationRoles.cs +++ b/src/Exceptionless.Core/Authorization/AuthorizationRoles.cs @@ -1,11 +1,11 @@ -namespace Exceptionless.Core.Authorization { - public static class AuthorizationRoles { - public const string ClientPolicy = nameof(ClientPolicy); - public const string Client = "client"; - public const string UserPolicy = nameof(UserPolicy); - public const string User = "user"; - public const string GlobalAdminPolicy = nameof(GlobalAdminPolicy); - public const string GlobalAdmin = "global"; - public static readonly string[] AllScopes = { "client", "user", "global" }; - } -} \ No newline at end of file +namespace Exceptionless.Core.Authorization; + +public static class AuthorizationRoles { + public const string ClientPolicy = nameof(ClientPolicy); + public const string Client = "client"; + public const string UserPolicy = nameof(UserPolicy); + public const string User = "user"; + public const string GlobalAdminPolicy = nameof(GlobalAdminPolicy); + public const string GlobalAdmin = "global"; + public static readonly string[] AllScopes = { "client", "user", "global" }; +} diff --git a/src/Exceptionless.Core/Billing/BillingManager.cs b/src/Exceptionless.Core/Billing/BillingManager.cs index 52b6393aae..f677584cc6 100644 --- a/src/Exceptionless.Core/Billing/BillingManager.cs +++ b/src/Exceptionless.Core/Billing/BillingManager.cs @@ -1,116 +1,113 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.Billing; using Exceptionless.Core.Repositories; using Exceptionless.Core.Models; using Foundatio.Utility; -namespace Exceptionless.Core.Billing { - public class BillingManager { - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IUserRepository _userRepository; - private readonly BillingPlans _plans; - - public BillingManager(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IUserRepository userRepository, BillingPlans plans) { - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _userRepository = userRepository; - _plans = plans; - } - - public async Task CanAddOrganizationAsync(User user) { - if (user == null) - return false; - - var organizations = (await _organizationRepository.GetByIdsAsync(user.OrganizationIds.ToArray()).AnyContext()).Where(o => o.PlanId == _plans.FreePlan.Id); - return !organizations.Any(); - } - - public async Task CanAddUserAsync(Organization organization) { - if (String.IsNullOrWhiteSpace(organization?.Id)) - return false; - - long numberOfUsers = (await _userRepository.GetByOrganizationIdAsync(organization.Id).AnyContext()).Total + organization.Invites.Count; - return organization.MaxUsers <= -1 || numberOfUsers < organization.MaxUsers; - } - - public async Task CanAddProjectAsync(Project project) { - if (String.IsNullOrWhiteSpace(project?.OrganizationId)) - return false; - - var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId).AnyContext(); - if (organization == null) - return false; - - long projectCount = await _projectRepository.GetCountByOrganizationIdAsync(project.OrganizationId).AnyContext(); - return organization.MaxProjects == -1 || projectCount < organization.MaxProjects; - } - - public async Task HasPremiumFeaturesAsync(string organizationId) { - var organization = await _organizationRepository.GetByIdAsync(organizationId).AnyContext(); - if (organization == null) - return false; - - return organization.HasPremiumFeatures; - } - - public async Task CanDownGradeAsync(Organization organization, BillingPlan plan, User user) { - if (String.IsNullOrWhiteSpace(organization?.Id)) - return ChangePlanResult.FailWithMessage("Invalid Organization"); - - long currentNumberOfUsers = (await _userRepository.GetByOrganizationIdAsync(organization.Id).AnyContext()).Total + organization.Invites.Count; - int maxUsers = plan.MaxUsers != -1 ? plan.MaxUsers : Int32.MaxValue; - if (currentNumberOfUsers > maxUsers) - return ChangePlanResult.FailWithMessage($"Please remove {currentNumberOfUsers - maxUsers} user{((currentNumberOfUsers - maxUsers) > 0 ? "s" : String.Empty)} and try again."); - - int maxProjects = plan.MaxProjects != -1 ? plan.MaxProjects : Int32.MaxValue; - long projectCount = await _projectRepository.GetCountByOrganizationIdAsync(organization.Id).AnyContext(); - if (projectCount > maxProjects) - return ChangePlanResult.FailWithMessage($"Please remove {projectCount - maxProjects} project{((projectCount - maxProjects) > 0 ? "s" : String.Empty)} and try again."); - - // Ensure the user can't be apart of more than one free plan. - if (String.Equals(plan.Id, _plans.FreePlan.Id) && user != null && (await _organizationRepository.GetByIdsAsync(user.OrganizationIds.ToArray()).AnyContext()).Any(o => String.Equals(o.PlanId, _plans.FreePlan.Id))) - return ChangePlanResult.FailWithMessage("You already have one free account. You are not allowed to create more than one free account."); - - return new ChangePlanResult { Success = true }; - } - - public BillingPlan GetBillingPlan(string planId) { - return _plans.Plans.FirstOrDefault(p => String.Equals(p.Id, planId, StringComparison.OrdinalIgnoreCase)); - } - - public BillingPlan GetBillingPlanByUpsellingRetentionPeriod(int retentionDays) { - return _plans.Plans.Where(p => p.RetentionDays > retentionDays && p.Price > 0).OrderBy(p => p.RetentionDays).ThenBy(p => p.Price).FirstOrDefault(); - } - - public void ApplyBillingPlan(Organization organization, BillingPlan plan, User user = null, bool updateBillingPrice = true) { - organization.PlanId = plan.Id; - organization.PlanName = plan.Name; - organization.PlanDescription = plan.Description; - organization.BillingChangeDate = SystemClock.UtcNow; - - if (updateBillingPrice) - organization.BillingPrice = plan.Price; - - if (user != null) - organization.BillingChangedByUserId = user.Id; - - organization.MaxUsers = plan.MaxUsers; - organization.MaxProjects = plan.MaxProjects; - organization.RetentionDays = plan.RetentionDays; - organization.MaxEventsPerMonth = plan.MaxEventsPerMonth; - organization.HasPremiumFeatures = plan.HasPremiumFeatures; - - organization.SetMonthlyUsage(organization.GetCurrentMonthlyTotal(), organization.GetCurrentMonthlyBlocked(), organization.GetCurrentMonthlyTooBig()); - } - - public void ApplyBonus(Organization organization, int bonusEvents, DateTime? expires = null) { - organization.BonusEventsPerMonth = bonusEvents; - organization.BonusExpiration = expires; - organization.SetMonthlyUsage(organization.GetCurrentMonthlyTotal(), organization.GetCurrentMonthlyBlocked(), organization.GetCurrentMonthlyTooBig()); - } +namespace Exceptionless.Core.Billing; + +public class BillingManager { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IUserRepository _userRepository; + private readonly BillingPlans _plans; + + public BillingManager(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IUserRepository userRepository, BillingPlans plans) { + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _userRepository = userRepository; + _plans = plans; + } + + public async Task CanAddOrganizationAsync(User user) { + if (user == null) + return false; + + var organizations = (await _organizationRepository.GetByIdsAsync(user.OrganizationIds.ToArray()).AnyContext()).Where(o => o.PlanId == _plans.FreePlan.Id); + return !organizations.Any(); + } + + public async Task CanAddUserAsync(Organization organization) { + if (String.IsNullOrWhiteSpace(organization?.Id)) + return false; + + long numberOfUsers = (await _userRepository.GetByOrganizationIdAsync(organization.Id).AnyContext()).Total + organization.Invites.Count; + return organization.MaxUsers <= -1 || numberOfUsers < organization.MaxUsers; + } + + public async Task CanAddProjectAsync(Project project) { + if (String.IsNullOrWhiteSpace(project?.OrganizationId)) + return false; + + var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId).AnyContext(); + if (organization == null) + return false; + + long projectCount = await _projectRepository.GetCountByOrganizationIdAsync(project.OrganizationId).AnyContext(); + return organization.MaxProjects == -1 || projectCount < organization.MaxProjects; + } + + public async Task HasPremiumFeaturesAsync(string organizationId) { + var organization = await _organizationRepository.GetByIdAsync(organizationId).AnyContext(); + if (organization == null) + return false; + + return organization.HasPremiumFeatures; + } + + public async Task CanDownGradeAsync(Organization organization, BillingPlan plan, User user) { + if (String.IsNullOrWhiteSpace(organization?.Id)) + return ChangePlanResult.FailWithMessage("Invalid Organization"); + + long currentNumberOfUsers = (await _userRepository.GetByOrganizationIdAsync(organization.Id).AnyContext()).Total + organization.Invites.Count; + int maxUsers = plan.MaxUsers != -1 ? plan.MaxUsers : Int32.MaxValue; + if (currentNumberOfUsers > maxUsers) + return ChangePlanResult.FailWithMessage($"Please remove {currentNumberOfUsers - maxUsers} user{((currentNumberOfUsers - maxUsers) > 0 ? "s" : String.Empty)} and try again."); + + int maxProjects = plan.MaxProjects != -1 ? plan.MaxProjects : Int32.MaxValue; + long projectCount = await _projectRepository.GetCountByOrganizationIdAsync(organization.Id).AnyContext(); + if (projectCount > maxProjects) + return ChangePlanResult.FailWithMessage($"Please remove {projectCount - maxProjects} project{((projectCount - maxProjects) > 0 ? "s" : String.Empty)} and try again."); + + // Ensure the user can't be apart of more than one free plan. + if (String.Equals(plan.Id, _plans.FreePlan.Id) && user != null && (await _organizationRepository.GetByIdsAsync(user.OrganizationIds.ToArray()).AnyContext()).Any(o => String.Equals(o.PlanId, _plans.FreePlan.Id))) + return ChangePlanResult.FailWithMessage("You already have one free account. You are not allowed to create more than one free account."); + + return new ChangePlanResult { Success = true }; + } + + public BillingPlan GetBillingPlan(string planId) { + return _plans.Plans.FirstOrDefault(p => String.Equals(p.Id, planId, StringComparison.OrdinalIgnoreCase)); + } + + public BillingPlan GetBillingPlanByUpsellingRetentionPeriod(int retentionDays) { + return _plans.Plans.Where(p => p.RetentionDays > retentionDays && p.Price > 0).OrderBy(p => p.RetentionDays).ThenBy(p => p.Price).FirstOrDefault(); + } + + public void ApplyBillingPlan(Organization organization, BillingPlan plan, User user = null, bool updateBillingPrice = true) { + organization.PlanId = plan.Id; + organization.PlanName = plan.Name; + organization.PlanDescription = plan.Description; + organization.BillingChangeDate = SystemClock.UtcNow; + + if (updateBillingPrice) + organization.BillingPrice = plan.Price; + + if (user != null) + organization.BillingChangedByUserId = user.Id; + + organization.MaxUsers = plan.MaxUsers; + organization.MaxProjects = plan.MaxProjects; + organization.RetentionDays = plan.RetentionDays; + organization.MaxEventsPerMonth = plan.MaxEventsPerMonth; + organization.HasPremiumFeatures = plan.HasPremiumFeatures; + + organization.SetMonthlyUsage(organization.GetCurrentMonthlyTotal(), organization.GetCurrentMonthlyBlocked(), organization.GetCurrentMonthlyTooBig()); + } + + public void ApplyBonus(Organization organization, int bonusEvents, DateTime? expires = null) { + organization.BonusEventsPerMonth = bonusEvents; + organization.BonusExpiration = expires; + organization.SetMonthlyUsage(organization.GetCurrentMonthlyTotal(), organization.GetCurrentMonthlyBlocked(), organization.GetCurrentMonthlyTooBig()); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Billing/BillingPlans.cs b/src/Exceptionless.Core/Billing/BillingPlans.cs index e1df7fecc6..02af8ee2bd 100644 --- a/src/Exceptionless.Core/Billing/BillingPlans.cs +++ b/src/Exceptionless.Core/Billing/BillingPlans.cs @@ -1,181 +1,180 @@ -using System.Collections.Generic; -using Exceptionless.Core.Models.Billing; - -namespace Exceptionless.Core.Billing { - public class BillingPlans { - public BillingPlans(AppOptions options) { - FreePlan = new BillingPlan { - Id = "EX_FREE", - Name = "Free", - Description = "Free", - Price = 0, - MaxProjects = 1, - MaxUsers = 1, - RetentionDays = 3, - MaxEventsPerMonth = 3000, - HasPremiumFeatures = false - }; - - SmallPlan = new BillingPlan { - Id = "EX_SMALL", - Name = "Small", - Description = "Small ($15/month)", - Price = 15, - MaxProjects = 5, - MaxUsers = 10, - RetentionDays = 30, - MaxEventsPerMonth = 15000, - HasPremiumFeatures = true - }; - - SmallYearlyPlan = new BillingPlan { - Id = "EX_SMALL_YEARLY", - Name = "Small (Yearly)", - Description = "Small Yearly ($165/year - Save $15)", - Price = 165, - MaxProjects = 5, - MaxUsers = 10, - RetentionDays = 30, - MaxEventsPerMonth = 15000, - HasPremiumFeatures = true - }; - - MediumPlan = new BillingPlan { - Id = "EX_MEDIUM", - Name = "Medium", - Description = "Medium ($49/month)", - Price = 49, - MaxProjects = 15, - MaxUsers = 25, - RetentionDays = 90, - MaxEventsPerMonth = 75000, - HasPremiumFeatures = true - }; - - MediumYearlyPlan = new BillingPlan { - Id = "EX_MEDIUM_YEARLY", - Name = "Medium (Yearly)", - Description = "Medium Yearly ($539/year - Save $49)", - Price = 539, - MaxProjects = 15, - MaxUsers = 25, - RetentionDays = 90, - MaxEventsPerMonth = 75000, - HasPremiumFeatures = true - }; - - LargePlan = new BillingPlan { - Id = "EX_LARGE", - Name = "Large", - Description = "Large ($99/month)", - Price = 99, - MaxProjects = -1, - MaxUsers = -1, - RetentionDays = 180, - MaxEventsPerMonth = 250000, - HasPremiumFeatures = true - }; - - LargeYearlyPlan = new BillingPlan { - Id = "EX_LARGE_YEARLY", - Name = "Large (Yearly)", - Description = "Large Yearly ($1,089/year - Save $99)", - Price = 1089, - MaxProjects = -1, - MaxUsers = -1, - RetentionDays = 180, - MaxEventsPerMonth = 250000, - HasPremiumFeatures = true - }; - - ExtraLargePlan = new BillingPlan { - Id = "EX_XL", - Name = "Extra Large", - Description = "Extra Large ($199/month)", - Price = 199, - MaxProjects = -1, - MaxUsers = -1, - RetentionDays = 180, - MaxEventsPerMonth = 1000000, - HasPremiumFeatures = true - }; - - ExtraLargeYearlyPlan = new BillingPlan { - Id = "EX_XL_YEARLY", - Name = "Extra Large (Yearly)", - Description = "Extra Large Yearly ($2,189/year - Save $199)", - Price = 2189, - MaxProjects = -1, - MaxUsers = -1, - RetentionDays = 180, - MaxEventsPerMonth = 1000000, - HasPremiumFeatures = true - }; - - EnterprisePlan = new BillingPlan { - Id = "EX_ENT", - Name = "Enterprise", - Description = "Enterprise ($499/month)", - Price = 499, - MaxProjects = -1, - MaxUsers = -1, - RetentionDays = 180, - MaxEventsPerMonth = 3000000, - HasPremiumFeatures = true - }; - - EnterpriseYearlyPlan = new BillingPlan { - Id = "EX_ENT_YEARLY", - Name = "Enterprise (Yearly)", - Description = "Enterprise Yearly ($5,489/year - Save $499)", - Price = 5489, - MaxProjects = -1, - MaxUsers = -1, - RetentionDays = 180, - MaxEventsPerMonth = 3000000, - HasPremiumFeatures = true - }; - - UnlimitedPlan = new BillingPlan { - Id = "EX_UNLIMITED", - Name = "Unlimited", - Description = "Unlimited", - IsHidden = true, - Price = 0, - MaxProjects = -1, - MaxUsers = -1, - RetentionDays = options.MaximumRetentionDays, - MaxEventsPerMonth = -1, - HasPremiumFeatures = true - }; - - Plans = new List { FreePlan, SmallYearlyPlan, MediumYearlyPlan, LargeYearlyPlan, ExtraLargeYearlyPlan, EnterpriseYearlyPlan, SmallPlan, MediumPlan, LargePlan, ExtraLargePlan, EnterprisePlan, UnlimitedPlan }; - } - - public BillingPlan FreePlan { get; } - - public BillingPlan SmallPlan { get; } - - public BillingPlan SmallYearlyPlan { get; } - - public BillingPlan MediumPlan { get; } - - public BillingPlan MediumYearlyPlan { get; } - - public BillingPlan LargePlan { get; } - - public BillingPlan LargeYearlyPlan { get; } - - public BillingPlan ExtraLargePlan { get; } - - public BillingPlan ExtraLargeYearlyPlan { get; } - - public BillingPlan EnterprisePlan { get; } - - public BillingPlan EnterpriseYearlyPlan { get; } - - public BillingPlan UnlimitedPlan { get; } - - public List Plans { get; } +using Exceptionless.Core.Models.Billing; + +namespace Exceptionless.Core.Billing; + +public class BillingPlans { + public BillingPlans(AppOptions options) { + FreePlan = new BillingPlan { + Id = "EX_FREE", + Name = "Free", + Description = "Free", + Price = 0, + MaxProjects = 1, + MaxUsers = 1, + RetentionDays = 3, + MaxEventsPerMonth = 3000, + HasPremiumFeatures = false + }; + + SmallPlan = new BillingPlan { + Id = "EX_SMALL", + Name = "Small", + Description = "Small ($15/month)", + Price = 15, + MaxProjects = 5, + MaxUsers = 10, + RetentionDays = 30, + MaxEventsPerMonth = 15000, + HasPremiumFeatures = true + }; + + SmallYearlyPlan = new BillingPlan { + Id = "EX_SMALL_YEARLY", + Name = "Small (Yearly)", + Description = "Small Yearly ($165/year - Save $15)", + Price = 165, + MaxProjects = 5, + MaxUsers = 10, + RetentionDays = 30, + MaxEventsPerMonth = 15000, + HasPremiumFeatures = true + }; + + MediumPlan = new BillingPlan { + Id = "EX_MEDIUM", + Name = "Medium", + Description = "Medium ($49/month)", + Price = 49, + MaxProjects = 15, + MaxUsers = 25, + RetentionDays = 90, + MaxEventsPerMonth = 75000, + HasPremiumFeatures = true + }; + + MediumYearlyPlan = new BillingPlan { + Id = "EX_MEDIUM_YEARLY", + Name = "Medium (Yearly)", + Description = "Medium Yearly ($539/year - Save $49)", + Price = 539, + MaxProjects = 15, + MaxUsers = 25, + RetentionDays = 90, + MaxEventsPerMonth = 75000, + HasPremiumFeatures = true + }; + + LargePlan = new BillingPlan { + Id = "EX_LARGE", + Name = "Large", + Description = "Large ($99/month)", + Price = 99, + MaxProjects = -1, + MaxUsers = -1, + RetentionDays = 180, + MaxEventsPerMonth = 250000, + HasPremiumFeatures = true + }; + + LargeYearlyPlan = new BillingPlan { + Id = "EX_LARGE_YEARLY", + Name = "Large (Yearly)", + Description = "Large Yearly ($1,089/year - Save $99)", + Price = 1089, + MaxProjects = -1, + MaxUsers = -1, + RetentionDays = 180, + MaxEventsPerMonth = 250000, + HasPremiumFeatures = true + }; + + ExtraLargePlan = new BillingPlan { + Id = "EX_XL", + Name = "Extra Large", + Description = "Extra Large ($199/month)", + Price = 199, + MaxProjects = -1, + MaxUsers = -1, + RetentionDays = 180, + MaxEventsPerMonth = 1000000, + HasPremiumFeatures = true + }; + + ExtraLargeYearlyPlan = new BillingPlan { + Id = "EX_XL_YEARLY", + Name = "Extra Large (Yearly)", + Description = "Extra Large Yearly ($2,189/year - Save $199)", + Price = 2189, + MaxProjects = -1, + MaxUsers = -1, + RetentionDays = 180, + MaxEventsPerMonth = 1000000, + HasPremiumFeatures = true + }; + + EnterprisePlan = new BillingPlan { + Id = "EX_ENT", + Name = "Enterprise", + Description = "Enterprise ($499/month)", + Price = 499, + MaxProjects = -1, + MaxUsers = -1, + RetentionDays = 180, + MaxEventsPerMonth = 3000000, + HasPremiumFeatures = true + }; + + EnterpriseYearlyPlan = new BillingPlan { + Id = "EX_ENT_YEARLY", + Name = "Enterprise (Yearly)", + Description = "Enterprise Yearly ($5,489/year - Save $499)", + Price = 5489, + MaxProjects = -1, + MaxUsers = -1, + RetentionDays = 180, + MaxEventsPerMonth = 3000000, + HasPremiumFeatures = true + }; + + UnlimitedPlan = new BillingPlan { + Id = "EX_UNLIMITED", + Name = "Unlimited", + Description = "Unlimited", + IsHidden = true, + Price = 0, + MaxProjects = -1, + MaxUsers = -1, + RetentionDays = options.MaximumRetentionDays, + MaxEventsPerMonth = -1, + HasPremiumFeatures = true + }; + + Plans = new List { FreePlan, SmallYearlyPlan, MediumYearlyPlan, LargeYearlyPlan, ExtraLargeYearlyPlan, EnterpriseYearlyPlan, SmallPlan, MediumPlan, LargePlan, ExtraLargePlan, EnterprisePlan, UnlimitedPlan }; } -} \ No newline at end of file + + public BillingPlan FreePlan { get; } + + public BillingPlan SmallPlan { get; } + + public BillingPlan SmallYearlyPlan { get; } + + public BillingPlan MediumPlan { get; } + + public BillingPlan MediumYearlyPlan { get; } + + public BillingPlan LargePlan { get; } + + public BillingPlan LargeYearlyPlan { get; } + + public BillingPlan ExtraLargePlan { get; } + + public BillingPlan ExtraLargeYearlyPlan { get; } + + public BillingPlan EnterprisePlan { get; } + + public BillingPlan EnterpriseYearlyPlan { get; } + + public BillingPlan UnlimitedPlan { get; } + + public List Plans { get; } +} diff --git a/src/Exceptionless.Core/Billing/StripeEventHandler.cs b/src/Exceptionless.Core/Billing/StripeEventHandler.cs index b59328bdef..7c5a5db267 100644 --- a/src/Exceptionless.Core/Billing/StripeEventHandler.cs +++ b/src/Exceptionless.Core/Billing/StripeEventHandler.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Mail; using Exceptionless.Core.Repositories; using Exceptionless.Core.Models; @@ -8,147 +7,148 @@ using Microsoft.Extensions.Logging; using Stripe; -namespace Exceptionless.Core.Billing { - public class StripeEventHandler { - private readonly ILogger _logger; - private readonly IOrganizationRepository _organizationRepository; - private readonly IUserRepository _userRepository; - private readonly IMailer _mailer; - - public StripeEventHandler(IOrganizationRepository organizationRepository, IUserRepository userRepository, IMailer mailer, ILogger logger) { - _logger = logger; - _organizationRepository = organizationRepository; - _userRepository = userRepository; - _mailer = mailer; - } +namespace Exceptionless.Core.Billing; + +public class StripeEventHandler { + private readonly ILogger _logger; + private readonly IOrganizationRepository _organizationRepository; + private readonly IUserRepository _userRepository; + private readonly IMailer _mailer; - public async Task HandleEventAsync(Stripe.Event stripeEvent) { - switch (stripeEvent.Type) { - case "customer.subscription.updated": { + public StripeEventHandler(IOrganizationRepository organizationRepository, IUserRepository userRepository, IMailer mailer, ILogger logger) { + _logger = logger; + _organizationRepository = organizationRepository; + _userRepository = userRepository; + _mailer = mailer; + } + + public async Task HandleEventAsync(Stripe.Event stripeEvent) { + switch (stripeEvent.Type) { + case "customer.subscription.updated": { await SubscriptionUpdatedAsync((Subscription)stripeEvent.Data.Object).AnyContext(); break; } - case "customer.subscription.deleted": { + case "customer.subscription.deleted": { await SubscriptionDeletedAsync((Subscription)stripeEvent.Data.Object).AnyContext(); break; } - case "invoice.payment_succeeded": { + case "invoice.payment_succeeded": { await InvoicePaymentSucceededAsync((Invoice)stripeEvent.Data.Object).AnyContext(); break; } - case "invoice.payment_failed": { + case "invoice.payment_failed": { await InvoicePaymentFailedAsync((Invoice)stripeEvent.Data.Object).AnyContext(); break; } - default: { + default: { _logger.LogTrace("Unhandled stripe webhook called. Type: {Type} Id: {Id} Account: {Account}", stripeEvent.Type, stripeEvent.Id, stripeEvent.Account); break; } - } } + } - private async Task SubscriptionUpdatedAsync(Subscription sub) { - var org = await _organizationRepository.GetByStripeCustomerIdAsync(sub.CustomerId).AnyContext(); - if (org == null) { - _logger.LogError("Unknown customer id in updated subscription: {CustomerId}", sub.CustomerId); - return; - } + private async Task SubscriptionUpdatedAsync(Subscription sub) { + var org = await _organizationRepository.GetByStripeCustomerIdAsync(sub.CustomerId).AnyContext(); + if (org == null) { + _logger.LogError("Unknown customer id in updated subscription: {CustomerId}", sub.CustomerId); + return; + } - _logger.LogInformation("Stripe subscription updated. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName}", sub.CustomerId, org.Id, org.Name); + _logger.LogInformation("Stripe subscription updated. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName}", sub.CustomerId, org.Id, org.Name); - BillingStatus? status = null; - switch (sub.Status) { - case "trialing": { + BillingStatus? status = null; + switch (sub.Status) { + case "trialing": { status = BillingStatus.Trialing; break; } - case "active": { + case "active": { status = BillingStatus.Active; break; } - case "past_due": { + case "past_due": { status = BillingStatus.PastDue; break; } - case "canceled": { + case "canceled": { status = BillingStatus.Canceled; break; } - case "unpaid": { + case "unpaid": { status = BillingStatus.Unpaid; break; } - } - - if (!status.HasValue || status.Value == org.BillingStatus) - return; - - org.BillingStatus = status.Value; - org.BillingChangeDate = SystemClock.UtcNow; - if (status.Value == BillingStatus.Unpaid || status.Value == BillingStatus.Canceled) { - org.IsSuspended = true; - org.SuspensionDate = SystemClock.UtcNow; - org.SuspensionCode = SuspensionCode.Billing; - org.SuspensionNotes = $"Stripe subscription status changed to \"{status.Value}\"."; - org.SuspendedByUserId = "Stripe"; - } else if (status.Value == BillingStatus.Active || status.Value == BillingStatus.Trialing) { - org.RemoveSuspension(); - } - - await _organizationRepository.SaveAsync(org, o => o.Cache()).AnyContext(); } - private async Task SubscriptionDeletedAsync(Subscription sub) { - var org = await _organizationRepository.GetByStripeCustomerIdAsync(sub.CustomerId).AnyContext(); - if (org == null) { - _logger.LogError("Unknown customer id in deleted subscription: {CustomerId}", sub.CustomerId); - return; - } - - _logger.LogInformation("Stripe subscription deleted. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName}", sub.CustomerId, org.Id, org.Name); + if (!status.HasValue || status.Value == org.BillingStatus) + return; - org.BillingStatus = BillingStatus.Canceled; + org.BillingStatus = status.Value; + org.BillingChangeDate = SystemClock.UtcNow; + if (status.Value == BillingStatus.Unpaid || status.Value == BillingStatus.Canceled) { org.IsSuspended = true; org.SuspensionDate = SystemClock.UtcNow; org.SuspensionCode = SuspensionCode.Billing; - org.SuspensionNotes = "Stripe subscription deleted."; + org.SuspensionNotes = $"Stripe subscription status changed to \"{status.Value}\"."; org.SuspendedByUserId = "Stripe"; + } + else if (status.Value == BillingStatus.Active || status.Value == BillingStatus.Trialing) { + org.RemoveSuspension(); + } + + await _organizationRepository.SaveAsync(org, o => o.Cache()).AnyContext(); + } + + private async Task SubscriptionDeletedAsync(Subscription sub) { + var org = await _organizationRepository.GetByStripeCustomerIdAsync(sub.CustomerId).AnyContext(); + if (org == null) { + _logger.LogError("Unknown customer id in deleted subscription: {CustomerId}", sub.CustomerId); + return; + } + + _logger.LogInformation("Stripe subscription deleted. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName}", sub.CustomerId, org.Id, org.Name); + + org.BillingStatus = BillingStatus.Canceled; + org.IsSuspended = true; + org.SuspensionDate = SystemClock.UtcNow; + org.SuspensionCode = SuspensionCode.Billing; + org.SuspensionNotes = "Stripe subscription deleted."; + org.SuspendedByUserId = "Stripe"; - org.BillingChangeDate = SystemClock.UtcNow; - await _organizationRepository.SaveAsync(org, o => o.Cache()).AnyContext(); + org.BillingChangeDate = SystemClock.UtcNow; + await _organizationRepository.SaveAsync(org, o => o.Cache()).AnyContext(); + } + + private async Task InvoicePaymentSucceededAsync(Invoice inv) { + var org = await _organizationRepository.GetByStripeCustomerIdAsync(inv.CustomerId).AnyContext(); + if (org == null) { + _logger.LogError("Unknown customer id in payment succeeded notification: {CustomerId}", inv.CustomerId); + return; } - private async Task InvoicePaymentSucceededAsync(Invoice inv) { - var org = await _organizationRepository.GetByStripeCustomerIdAsync(inv.CustomerId).AnyContext(); - if (org == null) { - _logger.LogError("Unknown customer id in payment succeeded notification: {CustomerId}", inv.CustomerId); - return; - } + var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId).AnyContext(); + if (user == null) { + _logger.LogError("Unable to find billing user: {user}", org.BillingChangedByUserId); + return; + } - var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId).AnyContext(); - if (user == null) { - _logger.LogError("Unable to find billing user: {user}", org.BillingChangedByUserId); - return; - } + _logger.LogInformation("Stripe payment succeeded. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName}", inv.CustomerId, org.Id, org.Name); + } - _logger.LogInformation("Stripe payment succeeded. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName}", inv.CustomerId, org.Id, org.Name); + private async Task InvoicePaymentFailedAsync(Invoice inv) { + var org = await _organizationRepository.GetByStripeCustomerIdAsync(inv.CustomerId).AnyContext(); + if (org == null) { + _logger.LogError("Unknown customer id in payment failed notification: {CustomerId}", inv.CustomerId); + return; } - private async Task InvoicePaymentFailedAsync(Invoice inv) { - var org = await _organizationRepository.GetByStripeCustomerIdAsync(inv.CustomerId).AnyContext(); - if (org == null) { - _logger.LogError("Unknown customer id in payment failed notification: {CustomerId}", inv.CustomerId); - return; - } - - var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId).AnyContext(); - if (user == null) { - _logger.LogError("Unable to find billing user: {0}", org.BillingChangedByUserId); - return; - } - - _logger.LogInformation("Stripe payment failed. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName} Email: {EmailAddress}", inv.CustomerId, org.Id, org.Name, user.EmailAddress); - await _mailer.SendOrganizationPaymentFailedAsync(user, org).AnyContext(); + var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId).AnyContext(); + if (user == null) { + _logger.LogError("Unable to find billing user: {0}", org.BillingChangedByUserId); + return; } + + _logger.LogInformation("Stripe payment failed. Customer: {CustomerId} Org: {organization} Org Name: {OrganizationName} Email: {EmailAddress}", inv.CustomerId, org.Id, org.Name, user.EmailAddress); + await _mailer.SendOrganizationPaymentFailedAsync(user, org).AnyContext(); } } diff --git a/src/Exceptionless.Core/Bootstrapper.cs b/src/Exceptionless.Core/Bootstrapper.cs index d43aeed057..1b9e3665a6 100644 --- a/src/Exceptionless.Core/Bootstrapper.cs +++ b/src/Exceptionless.Core/Bootstrapper.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using AutoMapper.EquivalencyExpression; using Exceptionless.Core.Authentication; using Exceptionless.Core.Billing; @@ -52,245 +49,245 @@ using DataDictionary = Exceptionless.Core.Models.DataDictionary; using MaintainIndexesJob = Foundatio.Repositories.Elasticsearch.Jobs.MaintainIndexesJob; -namespace Exceptionless.Core { - public class Bootstrapper { - public static void RegisterServices(IServiceCollection container) { - JsonConvert.DefaultSettings = () => new JsonSerializerSettings { - DateParseHandling = DateParseHandling.DateTimeOffset +namespace Exceptionless.Core; + +public class Bootstrapper { + public static void RegisterServices(IServiceCollection container) { + JsonConvert.DefaultSettings = () => new JsonSerializerSettings { + DateParseHandling = DateParseHandling.DateTimeOffset + }; + + container.AddSingleton(s => GetJsonContractResolver()); + container.AddSingleton(s => { + // NOTE: These settings may need to be synced in the Elastic Configuration. + var settings = new JsonSerializerSettings { + MissingMemberHandling = MissingMemberHandling.Ignore, + DateParseHandling = DateParseHandling.DateTimeOffset, + ContractResolver = s.GetRequiredService() }; - container.AddSingleton(s => GetJsonContractResolver()); - container.AddSingleton(s => { - // NOTE: These settings may need to be synced in the Elastic Configuration. - var settings = new JsonSerializerSettings { - MissingMemberHandling = MissingMemberHandling.Ignore, - DateParseHandling = DateParseHandling.DateTimeOffset, - ContractResolver = s.GetRequiredService() - }; - - settings.AddModelConverters(s.GetRequiredService>()); - return settings; + settings.AddModelConverters(s.GetRequiredService>()); + return settings; + }); + + container.AddSingleton(s => JsonSerializer.Create(s.GetRequiredService())); + container.AddSingleton(s => new JsonNetSerializer(s.GetRequiredService())); + container.AddSingleton(s => new JsonNetSerializer(s.GetRequiredService())); + + container.AddSingleton(s => new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = s.GetRequiredService(), CloneValues = true, Serializer = s.GetRequiredService() })); + container.AddSingleton(s => new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = s.GetRequiredService() })); + + container.AddSingleton(); + container.AddSingleton(s => s.GetRequiredService().Client); + container.AddSingleton(s => s.GetRequiredService()); + container.AddStartupAction(); + + container.AddStartupAction("Create Sample Data", CreateSampleDataAsync); + + container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); + container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); + container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); + container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); + container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); + container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); + + container.AddSingleton(typeof(IWorkItemHandler), typeof(Bootstrapper).Assembly, typeof(ReindexWorkItemHandler).Assembly); + container.AddSingleton(s => { + var handlers = new WorkItemHandlers(); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + return handlers; + }); + + container.AddSingleton(s => CreateQueue(s)); + container.AddSingleton(s => CreateQueue(s)); + container.AddSingleton(s => CreateQueue(s)); + container.AddSingleton(s => CreateQueue(s)); + container.AddSingleton(s => CreateQueue(s)); + container.AddSingleton(s => CreateQueue(s, TimeSpan.FromHours(1))); + + container.AddSingleton(); + container.AddSingleton(); + container.AddStartupAction(); + container.AddSingleton(s => new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = s.GetRequiredService(), Serializer = s.GetRequiredService() })); + container.AddSingleton(s => s.GetRequiredService()); + container.AddSingleton(s => s.GetRequiredService()); + + container.AddSingleton(s => new InMemoryFileStorage(new InMemoryFileStorageOptions { + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); + + container.AddSingleton(typeof(IMigration), typeof(Bootstrapper).Assembly); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(s => s.GetRequiredService().Migrations); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + + container.AddSingleton(); + container.AddSingleton(); + + container.AddSingleton(s => new ElasticQueryParser()); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + + container.AddSingleton(typeof(IValidator<>), typeof(Bootstrapper).Assembly); + container.AddSingleton(typeof(IPipelineAction), typeof(Bootstrapper).Assembly); + container.AddSingleton(typeof(IPlugin), typeof(Bootstrapper).Assembly); + container.AddSingleton(typeof(IJob), typeof(Bootstrapper).Assembly); + container.AddSingleton(); + container.AddSingleton(); + + container.AddSingleton(); + container.AddSingleton(s => new InMemoryMailSender()); + + container.AddSingleton(s => new CacheLockProvider(s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + container.AddSingleton(s => s.GetRequiredService()); + container.AddTransient(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + + container.AddTransient(); + + container.AddTransient(); + container.AddSingleton(s => { + var profiles = s.GetServices(); + var c = new MapperConfiguration(cfg => { + cfg.AddCollectionMappers(); + cfg.ConstructServicesUsing(s.GetRequiredService); + + foreach (var profile in profiles) + cfg.AddProfile(profile); }); - container.AddSingleton(s => JsonSerializer.Create(s.GetRequiredService())); - container.AddSingleton(s => new JsonNetSerializer(s.GetRequiredService())); - container.AddSingleton(s => new JsonNetSerializer(s.GetRequiredService())); - - container.AddSingleton(s => new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = s.GetRequiredService(), CloneValues = true, Serializer = s.GetRequiredService() })); - container.AddSingleton(s => new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = s.GetRequiredService() })); - - container.AddSingleton(); - container.AddSingleton(s => s.GetRequiredService().Client); - container.AddSingleton(s => s.GetRequiredService()); - container.AddStartupAction(); - - container.AddStartupAction("Create Sample Data", CreateSampleDataAsync); - - container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); - container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); - container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); - container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); - container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); - container.AddSingleton>(s => new MetricsQueueBehavior(s.GetRequiredService())); - - container.AddSingleton(typeof(IWorkItemHandler), typeof(Bootstrapper).Assembly, typeof(ReindexWorkItemHandler).Assembly); - container.AddSingleton(s => { - var handlers = new WorkItemHandlers(); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - return handlers; - }); + return c.CreateMapper(); + }); + } - container.AddSingleton(s => CreateQueue(s)); - container.AddSingleton(s => CreateQueue(s)); - container.AddSingleton(s => CreateQueue(s)); - container.AddSingleton(s => CreateQueue(s)); - container.AddSingleton(s => CreateQueue(s)); - container.AddSingleton(s => CreateQueue(s, TimeSpan.FromHours(1))); - - container.AddSingleton(); - container.AddSingleton(); - container.AddStartupAction(); - container.AddSingleton(s => new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = s.GetRequiredService(), Serializer = s.GetRequiredService()})); - container.AddSingleton(s => s.GetRequiredService()); - container.AddSingleton(s => s.GetRequiredService()); - - container.AddSingleton(s => new InMemoryFileStorage(new InMemoryFileStorageOptions { - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - - container.AddSingleton(typeof(IMigration), typeof(Bootstrapper).Assembly); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(s => s.GetRequiredService().Migrations); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - - container.AddSingleton(); - container.AddSingleton(); - - container.AddSingleton(s => new ElasticQueryParser()); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - - container.AddSingleton(typeof(IValidator<>), typeof(Bootstrapper).Assembly); - container.AddSingleton(typeof(IPipelineAction), typeof(Bootstrapper).Assembly); - container.AddSingleton(typeof(IPlugin), typeof(Bootstrapper).Assembly); - container.AddSingleton(typeof(IJob), typeof(Bootstrapper).Assembly); - container.AddSingleton(); - container.AddSingleton(); - - container.AddSingleton(); - container.AddSingleton(s => new InMemoryMailSender()); - - container.AddSingleton(s => new CacheLockProvider(s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - container.AddSingleton(s => s.GetRequiredService()); - container.AddTransient(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - container.AddSingleton(); - - container.AddTransient(); - - container.AddTransient(); - container.AddSingleton(s => { - var profiles = s.GetServices(); - var c = new MapperConfiguration(cfg => { - cfg.AddCollectionMappers(); - cfg.ConstructServicesUsing(s.GetRequiredService); - - foreach (var profile in profiles) - cfg.AddProfile(profile); - }); - - return c.CreateMapper(); - }); - } - - public static void LogConfiguration(IServiceProvider serviceProvider, AppOptions appOptions, ILogger logger) { - if (!logger.IsEnabled(LogLevel.Warning)) - return; - - if (String.IsNullOrEmpty(appOptions.CacheOptions.Provider)) - logger.LogWarning("Distributed cache is NOT enabled on {MachineName}.", Environment.MachineName); - - if (String.IsNullOrEmpty(appOptions.MessageBusOptions.Provider)) - logger.LogWarning("Distributed message bus is NOT enabled on {MachineName}.", Environment.MachineName); - - if (String.IsNullOrEmpty(appOptions.MetricOptions.Provider)) - logger.LogWarning("Metrics reporting is NOT enabled on {MachineName}.", Environment.MachineName); - - if (String.IsNullOrEmpty(appOptions.QueueOptions.Provider)) - logger.LogWarning("Distributed queue is NOT enabled on {MachineName}.", Environment.MachineName); - - if (String.IsNullOrEmpty(appOptions.StorageOptions.Provider)) - logger.LogWarning("Distributed storage is NOT enabled on {MachineName}.", Environment.MachineName); - - if (!appOptions.EnableWebSockets) - logger.LogWarning("Web Sockets is NOT enabled on {MachineName}", Environment.MachineName); - - if (String.IsNullOrEmpty(appOptions.EmailOptions.SmtpHost)) - logger.LogWarning("Emails will NOT be sent until the SmtpHost is configured on {MachineName}", Environment.MachineName); - - var fileStorage = serviceProvider.GetService(); - if (fileStorage is InMemoryFileStorage) - logger.LogWarning("Using in memory file storage on {MachineName}", Environment.MachineName); - - if (appOptions.ElasticsearchOptions.DisableIndexConfiguration) - logger.LogWarning("Index Configuration is NOT enabled on {MachineName}", Environment.MachineName); - - if (appOptions.EventSubmissionDisabled) - logger.LogWarning("Event Submission is NOT enabled on {MachineName}", Environment.MachineName); - - if (!appOptions.AuthOptions.EnableAccountCreation) - logger.LogWarning("Account Creation is NOT enabled on {MachineName}", Environment.MachineName); - } - - private static async Task CreateSampleDataAsync(IServiceProvider container) { - var options = container.GetRequiredService(); - if (!options.EnableSampleData) - return; - - var elasticsearchOptions = container.GetRequiredService(); - if (elasticsearchOptions.DisableIndexConfiguration) - return; - - var userRepository = container.GetRequiredService(); - if (await userRepository.CountAsync().AnyContext() != 0) - return; - - var dataHelper = container.GetRequiredService(); - await dataHelper.CreateDataAsync().AnyContext(); - } - - public static void AddHostedJobs(IServiceCollection services, ILoggerFactory loggerFactory) { - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - services.AddJob(true); - - services.AddCronJob("0 1 * * *"); - services.AddCronJob("30 */4 * * *"); - services.AddCronJob("45 */8 * * *"); - services.AddCronJob("10 */2 * * *"); - - var logger = loggerFactory.CreateLogger(); - logger.LogWarning("Jobs running in process."); - } - - public static DynamicTypeContractResolver GetJsonContractResolver() { - var resolver = new DynamicTypeContractResolver(new LowerCaseUnderscorePropertyNamesContractResolver()); - resolver.UseDefaultResolverFor(typeof(DataDictionary), typeof(SettingsDictionary), typeof(VersionOne.VersionOneWebHookStack), typeof(VersionOne.VersionOneWebHookEvent)); - return resolver; - } - - private static IQueue CreateQueue(IServiceProvider container, TimeSpan? workItemTimeout = null) where T : class { - var loggerFactory = container.GetRequiredService(); - - var behaviors = container.GetServices>().ToList(); - behaviors.Add(new MetricsQueueBehavior(container.GetRequiredService(), null, TimeSpan.FromSeconds(2), loggerFactory)); - - return new InMemoryQueue(new InMemoryQueueOptions { - Behaviors = behaviors, - WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), - Serializer = container.GetRequiredService(), - LoggerFactory = loggerFactory - }); - } + public static void LogConfiguration(IServiceProvider serviceProvider, AppOptions appOptions, ILogger logger) { + if (!logger.IsEnabled(LogLevel.Warning)) + return; + + if (String.IsNullOrEmpty(appOptions.CacheOptions.Provider)) + logger.LogWarning("Distributed cache is NOT enabled on {MachineName}.", Environment.MachineName); + + if (String.IsNullOrEmpty(appOptions.MessageBusOptions.Provider)) + logger.LogWarning("Distributed message bus is NOT enabled on {MachineName}.", Environment.MachineName); + + if (String.IsNullOrEmpty(appOptions.MetricOptions.Provider)) + logger.LogWarning("Metrics reporting is NOT enabled on {MachineName}.", Environment.MachineName); + + if (String.IsNullOrEmpty(appOptions.QueueOptions.Provider)) + logger.LogWarning("Distributed queue is NOT enabled on {MachineName}.", Environment.MachineName); + + if (String.IsNullOrEmpty(appOptions.StorageOptions.Provider)) + logger.LogWarning("Distributed storage is NOT enabled on {MachineName}.", Environment.MachineName); + + if (!appOptions.EnableWebSockets) + logger.LogWarning("Web Sockets is NOT enabled on {MachineName}", Environment.MachineName); + + if (String.IsNullOrEmpty(appOptions.EmailOptions.SmtpHost)) + logger.LogWarning("Emails will NOT be sent until the SmtpHost is configured on {MachineName}", Environment.MachineName); + + var fileStorage = serviceProvider.GetService(); + if (fileStorage is InMemoryFileStorage) + logger.LogWarning("Using in memory file storage on {MachineName}", Environment.MachineName); + + if (appOptions.ElasticsearchOptions.DisableIndexConfiguration) + logger.LogWarning("Index Configuration is NOT enabled on {MachineName}", Environment.MachineName); + + if (appOptions.EventSubmissionDisabled) + logger.LogWarning("Event Submission is NOT enabled on {MachineName}", Environment.MachineName); + + if (!appOptions.AuthOptions.EnableAccountCreation) + logger.LogWarning("Account Creation is NOT enabled on {MachineName}", Environment.MachineName); + } + + private static async Task CreateSampleDataAsync(IServiceProvider container) { + var options = container.GetRequiredService(); + if (!options.EnableSampleData) + return; + + var elasticsearchOptions = container.GetRequiredService(); + if (elasticsearchOptions.DisableIndexConfiguration) + return; + + var userRepository = container.GetRequiredService(); + if (await userRepository.CountAsync().AnyContext() != 0) + return; + + var dataHelper = container.GetRequiredService(); + await dataHelper.CreateDataAsync().AnyContext(); + } + + public static void AddHostedJobs(IServiceCollection services, ILoggerFactory loggerFactory) { + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + services.AddJob(true); + + services.AddCronJob("0 1 * * *"); + services.AddCronJob("30 */4 * * *"); + services.AddCronJob("45 */8 * * *"); + services.AddCronJob("10 */2 * * *"); + + var logger = loggerFactory.CreateLogger(); + logger.LogWarning("Jobs running in process."); + } + + public static DynamicTypeContractResolver GetJsonContractResolver() { + var resolver = new DynamicTypeContractResolver(new LowerCaseUnderscorePropertyNamesContractResolver()); + resolver.UseDefaultResolverFor(typeof(DataDictionary), typeof(SettingsDictionary), typeof(VersionOne.VersionOneWebHookStack), typeof(VersionOne.VersionOneWebHookEvent)); + return resolver; + } + + private static IQueue CreateQueue(IServiceProvider container, TimeSpan? workItemTimeout = null) where T : class { + var loggerFactory = container.GetRequiredService(); + + var behaviors = container.GetServices>().ToList(); + behaviors.Add(new MetricsQueueBehavior(container.GetRequiredService(), null, TimeSpan.FromSeconds(2), loggerFactory)); + + return new InMemoryQueue(new InMemoryQueueOptions { + Behaviors = behaviors, + WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), + Serializer = container.GetRequiredService(), + LoggerFactory = loggerFactory + }); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/AppOptions.cs b/src/Exceptionless.Core/Configuration/AppOptions.cs index 9197034242..6bd810bf2c 100644 --- a/src/Exceptionless.Core/Configuration/AppOptions.cs +++ b/src/Exceptionless.Core/Configuration/AppOptions.cs @@ -1,141 +1,139 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace Exceptionless.Core { - public class AppOptions { - public string BaseURL { get; internal set; } +namespace Exceptionless.Core; - /// - /// Internal project id keeps us from recursively logging to our self - /// - public string InternalProjectId { get; internal set; } +public class AppOptions { + public string BaseURL { get; internal set; } - /// - /// Configures the exceptionless client api key, which logs all internal errors and log messages. - /// - public string ExceptionlessApiKey { get; internal set; } + /// + /// Internal project id keeps us from recursively logging to our self + /// + public string InternalProjectId { get; internal set; } - /// - /// Configures the exceptionless client server url, which logs all internal errors and log messages. - /// - public string ExceptionlessServerUrl { get; internal set; } + /// + /// Configures the exceptionless client api key, which logs all internal errors and log messages. + /// + public string ExceptionlessApiKey { get; internal set; } - [JsonConverter(typeof(StringEnumConverter))] - public AppMode AppMode { get; internal set; } - public string AppScope { get; internal set; } + /// + /// Configures the exceptionless client server url, which logs all internal errors and log messages. + /// + public string ExceptionlessServerUrl { get; internal set; } - public bool RunJobsInProcess { get; internal set; } + [JsonConverter(typeof(StringEnumConverter))] + public AppMode AppMode { get; internal set; } + public string AppScope { get; internal set; } - public int JobsIterationLimit { get; set; } + public bool RunJobsInProcess { get; internal set; } - public int BotThrottleLimit { get; internal set; } + public int JobsIterationLimit { get; set; } - public int ApiThrottleLimit { get; internal set; } + public int BotThrottleLimit { get; internal set; } - public bool EnableArchive { get; internal set; } - - public bool EnableSampleData { get; internal set; } + public int ApiThrottleLimit { get; internal set; } - public bool EventSubmissionDisabled { get; internal set; } + public bool EnableArchive { get; internal set; } - internal List DisabledPipelineActions { get; set; } - internal List DisabledPlugins { get; set; } + public bool EnableSampleData { get; internal set; } - /// - /// In bytes - /// - public long MaximumEventPostSize { get; internal set; } + public bool EventSubmissionDisabled { get; internal set; } - public int MaximumRetentionDays { get; internal set; } + internal List DisabledPipelineActions { get; set; } + internal List DisabledPlugins { get; set; } - public bool EnableRepositoryNotifications { get; internal set; } + /// + /// In bytes + /// + public long MaximumEventPostSize { get; internal set; } - public bool EnableWebSockets { get; internal set; } + public int MaximumRetentionDays { get; internal set; } - public string Version { get; internal set; } + public bool EnableRepositoryNotifications { get; internal set; } - public string InformationalVersion { get; internal set; } + public bool EnableWebSockets { get; internal set; } - public string GoogleGeocodingApiKey { get; internal set; } + public string Version { get; internal set; } - public string MaxMindGeoIpKey { get; internal set; } + public string InformationalVersion { get; internal set; } - public int BulkBatchSize { get; internal set; } + public string GoogleGeocodingApiKey { get; internal set; } - public CacheOptions CacheOptions { get; internal set; } - public MessageBusOptions MessageBusOptions { get; internal set; } - public MetricOptions MetricOptions { get; internal set; } - public QueueOptions QueueOptions { get; internal set; } - public StorageOptions StorageOptions { get; internal set; } - public EmailOptions EmailOptions { get; internal set; } - public ElasticsearchOptions ElasticsearchOptions { get; internal set; } - public IntercomOptions IntercomOptions { get; internal set; } - public SlackOptions SlackOptions { get; internal set; } - public StripeOptions StripeOptions { get; internal set; } - public AuthOptions AuthOptions { get; internal set; } + public string MaxMindGeoIpKey { get; internal set; } - public static AppOptions ReadFromConfiguration(IConfiguration config) { - var options = new AppOptions(); - options.BaseURL = config.GetValue(nameof(options.BaseURL))?.TrimEnd('/'); - options.InternalProjectId = config.GetValue(nameof(options.InternalProjectId), "54b56e480ef9605a88a13153"); - options.ExceptionlessApiKey = config.GetValue(nameof(options.ExceptionlessApiKey)); - options.ExceptionlessServerUrl = config.GetValue(nameof(options.ExceptionlessServerUrl)); + public int BulkBatchSize { get; internal set; } - options.AppMode = config.GetValue(nameof(options.AppMode), AppMode.Production); - options.AppScope = config.GetValue(nameof(options.AppScope), options.AppMode.ToScope()); - options.RunJobsInProcess = config.GetValue(nameof(options.RunJobsInProcess), options.AppMode == AppMode.Development); - options.JobsIterationLimit = config.GetValue(nameof(options.JobsIterationLimit), -1); - options.BotThrottleLimit = config.GetValue(nameof(options.BotThrottleLimit), 25).NormalizeValue(); + public CacheOptions CacheOptions { get; internal set; } + public MessageBusOptions MessageBusOptions { get; internal set; } + public MetricOptions MetricOptions { get; internal set; } + public QueueOptions QueueOptions { get; internal set; } + public StorageOptions StorageOptions { get; internal set; } + public EmailOptions EmailOptions { get; internal set; } + public ElasticsearchOptions ElasticsearchOptions { get; internal set; } + public IntercomOptions IntercomOptions { get; internal set; } + public SlackOptions SlackOptions { get; internal set; } + public StripeOptions StripeOptions { get; internal set; } + public AuthOptions AuthOptions { get; internal set; } - options.ApiThrottleLimit = config.GetValue(nameof(options.ApiThrottleLimit), options.AppMode == AppMode.Development ? Int32.MaxValue : 3500).NormalizeValue(); - options.EnableArchive = config.GetValue(nameof(options.EnableArchive), false); - options.EnableSampleData = config.GetValue(nameof(options.EnableSampleData), options.AppMode == AppMode.Development); - options.EventSubmissionDisabled = config.GetValue(nameof(options.EventSubmissionDisabled), false); - options.DisabledPipelineActions = config.GetValueList(nameof(options.DisabledPipelineActions)); - options.DisabledPlugins = config.GetValueList(nameof(options.DisabledPlugins)); - options.MaximumEventPostSize = config.GetValue(nameof(options.MaximumEventPostSize), 200000).NormalizeValue(); - options.MaximumRetentionDays = config.GetValue(nameof(options.MaximumRetentionDays), 180).NormalizeValue(); + public static AppOptions ReadFromConfiguration(IConfiguration config) { + var options = new AppOptions(); + options.BaseURL = config.GetValue(nameof(options.BaseURL))?.TrimEnd('/'); + options.InternalProjectId = config.GetValue(nameof(options.InternalProjectId), "54b56e480ef9605a88a13153"); + options.ExceptionlessApiKey = config.GetValue(nameof(options.ExceptionlessApiKey)); + options.ExceptionlessServerUrl = config.GetValue(nameof(options.ExceptionlessServerUrl)); - options.GoogleGeocodingApiKey = config.GetValue(nameof(options.GoogleGeocodingApiKey)); - options.MaxMindGeoIpKey = config.GetValue(nameof(options.MaxMindGeoIpKey)); + options.AppMode = config.GetValue(nameof(options.AppMode), AppMode.Production); + options.AppScope = config.GetValue(nameof(options.AppScope), options.AppMode.ToScope()); + options.RunJobsInProcess = config.GetValue(nameof(options.RunJobsInProcess), options.AppMode == AppMode.Development); + options.JobsIterationLimit = config.GetValue(nameof(options.JobsIterationLimit), -1); + options.BotThrottleLimit = config.GetValue(nameof(options.BotThrottleLimit), 25).NormalizeValue(); - options.BulkBatchSize = config.GetValue(nameof(options.BulkBatchSize), 1000); + options.ApiThrottleLimit = config.GetValue(nameof(options.ApiThrottleLimit), options.AppMode == AppMode.Development ? Int32.MaxValue : 3500).NormalizeValue(); + options.EnableArchive = config.GetValue(nameof(options.EnableArchive), false); + options.EnableSampleData = config.GetValue(nameof(options.EnableSampleData), options.AppMode == AppMode.Development); + options.EventSubmissionDisabled = config.GetValue(nameof(options.EventSubmissionDisabled), false); + options.DisabledPipelineActions = config.GetValueList(nameof(options.DisabledPipelineActions)); + options.DisabledPlugins = config.GetValueList(nameof(options.DisabledPlugins)); + options.MaximumEventPostSize = config.GetValue(nameof(options.MaximumEventPostSize), 200000).NormalizeValue(); + options.MaximumRetentionDays = config.GetValue(nameof(options.MaximumRetentionDays), 180).NormalizeValue(); - options.EnableRepositoryNotifications = config.GetValue(nameof(options.EnableRepositoryNotifications), true); - options.EnableWebSockets = config.GetValue(nameof(options.EnableWebSockets), true); + options.GoogleGeocodingApiKey = config.GetValue(nameof(options.GoogleGeocodingApiKey)); + options.MaxMindGeoIpKey = config.GetValue(nameof(options.MaxMindGeoIpKey)); - try { - var versionInfo = FileVersionInfo.GetVersionInfo(typeof(AppOptions).Assembly.Location); - options.Version = versionInfo.FileVersion; - options.InformationalVersion = versionInfo.ProductVersion; - } - catch { } + options.BulkBatchSize = config.GetValue(nameof(options.BulkBatchSize), 1000); - options.CacheOptions = CacheOptions.ReadFromConfiguration(config, options); - options.MessageBusOptions = MessageBusOptions.ReadFromConfiguration(config, options); - options.MetricOptions = MetricOptions.ReadFromConfiguration(config); - options.QueueOptions = QueueOptions.ReadFromConfiguration(config, options); - options.StorageOptions = StorageOptions.ReadFromConfiguration(config, options); - options.EmailOptions = EmailOptions.ReadFromConfiguration(config, options); - options.ElasticsearchOptions = ElasticsearchOptions.ReadFromConfiguration(config, options); - options.IntercomOptions = IntercomOptions.ReadFromConfiguration(config); - options.SlackOptions = SlackOptions.ReadFromConfiguration(config); - options.StripeOptions = StripeOptions.ReadFromConfiguration(config); - options.AuthOptions = AuthOptions.ReadFromConfiguration(config); + options.EnableRepositoryNotifications = config.GetValue(nameof(options.EnableRepositoryNotifications), true); + options.EnableWebSockets = config.GetValue(nameof(options.EnableWebSockets), true); - return options; + try { + var versionInfo = FileVersionInfo.GetVersionInfo(typeof(AppOptions).Assembly.Location); + options.Version = versionInfo.FileVersion; + options.InformationalVersion = versionInfo.ProductVersion; } + catch { } + + options.CacheOptions = CacheOptions.ReadFromConfiguration(config, options); + options.MessageBusOptions = MessageBusOptions.ReadFromConfiguration(config, options); + options.MetricOptions = MetricOptions.ReadFromConfiguration(config); + options.QueueOptions = QueueOptions.ReadFromConfiguration(config, options); + options.StorageOptions = StorageOptions.ReadFromConfiguration(config, options); + options.EmailOptions = EmailOptions.ReadFromConfiguration(config, options); + options.ElasticsearchOptions = ElasticsearchOptions.ReadFromConfiguration(config, options); + options.IntercomOptions = IntercomOptions.ReadFromConfiguration(config); + options.SlackOptions = SlackOptions.ReadFromConfiguration(config); + options.StripeOptions = StripeOptions.ReadFromConfiguration(config); + options.AuthOptions = AuthOptions.ReadFromConfiguration(config); + + return options; } +} - public enum AppMode { - Development, - Staging, - Production - } -} \ No newline at end of file +public enum AppMode { + Development, + Staging, + Production +} diff --git a/src/Exceptionless.Core/Configuration/AuthOptions.cs b/src/Exceptionless.Core/Configuration/AuthOptions.cs index e8f89018f0..6d0f50a9dc 100644 --- a/src/Exceptionless.Core/Configuration/AuthOptions.cs +++ b/src/Exceptionless.Core/Configuration/AuthOptions.cs @@ -2,48 +2,48 @@ using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class AuthOptions { - public bool EnableAccountCreation { get; internal set; } - public bool EnableActiveDirectoryAuth { get; internal set; } +namespace Exceptionless.Core.Configuration; - public string MicrosoftId { get; internal set; } +public class AuthOptions { + public bool EnableAccountCreation { get; internal set; } + public bool EnableActiveDirectoryAuth { get; internal set; } - public string MicrosoftSecret { get; internal set; } + public string MicrosoftId { get; internal set; } - public string FacebookId { get; internal set; } + public string MicrosoftSecret { get; internal set; } - public string FacebookSecret { get; internal set; } + public string FacebookId { get; internal set; } - public string GitHubId { get; internal set; } + public string FacebookSecret { get; internal set; } - public string GitHubSecret { get; internal set; } + public string GitHubId { get; internal set; } - public string GoogleId { get; internal set; } + public string GitHubSecret { get; internal set; } - public string GoogleSecret { get; internal set; } + public string GoogleId { get; internal set; } - public string LdapConnectionString { get; internal set; } + public string GoogleSecret { get; internal set; } - public static AuthOptions ReadFromConfiguration(IConfiguration config) { - var options = new AuthOptions(); + public string LdapConnectionString { get; internal set; } - options.EnableAccountCreation = config.GetValue(nameof(options.EnableAccountCreation), true); + public static AuthOptions ReadFromConfiguration(IConfiguration config) { + var options = new AuthOptions(); - options.LdapConnectionString = config.GetConnectionString("LDAP"); - options.EnableActiveDirectoryAuth = config.GetValue(nameof(options.EnableActiveDirectoryAuth), options.LdapConnectionString != null); + options.EnableAccountCreation = config.GetValue(nameof(options.EnableAccountCreation), true); - var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); - options.GoogleId = oAuth.GetString(nameof(options.GoogleId)); - options.GoogleSecret = oAuth.GetString(nameof(options.GoogleSecret)); - options.MicrosoftId = oAuth.GetString(nameof(options.MicrosoftId)); - options.MicrosoftSecret = oAuth.GetString(nameof(options.MicrosoftSecret)); - options.FacebookId = oAuth.GetString(nameof(options.FacebookId)); - options.FacebookSecret = oAuth.GetString(nameof(options.FacebookSecret)); - options.GitHubId = oAuth.GetString(nameof(options.GitHubId)); - options.GitHubSecret = oAuth.GetString(nameof(options.GitHubSecret)); + options.LdapConnectionString = config.GetConnectionString("LDAP"); + options.EnableActiveDirectoryAuth = config.GetValue(nameof(options.EnableActiveDirectoryAuth), options.LdapConnectionString != null); - return options; - } + var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); + options.GoogleId = oAuth.GetString(nameof(options.GoogleId)); + options.GoogleSecret = oAuth.GetString(nameof(options.GoogleSecret)); + options.MicrosoftId = oAuth.GetString(nameof(options.MicrosoftId)); + options.MicrosoftSecret = oAuth.GetString(nameof(options.MicrosoftSecret)); + options.FacebookId = oAuth.GetString(nameof(options.FacebookId)); + options.FacebookSecret = oAuth.GetString(nameof(options.FacebookSecret)); + options.GitHubId = oAuth.GetString(nameof(options.GitHubId)); + options.GitHubSecret = oAuth.GetString(nameof(options.GitHubSecret)); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/CacheOptions.cs b/src/Exceptionless.Core/Configuration/CacheOptions.cs index 72ae528854..6c52c24f1f 100644 --- a/src/Exceptionless.Core/Configuration/CacheOptions.cs +++ b/src/Exceptionless.Core/Configuration/CacheOptions.cs @@ -1,36 +1,34 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class CacheOptions { - public string ConnectionString { get; internal set; } - public string Provider { get; internal set; } - public Dictionary Data { get; internal set; } +namespace Exceptionless.Core.Configuration; - public string Scope { get; internal set; } - public string ScopePrefix { get; internal set; } +public class CacheOptions { + public string ConnectionString { get; internal set; } + public string Provider { get; internal set; } + public Dictionary Data { get; internal set; } - public static CacheOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { - var options = new CacheOptions(); + public string Scope { get; internal set; } + public string ScopePrefix { get; internal set; } - options.Scope = appOptions.AppScope; - options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; + public static CacheOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { + var options = new CacheOptions(); - string cs = config.GetConnectionString("Cache"); - options.Data = cs.ParseConnectionString(); - options.Provider = options.Data.GetString(nameof(options.Provider)); + options.Scope = appOptions.AppScope; + options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; - var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - if (!String.IsNullOrEmpty(providerConnectionString)) - options.Data.AddRange(providerConnectionString.ParseConnectionString()); + string cs = config.GetConnectionString("Cache"); + options.Data = cs.ParseConnectionString(); + options.Provider = options.Data.GetString(nameof(options.Provider)); - options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; + if (!String.IsNullOrEmpty(providerConnectionString)) + options.Data.AddRange(providerConnectionString.ParseConnectionString()); - return options; - } + options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs index fbab5d2410..3168335a1e 100644 --- a/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs +++ b/src/Exceptionless.Core/Configuration/ElasticsearchOptions.cs @@ -1,74 +1,73 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class ElasticsearchOptions { - public string ServerUrl { get; internal set; } - public int NumberOfShards { get; internal set; } = 1; - public int NumberOfReplicas { get; internal set; } - public int FieldsLimit { get; internal set; } = 1500; - public bool EnableMapperSizePlugin { get; internal set; } +namespace Exceptionless.Core.Configuration; - public string Scope { get; internal set; } - public string ScopePrefix { get; internal set; } +public class ElasticsearchOptions { + public string ServerUrl { get; internal set; } + public int NumberOfShards { get; internal set; } = 1; + public int NumberOfReplicas { get; internal set; } + public int FieldsLimit { get; internal set; } = 1500; + public bool EnableMapperSizePlugin { get; internal set; } - public bool EnableSnapshotJobs { get; set; } - public bool DisableIndexConfiguration { get; set; } + public string Scope { get; internal set; } + public string ScopePrefix { get; internal set; } - public string Password { get; internal set; } - public string UserName { get; internal set; } - public DateTime ReindexCutOffDate { get; internal set; } - public ElasticsearchOptions ElasticsearchToMigrate { get; internal set; } + public bool EnableSnapshotJobs { get; set; } + public bool DisableIndexConfiguration { get; set; } - public static ElasticsearchOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { - var options = new ElasticsearchOptions(); + public string Password { get; internal set; } + public string UserName { get; internal set; } + public DateTime ReindexCutOffDate { get; internal set; } + public ElasticsearchOptions ElasticsearchToMigrate { get; internal set; } - options.Scope = appOptions.AppScope; - options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; + public static ElasticsearchOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { + var options = new ElasticsearchOptions(); - options.DisableIndexConfiguration = config.GetValue(nameof(options.DisableIndexConfiguration), false); - options.EnableSnapshotJobs = config.GetValue(nameof(options.EnableSnapshotJobs), String.IsNullOrEmpty(options.ScopePrefix) && appOptions.AppMode == AppMode.Production); - options.ReindexCutOffDate = config.GetValue(nameof(options.ReindexCutOffDate), DateTime.MinValue); + options.Scope = appOptions.AppScope; + options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; - string connectionString = config.GetConnectionString("Elasticsearch"); - ParseConnectionString(connectionString, options, appOptions.AppMode); + options.DisableIndexConfiguration = config.GetValue(nameof(options.DisableIndexConfiguration), false); + options.EnableSnapshotJobs = config.GetValue(nameof(options.EnableSnapshotJobs), String.IsNullOrEmpty(options.ScopePrefix) && appOptions.AppMode == AppMode.Production); + options.ReindexCutOffDate = config.GetValue(nameof(options.ReindexCutOffDate), DateTime.MinValue); - string connectionStringToMigrate = config.GetConnectionString("ElasticsearchToMigrate"); - if (!String.IsNullOrEmpty(connectionStringToMigrate)) { - options.ElasticsearchToMigrate = new ElasticsearchOptions { - ReindexCutOffDate = options.ReindexCutOffDate - }; + string connectionString = config.GetConnectionString("Elasticsearch"); + ParseConnectionString(connectionString, options, appOptions.AppMode); - ParseConnectionString(connectionStringToMigrate, options.ElasticsearchToMigrate, appOptions.AppMode); - } + string connectionStringToMigrate = config.GetConnectionString("ElasticsearchToMigrate"); + if (!String.IsNullOrEmpty(connectionStringToMigrate)) { + options.ElasticsearchToMigrate = new ElasticsearchOptions { + ReindexCutOffDate = options.ReindexCutOffDate + }; - return options; + ParseConnectionString(connectionStringToMigrate, options.ElasticsearchToMigrate, appOptions.AppMode); } - private static void ParseConnectionString(string connectionString, ElasticsearchOptions options, AppMode appMode) { - var pairs = connectionString.ParseConnectionString(); + return options; + } - options.ServerUrl = pairs.GetString("server", "http://localhost:9200"); + private static void ParseConnectionString(string connectionString, ElasticsearchOptions options, AppMode appMode) { + var pairs = connectionString.ParseConnectionString(); - int shards = pairs.GetValueOrDefault("shards", 1); - options.NumberOfShards = shards > 0 ? shards : 1; + options.ServerUrl = pairs.GetString("server", "http://localhost:9200"); - int replicas = pairs.GetValueOrDefault("replicas", appMode == AppMode.Production ? 1 : 0); - options.NumberOfReplicas = replicas > 0 ? replicas : 0; + int shards = pairs.GetValueOrDefault("shards", 1); + options.NumberOfShards = shards > 0 ? shards : 1; - int fieldsLimit = pairs.GetValueOrDefault("field-limit", 1500); - options.FieldsLimit = fieldsLimit > 0 ? fieldsLimit : 1500; + int replicas = pairs.GetValueOrDefault("replicas", appMode == AppMode.Production ? 1 : 0); + options.NumberOfReplicas = replicas > 0 ? replicas : 0; - options.EnableMapperSizePlugin = pairs.GetValueOrDefault("enable-size-plugin", appMode != AppMode.Development); + int fieldsLimit = pairs.GetValueOrDefault("field-limit", 1500); + options.FieldsLimit = fieldsLimit > 0 ? fieldsLimit : 1500; - options.UserName = pairs.GetString("username"); - options.Password = pairs.GetString("password"); + options.EnableMapperSizePlugin = pairs.GetValueOrDefault("enable-size-plugin", appMode != AppMode.Development); - string scope = pairs.GetString(nameof(options.Scope).ToLowerInvariant()); - if (!String.IsNullOrEmpty(scope)) - options.Scope = scope; - } + options.UserName = pairs.GetString("username"); + options.Password = pairs.GetString("password"); + + string scope = pairs.GetString(nameof(options.Scope).ToLowerInvariant()); + if (!String.IsNullOrEmpty(scope)) + options.Scope = scope; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/EmailOptions.cs b/src/Exceptionless.Core/Configuration/EmailOptions.cs index a379b3dde9..495f713c5c 100644 --- a/src/Exceptionless.Core/Configuration/EmailOptions.cs +++ b/src/Exceptionless.Core/Configuration/EmailOptions.cs @@ -1,77 +1,74 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Utility; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace Exceptionless.Core.Configuration { - public class EmailOptions { - public bool EnableDailySummary { get; internal set; } +namespace Exceptionless.Core.Configuration; - /// - /// All emails that do not match the AllowedOutboundAddresses will be sent to this address in QA mode - /// - public string TestEmailAddress { get; internal set; } +public class EmailOptions { + public bool EnableDailySummary { get; internal set; } - /// - /// Email addresses that match this comma delimited list of domains and email addresses will be allowed to be sent out in QA mode - /// - public List AllowedOutboundAddresses { get; internal set; } + /// + /// All emails that do not match the AllowedOutboundAddresses will be sent to this address in QA mode + /// + public string TestEmailAddress { get; internal set; } - public string SmtpFrom { get; internal set; } + /// + /// Email addresses that match this comma delimited list of domains and email addresses will be allowed to be sent out in QA mode + /// + public List AllowedOutboundAddresses { get; internal set; } - public string SmtpHost { get; internal set; } + public string SmtpFrom { get; internal set; } - public int SmtpPort { get; internal set; } + public string SmtpHost { get; internal set; } - [JsonConverter(typeof(StringEnumConverter))] - public SmtpEncryption SmtpEncryption { get; internal set; } + public int SmtpPort { get; internal set; } - public string SmtpUser { get; internal set; } + [JsonConverter(typeof(StringEnumConverter))] + public SmtpEncryption SmtpEncryption { get; internal set; } - public string SmtpPassword { get; internal set; } + public string SmtpUser { get; internal set; } - public static EmailOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { - var options = new EmailOptions(); + public string SmtpPassword { get; internal set; } - options.EnableDailySummary = config.GetValue(nameof(options.EnableDailySummary), appOptions.AppMode == AppMode.Production); - options.AllowedOutboundAddresses = config.GetValueList(nameof(options.AllowedOutboundAddresses)).Select(v => v.ToLowerInvariant()).ToList(); - options.TestEmailAddress = config.GetValue(nameof(options.TestEmailAddress), appOptions.AppMode == AppMode.Development ? "test@localhost" : ""); + public static EmailOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { + var options = new EmailOptions(); - string emailConnectionString = config.GetConnectionString("Email"); - if (!String.IsNullOrEmpty(emailConnectionString)) { - var uri = new SmtpUri(emailConnectionString); - options.SmtpHost = uri.Host; - options.SmtpPort = uri.Port; - options.SmtpUser = uri.User; - options.SmtpPassword = uri.Password; - } + options.EnableDailySummary = config.GetValue(nameof(options.EnableDailySummary), appOptions.AppMode == AppMode.Production); + options.AllowedOutboundAddresses = config.GetValueList(nameof(options.AllowedOutboundAddresses)).Select(v => v.ToLowerInvariant()).ToList(); + options.TestEmailAddress = config.GetValue(nameof(options.TestEmailAddress), appOptions.AppMode == AppMode.Development ? "test@localhost" : ""); - options.SmtpFrom = config.GetValue(nameof(options.SmtpFrom), appOptions.AppMode == AppMode.Development ? "Exceptionless " : ""); - options.SmtpEncryption = config.GetValue(nameof(options.SmtpEncryption), GetDefaultSmtpEncryption(options.SmtpPort)); + string emailConnectionString = config.GetConnectionString("Email"); + if (!String.IsNullOrEmpty(emailConnectionString)) { + var uri = new SmtpUri(emailConnectionString); + options.SmtpHost = uri.Host; + options.SmtpPort = uri.Port; + options.SmtpUser = uri.User; + options.SmtpPassword = uri.Password; + } - if (String.IsNullOrWhiteSpace(options.SmtpUser) != String.IsNullOrWhiteSpace(options.SmtpPassword)) - throw new ArgumentException("Must specify both the SmtpUser and the SmtpPassword, or neither."); + options.SmtpFrom = config.GetValue(nameof(options.SmtpFrom), appOptions.AppMode == AppMode.Development ? "Exceptionless " : ""); + options.SmtpEncryption = config.GetValue(nameof(options.SmtpEncryption), GetDefaultSmtpEncryption(options.SmtpPort)); - return options; - } + if (String.IsNullOrWhiteSpace(options.SmtpUser) != String.IsNullOrWhiteSpace(options.SmtpPassword)) + throw new ArgumentException("Must specify both the SmtpUser and the SmtpPassword, or neither."); - private static SmtpEncryption GetDefaultSmtpEncryption(int port) { - return port switch { - 465 => SmtpEncryption.SSL, - 587 => SmtpEncryption.StartTLS, - 2525 => SmtpEncryption.StartTLS, - _ => SmtpEncryption.None - }; - } + return options; } - public enum SmtpEncryption { - None, - StartTLS, - SSL + private static SmtpEncryption GetDefaultSmtpEncryption(int port) { + return port switch { + 465 => SmtpEncryption.SSL, + 587 => SmtpEncryption.StartTLS, + 2525 => SmtpEncryption.StartTLS, + _ => SmtpEncryption.None + }; } -} \ No newline at end of file +} + +public enum SmtpEncryption { + None, + StartTLS, + SSL +} diff --git a/src/Exceptionless.Core/Configuration/IntercomOptions.cs b/src/Exceptionless.Core/Configuration/IntercomOptions.cs index 94eb272a00..a4abcc08da 100644 --- a/src/Exceptionless.Core/Configuration/IntercomOptions.cs +++ b/src/Exceptionless.Core/Configuration/IntercomOptions.cs @@ -1,21 +1,20 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class IntercomOptions { - public bool EnableIntercom => !String.IsNullOrEmpty(IntercomSecret); +namespace Exceptionless.Core.Configuration; - public string IntercomSecret { get; internal set; } +public class IntercomOptions { + public bool EnableIntercom => !String.IsNullOrEmpty(IntercomSecret); - public static IntercomOptions ReadFromConfiguration(IConfiguration config) { - var options = new IntercomOptions(); + public string IntercomSecret { get; internal set; } - var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); - options.IntercomSecret = oAuth.GetString(nameof(options.IntercomSecret)); + public static IntercomOptions ReadFromConfiguration(IConfiguration config) { + var options = new IntercomOptions(); - return options; - } + var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); + options.IntercomSecret = oAuth.GetString(nameof(options.IntercomSecret)); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index 04958e09a4..10be9a14f4 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -1,39 +1,37 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class MessageBusOptions { - public string ConnectionString { get; internal set; } - public string Provider { get; internal set; } - public Dictionary Data { get; internal set; } +namespace Exceptionless.Core.Configuration; - public string Scope { get; internal set; } - public string ScopePrefix { get; internal set; } - public string Topic { get; internal set; } +public class MessageBusOptions { + public string ConnectionString { get; internal set; } + public string Provider { get; internal set; } + public Dictionary Data { get; internal set; } - public static MessageBusOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { - var options = new MessageBusOptions(); + public string Scope { get; internal set; } + public string ScopePrefix { get; internal set; } + public string Topic { get; internal set; } - options.Scope = appOptions.AppScope; - options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; + public static MessageBusOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { + var options = new MessageBusOptions(); - options.Topic = config.GetValue(nameof(options.Topic), $"{options.ScopePrefix}messages"); + options.Scope = appOptions.AppScope; + options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; - string cs = config.GetConnectionString("MessageBus"); - options.Data = cs.ParseConnectionString(); - options.Provider = options.Data.GetString(nameof(options.Provider)); + options.Topic = config.GetValue(nameof(options.Topic), $"{options.ScopePrefix}messages"); - var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - if (!String.IsNullOrEmpty(providerConnectionString)) - options.Data.AddRange(providerConnectionString.ParseConnectionString()); + string cs = config.GetConnectionString("MessageBus"); + options.Data = cs.ParseConnectionString(); + options.Provider = options.Data.GetString(nameof(options.Provider)); - options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; + if (!String.IsNullOrEmpty(providerConnectionString)) + options.Data.AddRange(providerConnectionString.ParseConnectionString()); - return options; - } + options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/MetricOptions.cs b/src/Exceptionless.Core/Configuration/MetricOptions.cs index b4c2057f72..1bcf525976 100644 --- a/src/Exceptionless.Core/Configuration/MetricOptions.cs +++ b/src/Exceptionless.Core/Configuration/MetricOptions.cs @@ -1,30 +1,28 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class MetricOptions { - public string ConnectionString { get; internal set; } - public string Provider { get; internal set; } - public Dictionary Data { get; internal set; } +namespace Exceptionless.Core.Configuration; - public static MetricOptions ReadFromConfiguration(IConfiguration config) { - var options = new MetricOptions(); +public class MetricOptions { + public string ConnectionString { get; internal set; } + public string Provider { get; internal set; } + public Dictionary Data { get; internal set; } - string cs = config.GetConnectionString("Metrics"); - options.Data = cs.ParseConnectionString(); - options.Provider = options.Data.GetString(nameof(options.Provider)); + public static MetricOptions ReadFromConfiguration(IConfiguration config) { + var options = new MetricOptions(); - var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - if (!String.IsNullOrEmpty(providerConnectionString)) - options.Data.AddRange(providerConnectionString.ParseConnectionString()); + string cs = config.GetConnectionString("Metrics"); + options.Data = cs.ParseConnectionString(); + options.Provider = options.Data.GetString(nameof(options.Provider)); - options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; + if (!String.IsNullOrEmpty(providerConnectionString)) + options.Data.AddRange(providerConnectionString.ParseConnectionString()); - return options; - } + options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/QueueOptions.cs b/src/Exceptionless.Core/Configuration/QueueOptions.cs index 0d7c4b8d41..9d16bb9705 100644 --- a/src/Exceptionless.Core/Configuration/QueueOptions.cs +++ b/src/Exceptionless.Core/Configuration/QueueOptions.cs @@ -1,36 +1,34 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class QueueOptions { - public string ConnectionString { get; internal set; } - public string Provider { get; internal set; } - public Dictionary Data { get; internal set; } +namespace Exceptionless.Core.Configuration; - public string Scope { get; internal set; } - public string ScopePrefix { get; internal set; } +public class QueueOptions { + public string ConnectionString { get; internal set; } + public string Provider { get; internal set; } + public Dictionary Data { get; internal set; } - public static QueueOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { - var options = new QueueOptions(); + public string Scope { get; internal set; } + public string ScopePrefix { get; internal set; } - options.Scope = appOptions.AppScope; - options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; + public static QueueOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { + var options = new QueueOptions(); - string cs = config.GetConnectionString("Queue"); - options.Data = cs.ParseConnectionString(); - options.Provider = options.Data.GetString(nameof(options.Provider)); + options.Scope = appOptions.AppScope; + options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; - var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - if (!String.IsNullOrEmpty(providerConnectionString)) - options.Data.AddRange(providerConnectionString.ParseConnectionString()); + string cs = config.GetConnectionString("Queue"); + options.Data = cs.ParseConnectionString(); + options.Provider = options.Data.GetString(nameof(options.Provider)); - options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; + if (!String.IsNullOrEmpty(providerConnectionString)) + options.Data.AddRange(providerConnectionString.ParseConnectionString()); - return options; - } + options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/SlackOptions.cs b/src/Exceptionless.Core/Configuration/SlackOptions.cs index b00bdeef1a..b9e37603d3 100644 --- a/src/Exceptionless.Core/Configuration/SlackOptions.cs +++ b/src/Exceptionless.Core/Configuration/SlackOptions.cs @@ -1,24 +1,23 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class SlackOptions { - public string SlackId { get; internal set; } +namespace Exceptionless.Core.Configuration; - public string SlackSecret { get; internal set; } +public class SlackOptions { + public string SlackId { get; internal set; } - public bool EnableSlack => !String.IsNullOrEmpty(SlackId); + public string SlackSecret { get; internal set; } - public static SlackOptions ReadFromConfiguration(IConfiguration config) { - var options = new SlackOptions(); + public bool EnableSlack => !String.IsNullOrEmpty(SlackId); - var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); - options.SlackId = oAuth.GetString(nameof(options.SlackId)); - options.SlackSecret = oAuth.GetString(nameof(options.SlackSecret)); + public static SlackOptions ReadFromConfiguration(IConfiguration config) { + var options = new SlackOptions(); - return options; - } + var oAuth = config.GetConnectionString("OAuth").ParseConnectionString(); + options.SlackId = oAuth.GetString(nameof(options.SlackId)); + options.SlackSecret = oAuth.GetString(nameof(options.SlackSecret)); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/StorageOptions.cs b/src/Exceptionless.Core/Configuration/StorageOptions.cs index c393f14836..3dc4fc1d15 100644 --- a/src/Exceptionless.Core/Configuration/StorageOptions.cs +++ b/src/Exceptionless.Core/Configuration/StorageOptions.cs @@ -1,36 +1,34 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; -namespace Exceptionless.Core.Configuration { - public class StorageOptions { - public string ConnectionString { get; internal set; } - public string Provider { get; internal set; } - public Dictionary Data { get; internal set; } +namespace Exceptionless.Core.Configuration; - public string Scope { get; internal set; } - public string ScopePrefix { get; internal set; } +public class StorageOptions { + public string ConnectionString { get; internal set; } + public string Provider { get; internal set; } + public Dictionary Data { get; internal set; } - public static StorageOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { - var options = new StorageOptions(); + public string Scope { get; internal set; } + public string ScopePrefix { get; internal set; } - options.Scope = appOptions.AppScope; - options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; + public static StorageOptions ReadFromConfiguration(IConfiguration config, AppOptions appOptions) { + var options = new StorageOptions(); - string cs = config.GetConnectionString("Storage"); - options.Data = cs.ParseConnectionString(); - options.Provider = options.Data.GetString(nameof(options.Provider)); + options.Scope = appOptions.AppScope; + options.ScopePrefix = !String.IsNullOrEmpty(options.Scope) ? options.Scope + "-" : String.Empty; - var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - if (!String.IsNullOrEmpty(providerConnectionString)) - options.Data.AddRange(providerConnectionString.ParseConnectionString()); + string cs = config.GetConnectionString("Storage"); + options.Data = cs.ParseConnectionString(); + options.Provider = options.Data.GetString(nameof(options.Provider)); - options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + var providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; + if (!String.IsNullOrEmpty(providerConnectionString)) + options.Data.AddRange(providerConnectionString.ParseConnectionString()); - return options; - } + options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Configuration/StripeOptions.cs b/src/Exceptionless.Core/Configuration/StripeOptions.cs index ae2a98318a..a0d22ba2b4 100644 --- a/src/Exceptionless.Core/Configuration/StripeOptions.cs +++ b/src/Exceptionless.Core/Configuration/StripeOptions.cs @@ -1,24 +1,23 @@ -using System; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Stripe; -namespace Exceptionless.Core.Configuration { - public class StripeOptions { - public bool EnableBilling => !String.IsNullOrEmpty(StripeApiKey); +namespace Exceptionless.Core.Configuration; - public string StripeApiKey { get; internal set; } +public class StripeOptions { + public bool EnableBilling => !String.IsNullOrEmpty(StripeApiKey); - public string StripeWebHookSigningSecret { get; set; } + public string StripeApiKey { get; internal set; } - public static StripeOptions ReadFromConfiguration(IConfiguration config) { - var options = new StripeOptions(); + public string StripeWebHookSigningSecret { get; set; } - options.StripeApiKey = config.GetValue(nameof(options.StripeApiKey)); - options.StripeWebHookSigningSecret = config.GetValue(nameof(options.StripeWebHookSigningSecret)); - if (options.EnableBilling) - StripeConfiguration.ApiKey = options.StripeApiKey; + public static StripeOptions ReadFromConfiguration(IConfiguration config) { + var options = new StripeOptions(); - return options; - } + options.StripeApiKey = config.GetValue(nameof(options.StripeApiKey)); + options.StripeWebHookSigningSecret = config.GetValue(nameof(options.StripeWebHookSigningSecret)); + if (options.EnableBilling) + StripeConfiguration.ApiKey = options.StripeApiKey; + + return options; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/AppDomainExtensions.cs b/src/Exceptionless.Core/Extensions/AppDomainExtensions.cs index d400f3b268..f787e17c79 100644 --- a/src/Exceptionless.Core/Extensions/AppDomainExtensions.cs +++ b/src/Exceptionless.Core/Extensions/AppDomainExtensions.cs @@ -1,12 +1,9 @@ -using System; -using System.IO; +namespace Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Extensions { - public static class AppDomainExtensions { - public static void SetDataDirectory(this AppDomain appDomain) { - string path = Path.GetFullPath(Path.Combine(appDomain.BaseDirectory, @"..\..\..\..\..\Exceptionless.Web\App_Data")); - if (Directory.Exists(path)) - appDomain.SetData("DataDirectory", path); - } +public static class AppDomainExtensions { + public static void SetDataDirectory(this AppDomain appDomain) { + string path = Path.GetFullPath(Path.Combine(appDomain.BaseDirectory, @"..\..\..\..\..\Exceptionless.Web\App_Data")); + if (Directory.Exists(path)) + appDomain.SetData("DataDirectory", path); } } diff --git a/src/Exceptionless.Core/Extensions/ByteArrayExtensions.cs b/src/Exceptionless.Core/Extensions/ByteArrayExtensions.cs index 7ae1321375..446eadd4ab 100644 --- a/src/Exceptionless.Core/Extensions/ByteArrayExtensions.cs +++ b/src/Exceptionless.Core/Extensions/ByteArrayExtensions.cs @@ -1,43 +1,41 @@ -using System; -using System.IO; -using System.IO.Compression; +using System.IO.Compression; -namespace Exceptionless.Core.Extensions { - public static class ByteArrayExtensions { - public static byte[] Decompress(this byte[] data, string encoding) { - byte[] decompressedData; - using (var outputStream = new MemoryStream()) { - using (var inputStream = new MemoryStream(data)) { - if (encoding == "gzip") - using (var zip = new GZipStream(inputStream, CompressionMode.Decompress)) { - zip.CopyTo(outputStream); - } - else if (encoding == "deflate") - using (var zip = new DeflateStream(inputStream, CompressionMode.Decompress)) { - zip.CopyTo(outputStream); - } - else - throw new InvalidOperationException($"Unsupported encoding type \"{encoding}\"."); - } +namespace Exceptionless.Core.Extensions; - decompressedData = outputStream.ToArray(); +public static class ByteArrayExtensions { + public static byte[] Decompress(this byte[] data, string encoding) { + byte[] decompressedData; + using (var outputStream = new MemoryStream()) { + using (var inputStream = new MemoryStream(data)) { + if (encoding == "gzip") + using (var zip = new GZipStream(inputStream, CompressionMode.Decompress)) { + zip.CopyTo(outputStream); + } + else if (encoding == "deflate") + using (var zip = new DeflateStream(inputStream, CompressionMode.Decompress)) { + zip.CopyTo(outputStream); + } + else + throw new InvalidOperationException($"Unsupported encoding type \"{encoding}\"."); } - return decompressedData; + decompressedData = outputStream.ToArray(); } - public static byte[] Compress(this byte[] data) { - byte[] compressesData; - using (var outputStream = new MemoryStream()) { - using (var zip = new GZipStream(outputStream, CompressionMode.Compress, true)) { - zip.Write(data, 0, data.Length); - } + return decompressedData; + } - outputStream.Flush(); - compressesData = outputStream.ToArray(); + public static byte[] Compress(this byte[] data) { + byte[] compressesData; + using (var outputStream = new MemoryStream()) { + using (var zip = new GZipStream(outputStream, CompressionMode.Compress, true)) { + zip.Write(data, 0, data.Length); } - return compressesData; + outputStream.Flush(); + compressesData = outputStream.ToArray(); } + + return compressesData; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/CacheClientExtensions.cs b/src/Exceptionless.Core/Extensions/CacheClientExtensions.cs index 0a5d3113d5..23a8e9760e 100644 --- a/src/Exceptionless.Core/Extensions/CacheClientExtensions.cs +++ b/src/Exceptionless.Core/Extensions/CacheClientExtensions.cs @@ -1,37 +1,35 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Caching; -namespace Exceptionless.Extensions { - public static class CacheClientExtensions { - public static async Task IncrementIfAsync(this ICacheClient client, string key, int value, TimeSpan timeToLive, bool shouldIncrement, long? startingValue = null) { - if (!startingValue.HasValue) - startingValue = 0; +namespace Exceptionless.Extensions; - var count = await client.GetAsync(key).AnyContext(); - if (!shouldIncrement) - return count.HasValue ? count.Value : startingValue.Value; +public static class CacheClientExtensions { + public static async Task IncrementIfAsync(this ICacheClient client, string key, int value, TimeSpan timeToLive, bool shouldIncrement, long? startingValue = null) { + if (!startingValue.HasValue) + startingValue = 0; - if (count.HasValue) - return await client.IncrementAsync(key, value, timeToLive).AnyContext(); + var count = await client.GetAsync(key).AnyContext(); + if (!shouldIncrement) + return count.HasValue ? count.Value : startingValue.Value; - long newValue = startingValue.Value + value; - await client.SetAsync(key, newValue, timeToLive).AnyContext(); - return newValue; - } + if (count.HasValue) + return await client.IncrementAsync(key, value, timeToLive).AnyContext(); - public static async Task IncrementAsync(this ICacheClient client, string key, int value, TimeSpan timeToLive, long? startingValue = null) { - if (!startingValue.HasValue) - startingValue = 0; + long newValue = startingValue.Value + value; + await client.SetAsync(key, newValue, timeToLive).AnyContext(); + return newValue; + } + + public static async Task IncrementAsync(this ICacheClient client, string key, int value, TimeSpan timeToLive, long? startingValue = null) { + if (!startingValue.HasValue) + startingValue = 0; - var count = await client.GetAsync(key).AnyContext(); - if (count.HasValue) - return await client.IncrementAsync(key, value, timeToLive).AnyContext(); + var count = await client.GetAsync(key).AnyContext(); + if (count.HasValue) + return await client.IncrementAsync(key, value, timeToLive).AnyContext(); - long newValue = startingValue.Value + value; - await client.SetAsync(key, newValue, timeToLive).AnyContext(); - return newValue; - } + long newValue = startingValue.Value + value; + await client.SetAsync(key, newValue, timeToLive).AnyContext(); + return newValue; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/ConfigurationExtensions.cs b/src/Exceptionless.Core/Extensions/ConfigurationExtensions.cs index d22f085731..1b1ba0f480 100644 --- a/src/Exceptionless.Core/Extensions/ConfigurationExtensions.cs +++ b/src/Exceptionless.Core/Extensions/ConfigurationExtensions.cs @@ -1,74 +1,71 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace Exceptionless.Core.Extensions { - public static class ConfigurationExtensions { - public static IServiceCollection AddAppOptions(this IServiceCollection services, AppOptions appOptions) { - services.AddSingleton(appOptions); - services.AddSingleton(appOptions.CacheOptions); - services.AddSingleton(appOptions.MessageBusOptions); - services.AddSingleton(appOptions.MetricOptions); - services.AddSingleton(appOptions.QueueOptions); - services.AddSingleton(appOptions.StorageOptions); - services.AddSingleton(appOptions.EmailOptions); - services.AddSingleton(appOptions.ElasticsearchOptions); - services.AddSingleton(appOptions.IntercomOptions); - services.AddSingleton(appOptions.SlackOptions); - services.AddSingleton(appOptions.StripeOptions); - services.AddSingleton(appOptions.AuthOptions); +namespace Exceptionless.Core.Extensions; - return services; - } +public static class ConfigurationExtensions { + public static IServiceCollection AddAppOptions(this IServiceCollection services, AppOptions appOptions) { + services.AddSingleton(appOptions); + services.AddSingleton(appOptions.CacheOptions); + services.AddSingleton(appOptions.MessageBusOptions); + services.AddSingleton(appOptions.MetricOptions); + services.AddSingleton(appOptions.QueueOptions); + services.AddSingleton(appOptions.StorageOptions); + services.AddSingleton(appOptions.EmailOptions); + services.AddSingleton(appOptions.ElasticsearchOptions); + services.AddSingleton(appOptions.IntercomOptions); + services.AddSingleton(appOptions.SlackOptions); + services.AddSingleton(appOptions.StripeOptions); + services.AddSingleton(appOptions.AuthOptions); - public static string ToScope(this AppMode mode) { - switch (mode) { - case AppMode.Development: - return "dev"; - case AppMode.Staging: - return "stage"; - case AppMode.Production: - return "prod"; - } + return services; + } - return String.Empty; + public static string ToScope(this AppMode mode) { + switch (mode) { + case AppMode.Development: + return "dev"; + case AppMode.Staging: + return "stage"; + case AppMode.Production: + return "prod"; } - - public static List GetValueList(this IConfiguration config, string key, char[] separators = null) { - string value = config.GetValue(key); - if (String.IsNullOrEmpty(value)) - return new List(); - if (separators == null) - separators = new[] { ',' }; + return String.Empty; + } - return value.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList(); - } + public static List GetValueList(this IConfiguration config, string key, char[] separators = null) { + string value = config.GetValue(key); + if (String.IsNullOrEmpty(value)) + return new List(); + + if (separators == null) + separators = new[] { ',' }; - public static Dictionary ToDictionary(this IConfiguration section, params string[] sectionsToSkip) { - if (sectionsToSkip == null) - sectionsToSkip = new string[0]; + return value.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList(); + } + + public static Dictionary ToDictionary(this IConfiguration section, params string[] sectionsToSkip) { + if (sectionsToSkip == null) + sectionsToSkip = new string[0]; + + var dict = new Dictionary(); + foreach (var value in section.GetChildren()) { + // kubernetes service variables + if (value.Key.StartsWith("DEV_", StringComparison.Ordinal)) + continue; - var dict = new Dictionary(); - foreach (var value in section.GetChildren()) { - // kubernetes service variables - if (value.Key.StartsWith("DEV_", StringComparison.Ordinal)) - continue; - - if (String.IsNullOrEmpty(value.Key) || sectionsToSkip.Contains(value.Key, StringComparer.OrdinalIgnoreCase)) - continue; - - if (value.Value != null) - dict[value.Key] = value.Value; - - var subDict = ToDictionary(value); - if (subDict.Count > 0) - dict[value.Key] = subDict; - } + if (String.IsNullOrEmpty(value.Key) || sectionsToSkip.Contains(value.Key, StringComparer.OrdinalIgnoreCase)) + continue; - return dict; + if (value.Value != null) + dict[value.Key] = value.Value; + + var subDict = ToDictionary(value); + if (subDict.Count > 0) + dict[value.Key] = subDict; } + + return dict; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/DataDictionaryExtensions.cs b/src/Exceptionless.Core/Extensions/DataDictionaryExtensions.cs index a24d81735a..1f3af8521e 100644 --- a/src/Exceptionless.Core/Extensions/DataDictionaryExtensions.cs +++ b/src/Exceptionless.Core/Extensions/DataDictionaryExtensions.cs @@ -1,43 +1,44 @@ -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Extensions { - public static class DataDictionaryExtensions { - public static T GetValue(this DataDictionary extendedData, string key) { - if (!extendedData.ContainsKey(key)) - throw new KeyNotFoundException($"Key \"{key}\" not found in the dictionary."); +namespace Exceptionless.Core.Extensions; - object data = extendedData[key]; - if (data is T) - return (T)data; +public static class DataDictionaryExtensions { + public static T GetValue(this DataDictionary extendedData, string key) { + if (!extendedData.ContainsKey(key)) + throw new KeyNotFoundException($"Key \"{key}\" not found in the dictionary."); - if (data is JObject) { - try { - return ((JObject)data).ToObject(); - } catch {} - } + object data = extendedData[key]; + if (data is T) + return (T)data; - string json = data as string; - if (json.IsJson()) { - try { - return JsonConvert.DeserializeObject(json); - } catch {} + if (data is JObject) { + try { + return ((JObject)data).ToObject(); } + catch { } + } + string json = data as string; + if (json.IsJson()) { try { - return data.ToType(); - } catch {} - - return default; + return JsonConvert.DeserializeObject(json); + } + catch { } } - public static void RemoveSensitiveData(this DataDictionary extendedData) { - string[] removeKeys = extendedData.Keys.Where(k => k.StartsWith("-")).ToArray(); - foreach (string key in removeKeys) - extendedData.Remove(key); + try { + return data.ToType(); } + catch { } + + return default; + } + + public static void RemoveSensitiveData(this DataDictionary extendedData) { + string[] removeKeys = extendedData.Keys.Where(k => k.StartsWith("-")).ToArray(); + foreach (string key in removeKeys) + extendedData.Remove(key); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs b/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs index efe0cffa57..df206fe8b1 100644 --- a/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs +++ b/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs @@ -1,143 +1,142 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace Exceptionless.Core.Extensions { - public static class DictionaryExtensions { - public static void Trim(this HashSet items, Predicate itemsToRemove, Predicate itemsToAlwaysInclude, int maxLength) { - if (items == null) - return; - - items.RemoveWhere(itemsToRemove); - if (maxLength > 0 && items.Count > maxLength) { - foreach (string item in items.ToList()) { - if (items.Count <= maxLength) - break; - - if (itemsToAlwaysInclude(item)) - continue; - - items.Remove(item); - } +using System.Collections.Concurrent; + +namespace Exceptionless.Core.Extensions; + +public static class DictionaryExtensions { + public static void Trim(this HashSet items, Predicate itemsToRemove, Predicate itemsToAlwaysInclude, int maxLength) { + if (items == null) + return; + + items.RemoveWhere(itemsToRemove); + if (maxLength > 0 && items.Count > maxLength) { + foreach (string item in items.ToList()) { + if (items.Count <= maxLength) + break; + + if (itemsToAlwaysInclude(item)) + continue; + + items.Remove(item); } } + } - public static void AddItemIfNotEmpty(this IDictionary dictionary, string key, string value) { - if (key == null) - throw new ArgumentNullException(nameof(key)); + public static void AddItemIfNotEmpty(this IDictionary dictionary, string key, string value) { + if (key == null) + throw new ArgumentNullException(nameof(key)); - if (!String.IsNullOrEmpty(value)) - dictionary[key] = value; - } + if (!String.IsNullOrEmpty(value)) + dictionary[key] = value; + } - /// - /// Adds or overwrites the existing value. - /// - public static void AddOrUpdate(this ConcurrentDictionary dictionary, TKey key, TValue value) { - dictionary.AddOrUpdate(key, value, (oldkey, oldvalue) => value); - } + /// + /// Adds or overwrites the existing value. + /// + public static void AddOrUpdate(this ConcurrentDictionary dictionary, TKey key, TValue value) { + dictionary.AddOrUpdate(key, value, (oldkey, oldvalue) => value); + } - public static bool ContainsKeyWithValue(this IDictionary dictionary, TKey key, params TValue[] values) { - if (dictionary == null || values == null || values.Length == 0) - return false; + public static bool ContainsKeyWithValue(this IDictionary dictionary, TKey key, params TValue[] values) { + if (dictionary == null || values == null || values.Length == 0) + return false; - TValue temp; - try { - if (!dictionary.TryGetValue(key, out temp)) - return false; - } catch (ArgumentNullException) { + TValue temp; + try { + if (!dictionary.TryGetValue(key, out temp)) return false; - } - - return values.Any(v => v.Equals(temp)); + } + catch (ArgumentNullException) { + return false; } - public static TValue TryGetAndReturn(this IDictionary dictionary, TKey key) { - if (!dictionary.TryGetValue(key, out var value)) - value = default; + return values.Any(v => v.Equals(temp)); + } - return value; - } + public static TValue TryGetAndReturn(this IDictionary dictionary, TKey key) { + if (!dictionary.TryGetValue(key, out var value)) + value = default; - public static TValue GetOrDefault(this IDictionary dictionary, TKey key) { - dictionary.TryGetValue(key, out var obj); - return obj; - } + return value; + } - public static bool CollectionEquals(this IDictionary source, IDictionary other) { - if (source.Count != other.Count) - return false; + public static TValue GetOrDefault(this IDictionary dictionary, TKey key) { + dictionary.TryGetValue(key, out var obj); + return obj; + } - foreach (string key in source.Keys) { - var sourceValue = source[key]; + public static bool CollectionEquals(this IDictionary source, IDictionary other) { + if (source.Count != other.Count) + return false; - if (!other.TryGetValue(key, out var otherValue)) - return false; + foreach (string key in source.Keys) { + var sourceValue = source[key]; - if (sourceValue.Equals(otherValue)) - return false; - } + if (!other.TryGetValue(key, out var otherValue)) + return false; - return true; + if (sourceValue.Equals(otherValue)) + return false; } + return true; + } - public static int GetCollectionHashCode(this IDictionary source, IList exclusions = null) { - string assemblyQualifiedName = typeof(TValue).AssemblyQualifiedName; - int hashCode = assemblyQualifiedName?.GetHashCode() ?? 0; - var keyValuePairHashes = new List(source.Keys.Count); + public static int GetCollectionHashCode(this IDictionary source, IList exclusions = null) { + string assemblyQualifiedName = typeof(TValue).AssemblyQualifiedName; + int hashCode = assemblyQualifiedName?.GetHashCode() ?? 0; - foreach (string key in source.Keys.OrderBy(x => x)) { - if (exclusions != null && exclusions.Contains(key)) - continue; + var keyValuePairHashes = new List(source.Keys.Count); - var item = source[key]; - unchecked { - int kvpHash = key.GetHashCode(); - kvpHash = (kvpHash * 397) ^ item.GetHashCode(); - keyValuePairHashes.Add(kvpHash); - } - } + foreach (string key in source.Keys.OrderBy(x => x)) { + if (exclusions != null && exclusions.Contains(key)) + continue; - keyValuePairHashes.Sort(); - foreach (int kvpHash in keyValuePairHashes) { - unchecked { - hashCode = (hashCode * 397) ^ kvpHash; - } + var item = source[key]; + unchecked { + int kvpHash = key.GetHashCode(); + kvpHash = (kvpHash * 397) ^ item.GetHashCode(); + keyValuePairHashes.Add(kvpHash); } + } - return hashCode; + keyValuePairHashes.Sort(); + foreach (int kvpHash in keyValuePairHashes) { + unchecked { + hashCode = (hashCode * 397) ^ kvpHash; + } } - - public static T GetValueOrDefault(this IDictionary source, string key, T defaultValue = default) { - if (!source.ContainsKey(key)) - return defaultValue; - object data = source[key]; - if (data is T variable) - return variable; + return hashCode; + } - if (data == null) - return defaultValue; + public static T GetValueOrDefault(this IDictionary source, string key, T defaultValue = default) { + if (!source.ContainsKey(key)) + return defaultValue; - try { - return data.ToType(); - } catch {} + object data = source[key]; + if (data is T variable) + return variable; + if (data == null) return defaultValue; - } - public static string GetString(this IDictionary source, string name) { - return source.GetString(name, String.Empty); + try { + return data.ToType(); } + catch { } - public static string GetString(this IDictionary source, string name, string @default) { - if (!source.TryGetValue(name, out string value)) - return @default; + return defaultValue; + } - return value ?? @default; - } + public static string GetString(this IDictionary source, string name) { + return source.GetString(name, String.Empty); + } + + public static string GetString(this IDictionary source, string name, string @default) { + if (!source.TryGetValue(name, out string value)) + return @default; + + return value ?? @default; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/EnumHelper.cs b/src/Exceptionless.Core/Extensions/EnumHelper.cs index 38a4d0371a..0ec2761ed1 100644 --- a/src/Exceptionless.Core/Extensions/EnumHelper.cs +++ b/src/Exceptionless.Core/Extensions/EnumHelper.cs @@ -1,51 +1,50 @@ -using System; +namespace Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Extensions { - public static class EnumHelper { - /// - /// Will try and parse an enum and it's default type. - /// - /// - /// - /// True if the enum value is defined. - public static bool TryEnumIsDefined(Type type, object value) { - if (type == null || value == null || !type.IsEnum) - return false; - - // Return true if the value is an enum and is a matching type. - if (type == value.GetType()) - return true; +public static class EnumHelper { + /// + /// Will try and parse an enum and it's default type. + /// + /// + /// + /// True if the enum value is defined. + public static bool TryEnumIsDefined(Type type, object value) { + if (type == null || value == null || !type.IsEnum) + return false; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; - if (TryEnumIsDefined(type, value)) - return true; + // Return true if the value is an enum and is a matching type. + if (type == value.GetType()) + return true; - return false; - } + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; + if (TryEnumIsDefined(type, value)) + return true; - private static bool TryEnumIsDefined(Type type, object value) { - // Catch any casting errors that can occur or if 0 is not defined as a default value. - try { - if (value is T && Enum.IsDefined(type, (T)value)) - return true; - } catch (Exception) {} + return false; + } - return false; + private static bool TryEnumIsDefined(Type type, object value) { + // Catch any casting errors that can occur or if 0 is not defined as a default value. + try { + if (value is T && Enum.IsDefined(type, (T)value)) + return true; } + catch (Exception) { } + + return false; } } diff --git a/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs b/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs index 0e1f307bff..4946d8bf69 100644 --- a/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs +++ b/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs @@ -1,106 +1,103 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Diagnostics.Contracts; -using System.Linq; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Extensions { - public static class EnumerableExtensions { - public static IReadOnlyCollection UnionOriginalAndModified(this IReadOnlyCollection> documents) where T : class, new() { - return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).Where(d => d != null)).ToList(); - } +namespace Exceptionless.Core.Extensions; - public static bool Contains(this IEnumerable enumerable, Func function) { - var a = enumerable.FirstOrDefault(function); - var b = default(T); - return !Equals(a, b); - } +public static class EnumerableExtensions { + public static IReadOnlyCollection UnionOriginalAndModified(this IReadOnlyCollection> documents) where T : class, new() { + return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).Where(d => d != null)).ToList(); + } - public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) { - return source.DistinctBy(keySelector, EqualityComparer.Default); - } + public static bool Contains(this IEnumerable enumerable, Func function) { + var a = enumerable.FirstOrDefault(function); + var b = default(T); + return !Equals(a, b); + } - public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector, IEqualityComparer comparer) { - if (source == null) - throw new ArgumentNullException(nameof(source)); - if (keySelector == null) - throw new ArgumentNullException(nameof(keySelector)); - if (comparer == null) - throw new ArgumentNullException(nameof(comparer)); + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) { + return source.DistinctBy(keySelector, EqualityComparer.Default); + } - return DistinctByImpl(source, keySelector, comparer); - } + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector, IEqualityComparer comparer) { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (keySelector == null) + throw new ArgumentNullException(nameof(keySelector)); + if (comparer == null) + throw new ArgumentNullException(nameof(comparer)); - private static IEnumerable DistinctByImpl(IEnumerable source, Func keySelector, IEqualityComparer comparer) { - var knownKeys = new HashSet(comparer); - foreach (var element in source) - if (knownKeys.Add(keySelector(element))) - yield return element; - } + return DistinctByImpl(source, keySelector, comparer); + } - public static void ForEach(this IEnumerable collection, Action action) { - foreach (var item in collection ?? new List()) - action(item); - } + private static IEnumerable DistinctByImpl(IEnumerable source, Func keySelector, IEqualityComparer comparer) { + var knownKeys = new HashSet(comparer); + foreach (var element in source) + if (knownKeys.Add(keySelector(element))) + yield return element; + } - public static bool CollectionEquals(this IEnumerable source, IEnumerable other) { - using var sourceEnumerator = source.GetEnumerator(); - using var otherEnumerator = other.GetEnumerator(); + public static void ForEach(this IEnumerable collection, Action action) { + foreach (var item in collection ?? new List()) + action(item); + } - while (sourceEnumerator.MoveNext()) { - if (!otherEnumerator.MoveNext()) { - // counts differ - return false; - } + public static bool CollectionEquals(this IEnumerable source, IEnumerable other) { + using var sourceEnumerator = source.GetEnumerator(); + using var otherEnumerator = other.GetEnumerator(); - if (sourceEnumerator.Current != null && sourceEnumerator.Current.Equals(otherEnumerator.Current)) { - // values aren't equal - return false; - } + while (sourceEnumerator.MoveNext()) { + if (!otherEnumerator.MoveNext()) { + // counts differ + return false; } - return !otherEnumerator.MoveNext(); + if (sourceEnumerator.Current != null && sourceEnumerator.Current.Equals(otherEnumerator.Current)) { + // values aren't equal + return false; + } } - public static int GetCollectionHashCode(this IEnumerable source) { - string assemblyQualifiedName = typeof(T).AssemblyQualifiedName; - int hashCode = assemblyQualifiedName?.GetHashCode() ?? 0; + return !otherEnumerator.MoveNext(); + } - foreach (var item in source) { - if (item == null) - continue; + public static int GetCollectionHashCode(this IEnumerable source) { + string assemblyQualifiedName = typeof(T).AssemblyQualifiedName; + int hashCode = assemblyQualifiedName?.GetHashCode() ?? 0; - unchecked { - hashCode = (hashCode * 397) ^ item.GetHashCode(); - } - } + foreach (var item in source) { + if (item == null) + continue; - return hashCode; + unchecked { + hashCode = (hashCode * 397) ^ item.GetHashCode(); + } } - - /// - /// Helper method for paging objects in a given source - /// - /// type of object in source collection - /// source collection to be paged - /// page size - /// a collection of sub-collections by page size - public static IEnumerable> Page(this IEnumerable source, int pageSize) { - Contract.Requires(source != null); - Contract.Requires(pageSize > 0); - Contract.Ensures(Contract.Result>>() != null); - - using (var enumerator = source.GetEnumerator()) { - while (enumerator.MoveNext()) { - var currentPage = new List(pageSize) { enumerator.Current }; - - while (currentPage.Count < pageSize && enumerator.MoveNext()) { - currentPage.Add(enumerator.Current); - } - - yield return new ReadOnlyCollection(currentPage); + + return hashCode; + } + + /// + /// Helper method for paging objects in a given source + /// + /// type of object in source collection + /// source collection to be paged + /// page size + /// a collection of sub-collections by page size + public static IEnumerable> Page(this IEnumerable source, int pageSize) { + Contract.Requires(source != null); + Contract.Requires(pageSize > 0); + Contract.Ensures(Contract.Result>>() != null); + + using (var enumerator = source.GetEnumerator()) { + while (enumerator.MoveNext()) { + var currentPage = new List(pageSize) { enumerator.Current }; + + while (currentPage.Count < pageSize && enumerator.MoveNext()) { + currentPage.Add(enumerator.Current); } + + yield return new ReadOnlyCollection(currentPage); } } } diff --git a/src/Exceptionless.Core/Extensions/ErrorExtensions.cs b/src/Exceptionless.Core/Extensions/ErrorExtensions.cs index d5d1009991..0a1cefb651 100644 --- a/src/Exceptionless.Core/Extensions/ErrorExtensions.cs +++ b/src/Exceptionless.Core/Extensions/ErrorExtensions.cs @@ -1,65 +1,63 @@ -using System; -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Extensions { - public static class ErrorExtensions { - public static StackingTarget GetStackingTarget(this Error error) { - if (error == null) - return null; - - InnerError targetError = error; - while (targetError != null) { - var frame = targetError.StackTrace?.FirstOrDefault(st => st.IsSignatureTarget); - if (frame != null) - return new StackingTarget { - Error = targetError, - Method = frame - }; - - if (targetError.TargetMethod != null && targetError.TargetMethod.IsSignatureTarget) - return new StackingTarget { - Error = targetError, - Method = targetError.TargetMethod - }; - - targetError = targetError.Inner; - } - - // fallback to default - var defaultError = error.GetInnermostError(); - var defaultMethod = defaultError.StackTrace?.FirstOrDefault(); - if (defaultMethod == null && error.StackTrace != null) { - defaultMethod = error.StackTrace.FirstOrDefault(); - defaultError = error; - } - - return new StackingTarget { - Error = defaultError, - Method = defaultMethod - }; +namespace Exceptionless.Core.Extensions; + +public static class ErrorExtensions { + public static StackingTarget GetStackingTarget(this Error error) { + if (error == null) + return null; + + InnerError targetError = error; + while (targetError != null) { + var frame = targetError.StackTrace?.FirstOrDefault(st => st.IsSignatureTarget); + if (frame != null) + return new StackingTarget { + Error = targetError, + Method = frame + }; + + if (targetError.TargetMethod != null && targetError.TargetMethod.IsSignatureTarget) + return new StackingTarget { + Error = targetError, + Method = targetError.TargetMethod + }; + + targetError = targetError.Inner; } - public static StackingTarget GetStackingTarget(this Event ev) { - var error = ev.GetError(); - return error?.GetStackingTarget(); + // fallback to default + var defaultError = error.GetInnermostError(); + var defaultMethod = defaultError.StackTrace?.FirstOrDefault(); + if (defaultMethod == null && error.StackTrace != null) { + defaultMethod = error.StackTrace.FirstOrDefault(); + defaultError = error; } - public static InnerError GetInnermostError(this InnerError error) { - if (error == null) - throw new ArgumentNullException(nameof(error)); - - var current = error; - while (current.Inner != null) - current = current.Inner; + return new StackingTarget { + Error = defaultError, + Method = defaultMethod + }; + } - return current; - } + public static StackingTarget GetStackingTarget(this Event ev) { + var error = ev.GetError(); + return error?.GetStackingTarget(); } - public class StackingTarget { - public Method Method { get; set; } - public InnerError Error { get; set; } + public static InnerError GetInnermostError(this InnerError error) { + if (error == null) + throw new ArgumentNullException(nameof(error)); + + var current = error; + while (current.Inner != null) + current = current.Inner; + + return current; } -} \ No newline at end of file +} + +public class StackingTarget { + public Method Method { get; set; } + public InnerError Error { get; set; } +} diff --git a/src/Exceptionless.Core/Extensions/EventExtensions.cs b/src/Exceptionless.Core/Extensions/EventExtensions.cs index 3d36b9a84a..1aa66e6b91 100644 --- a/src/Exceptionless.Core/Extensions/EventExtensions.cs +++ b/src/Exceptionless.Core/Extensions/EventExtensions.cs @@ -1,332 +1,334 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Text; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Newtonsoft.Json; -namespace Exceptionless { - public static class EventExtensions { - public static Error GetError(this Event ev) { - if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.Error)) - return null; - - try { - return ev.Data.GetValue(Event.KnownDataKeys.Error); - } catch (Exception) {} +namespace Exceptionless; +public static class EventExtensions { + public static Error GetError(this Event ev) { + if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.Error)) return null; - } - public static SimpleError GetSimpleError(this Event ev) { - if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.SimpleError)) - return null; + try { + return ev.Data.GetValue(Event.KnownDataKeys.Error); + } + catch (Exception) { } - try { - return ev.Data.GetValue(Event.KnownDataKeys.SimpleError); - } catch (Exception) {} + return null; + } + public static SimpleError GetSimpleError(this Event ev) { + if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.SimpleError)) return null; - } - public static RequestInfo GetRequestInfo(this Event ev) { - if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.RequestInfo)) - return null; + try { + return ev.Data.GetValue(Event.KnownDataKeys.SimpleError); + } + catch (Exception) { } - try { - return ev.Data.GetValue(Event.KnownDataKeys.RequestInfo); - } catch (Exception) {} + return null; + } + public static RequestInfo GetRequestInfo(this Event ev) { + if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.RequestInfo)) return null; - } - public static EnvironmentInfo GetEnvironmentInfo(this Event ev) { - if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.EnvironmentInfo)) - return null; + try { + return ev.Data.GetValue(Event.KnownDataKeys.RequestInfo); + } + catch (Exception) { } - try { - return ev.Data.GetValue(Event.KnownDataKeys.EnvironmentInfo); - } catch (Exception) {} + return null; + } + public static EnvironmentInfo GetEnvironmentInfo(this Event ev) { + if (ev == null || !ev.Data.ContainsKey(Event.KnownDataKeys.EnvironmentInfo)) return null; + + try { + return ev.Data.GetValue(Event.KnownDataKeys.EnvironmentInfo); } + catch (Exception) { } - public static TagSet RemoveExcessTags(this TagSet tags) { - tags?.Trim( - t => String.IsNullOrEmpty(t) || t.Length > 100, - t => String.Equals(t, Event.KnownTags.Critical, StringComparison.OrdinalIgnoreCase) || String.Equals(t, Event.KnownTags.Internal, StringComparison.OrdinalIgnoreCase), - 50); + return null; + } - return tags; - } + public static TagSet RemoveExcessTags(this TagSet tags) { + tags?.Trim( + t => String.IsNullOrEmpty(t) || t.Length > 100, + t => String.Equals(t, Event.KnownTags.Critical, StringComparison.OrdinalIgnoreCase) || String.Equals(t, Event.KnownTags.Internal, StringComparison.OrdinalIgnoreCase), + 50); - /// - /// Indicates whether the event has been marked as critical. - /// - public static bool IsCritical(this Event ev) { - return ev.Tags != null && ev.Tags.Contains(Event.KnownTags.Critical); - } + return tags; + } - /// - /// Marks the event as being a critical occurrence. - /// - public static void MarkAsCritical(this Event ev) { - if (ev.Tags == null) - ev.Tags = new TagSet(); + /// + /// Indicates whether the event has been marked as critical. + /// + public static bool IsCritical(this Event ev) { + return ev.Tags != null && ev.Tags.Contains(Event.KnownTags.Critical); + } - ev.Tags.Add(Event.KnownTags.Critical); - ev.Tags.RemoveExcessTags(); - } + /// + /// Marks the event as being a critical occurrence. + /// + public static void MarkAsCritical(this Event ev) { + if (ev.Tags == null) + ev.Tags = new TagSet(); - /// - /// Returns true if the event type is not found. - /// - public static bool IsNotFound(this Event ev) { - return ev.Type == Event.KnownTypes.NotFound; - } + ev.Tags.Add(Event.KnownTags.Critical); + ev.Tags.RemoveExcessTags(); + } - /// - /// Returns true if the event type is error. - /// - public static bool IsError(this Event ev) { - return ev.Type == Event.KnownTypes.Error; - } + /// + /// Returns true if the event type is not found. + /// + public static bool IsNotFound(this Event ev) { + return ev.Type == Event.KnownTypes.NotFound; + } - /// - /// Returns true if the event type is log. - /// - public static bool IsLog(this Event ev) { - return ev.Type == Event.KnownTypes.Log; - } + /// + /// Returns true if the event type is error. + /// + public static bool IsError(this Event ev) { + return ev.Type == Event.KnownTypes.Error; + } - /// - /// Returns true if the event type is feature usage. - /// - public static bool IsFeatureUsage(this Event ev) { - return ev.Type == Event.KnownTypes.FeatureUsage; - } + /// + /// Returns true if the event type is log. + /// + public static bool IsLog(this Event ev) { + return ev.Type == Event.KnownTypes.Log; + } - /// - /// Returns true if the event type is session heartbeat. - /// - public static bool IsSessionHeartbeat(this Event ev) { - return ev.Type == Event.KnownTypes.SessionHeartbeat; - } + /// + /// Returns true if the event type is feature usage. + /// + public static bool IsFeatureUsage(this Event ev) { + return ev.Type == Event.KnownTypes.FeatureUsage; + } - /// - /// Returns true if the event type is session start. - /// - public static bool IsSessionStart(this Event ev) { - return ev.Type == Event.KnownTypes.Session; - } + /// + /// Returns true if the event type is session heartbeat. + /// + public static bool IsSessionHeartbeat(this Event ev) { + return ev.Type == Event.KnownTypes.SessionHeartbeat; + } - /// - /// Returns true if the event type is session end. - /// - public static bool IsSessionEnd(this Event ev) { - return ev.Type == Event.KnownTypes.SessionEnd; - } + /// + /// Returns true if the event type is session start. + /// + public static bool IsSessionStart(this Event ev) { + return ev.Type == Event.KnownTypes.Session; + } - /// - /// Adds the request info to the event. - /// - public static void AddRequestInfo(this Event ev, RequestInfo request) { - if (request == null) - return; + /// + /// Returns true if the event type is session end. + /// + public static bool IsSessionEnd(this Event ev) { + return ev.Type == Event.KnownTypes.SessionEnd; + } - ev.Data[Event.KnownDataKeys.RequestInfo] = request; - } + /// + /// Adds the request info to the event. + /// + public static void AddRequestInfo(this Event ev, RequestInfo request) { + if (request == null) + return; - /// - /// Gets the user info object from extended data. - /// - public static UserInfo GetUserIdentity(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.UserInfo, out object value) ? value as UserInfo : null; - } + ev.Data[Event.KnownDataKeys.RequestInfo] = request; + } - public static string GetVersion(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.Version, out object value) ? value as string : null; - } + /// + /// Gets the user info object from extended data. + /// + public static UserInfo GetUserIdentity(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.UserInfo, out object value) ? value as UserInfo : null; + } - /// - /// Sets the version that the event happened on. - /// - /// The event - /// The version. - public static void SetVersion(this Event ev, string version) { - if (String.IsNullOrWhiteSpace(version)) - return; + public static string GetVersion(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.Version, out object value) ? value as string : null; + } - ev.Data[Event.KnownDataKeys.Version] = version.Trim(); - } + /// + /// Sets the version that the event happened on. + /// + /// The event + /// The version. + public static void SetVersion(this Event ev, string version) { + if (String.IsNullOrWhiteSpace(version)) + return; - public static SubmissionClient GetSubmissionClient(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.SubmissionClient, out object value) ? value as SubmissionClient : null; - } + ev.Data[Event.KnownDataKeys.Version] = version.Trim(); + } - public static Location GetLocation(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.Location, out object value) ? value as Location : null; - } + public static SubmissionClient GetSubmissionClient(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.SubmissionClient, out object value) ? value as SubmissionClient : null; + } - public static string GetLevel(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.Level, out object value) ? value as string : null; - } + public static Location GetLocation(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.Location, out object value) ? value as Location : null; + } - public static void SetLevel(this Event ev, string level) { - ev.Data[Event.KnownDataKeys.Level] = level; - } + public static string GetLevel(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.Level, out object value) ? value as string : null; + } - public static string GetSubmissionMethod(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.SubmissionMethod, out object value) ? value as string : null; - } + public static void SetLevel(this Event ev, string level) { + ev.Data[Event.KnownDataKeys.Level] = level; + } - public static void SetSubmissionClient(this Event ev, SubmissionClient client) { - if (client == null) - return; + public static string GetSubmissionMethod(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.SubmissionMethod, out object value) ? value as string : null; + } - ev.Data[Event.KnownDataKeys.SubmissionClient] = client; - } + public static void SetSubmissionClient(this Event ev, SubmissionClient client) { + if (client == null) + return; - public static void SetLocation(this Event ev, Location location) { - if (location == null) - return; + ev.Data[Event.KnownDataKeys.SubmissionClient] = client; + } - ev.Data[Event.KnownDataKeys.Location] = location; - } + public static void SetLocation(this Event ev, Location location) { + if (location == null) + return; - public static void SetEnvironmentInfo(this Event ev, EnvironmentInfo environmentInfo) { - if (environmentInfo == null) - return; + ev.Data[Event.KnownDataKeys.Location] = location; + } - ev.Data[Event.KnownDataKeys.EnvironmentInfo] = environmentInfo; - } + public static void SetEnvironmentInfo(this Event ev, EnvironmentInfo environmentInfo) { + if (environmentInfo == null) + return; - /// - /// Gets the stacking info from extended data. - /// - public static ManualStackingInfo GetManualStackingInfo(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.ManualStackingInfo, out object value) ? value as ManualStackingInfo : null; - } + ev.Data[Event.KnownDataKeys.EnvironmentInfo] = environmentInfo; + } - /// - /// Changes default stacking behavior - /// - /// The event - /// Key value pair that determines how the event is stacked. - public static void SetManualStackingInfo(this Event ev, IDictionary signatureData) { - if (signatureData == null || signatureData.Count == 0) - return; + /// + /// Gets the stacking info from extended data. + /// + public static ManualStackingInfo GetManualStackingInfo(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.ManualStackingInfo, out object value) ? value as ManualStackingInfo : null; + } - ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(signatureData); - } + /// + /// Changes default stacking behavior + /// + /// The event + /// Key value pair that determines how the event is stacked. + public static void SetManualStackingInfo(this Event ev, IDictionary signatureData) { + if (signatureData == null || signatureData.Count == 0) + return; - /// - /// Changes default stacking behavior - /// - /// The event - /// The stack title. - /// Key value pair that determines how the event is stacked. - public static void SetManualStackingInfo(this Event ev, string title, IDictionary signatureData) { - if (String.IsNullOrWhiteSpace(title) || signatureData == null || signatureData.Count == 0) - return; - - ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(title, signatureData); - } + ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(signatureData); + } - /// - /// Changes default stacking behavior by setting the stacking info. - /// - /// The event - /// The manual stacking key. - public static void SetManualStackingKey(this Event ev, string manualStackingKey) { - if (String.IsNullOrWhiteSpace(manualStackingKey)) - return; + /// + /// Changes default stacking behavior + /// + /// The event + /// The stack title. + /// Key value pair that determines how the event is stacked. + public static void SetManualStackingInfo(this Event ev, string title, IDictionary signatureData) { + if (String.IsNullOrWhiteSpace(title) || signatureData == null || signatureData.Count == 0) + return; + + ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(title, signatureData); + } - ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(null, new Dictionary { { "ManualStackingKey", manualStackingKey } }); - } + /// + /// Changes default stacking behavior by setting the stacking info. + /// + /// The event + /// The manual stacking key. + public static void SetManualStackingKey(this Event ev, string manualStackingKey) { + if (String.IsNullOrWhiteSpace(manualStackingKey)) + return; - /// - /// Changes default stacking behavior by setting the stacking info. - /// - /// The event - /// The stack title. - /// The manual stacking key. - public static void SetManualStackingKey(this Event ev, string title, string manualStackingKey) { - if (String.IsNullOrWhiteSpace(title) || String.IsNullOrWhiteSpace(manualStackingKey)) - return; - - ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(title, new Dictionary { { "ManualStackingKey", manualStackingKey } }); - } + ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(null, new Dictionary { { "ManualStackingKey", manualStackingKey } }); + } - /// - /// Sets the user's identity (ie. email address, username, user id) that the event happened to. - /// - /// The event - /// The user's identity that the event happened to. - public static void SetUserIdentity(this Event ev, string identity) { - ev.SetUserIdentity(identity, null); - } + /// + /// Changes default stacking behavior by setting the stacking info. + /// + /// The event + /// The stack title. + /// The manual stacking key. + public static void SetManualStackingKey(this Event ev, string title, string manualStackingKey) { + if (String.IsNullOrWhiteSpace(title) || String.IsNullOrWhiteSpace(manualStackingKey)) + return; + + ev.Data[Event.KnownDataKeys.ManualStackingInfo] = new ManualStackingInfo(title, new Dictionary { { "ManualStackingKey", manualStackingKey } }); + } - /// - /// Sets the user's identity (ie. email address, username, user id) and name that the event happened to. - /// - /// The event - /// The user's identity that the event happened to. - /// The user's friendly name that the event happened to. - public static void SetUserIdentity(this Event ev, string identity, string name) { - if (String.IsNullOrWhiteSpace(identity) && String.IsNullOrWhiteSpace(name)) - return; - - ev.SetUserIdentity(new UserInfo(identity, name)); - } + /// + /// Sets the user's identity (ie. email address, username, user id) that the event happened to. + /// + /// The event + /// The user's identity that the event happened to. + public static void SetUserIdentity(this Event ev, string identity) { + ev.SetUserIdentity(identity, null); + } + + /// + /// Sets the user's identity (ie. email address, username, user id) and name that the event happened to. + /// + /// The event + /// The user's identity that the event happened to. + /// The user's friendly name that the event happened to. + public static void SetUserIdentity(this Event ev, string identity, string name) { + if (String.IsNullOrWhiteSpace(identity) && String.IsNullOrWhiteSpace(name)) + return; + + ev.SetUserIdentity(new UserInfo(identity, name)); + } - /// - /// Sets the user's identity (ie. email address, username, user id) and name that the event happened to. - /// - /// The event - /// The user's identity that the event happened to. - public static void SetUserIdentity(this Event ev, UserInfo userInfo) { - if (userInfo == null) - return; + /// + /// Sets the user's identity (ie. email address, username, user id) and name that the event happened to. + /// + /// The event + /// The user's identity that the event happened to. + public static void SetUserIdentity(this Event ev, UserInfo userInfo) { + if (userInfo == null) + return; - ev.Data[Event.KnownDataKeys.UserInfo] = userInfo; - } + ev.Data[Event.KnownDataKeys.UserInfo] = userInfo; + } - /// - /// Gets the user description from extended data. - /// - public static UserDescription GetUserDescription(this Event ev) { - return ev.Data.TryGetValue(Event.KnownDataKeys.UserDescription, out object value) ? value as UserDescription : null; - } + /// + /// Gets the user description from extended data. + /// + public static UserDescription GetUserDescription(this Event ev) { + return ev.Data.TryGetValue(Event.KnownDataKeys.UserDescription, out object value) ? value as UserDescription : null; + } - /// - /// Sets the user's description of the event. - /// - /// The event - /// The email address - /// The user's description of the event. - public static void SetUserDescription(this Event ev, string emailAddress, string description) { - if (String.IsNullOrWhiteSpace(emailAddress) && String.IsNullOrWhiteSpace(description)) - return; - - ev.Data[Event.KnownDataKeys.UserDescription] = new UserDescription(emailAddress, description); - } + /// + /// Sets the user's description of the event. + /// + /// The event + /// The email address + /// The user's description of the event. + public static void SetUserDescription(this Event ev, string emailAddress, string description) { + if (String.IsNullOrWhiteSpace(emailAddress) && String.IsNullOrWhiteSpace(description)) + return; + + ev.Data[Event.KnownDataKeys.UserDescription] = new UserDescription(emailAddress, description); + } - /// - /// Sets the user's description of the event. - /// - /// The event. - /// The user's description. - public static void SetUserDescription(this Event ev, UserDescription description) { - if (description == null || (String.IsNullOrWhiteSpace(description.EmailAddress) && String.IsNullOrWhiteSpace(description.Description))) - return; + /// + /// Sets the user's description of the event. + /// + /// The event. + /// The user's description. + public static void SetUserDescription(this Event ev, UserDescription description) { + if (description == null || (String.IsNullOrWhiteSpace(description.EmailAddress) && String.IsNullOrWhiteSpace(description.Description))) + return; - ev.Data[Event.KnownDataKeys.UserDescription] = description; - } + ev.Data[Event.KnownDataKeys.UserDescription] = description; + } - public static byte[] GetBytes(this Event ev, JsonSerializerSettings settings) { - return Encoding.UTF8.GetBytes(ev.ToJson(Formatting.None, settings)); - } + public static byte[] GetBytes(this Event ev, JsonSerializerSettings settings) { + return Encoding.UTF8.GetBytes(ev.ToJson(Formatting.None, settings)); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/HashExtensions.cs b/src/Exceptionless.Core/Extensions/HashExtensions.cs index 0128f294d6..df88adf09e 100644 --- a/src/Exceptionless.Core/Extensions/HashExtensions.cs +++ b/src/Exceptionless.Core/Extensions/HashExtensions.cs @@ -1,59 +1,57 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Text; -namespace Exceptionless.Core.Extensions { - public static class HashExtensions { - /// Compute hash on input string - /// The string to compute hash on. - /// - /// The hash as a hexadecimal String. - public static string ComputeHash(this string input, HashAlgorithm algorithm) { - if (String.IsNullOrEmpty(input)) - throw new ArgumentNullException(nameof(input)); - - byte[] data = algorithm.ComputeHash(Encoding.Unicode.GetBytes(input)); - - return ToHex(data); - } - - /// Compute SHA1 hash on input string - /// The string to compute hash on. - /// The hash as a hexadecimal String. - public static string ToSHA1(this string input) { - return ComputeHash(input, SHA1.Create()); - } - - /// Compute SHA1 hash on a collection of input string - /// The collection of strings to compute hash on. - /// The hash as a hexadecimal String. - public static string ToSHA1(this IEnumerable inputs) { - var builder = new StringBuilder(); - - foreach (string input in inputs) - builder.Append(input); - - return builder.ToString().ToSHA1(); - } - - /// Compute SHA256 hash on input string - /// The string to compute hash on. - /// The hash as a hexadecimal String. - public static string ToSHA256(this string input) { - return ComputeHash(input, SHA256.Create()); - } - - /// - /// Converts a byte array to Hexadecimal. - /// - /// The bytes to convert. - /// Hexadecimal string of the byte array. - public static string ToHex(this IEnumerable bytes) { - var sb = new StringBuilder(); - foreach (byte b in bytes) - sb.Append(b.ToString("x2")); - return sb.ToString(); - } +namespace Exceptionless.Core.Extensions; + +public static class HashExtensions { + /// Compute hash on input string + /// The string to compute hash on. + /// + /// The hash as a hexadecimal String. + public static string ComputeHash(this string input, HashAlgorithm algorithm) { + if (String.IsNullOrEmpty(input)) + throw new ArgumentNullException(nameof(input)); + + byte[] data = algorithm.ComputeHash(Encoding.Unicode.GetBytes(input)); + + return ToHex(data); + } + + /// Compute SHA1 hash on input string + /// The string to compute hash on. + /// The hash as a hexadecimal String. + public static string ToSHA1(this string input) { + return ComputeHash(input, SHA1.Create()); + } + + /// Compute SHA1 hash on a collection of input string + /// The collection of strings to compute hash on. + /// The hash as a hexadecimal String. + public static string ToSHA1(this IEnumerable inputs) { + var builder = new StringBuilder(); + + foreach (string input in inputs) + builder.Append(input); + + return builder.ToString().ToSHA1(); + } + + /// Compute SHA256 hash on input string + /// The string to compute hash on. + /// The hash as a hexadecimal String. + public static string ToSHA256(this string input) { + return ComputeHash(input, SHA256.Create()); + } + + /// + /// Converts a byte array to Hexadecimal. + /// + /// The bytes to convert. + /// Hexadecimal string of the byte array. + public static string ToHex(this IEnumerable bytes) { + var sb = new StringBuilder(); + foreach (byte b in bytes) + sb.Append(b.ToString("x2")); + return sb.ToString(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/HttpClientExtensions.cs b/src/Exceptionless.Core/Extensions/HttpClientExtensions.cs index b23ac7525c..4a693479e5 100644 --- a/src/Exceptionless.Core/Extensions/HttpClientExtensions.cs +++ b/src/Exceptionless.Core/Extensions/HttpClientExtensions.cs @@ -1,23 +1,19 @@ // Copyright Exceptionless. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Net.Http; using System.Text; -using System.Threading; -using System.Threading.Tasks; -namespace Exceptionless.Core.Extensions { - public static class HttpClientExtensions { - public static Task PostAsync(this HttpClient httpClient, string url, CancellationToken cancellationToken = default) { - return httpClient.PostAsync(url, new StringContent(String.Empty), cancellationToken); - } +namespace Exceptionless.Core.Extensions; - public static Task PostAsJsonAsync(this HttpClient httpClient, string url, string json, CancellationToken cancellationToken = default) { - var message = new HttpRequestMessage(HttpMethod.Post, new Uri(url)) { - Content = new StringContent(json, Encoding.UTF8, "application/json") - }; +public static class HttpClientExtensions { + public static Task PostAsync(this HttpClient httpClient, string url, CancellationToken cancellationToken = default) { + return httpClient.PostAsync(url, new StringContent(String.Empty), cancellationToken); + } + + public static Task PostAsJsonAsync(this HttpClient httpClient, string url, string json, CancellationToken cancellationToken = default) { + var message = new HttpRequestMessage(HttpMethod.Post, new Uri(url)) { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; - return httpClient.SendAsync(message, cancellationToken); - } + return httpClient.SendAsync(message, cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/ILGeneratorExtensions.cs b/src/Exceptionless.Core/Extensions/ILGeneratorExtensions.cs index a0f2b989db..644cf61073 100644 --- a/src/Exceptionless.Core/Extensions/ILGeneratorExtensions.cs +++ b/src/Exceptionless.Core/Extensions/ILGeneratorExtensions.cs @@ -1,40 +1,39 @@ -using System; -using System.Reflection; +using System.Reflection; using System.Reflection.Emit; -namespace Exceptionless.Core.Extensions { - internal static class ILGeneratorExtensions { - public static void PushInstance(this ILGenerator generator, Type type) { - generator.Emit(OpCodes.Ldarg_0); - if (type.IsValueType) - generator.Emit(OpCodes.Unbox, type); - else - generator.Emit(OpCodes.Castclass, type); - } +namespace Exceptionless.Core.Extensions; - public static void BoxIfNeeded(this ILGenerator generator, Type type) { - if (type.IsValueType) - generator.Emit(OpCodes.Box, type); - else - generator.Emit(OpCodes.Castclass, type); - } +internal static class ILGeneratorExtensions { + public static void PushInstance(this ILGenerator generator, Type type) { + generator.Emit(OpCodes.Ldarg_0); + if (type.IsValueType) + generator.Emit(OpCodes.Unbox, type); + else + generator.Emit(OpCodes.Castclass, type); + } - public static void UnboxIfNeeded(this ILGenerator generator, Type type) { - if (type.IsValueType) - generator.Emit(OpCodes.Unbox_Any, type); - else - generator.Emit(OpCodes.Castclass, type); - } + public static void BoxIfNeeded(this ILGenerator generator, Type type) { + if (type.IsValueType) + generator.Emit(OpCodes.Box, type); + else + generator.Emit(OpCodes.Castclass, type); + } - public static void CallMethod(this ILGenerator generator, MethodInfo methodInfo) { - if (methodInfo.IsFinal || !methodInfo.IsVirtual) - generator.Emit(OpCodes.Call, methodInfo); - else - generator.Emit(OpCodes.Callvirt, methodInfo); - } + public static void UnboxIfNeeded(this ILGenerator generator, Type type) { + if (type.IsValueType) + generator.Emit(OpCodes.Unbox_Any, type); + else + generator.Emit(OpCodes.Castclass, type); + } + + public static void CallMethod(this ILGenerator generator, MethodInfo methodInfo) { + if (methodInfo.IsFinal || !methodInfo.IsVirtual) + generator.Emit(OpCodes.Call, methodInfo); + else + generator.Emit(OpCodes.Callvirt, methodInfo); + } - public static void Return(this ILGenerator generator) { - generator.Emit(OpCodes.Ret); - } + public static void Return(this ILGenerator generator) { + generator.Emit(OpCodes.Ret); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/IdentityUtils.cs b/src/Exceptionless.Core/Extensions/IdentityUtils.cs index 7a622a35f1..f064859ab7 100644 --- a/src/Exceptionless.Core/Extensions/IdentityUtils.cs +++ b/src/Exceptionless.Core/Extensions/IdentityUtils.cs @@ -1,165 +1,164 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; +using System.Security.Claims; using Exceptionless.Core.Authorization; using Exceptionless.Core.Models; using IIdentity = System.Security.Principal.IIdentity; -namespace Exceptionless.Core.Extensions { - public static class IdentityUtils { - public const string TokenAuthenticationType = "Token"; - public const string UserAuthenticationType = "User"; - public const string LoggedInUsersTokenId = "LoggedInUsersTokenId"; - public const string OrganizationIdsClaim = "OrganizationIds"; - public const string ProjectIdClaim = "ProjectId"; - public const string DefaultProjectIdClaim = "DefaultProjectId"; +namespace Exceptionless.Core.Extensions; - public static ClaimsIdentity ToIdentity(this Token token) { - if (token == null || token.Type != TokenType.Access) - return new ClaimsIdentity(); +public static class IdentityUtils { + public const string TokenAuthenticationType = "Token"; + public const string UserAuthenticationType = "User"; + public const string LoggedInUsersTokenId = "LoggedInUsersTokenId"; + public const string OrganizationIdsClaim = "OrganizationIds"; + public const string ProjectIdClaim = "ProjectId"; + public const string DefaultProjectIdClaim = "DefaultProjectId"; - if (!String.IsNullOrEmpty(token.UserId)) - throw new ApplicationException("Can't create token type identity for user token."); + public static ClaimsIdentity ToIdentity(this Token token) { + if (token == null || token.Type != TokenType.Access) + return new ClaimsIdentity(); - var claims = new List(5 + token.Scopes.Count) { + if (!String.IsNullOrEmpty(token.UserId)) + throw new ApplicationException("Can't create token type identity for user token."); + + var claims = new List(5 + token.Scopes.Count) { new Claim(ClaimTypes.NameIdentifier, token.Id), new Claim(OrganizationIdsClaim, token.OrganizationId) }; - if (!String.IsNullOrEmpty(token.ProjectId)) - claims.Add(new Claim(ProjectIdClaim, token.ProjectId)); + if (!String.IsNullOrEmpty(token.ProjectId)) + claims.Add(new Claim(ProjectIdClaim, token.ProjectId)); - if (!String.IsNullOrEmpty(token.DefaultProjectId)) - claims.Add(new Claim(DefaultProjectIdClaim, token.DefaultProjectId)); + if (!String.IsNullOrEmpty(token.DefaultProjectId)) + claims.Add(new Claim(DefaultProjectIdClaim, token.DefaultProjectId)); - if (token.Scopes.Count > 0) { - foreach (string scope in token.Scopes) - claims.Add(new Claim(ClaimTypes.Role, scope)); - } else { - claims.Add(new Claim(ClaimTypes.Role, AuthorizationRoles.Client)); - } - - return new ClaimsIdentity(claims, TokenAuthenticationType); + if (token.Scopes.Count > 0) { + foreach (string scope in token.Scopes) + claims.Add(new Claim(ClaimTypes.Role, scope)); + } + else { + claims.Add(new Claim(ClaimTypes.Role, AuthorizationRoles.Client)); } - public static ClaimsIdentity ToIdentity(this User user, Token token = null) { - if (user == null) - return new ClaimsIdentity(); + return new ClaimsIdentity(claims, TokenAuthenticationType); + } + + public static ClaimsIdentity ToIdentity(this User user, Token token = null) { + if (user == null) + return new ClaimsIdentity(); - var claims = new List(7 + user.Roles.Count) { + var claims = new List(7 + user.Roles.Count) { new Claim(ClaimTypes.Name, user.EmailAddress), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(OrganizationIdsClaim, String.Join(",", user.OrganizationIds)) }; - if (token != null) { - claims.Add(new Claim(LoggedInUsersTokenId, token.Id)); - - if (!String.IsNullOrEmpty(token.DefaultProjectId)) - claims.Add(new Claim(DefaultProjectIdClaim, token.DefaultProjectId)); - } + if (token != null) { + claims.Add(new Claim(LoggedInUsersTokenId, token.Id)); - if (user.Roles.Count > 0) { - // add implied scopes - var roles = user.Roles.ToHashSet(); - if (roles.Contains(AuthorizationRoles.GlobalAdmin)) - roles.Add(AuthorizationRoles.User); + if (!String.IsNullOrEmpty(token.DefaultProjectId)) + claims.Add(new Claim(DefaultProjectIdClaim, token.DefaultProjectId)); + } - if (roles.Contains(AuthorizationRoles.User)) - roles.Add(AuthorizationRoles.Client); + if (user.Roles.Count > 0) { + // add implied scopes + var roles = user.Roles.ToHashSet(); + if (roles.Contains(AuthorizationRoles.GlobalAdmin)) + roles.Add(AuthorizationRoles.User); - foreach (string role in roles) - claims.Add(new Claim(ClaimTypes.Role, role)); - } else { - claims.Add(new Claim(ClaimTypes.Role, AuthorizationRoles.Client)); - claims.Add(new Claim(ClaimTypes.Role, AuthorizationRoles.User)); - } + if (roles.Contains(AuthorizationRoles.User)) + roles.Add(AuthorizationRoles.Client); - return new ClaimsIdentity(claims, UserAuthenticationType); + foreach (string role in roles) + claims.Add(new Claim(ClaimTypes.Role, role)); } - - public static AuthType GetAuthType(this ClaimsPrincipal principal) { - if (principal?.Identity == null || !principal.Identity.IsAuthenticated) - return AuthType.Anonymous; - - return IsTokenAuthType(principal) ? AuthType.Token : AuthType.User; + else { + claims.Add(new Claim(ClaimTypes.Role, AuthorizationRoles.Client)); + claims.Add(new Claim(ClaimTypes.Role, AuthorizationRoles.User)); } - public static bool IsTokenAuthType(this ClaimsPrincipal principal) { - var identity = GetClaimsIdentity(principal); - if (identity == null) - return false; + return new ClaimsIdentity(claims, UserAuthenticationType); + } - return identity.AuthenticationType == TokenAuthenticationType; - } + public static AuthType GetAuthType(this ClaimsPrincipal principal) { + if (principal?.Identity == null || !principal.Identity.IsAuthenticated) + return AuthType.Anonymous; - public static bool IsUserAuthType(this ClaimsPrincipal principal) { - var identity = GetClaimsIdentity(principal); - if (identity == null) - return false; + return IsTokenAuthType(principal) ? AuthType.Token : AuthType.User; + } - return identity.AuthenticationType == UserAuthenticationType; - } + public static bool IsTokenAuthType(this ClaimsPrincipal principal) { + var identity = GetClaimsIdentity(principal); + if (identity == null) + return false; - public static ClaimsIdentity GetClaimsIdentity(this ClaimsPrincipal principal) { - return principal?.Identity as ClaimsIdentity; - } + return identity.AuthenticationType == TokenAuthenticationType; + } - public static string GetUserId(this ClaimsPrincipal principal) { - return IsUserAuthType(principal) ? GetClaimValue(principal, ClaimTypes.NameIdentifier) : null; - } + public static bool IsUserAuthType(this ClaimsPrincipal principal) { + var identity = GetClaimsIdentity(principal); + if (identity == null) + return false; - /// - /// Gets the token id that authenticated the current user. If null, user logged in via oauth. - /// - /// - /// - public static string GetLoggedInUsersTokenId(this ClaimsPrincipal principal) { - return IsUserAuthType(principal) ? GetClaimValue(principal, LoggedInUsersTokenId) : null; - } + return identity.AuthenticationType == UserAuthenticationType; + } - public static string GetTokenOrganizationId(this ClaimsPrincipal principal) { - return GetClaimValue(principal, OrganizationIdsClaim); - } + public static ClaimsIdentity GetClaimsIdentity(this ClaimsPrincipal principal) { + return principal?.Identity as ClaimsIdentity; + } - public static string[] GetOrganizationIds(this ClaimsPrincipal principal) { - string ids = GetClaimValue(principal, OrganizationIdsClaim); - if (String.IsNullOrEmpty(ids)) - return Array.Empty(); + public static string GetUserId(this ClaimsPrincipal principal) { + return IsUserAuthType(principal) ? GetClaimValue(principal, ClaimTypes.NameIdentifier) : null; + } - return ids.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } + /// + /// Gets the token id that authenticated the current user. If null, user logged in via oauth. + /// + /// + /// + public static string GetLoggedInUsersTokenId(this ClaimsPrincipal principal) { + return IsUserAuthType(principal) ? GetClaimValue(principal, LoggedInUsersTokenId) : null; + } - public static string GetProjectId(this ClaimsPrincipal principal) { - return GetClaimValue(principal, ProjectIdClaim); - } + public static string GetTokenOrganizationId(this ClaimsPrincipal principal) { + return GetClaimValue(principal, OrganizationIdsClaim); + } - public static string GetDefaultProjectId(this ClaimsPrincipal principal) { - // if this claim is for a specific project, then that is always the default project. - return GetClaimValue(principal, ProjectIdClaim) ?? GetClaimValue(principal, DefaultProjectIdClaim); - } + public static string[] GetOrganizationIds(this ClaimsPrincipal principal) { + string ids = GetClaimValue(principal, OrganizationIdsClaim); + if (String.IsNullOrEmpty(ids)) + return Array.Empty(); - public static string GetClaimValue(this ClaimsPrincipal principal, string type) { - var identity = principal?.GetClaimsIdentity(); - if (identity == null) - return null; + return ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } - return GetClaimValue(identity, type); - } + public static string GetProjectId(this ClaimsPrincipal principal) { + return GetClaimValue(principal, ProjectIdClaim); + } - public static string GetClaimValue(this IIdentity identity, string type) { - if (identity is not ClaimsIdentity claimsIdentity) - return null; + public static string GetDefaultProjectId(this ClaimsPrincipal principal) { + // if this claim is for a specific project, then that is always the default project. + return GetClaimValue(principal, ProjectIdClaim) ?? GetClaimValue(principal, DefaultProjectIdClaim); + } - var claim = claimsIdentity.FindAll(type).FirstOrDefault(); - return claim?.Value; - } + public static string GetClaimValue(this ClaimsPrincipal principal, string type) { + var identity = principal?.GetClaimsIdentity(); + if (identity == null) + return null; + + return GetClaimValue(identity, type); } - public enum AuthType { - User, - Token, - Anonymous + public static string GetClaimValue(this IIdentity identity, string type) { + if (identity is not ClaimsIdentity claimsIdentity) + return null; + + var claim = claimsIdentity.FindAll(type).FirstOrDefault(); + return claim?.Value; } } + +public enum AuthType { + User, + Token, + Anonymous +} diff --git a/src/Exceptionless.Core/Extensions/JsonExtensions.cs b/src/Exceptionless.Core/Extensions/JsonExtensions.cs index 4ece8d964b..06ce88ce88 100644 --- a/src/Exceptionless.Core/Extensions/JsonExtensions.cs +++ b/src/Exceptionless.Core/Extensions/JsonExtensions.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Exceptionless.Core.Reflection; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; @@ -13,214 +9,217 @@ using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; -namespace Exceptionless.Core.Extensions { - [System.Runtime.InteropServices.GuidAttribute("4186FC77-AF28-4D51-AAC3-49055DD855A4")] - public static class JsonExtensions { - public static bool IsNullOrEmpty(this JToken target) { - if (target == null || target.Type == JTokenType.Null) - return true; +namespace Exceptionless.Core.Extensions; - if (target.Type == JTokenType.Object || target.Type == JTokenType.Array) - return !target.HasValues; - - if (target.Type != JTokenType.Property) - return false; - - var value = ((JProperty)target).Value; - if (value.Type == JTokenType.String) - return value.ToString().IsNullOrEmpty(); +[System.Runtime.InteropServices.GuidAttribute("4186FC77-AF28-4D51-AAC3-49055DD855A4")] +public static class JsonExtensions { + public static bool IsNullOrEmpty(this JToken target) { + if (target == null || target.Type == JTokenType.Null) + return true; - return IsNullOrEmpty(value); - } + if (target.Type == JTokenType.Object || target.Type == JTokenType.Array) + return !target.HasValues; - public static bool IsPropertyNullOrEmpty(this JObject target, string name) { - if (target[name] == null) - return true; + if (target.Type != JTokenType.Property) + return false; - return target.Property(name).Value.IsNullOrEmpty(); - } + var value = ((JProperty)target).Value; + if (value.Type == JTokenType.String) + return value.ToString().IsNullOrEmpty(); - public static bool RemoveIfNullOrEmpty(this JObject target, string name) { - if (!target.IsPropertyNullOrEmpty(name)) - return false; + return IsNullOrEmpty(value); + } - target.Remove(name); + public static bool IsPropertyNullOrEmpty(this JObject target, string name) { + if (target[name] == null) return true; - } - public static void RemoveAll(this JObject target, params string[] names) { - foreach (string name in names) - target.Remove(name); - } + return target.Property(name).Value.IsNullOrEmpty(); + } + public static bool RemoveIfNullOrEmpty(this JObject target, string name) { + if (!target.IsPropertyNullOrEmpty(name)) + return false; - public static bool RemoveAllIfNullOrEmpty(this JObject target, params string[] names) { - if (target.IsNullOrEmpty()) - return false; + target.Remove(name); + return true; + } - var properties = target.Descendants().OfType().Where(t => names.Contains(t.Name) && t.IsNullOrEmpty()).ToList(); - foreach(var p in properties) - p.Remove(); + public static void RemoveAll(this JObject target, params string[] names) { + foreach (string name in names) + target.Remove(name); + } - return true; - } - public static bool Rename(this JObject target, string currentName, string newName) { - if (String.Equals(currentName, newName)) - return true; + public static bool RemoveAllIfNullOrEmpty(this JObject target, params string[] names) { + if (target.IsNullOrEmpty()) + return false; - if (target[currentName] == null) - return false; + var properties = target.Descendants().OfType().Where(t => names.Contains(t.Name) && t.IsNullOrEmpty()).ToList(); + foreach (var p in properties) + p.Remove(); - var p = target.Property(currentName); - p.Replace(new JProperty(newName, p.Value)); + return true; + } + public static bool Rename(this JObject target, string currentName, string newName) { + if (String.Equals(currentName, newName)) return true; - } - public static bool RenameOrRemoveIfNullOrEmpty(this JObject target, string currentName, string newName) { - if (target[currentName] == null) - return false; + if (target[currentName] == null) + return false; - bool isNullOrEmpty = target.IsPropertyNullOrEmpty(currentName); - var p = target.Property(currentName); - if (isNullOrEmpty) { - target.Remove(p.Name); - return false; - } + var p = target.Property(currentName); + p.Replace(new JProperty(newName, p.Value)); - p.Replace(new JProperty(newName, p.Value)); - return true; + return true; + } + + public static bool RenameOrRemoveIfNullOrEmpty(this JObject target, string currentName, string newName) { + if (target[currentName] == null) + return false; + + bool isNullOrEmpty = target.IsPropertyNullOrEmpty(currentName); + var p = target.Property(currentName); + if (isNullOrEmpty) { + target.Remove(p.Name); + return false; } - public static void MoveOrRemoveIfNullOrEmpty(this JObject target, JObject source, params string[] names) { - foreach (string name in names) { - if (source[name] == null) - continue; + p.Replace(new JProperty(newName, p.Value)); + return true; + } - bool isNullOrEmpty = source.IsPropertyNullOrEmpty(name); - var p = source.Property(name); - source.Remove(p.Name); + public static void MoveOrRemoveIfNullOrEmpty(this JObject target, JObject source, params string[] names) { + foreach (string name in names) { + if (source[name] == null) + continue; - if (isNullOrEmpty) - continue; + bool isNullOrEmpty = source.IsPropertyNullOrEmpty(name); + var p = source.Property(name); + source.Remove(p.Name); - target.Add(name, p.Value); - } - } + if (isNullOrEmpty) + continue; - public static bool RenameAll(this JObject target, string currentName, string newName) { - var properties = target.Descendants().OfType().Where(t => t.Name == currentName).ToList(); - foreach (var p in properties) { - if (p.Parent is JObject parent) - parent.Rename(currentName, newName); - } + target.Add(name, p.Value); + } + } - return true; + public static bool RenameAll(this JObject target, string currentName, string newName) { + var properties = target.Descendants().OfType().Where(t => t.Name == currentName).ToList(); + foreach (var p in properties) { + if (p.Parent is JObject parent) + parent.Rename(currentName, newName); } - public static string GetPropertyStringValue(this JObject target, string name) { - if (target.IsPropertyNullOrEmpty(name)) - return null; + return true; + } - return target.Property(name).Value.ToString(); - } + public static string GetPropertyStringValue(this JObject target, string name) { + if (target.IsPropertyNullOrEmpty(name)) + return null; + return target.Property(name).Value.ToString(); + } - public static string GetPropertyStringValueAndRemove(this JObject target, string name) { - string value = target.GetPropertyStringValue(name); - target.Remove(name); - return value; - } - public static bool IsJson(this string value) { - return value.GetJsonType() != JsonType.None; - } + public static string GetPropertyStringValueAndRemove(this JObject target, string name) { + string value = target.GetPropertyStringValue(name); + target.Remove(name); + return value; + } - public static JsonType GetJsonType(this string value) { - if (String.IsNullOrEmpty(value)) - return JsonType.None; + public static bool IsJson(this string value) { + return value.GetJsonType() != JsonType.None; + } - for (int i = 0; i < value.Length; i++) { - if (Char.IsWhiteSpace(value[i])) - continue; + public static JsonType GetJsonType(this string value) { + if (String.IsNullOrEmpty(value)) + return JsonType.None; - if (value[i] == '{') - return JsonType.Object; + for (int i = 0; i < value.Length; i++) { + if (Char.IsWhiteSpace(value[i])) + continue; - if (value[i] == '[') - return JsonType.Array; + if (value[i] == '{') + return JsonType.Object; - break; - } + if (value[i] == '[') + return JsonType.Array; - return JsonType.None; + break; } - public static string ToJson(this T data, Formatting formatting = Formatting.None, JsonSerializerSettings settings = null) { - var serializer = settings == null ? JsonSerializer.CreateDefault() : JsonSerializer.CreateDefault(settings); - serializer.Formatting = formatting; + return JsonType.None; + } - using (var sw = new StringWriter()) { - serializer.Serialize(sw, data, typeof(T)); - return sw.ToString(); - } - } + public static string ToJson(this T data, Formatting formatting = Formatting.None, JsonSerializerSettings settings = null) { + var serializer = settings == null ? JsonSerializer.CreateDefault() : JsonSerializer.CreateDefault(settings); + serializer.Formatting = formatting; - public static List FromJson(this JArray data, JsonSerializerSettings settings = null) { - var serializer = settings == null ? JsonSerializer.CreateDefault() : JsonSerializer.CreateDefault(settings); - return data.ToObject>(serializer); + using (var sw = new StringWriter()) { + serializer.Serialize(sw, data, typeof(T)); + return sw.ToString(); } + } - public static T FromJson(this string data, JsonSerializerSettings settings = null) { - var serializer = settings == null ? JsonSerializer.CreateDefault() : JsonSerializer.CreateDefault(settings); + public static List FromJson(this JArray data, JsonSerializerSettings settings = null) { + var serializer = settings == null ? JsonSerializer.CreateDefault() : JsonSerializer.CreateDefault(settings); + return data.ToObject>(serializer); + } - using (var sw = new StringReader(data)) - using (var sr = new JsonTextReader(sw)) - return serializer.Deserialize(sr); - } + public static T FromJson(this string data, JsonSerializerSettings settings = null) { + var serializer = settings == null ? JsonSerializer.CreateDefault() : JsonSerializer.CreateDefault(settings); - public static bool TryFromJson(this string data, out T value, JsonSerializerSettings settings = null) { - try { - value = data.FromJson(settings); - return true; - } catch (Exception) { - value = default; - return false; - } + using (var sw = new StringReader(data)) + using (var sr = new JsonTextReader(sw)) + return serializer.Deserialize(sr); + } + + public static bool TryFromJson(this string data, out T value, JsonSerializerSettings settings = null) { + try { + value = data.FromJson(settings); + return true; } + catch (Exception) { + value = default; + return false; + } + } + + private static readonly ConcurrentDictionary _countAccessors = new ConcurrentDictionary(); + public static bool IsValueEmptyCollection(this JsonProperty property, object target) { + object value = property.ValueProvider.GetValue(target); + if (value == null) + return true; - private static readonly ConcurrentDictionary _countAccessors = new ConcurrentDictionary(); - public static bool IsValueEmptyCollection(this JsonProperty property, object target) { - object value = property.ValueProvider.GetValue(target); - if (value == null) - return true; - - if (value is ICollection collection) - return collection.Count == 0; - - if (!_countAccessors.ContainsKey(property.PropertyType)) { - if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) { - var countProperty = property.PropertyType.GetProperty("Count"); - if (countProperty != null) - _countAccessors.AddOrUpdate(property.PropertyType, LateBinder.GetPropertyAccessor(countProperty)); - else - _countAccessors.AddOrUpdate(property.PropertyType, null); - } else { + if (value is ICollection collection) + return collection.Count == 0; + + if (!_countAccessors.ContainsKey(property.PropertyType)) { + if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) { + var countProperty = property.PropertyType.GetProperty("Count"); + if (countProperty != null) + _countAccessors.AddOrUpdate(property.PropertyType, LateBinder.GetPropertyAccessor(countProperty)); + else _countAccessors.AddOrUpdate(property.PropertyType, null); - } } + else { + _countAccessors.AddOrUpdate(property.PropertyType, null); + } + } - var countAccessor = _countAccessors[property.PropertyType]; - if (countAccessor == null) - return false; + var countAccessor = _countAccessors[property.PropertyType]; + if (countAccessor == null) + return false; - int count = (int)countAccessor.GetValue(value); - return count == 0; - } + int count = (int)countAccessor.GetValue(value); + return count == 0; + } - public static void AddModelConverters(this JsonSerializerSettings settings, ILogger logger) { - var knownEventDataTypes = new Dictionary { + public static void AddModelConverters(this JsonSerializerSettings settings, ILogger logger) { + var knownEventDataTypes = new Dictionary { { Event.KnownDataKeys.Error, typeof(Error) }, { Event.KnownDataKeys.EnvironmentInfo, typeof(EnvironmentInfo) }, { Event.KnownDataKeys.Location, typeof(Location) }, @@ -232,31 +231,30 @@ public static void AddModelConverters(this JsonSerializerSettings settings, ILog { Event.KnownDataKeys.UserInfo, typeof(UserInfo) } }; - var knownProjectDataTypes = new Dictionary { + var knownProjectDataTypes = new Dictionary { { Project.KnownDataKeys.SlackToken, typeof(SlackToken) } }; - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger, knownProjectDataTypes)); - settings.Converters.Add(new DataObjectConverter(logger, knownEventDataTypes)); - settings.Converters.Add(new DataObjectConverter(logger, knownEventDataTypes)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - settings.Converters.Add(new DataObjectConverter(logger)); - } + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger, knownProjectDataTypes)); + settings.Converters.Add(new DataObjectConverter(logger, knownEventDataTypes)); + settings.Converters.Add(new DataObjectConverter(logger, knownEventDataTypes)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); + settings.Converters.Add(new DataObjectConverter(logger)); } +} - public enum JsonType : byte { - None, - Object, - Array - } -} \ No newline at end of file +public enum JsonType : byte { + None, + Object, + Array +} diff --git a/src/Exceptionless.Core/Extensions/LogBuilderExtensions.cs b/src/Exceptionless.Core/Extensions/LogBuilderExtensions.cs index 8a549a227f..2838c37bee 100644 --- a/src/Exceptionless.Core/Extensions/LogBuilderExtensions.cs +++ b/src/Exceptionless.Core/Extensions/LogBuilderExtensions.cs @@ -1,108 +1,105 @@ -using System; -using System.Collections.Generic; +namespace Microsoft.Extensions.Logging; -namespace Microsoft.Extensions.Logging { - public class ExceptionlessState : Dictionary { - public ExceptionlessState() {} +public class ExceptionlessState : Dictionary { + public ExceptionlessState() { } - public ExceptionlessState Project(string projectId) { - if (!String.IsNullOrEmpty(projectId)) - base["project"] = projectId; + public ExceptionlessState Project(string projectId) { + if (!String.IsNullOrEmpty(projectId)) + base["project"] = projectId; - return this; - } + return this; + } + + public ExceptionlessState Organization(string organizationId) { + if (!String.IsNullOrEmpty(organizationId)) + base["organization"] = organizationId; - public ExceptionlessState Organization(string organizationId) { - if (!String.IsNullOrEmpty(organizationId)) - base["organization"] = organizationId; + return this; + } + /// + /// Adds one or more tags to the event. + /// + /// The tag to be added to the event. + public ExceptionlessState Tag(string tag) { + if (String.IsNullOrEmpty(tag)) return this; - } - /// - /// Adds one or more tags to the event. - /// - /// The tag to be added to the event. - public ExceptionlessState Tag(string tag) { - if (String.IsNullOrEmpty(tag)) - return this; + HashSet tagList = null; + if (TryGetValue(Tags, out object v) && v is HashSet t) + tagList = t; - HashSet tagList = null; - if (TryGetValue(Tags, out object v) && v is HashSet t) - tagList = t; + if (tagList == null) + tagList = new HashSet(); - if (tagList == null) - tagList = new HashSet(); + tagList.Add(tag); + base[Tags] = tagList; + return this; + } - tagList.Add(tag); - base[Tags] = tagList; - return this; - } + public ExceptionlessState Value(decimal value) { + base["@value"] = value; + return this; + } - public ExceptionlessState Value(decimal value) { - base["@value"] = value; - return this; - } + public ExceptionlessState ManualStackingKey(string stackingKey) { + if (!String.IsNullOrEmpty(stackingKey)) + base["@stack"] = stackingKey; - public ExceptionlessState ManualStackingKey(string stackingKey) { - if (!String.IsNullOrEmpty(stackingKey)) - base["@stack"] = stackingKey; + return this; + } - return this; - } - - /// - /// Sets the user's identity (ie. email address, username, user id) that the event happened to. - /// - /// The user's identity that the event happened to. - public ExceptionlessState Identity(string identity) { - return Identity(identity, null); - } - - /// - /// Sets the user's identity (ie. email address, username, user id) that the event happened to. - /// - /// The user's identity that the event happened to. - /// The user's friendly name that the event happened to. - public ExceptionlessState Identity(string identity, string name) { - if (String.IsNullOrWhiteSpace(identity) && String.IsNullOrWhiteSpace(name)) - return this; - - base["@user"] = new { Identity = identity, Name = name }; - return this; - } + /// + /// Sets the user's identity (ie. email address, username, user id) that the event happened to. + /// + /// The user's identity that the event happened to. + public ExceptionlessState Identity(string identity) { + return Identity(identity, null); + } - public ExceptionlessState Property(string key, object value) { - base[key] = value; - return this; - } - - /// - /// Marks the event as being a unhandled occurrence and sets the submission method. - /// - /// The submission method. - public ExceptionlessState MarkUnhandled(string submissionMethod = null) { - return MarkAsUnhandledError().SetSubmissionMethod(submissionMethod); - } - - /// - /// Marks the event as being a unhandled error occurrence. - /// - public ExceptionlessState MarkAsUnhandledError() { - base[IsUnhandledError] = true; + /// + /// Sets the user's identity (ie. email address, username, user id) that the event happened to. + /// + /// The user's identity that the event happened to. + /// The user's friendly name that the event happened to. + public ExceptionlessState Identity(string identity, string name) { + if (String.IsNullOrWhiteSpace(identity) && String.IsNullOrWhiteSpace(name)) return this; - } - /// - /// Sets the submission method that created the event (E.G., UnobservedTaskException) - /// - public ExceptionlessState SetSubmissionMethod(string submissionMethod) { - base[SubmissionMethod] = submissionMethod; - return this; - } + base["@user"] = new { Identity = identity, Name = name }; + return this; + } + + public ExceptionlessState Property(string key, object value) { + base[key] = value; + return this; + } - private const string IsUnhandledError = "@@_IsUnhandledError"; - private const string SubmissionMethod = "@@_SubmissionMethod"; - private const string Tags = "Tags"; + /// + /// Marks the event as being a unhandled occurrence and sets the submission method. + /// + /// The submission method. + public ExceptionlessState MarkUnhandled(string submissionMethod = null) { + return MarkAsUnhandledError().SetSubmissionMethod(submissionMethod); } -} \ No newline at end of file + + /// + /// Marks the event as being a unhandled error occurrence. + /// + public ExceptionlessState MarkAsUnhandledError() { + base[IsUnhandledError] = true; + return this; + } + + /// + /// Sets the submission method that created the event (E.G., UnobservedTaskException) + /// + public ExceptionlessState SetSubmissionMethod(string submissionMethod) { + base[SubmissionMethod] = submissionMethod; + return this; + } + + private const string IsUnhandledError = "@@_IsUnhandledError"; + private const string SubmissionMethod = "@@_SubmissionMethod"; + private const string Tags = "Tags"; +} diff --git a/src/Exceptionless.Core/Extensions/LoggerExtensions.cs b/src/Exceptionless.Core/Extensions/LoggerExtensions.cs index ca6b13a091..b774bf55c6 100644 --- a/src/Exceptionless.Core/Extensions/LoggerExtensions.cs +++ b/src/Exceptionless.Core/Extensions/LoggerExtensions.cs @@ -1,202 +1,201 @@ -using System; -using System.Net; +using System.Net; using Microsoft.Extensions.Logging; #nullable enable -namespace Exceptionless.Core.Extensions { - internal static class LoggerExtensions { - - private static readonly Action _recordWebHook = - LoggerMessage.Define( - LogLevel.Trace, - new EventId(0, nameof(RecordWebHook)), - "Process web hook call: id={Id} project={project} url={Url}"); - - private static readonly Action _webHookCancelled = - LoggerMessage.Define( - LogLevel.Information, - new EventId(1, nameof(WebHookCancelled)), - "Web hook cancelled: Web hook is disabled"); - - private static readonly Action _webHookCancelledBackoff = - LoggerMessage.Define( - LogLevel.Information, - new EventId(2, nameof(WebHookCancelledBackoff)), - "Web hook cancelled due to {FailureCount} consecutive failed attempts. Will be allowed to try again at {NextAttempt}."); - - private static readonly Action _webHookTimeout = - LoggerMessage.Define( - LogLevel.Error, - new EventId(3, nameof(WebHookTimeout)), - "Timeout calling web hook: status={Status} org={organization} project={project} url={Url}"); - - private static readonly Action _webHookError = - LoggerMessage.Define( - LogLevel.Error, - new EventId(4, nameof(WebHookError)), - "Error calling web hook: status={Status} org={organization} project={project} url={Url}"); - - private static readonly Action _webHookComplete = - LoggerMessage.Define( - LogLevel.Information, - new EventId(5, nameof(WebHookError)), - "Web hook POST complete: status={Status} org={organization} project={project} url={Url}"); - - private static readonly Action _webHookDisabledStatusCode = - LoggerMessage.Define( - LogLevel.Warning, - new EventId(6, nameof(WebHookDisabledStatusCode)), - "Disabling Web hook instance {WebHookId} due to status code: status={Status} org={organization} project={project} url={Url}"); - - private static readonly Action _webHookDisabledTooManyErrors = - LoggerMessage.Define( - LogLevel.Warning, - new EventId(7, nameof(WebHookDisabledTooManyErrors)), - "Disabling Web hook instance {WebHookId} due to too many consecutive failures."); - - private static readonly Action _cleanupFinished = - LoggerMessage.Define( - LogLevel.Information, - new EventId(8, nameof(CleanupFinished)), - "Finished cleaning up data"); - - private static readonly Action _cleanupOrganizationSoftDeletes = - LoggerMessage.Define( - LogLevel.Information, - new EventId(9, nameof(CleanupOrganizationSoftDeletes)), - "Cleaning up {OrganizationTotal} soft deleted organization(s)"); - - private static readonly Action _cleanupProjectSoftDeletes = - LoggerMessage.Define( - LogLevel.Information, - new EventId(10, nameof(CleanupProjectSoftDeletes)), - "Cleaning up {ProjectTotal} soft deleted project(s)"); - - private static readonly Action _cleanupStackSoftDeletes = - LoggerMessage.Define( - LogLevel.Information, - new EventId(11, nameof(CleanupStackSoftDeletes)), - "Cleaning up {StackTotal} soft deleted stack(s)"); - - private static readonly Action _removeOrganizationStart = - LoggerMessage.Define( - LogLevel.Information, - new EventId(12, nameof(RemoveOrganizationStart)), - "Removing organization: {Organization} ({OrganizationId})"); - - private static readonly Action _removeOrganizationComplete = - LoggerMessage.Define( - LogLevel.Information, - new EventId(14, nameof(RemoveOrganizationComplete)), - "Removed organization: {Organization} ({OrganizationId}), Removed {RemovedProjects} Projects, {RemovedStacks} Stacks, {RemovedEvents} Events"); - - private static readonly Action _removeProjectStart = - LoggerMessage.Define( - LogLevel.Information, - new EventId(15, nameof(RemoveProjectStart)), - "Removing project: {Project} ({ProjectId})"); - - private static readonly Action _removeProjectComplete = - LoggerMessage.Define( - LogLevel.Information, - new EventId(16, nameof(RemoveProjectComplete)), - "Removed project: {Project} ({ProjectId}), Removed {RemovedStacks} Stacks, {RemovedEvents} Events"); - - - private static readonly Action _removeStacksComplete = - LoggerMessage.Define( - LogLevel.Information, - new EventId(17, nameof(RemoveStacksComplete)), - "Removed {RemovedStacks} Stacks and {RemovedEvents} Events"); - - private static readonly Action _retentionEnforcementStackStart = - LoggerMessage.Define( - LogLevel.Information, - new EventId(18, nameof(RetentionEnforcementStackStart)), - "Enforcing stack retention period older than {RetentionPeriod:g} for organization {OrganizationName} ({OrganizationId}), Found {TotalStacks} Stacks"); - - private static readonly Action _retentionEnforcementStackComplete = - LoggerMessage.Define( - LogLevel.Information, - new EventId(19, nameof(RetentionEnforcementStackComplete)), - "Enforced stack retention period for {OrganizationName} ({OrganizationId}), Removed {RemovedStacks} Stacks"); - - private static readonly Action _retentionEnforcementEventStart = - LoggerMessage.Define( - LogLevel.Information, - new EventId(20, nameof(RetentionEnforcementEventStart)), - "Enforcing event retention period older than {RetentionPeriod:g} for organization {OrganizationName} ({OrganizationId})."); - - private static readonly Action _retentionEnforcementEventComplete = - LoggerMessage.Define( - LogLevel.Information, - new EventId(21, nameof(RetentionEnforcementEventComplete)), - "Enforced event retention period for {OrganizationName} ({OrganizationId}), Removed {RemovedEvents} Events"); - - public static void RemoveStacksComplete(this ILogger logger, long removedStacks, long removedEvents) - => _removeStacksComplete(logger, removedStacks, removedEvents, null); +namespace Exceptionless.Core.Extensions; + +internal static class LoggerExtensions { + + private static readonly Action _recordWebHook = + LoggerMessage.Define( + LogLevel.Trace, + new EventId(0, nameof(RecordWebHook)), + "Process web hook call: id={Id} project={project} url={Url}"); + + private static readonly Action _webHookCancelled = + LoggerMessage.Define( + LogLevel.Information, + new EventId(1, nameof(WebHookCancelled)), + "Web hook cancelled: Web hook is disabled"); + + private static readonly Action _webHookCancelledBackoff = + LoggerMessage.Define( + LogLevel.Information, + new EventId(2, nameof(WebHookCancelledBackoff)), + "Web hook cancelled due to {FailureCount} consecutive failed attempts. Will be allowed to try again at {NextAttempt}."); + + private static readonly Action _webHookTimeout = + LoggerMessage.Define( + LogLevel.Error, + new EventId(3, nameof(WebHookTimeout)), + "Timeout calling web hook: status={Status} org={organization} project={project} url={Url}"); + + private static readonly Action _webHookError = + LoggerMessage.Define( + LogLevel.Error, + new EventId(4, nameof(WebHookError)), + "Error calling web hook: status={Status} org={organization} project={project} url={Url}"); + + private static readonly Action _webHookComplete = + LoggerMessage.Define( + LogLevel.Information, + new EventId(5, nameof(WebHookError)), + "Web hook POST complete: status={Status} org={organization} project={project} url={Url}"); + + private static readonly Action _webHookDisabledStatusCode = + LoggerMessage.Define( + LogLevel.Warning, + new EventId(6, nameof(WebHookDisabledStatusCode)), + "Disabling Web hook instance {WebHookId} due to status code: status={Status} org={organization} project={project} url={Url}"); + + private static readonly Action _webHookDisabledTooManyErrors = + LoggerMessage.Define( + LogLevel.Warning, + new EventId(7, nameof(WebHookDisabledTooManyErrors)), + "Disabling Web hook instance {WebHookId} due to too many consecutive failures."); + + private static readonly Action _cleanupFinished = + LoggerMessage.Define( + LogLevel.Information, + new EventId(8, nameof(CleanupFinished)), + "Finished cleaning up data"); + + private static readonly Action _cleanupOrganizationSoftDeletes = + LoggerMessage.Define( + LogLevel.Information, + new EventId(9, nameof(CleanupOrganizationSoftDeletes)), + "Cleaning up {OrganizationTotal} soft deleted organization(s)"); + + private static readonly Action _cleanupProjectSoftDeletes = + LoggerMessage.Define( + LogLevel.Information, + new EventId(10, nameof(CleanupProjectSoftDeletes)), + "Cleaning up {ProjectTotal} soft deleted project(s)"); + + private static readonly Action _cleanupStackSoftDeletes = + LoggerMessage.Define( + LogLevel.Information, + new EventId(11, nameof(CleanupStackSoftDeletes)), + "Cleaning up {StackTotal} soft deleted stack(s)"); + + private static readonly Action _removeOrganizationStart = + LoggerMessage.Define( + LogLevel.Information, + new EventId(12, nameof(RemoveOrganizationStart)), + "Removing organization: {Organization} ({OrganizationId})"); + + private static readonly Action _removeOrganizationComplete = + LoggerMessage.Define( + LogLevel.Information, + new EventId(14, nameof(RemoveOrganizationComplete)), + "Removed organization: {Organization} ({OrganizationId}), Removed {RemovedProjects} Projects, {RemovedStacks} Stacks, {RemovedEvents} Events"); + + private static readonly Action _removeProjectStart = + LoggerMessage.Define( + LogLevel.Information, + new EventId(15, nameof(RemoveProjectStart)), + "Removing project: {Project} ({ProjectId})"); + + private static readonly Action _removeProjectComplete = + LoggerMessage.Define( + LogLevel.Information, + new EventId(16, nameof(RemoveProjectComplete)), + "Removed project: {Project} ({ProjectId}), Removed {RemovedStacks} Stacks, {RemovedEvents} Events"); + + + private static readonly Action _removeStacksComplete = + LoggerMessage.Define( + LogLevel.Information, + new EventId(17, nameof(RemoveStacksComplete)), + "Removed {RemovedStacks} Stacks and {RemovedEvents} Events"); + + private static readonly Action _retentionEnforcementStackStart = + LoggerMessage.Define( + LogLevel.Information, + new EventId(18, nameof(RetentionEnforcementStackStart)), + "Enforcing stack retention period older than {RetentionPeriod:g} for organization {OrganizationName} ({OrganizationId}), Found {TotalStacks} Stacks"); + + private static readonly Action _retentionEnforcementStackComplete = + LoggerMessage.Define( + LogLevel.Information, + new EventId(19, nameof(RetentionEnforcementStackComplete)), + "Enforced stack retention period for {OrganizationName} ({OrganizationId}), Removed {RemovedStacks} Stacks"); + + private static readonly Action _retentionEnforcementEventStart = + LoggerMessage.Define( + LogLevel.Information, + new EventId(20, nameof(RetentionEnforcementEventStart)), + "Enforcing event retention period older than {RetentionPeriod:g} for organization {OrganizationName} ({OrganizationId})."); + + private static readonly Action _retentionEnforcementEventComplete = + LoggerMessage.Define( + LogLevel.Information, + new EventId(21, nameof(RetentionEnforcementEventComplete)), + "Enforced event retention period for {OrganizationName} ({OrganizationId}), Removed {RemovedEvents} Events"); + + public static void RemoveStacksComplete(this ILogger logger, long removedStacks, long removedEvents) + => _removeStacksComplete(logger, removedStacks, removedEvents, null); - public static void RetentionEnforcementStackStart(this ILogger logger, DateTime cutoff, string organizationName, string organizationId, long totalStacks) - => _retentionEnforcementStackStart(logger, cutoff, organizationName, organizationId, totalStacks, null); - - public static void RetentionEnforcementStackComplete(this ILogger logger, string organizationName, string organizationId, long removedEvents) - => _retentionEnforcementStackComplete(logger, organizationName, organizationId, removedEvents, null); + public static void RetentionEnforcementStackStart(this ILogger logger, DateTime cutoff, string organizationName, string organizationId, long totalStacks) + => _retentionEnforcementStackStart(logger, cutoff, organizationName, organizationId, totalStacks, null); + public static void RetentionEnforcementStackComplete(this ILogger logger, string organizationName, string organizationId, long removedEvents) + => _retentionEnforcementStackComplete(logger, organizationName, organizationId, removedEvents, null); - public static void RetentionEnforcementEventStart(this ILogger logger, DateTime cutoff, string organizationName, string organizationId) - => _retentionEnforcementEventStart(logger, cutoff, organizationName, organizationId, null); - - public static void RetentionEnforcementEventComplete(this ILogger logger, string organizationName, string organizationId, long removedEvents) - => _retentionEnforcementEventComplete(logger, organizationName, organizationId, removedEvents, null); + public static void RetentionEnforcementEventStart(this ILogger logger, DateTime cutoff, string organizationName, string organizationId) + => _retentionEnforcementEventStart(logger, cutoff, organizationName, organizationId, null); - public static void RemoveProjectComplete(this ILogger logger, string projectName, string projectId, long removedStacks, long removedEvents) - => _removeProjectComplete(logger, projectName, projectId, removedStacks, removedEvents, null); + public static void RetentionEnforcementEventComplete(this ILogger logger, string organizationName, string organizationId, long removedEvents) + => _retentionEnforcementEventComplete(logger, organizationName, organizationId, removedEvents, null); - public static void RemoveProjectStart(this ILogger logger, string projectName, string projectId) - => _removeProjectStart(logger, projectName, projectId, null); - public static void RemoveOrganizationComplete(this ILogger logger, string organizationName, string organizationId, long removedProjects, long removedStacks, long removedEvents) - => _removeOrganizationComplete(logger, organizationName, organizationId, removedProjects, removedStacks, removedEvents, null); + public static void RemoveProjectComplete(this ILogger logger, string projectName, string projectId, long removedStacks, long removedEvents) + => _removeProjectComplete(logger, projectName, projectId, removedStacks, removedEvents, null); - public static void RemoveOrganizationStart(this ILogger logger, string organizationName, string organizationId) - => _removeOrganizationStart(logger, organizationName, organizationId, null); + public static void RemoveProjectStart(this ILogger logger, string projectName, string projectId) + => _removeProjectStart(logger, projectName, projectId, null); - public static void CleanupStackSoftDeletes(this ILogger logger, long total) - => _cleanupStackSoftDeletes(logger, total, null); + public static void RemoveOrganizationComplete(this ILogger logger, string organizationName, string organizationId, long removedProjects, long removedStacks, long removedEvents) + => _removeOrganizationComplete(logger, organizationName, organizationId, removedProjects, removedStacks, removedEvents, null); - public static void CleanupProjectSoftDeletes(this ILogger logger, long total) - => _cleanupProjectSoftDeletes(logger, total, null); + public static void RemoveOrganizationStart(this ILogger logger, string organizationName, string organizationId) + => _removeOrganizationStart(logger, organizationName, organizationId, null); - public static void CleanupOrganizationSoftDeletes(this ILogger logger, long total) - => _cleanupOrganizationSoftDeletes(logger, total, null); + public static void CleanupStackSoftDeletes(this ILogger logger, long total) + => _cleanupStackSoftDeletes(logger, total, null); - public static void CleanupFinished(this ILogger logger) - => _cleanupFinished(logger, null); + public static void CleanupProjectSoftDeletes(this ILogger logger, long total) + => _cleanupProjectSoftDeletes(logger, total, null); - public static void WebHookDisabledTooManyErrors(this ILogger logger, string webHookId) - => _webHookDisabledTooManyErrors(logger, webHookId, null); + public static void CleanupOrganizationSoftDeletes(this ILogger logger, long total) + => _cleanupOrganizationSoftDeletes(logger, total, null); - public static void WebHookDisabledStatusCode(this ILogger logger, string webHookId, HttpStatusCode? statusCode, string organizationId, string projectId, string url) - => _webHookDisabledStatusCode(logger, webHookId, statusCode, organizationId, projectId, url, null); + public static void CleanupFinished(this ILogger logger) + => _cleanupFinished(logger, null); - public static void WebHookComplete(this ILogger logger, HttpStatusCode? statusCode, string organizationId, string projectId, string url) - => _webHookComplete(logger, statusCode, organizationId, projectId, url, null); + public static void WebHookDisabledTooManyErrors(this ILogger logger, string webHookId) + => _webHookDisabledTooManyErrors(logger, webHookId, null); - public static void WebHookError(this ILogger logger, HttpStatusCode? statusCode, string organizationId, string projectId, string url, Exception exception) - => _webHookError(logger, statusCode, organizationId, projectId, url, exception); + public static void WebHookDisabledStatusCode(this ILogger logger, string webHookId, HttpStatusCode? statusCode, string organizationId, string projectId, string url) + => _webHookDisabledStatusCode(logger, webHookId, statusCode, organizationId, projectId, url, null); - public static void WebHookTimeout(this ILogger logger, HttpStatusCode? statusCode, string organizationId, string projectId, string url, Exception exception) - => _webHookTimeout(logger, statusCode, organizationId, projectId, url, exception); + public static void WebHookComplete(this ILogger logger, HttpStatusCode? statusCode, string organizationId, string projectId, string url) + => _webHookComplete(logger, statusCode, organizationId, projectId, url, null); - public static void WebHookCancelledBackoff(this ILogger logger, long consecutiveErrors, DateTime nextAttemptAllowedAt) - => _webHookCancelledBackoff(logger, consecutiveErrors, nextAttemptAllowedAt, null); + public static void WebHookError(this ILogger logger, HttpStatusCode? statusCode, string organizationId, string projectId, string url, Exception exception) + => _webHookError(logger, statusCode, organizationId, projectId, url, exception); - public static void WebHookCancelled(this ILogger logger) - => _webHookCancelled(logger, null); + public static void WebHookTimeout(this ILogger logger, HttpStatusCode? statusCode, string organizationId, string projectId, string url, Exception exception) + => _webHookTimeout(logger, statusCode, organizationId, projectId, url, exception); - public static void RecordWebHook(this ILogger logger, string id, string projectId, string url) - => _recordWebHook(logger, id, projectId, url, null); - } + public static void WebHookCancelledBackoff(this ILogger logger, long consecutiveErrors, DateTime nextAttemptAllowedAt) + => _webHookCancelledBackoff(logger, consecutiveErrors, nextAttemptAllowedAt, null); + + public static void WebHookCancelled(this ILogger logger) + => _webHookCancelled(logger, null); + + public static void RecordWebHook(this ILogger logger, string id, string projectId, string url) + => _recordWebHook(logger, id, projectId, url, null); } diff --git a/src/Exceptionless.Core/Extensions/MethodExtensions.cs b/src/Exceptionless.Core/Extensions/MethodExtensions.cs index e55ea1330c..fd6b75bf04 100644 --- a/src/Exceptionless.Core/Extensions/MethodExtensions.cs +++ b/src/Exceptionless.Core/Extensions/MethodExtensions.cs @@ -1,77 +1,76 @@ -using System; -using System.Text; +using System.Text; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Extensions { - public static class MethodExtensions { - public static string GetFullName(this Method method) { - if (method == null) - return null; +namespace Exceptionless.Core.Extensions; - var sb = new StringBuilder(); - AppendMethod(method, sb, includeParameters: false); - return sb.ToString(); - } +public static class MethodExtensions { + public static string GetFullName(this Method method) { + if (method == null) + return null; - public static string GetSignature(this Method method) { - if (method == null) - return null; + var sb = new StringBuilder(); + AppendMethod(method, sb, includeParameters: false); + return sb.ToString(); + } - var sb = new StringBuilder(); - AppendMethod(method, sb); - return sb.ToString(); - } + public static string GetSignature(this Method method) { + if (method == null) + return null; - internal static void AppendMethod(Method method, StringBuilder sb, bool includeParameters = true) { - if (String.IsNullOrEmpty(method?.Name)) { - sb.Append(""); - return; - } + var sb = new StringBuilder(); + AppendMethod(method, sb); + return sb.ToString(); + } - if (!String.IsNullOrEmpty(method.DeclaringNamespace)) - sb.Append(method.DeclaringNamespace).Append("."); + internal static void AppendMethod(Method method, StringBuilder sb, bool includeParameters = true) { + if (String.IsNullOrEmpty(method?.Name)) { + sb.Append(""); + return; + } - if (!String.IsNullOrEmpty(method.DeclaringType)) - sb.Append(method.DeclaringType.Replace('+', '.')).Append("."); + if (!String.IsNullOrEmpty(method.DeclaringNamespace)) + sb.Append(method.DeclaringNamespace).Append("."); - sb.Append(method.Name); + if (!String.IsNullOrEmpty(method.DeclaringType)) + sb.Append(method.DeclaringType.Replace('+', '.')).Append("."); - if (method.GenericArguments?.Count > 0) { - sb.Append("["); - bool first = true; - foreach (string arg in method.GenericArguments) { - if (first) - first = false; - else - sb.Append(","); + sb.Append(method.Name); - sb.Append(arg); - } + if (method.GenericArguments?.Count > 0) { + sb.Append("["); + bool first = true; + foreach (string arg in method.GenericArguments) { + if (first) + first = false; + else + sb.Append(","); - sb.Append("]"); + sb.Append(arg); } - if (includeParameters) { - sb.Append("("); - bool first = true; - if (method.Parameters?.Count > 0) { - foreach (var p in method.Parameters) { - if (first) - first = false; - else - sb.Append(", "); + sb.Append("]"); + } + + if (includeParameters) { + sb.Append("("); + bool first = true; + if (method.Parameters?.Count > 0) { + foreach (var p in method.Parameters) { + if (first) + first = false; + else + sb.Append(", "); - if (String.IsNullOrEmpty(p.Type)) - sb.Append(""); - else - sb.Append(p.Type.Replace('+', '.')); + if (String.IsNullOrEmpty(p.Type)) + sb.Append(""); + else + sb.Append(p.Type.Replace('+', '.')); - sb.Append(" "); - sb.Append(p.Name); - } + sb.Append(" "); + sb.Append(p.Name); } - sb.Append(")"); } + sb.Append(")"); } } } diff --git a/src/Exceptionless.Core/Extensions/NameValueCollectionExtensions.cs b/src/Exceptionless.Core/Extensions/NameValueCollectionExtensions.cs index 1c3f55fffd..d6f5823785 100644 --- a/src/Exceptionless.Core/Extensions/NameValueCollectionExtensions.cs +++ b/src/Exceptionless.Core/Extensions/NameValueCollectionExtensions.cs @@ -1,95 +1,92 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; - -namespace Exceptionless.Core.Extensions { - public static class NameValueCollectionExtensions { - public static string GetValue(this NameValueCollection collection, string name, string defaultValue = null) { - return collection[name] ?? defaultValue; - } +using System.Collections.Specialized; - public static int? GetInt(this NameValueCollection collection, string name) - { - string value = collection[name]; - if (value == null) - return null; +namespace Exceptionless.Core.Extensions; - if (Int32.TryParse(value, out int number)) - return number; +public static class NameValueCollectionExtensions { + public static string GetValue(this NameValueCollection collection, string name, string defaultValue = null) { + return collection[name] ?? defaultValue; + } + public static int? GetInt(this NameValueCollection collection, string name) { + string value = collection[name]; + if (value == null) return null; - } - public static int GetInt(this NameValueCollection collection, string name, int defaultValue) { - return GetInt(collection, name) ?? defaultValue; - } + if (Int32.TryParse(value, out int number)) + return number; - public static long GetInt64(this NameValueCollection collection, string name, long defaultValue) { - return GetInt64(collection, name) ?? defaultValue; - } + return null; + } - public static long? GetInt64(this NameValueCollection collection, string name) { - string value = collection[name]; - if (value == null) - return null; + public static int GetInt(this NameValueCollection collection, string name, int defaultValue) { + return GetInt(collection, name) ?? defaultValue; + } - if (Int64.TryParse(value, out long number)) - return number; + public static long GetInt64(this NameValueCollection collection, string name, long defaultValue) { + return GetInt64(collection, name) ?? defaultValue; + } + public static long? GetInt64(this NameValueCollection collection, string name) { + string value = collection[name]; + if (value == null) return null; - } - public static bool? GetBool(this NameValueCollection collection, string name) { - string value = collection[name]; - if (value == null) - return null; + if (Int64.TryParse(value, out long number)) + return number; - if (Boolean.TryParse(value, out bool boolean)) - return boolean; + return null; + } + public static bool? GetBool(this NameValueCollection collection, string name) { + string value = collection[name]; + if (value == null) return null; - } - public static bool GetBool(this NameValueCollection collection, string name, bool defaultValue) { - return GetBool(collection, name) ?? defaultValue; - } + if (Boolean.TryParse(value, out bool boolean)) + return boolean; + + return null; + } + + public static bool GetBool(this NameValueCollection collection, string name, bool defaultValue) { + return GetBool(collection, name) ?? defaultValue; + } - public static T GetEnum(this NameValueCollection collection, string name, T? defaultValue = null) where T: struct { - string value = GetValue(collection, name); - if (value == null) { - if (defaultValue.HasValue && defaultValue is T) - return (T)defaultValue; + public static T GetEnum(this NameValueCollection collection, string name, T? defaultValue = null) where T : struct { + string value = GetValue(collection, name); + if (value == null) { + if (defaultValue.HasValue && defaultValue is T) + return (T)defaultValue; - throw new Exception($"The configuration key '{name}' was not found and no default value was specified."); - } + throw new Exception($"The configuration key '{name}' was not found and no default value was specified."); + } - try { - return (T)Enum.Parse(typeof(T), value, true); - } catch (ArgumentException ex) { - if (defaultValue.HasValue && defaultValue is T) - return (T)defaultValue; + try { + return (T)Enum.Parse(typeof(T), value, true); + } + catch (ArgumentException ex) { + if (defaultValue.HasValue && defaultValue is T) + return (T)defaultValue; - string message = $"Configuration key '{name}' has value '{value}' that could not be parsed as a member of the {typeof(T).Name} enum type."; - throw new Exception(message, ex); - } + string message = $"Configuration key '{name}' has value '{value}' that could not be parsed as a member of the {typeof(T).Name} enum type."; + throw new Exception(message, ex); } + } - public static List GetStringList(this NameValueCollection collection, string name, string defaultValues = null, char[] separators = null) { - string value = collection[name]; - if (value == null && defaultValues == null) - return null; + public static List GetStringList(this NameValueCollection collection, string name, string defaultValues = null, char[] separators = null) { + string value = collection[name]; + if (value == null && defaultValues == null) + return null; - if (value == null) - value = defaultValues; + if (value == null) + value = defaultValues; - if (separators == null) - separators = new[] { ',' }; + if (separators == null) + separators = new[] { ',' }; - return value - .Split(separators, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .ToList(); - } + return value + .Split(separators, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .ToList(); } } diff --git a/src/Exceptionless.Core/Extensions/NumericExtensions.cs b/src/Exceptionless.Core/Extensions/NumericExtensions.cs index ed4b5e851e..a2fa25c0f4 100644 --- a/src/Exceptionless.Core/Extensions/NumericExtensions.cs +++ b/src/Exceptionless.Core/Extensions/NumericExtensions.cs @@ -1,9 +1,7 @@ -using System; +namespace Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Extensions { - public static class NumericExtensions { - public static int NormalizeValue(this int value) { - return value != -1 ? value : Int32.MaxValue; - } +public static class NumericExtensions { + public static int NormalizeValue(this int value) { + return value != -1 ? value : Int32.MaxValue; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/OrganizationExtensions.cs b/src/Exceptionless.Core/Extensions/OrganizationExtensions.cs index ff4ecb312a..ed96df3f21 100644 --- a/src/Exceptionless.Core/Extensions/OrganizationExtensions.cs +++ b/src/Exceptionless.Core/Extensions/OrganizationExtensions.cs @@ -1,148 +1,144 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.DateTimeExtensions; using Exceptionless.Core.Models; using Foundatio.Caching; using Foundatio.Utility; -namespace Exceptionless.Core.Extensions { - public static class OrganizationExtensions { - public static Invite GetInvite(this Organization organization, string token) { - if (organization == null || String.IsNullOrEmpty(token)) - return null; - - return organization.Invites.FirstOrDefault(i => String.Equals(i.Token, token, StringComparison.OrdinalIgnoreCase)); - } - - public static DateTime GetRetentionUtcCutoff(this Organization organization, Project project, int maximumRetentionDays) { - return organization.GetRetentionUtcCutoff(maximumRetentionDays, project.CreatedUtc.SafeSubtract(TimeSpan.FromDays(3))); - } - - public static DateTime GetRetentionUtcCutoff(this Organization organization, Stack stack, int maximumRetentionDays) { - return organization.GetRetentionUtcCutoff(maximumRetentionDays, stack.FirstOccurrence); - } - - public static DateTime GetRetentionUtcCutoff(this Organization organization, int maximumRetentionDays, DateTime? oldestPossibleEventAge = null) { - // NOTE: We allow you to submit events 3 days before your creation date. - var oldestPossibleOrganizationEventAge = organization.CreatedUtc.Date.SafeSubtract(TimeSpan.FromDays(3)); - if (!oldestPossibleEventAge.HasValue || oldestPossibleEventAge.Value.IsBefore(oldestPossibleOrganizationEventAge)) - oldestPossibleEventAge = oldestPossibleOrganizationEventAge; - - int retentionDays = organization.RetentionDays > 0 ? organization.RetentionDays : maximumRetentionDays; - var retentionDate = retentionDays <= 0 ? oldestPossibleEventAge.Value : SystemClock.UtcNow.Date.AddDays(-retentionDays); - return retentionDate.IsAfter(oldestPossibleEventAge.Value) ? retentionDate : oldestPossibleEventAge.Value; - } - - public static DateTime GetRetentionUtcCutoff(this IReadOnlyCollection organizations, int maximumRetentionDays) { - return organizations.Count > 0 ? organizations.Min(o => o.GetRetentionUtcCutoff(maximumRetentionDays)) : DateTime.MinValue; - } - - public static void RemoveSuspension(this Organization organization) { - organization.IsSuspended = false; - organization.SuspensionDate = null; - organization.SuspensionCode = null; - organization.SuspensionNotes = null; - organization.SuspendedByUserId = null; - } - - public static int GetHourlyEventLimit(this Organization organization, BillingPlans plans) { - if (organization.MaxEventsPerMonth <= 0) - return Int32.MaxValue; - - int eventsLeftInMonth = organization.GetMaxEventsPerMonthWithBonus() - (organization.GetCurrentMonthlyTotal() - organization.GetCurrentMonthlyBlocked()); - if (eventsLeftInMonth < 0) - return 0; - - if (organization.PlanId == plans.FreePlan.Id) - return eventsLeftInMonth; - - var utcNow = SystemClock.UtcNow; - double hoursLeftInMonth = (utcNow.EndOfMonth() - utcNow).TotalHours; - if (hoursLeftInMonth < 10.0) - return eventsLeftInMonth; - - return (int)Math.Ceiling(eventsLeftInMonth / hoursLeftInMonth * 10d); - } - - public static int GetMaxEventsPerMonthWithBonus(this Organization organization) { - if (organization.MaxEventsPerMonth <= 0) - return -1; - - int bonusEvents = organization.BonusExpiration.HasValue && organization.BonusExpiration > SystemClock.UtcNow ? organization.BonusEventsPerMonth : 0; - return organization.MaxEventsPerMonth + bonusEvents; - } - - public static async Task IsOverRequestLimitAsync(string organizationId, ICacheClient cacheClient, int apiThrottleLimit) { - if (apiThrottleLimit == Int32.MaxValue) - return false; - - string cacheKey = String.Concat("api", ":", organizationId, ":", SystemClock.UtcNow.Floor(TimeSpan.FromMinutes(15)).Ticks); - var limit = await cacheClient.GetAsync(cacheKey).AnyContext(); - return limit.HasValue && limit.Value >= apiThrottleLimit; - } - - public static bool IsOverMonthlyLimit(this Organization organization) { - if (organization.MaxEventsPerMonth < 0) - return false; - - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); - return usageInfo != null && (usageInfo.Total - usageInfo.Blocked) >= organization.GetMaxEventsPerMonthWithBonus(); - } - - public static bool IsOverHourlyLimit(this Organization organization, BillingPlans plans) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); - return usageInfo != null && usageInfo.Total > organization.GetHourlyEventLimit(plans); - } - - public static int GetCurrentHourlyTotal(this Organization organization) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); - return usageInfo?.Total ?? 0; - } - - public static int GetCurrentHourlyBlocked(this Organization organization) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); - return usageInfo?.Blocked ?? 0; - } - - public static int GetCurrentHourlyTooBig(this Organization organization) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); - return usageInfo?.TooBig ?? 0; - } - - public static int GetCurrentMonthlyTotal(this Organization organization) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); - return usageInfo?.Total ?? 0; - } - - public static int GetCurrentMonthlyBlocked(this Organization organization) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); - return usageInfo?.Blocked ?? 0; - } - - public static int GetCurrentMonthlyTooBig(this Organization organization) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); - return usageInfo?.TooBig ?? 0; - } - - public static void SetHourlyOverage(this Organization organization, double total, double blocked, double tooBig, BillingPlans plans) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - organization.OverageHours.SetUsage(date, (int)total, (int)blocked, (int)tooBig, organization.GetHourlyEventLimit(plans), TimeSpan.FromDays(3)); - } - - public static void SetMonthlyUsage(this Organization organization, double total, double blocked, double tooBig) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - organization.Usage.SetUsage(date, (int)total, (int)blocked, (int)tooBig, organization.GetMaxEventsPerMonthWithBonus(), TimeSpan.FromDays(366)); - } - } -} \ No newline at end of file +namespace Exceptionless.Core.Extensions; + +public static class OrganizationExtensions { + public static Invite GetInvite(this Organization organization, string token) { + if (organization == null || String.IsNullOrEmpty(token)) + return null; + + return organization.Invites.FirstOrDefault(i => String.Equals(i.Token, token, StringComparison.OrdinalIgnoreCase)); + } + + public static DateTime GetRetentionUtcCutoff(this Organization organization, Project project, int maximumRetentionDays) { + return organization.GetRetentionUtcCutoff(maximumRetentionDays, project.CreatedUtc.SafeSubtract(TimeSpan.FromDays(3))); + } + + public static DateTime GetRetentionUtcCutoff(this Organization organization, Stack stack, int maximumRetentionDays) { + return organization.GetRetentionUtcCutoff(maximumRetentionDays, stack.FirstOccurrence); + } + + public static DateTime GetRetentionUtcCutoff(this Organization organization, int maximumRetentionDays, DateTime? oldestPossibleEventAge = null) { + // NOTE: We allow you to submit events 3 days before your creation date. + var oldestPossibleOrganizationEventAge = organization.CreatedUtc.Date.SafeSubtract(TimeSpan.FromDays(3)); + if (!oldestPossibleEventAge.HasValue || oldestPossibleEventAge.Value.IsBefore(oldestPossibleOrganizationEventAge)) + oldestPossibleEventAge = oldestPossibleOrganizationEventAge; + + int retentionDays = organization.RetentionDays > 0 ? organization.RetentionDays : maximumRetentionDays; + var retentionDate = retentionDays <= 0 ? oldestPossibleEventAge.Value : SystemClock.UtcNow.Date.AddDays(-retentionDays); + return retentionDate.IsAfter(oldestPossibleEventAge.Value) ? retentionDate : oldestPossibleEventAge.Value; + } + + public static DateTime GetRetentionUtcCutoff(this IReadOnlyCollection organizations, int maximumRetentionDays) { + return organizations.Count > 0 ? organizations.Min(o => o.GetRetentionUtcCutoff(maximumRetentionDays)) : DateTime.MinValue; + } + + public static void RemoveSuspension(this Organization organization) { + organization.IsSuspended = false; + organization.SuspensionDate = null; + organization.SuspensionCode = null; + organization.SuspensionNotes = null; + organization.SuspendedByUserId = null; + } + + public static int GetHourlyEventLimit(this Organization organization, BillingPlans plans) { + if (organization.MaxEventsPerMonth <= 0) + return Int32.MaxValue; + + int eventsLeftInMonth = organization.GetMaxEventsPerMonthWithBonus() - (organization.GetCurrentMonthlyTotal() - organization.GetCurrentMonthlyBlocked()); + if (eventsLeftInMonth < 0) + return 0; + + if (organization.PlanId == plans.FreePlan.Id) + return eventsLeftInMonth; + + var utcNow = SystemClock.UtcNow; + double hoursLeftInMonth = (utcNow.EndOfMonth() - utcNow).TotalHours; + if (hoursLeftInMonth < 10.0) + return eventsLeftInMonth; + + return (int)Math.Ceiling(eventsLeftInMonth / hoursLeftInMonth * 10d); + } + + public static int GetMaxEventsPerMonthWithBonus(this Organization organization) { + if (organization.MaxEventsPerMonth <= 0) + return -1; + + int bonusEvents = organization.BonusExpiration.HasValue && organization.BonusExpiration > SystemClock.UtcNow ? organization.BonusEventsPerMonth : 0; + return organization.MaxEventsPerMonth + bonusEvents; + } + + public static async Task IsOverRequestLimitAsync(string organizationId, ICacheClient cacheClient, int apiThrottleLimit) { + if (apiThrottleLimit == Int32.MaxValue) + return false; + + string cacheKey = String.Concat("api", ":", organizationId, ":", SystemClock.UtcNow.Floor(TimeSpan.FromMinutes(15)).Ticks); + var limit = await cacheClient.GetAsync(cacheKey).AnyContext(); + return limit.HasValue && limit.Value >= apiThrottleLimit; + } + + public static bool IsOverMonthlyLimit(this Organization organization) { + if (organization.MaxEventsPerMonth < 0) + return false; + + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); + return usageInfo != null && (usageInfo.Total - usageInfo.Blocked) >= organization.GetMaxEventsPerMonthWithBonus(); + } + + public static bool IsOverHourlyLimit(this Organization organization, BillingPlans plans) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); + return usageInfo != null && usageInfo.Total > organization.GetHourlyEventLimit(plans); + } + + public static int GetCurrentHourlyTotal(this Organization organization) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); + return usageInfo?.Total ?? 0; + } + + public static int GetCurrentHourlyBlocked(this Organization organization) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); + return usageInfo?.Blocked ?? 0; + } + + public static int GetCurrentHourlyTooBig(this Organization organization) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + var usageInfo = organization.OverageHours.FirstOrDefault(o => o.Date == date); + return usageInfo?.TooBig ?? 0; + } + + public static int GetCurrentMonthlyTotal(this Organization organization) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); + return usageInfo?.Total ?? 0; + } + + public static int GetCurrentMonthlyBlocked(this Organization organization) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); + return usageInfo?.Blocked ?? 0; + } + + public static int GetCurrentMonthlyTooBig(this Organization organization) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var usageInfo = organization.Usage.FirstOrDefault(o => o.Date == date); + return usageInfo?.TooBig ?? 0; + } + + public static void SetHourlyOverage(this Organization organization, double total, double blocked, double tooBig, BillingPlans plans) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + organization.OverageHours.SetUsage(date, (int)total, (int)blocked, (int)tooBig, organization.GetHourlyEventLimit(plans), TimeSpan.FromDays(3)); + } + + public static void SetMonthlyUsage(this Organization organization, double total, double blocked, double tooBig) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + organization.Usage.SetUsage(date, (int)total, (int)blocked, (int)tooBig, organization.GetMaxEventsPerMonthWithBonus(), TimeSpan.FromDays(366)); + } +} diff --git a/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs b/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs index 28f21344d5..c78d24391d 100644 --- a/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs +++ b/src/Exceptionless.Core/Extensions/PersistentEventExtensions.cs @@ -1,247 +1,248 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; -namespace Exceptionless { - public static class PersistentEventExtensions { - public static void CopyDataToIndex(this PersistentEvent ev, string[] keysToCopy = null) { - keysToCopy = keysToCopy?.Length > 0 ? keysToCopy : ev.Data.Keys.ToArray(); +namespace Exceptionless; - foreach (string key in keysToCopy.Where(k => !String.IsNullOrEmpty(k) && ev.Data.ContainsKey(k))) { - string field = key.Trim().ToLowerInvariant(); +public static class PersistentEventExtensions { + public static void CopyDataToIndex(this PersistentEvent ev, string[] keysToCopy = null) { + keysToCopy = keysToCopy?.Length > 0 ? keysToCopy : ev.Data.Keys.ToArray(); - if (field.StartsWith("@ref:")) { - field = field.Substring(5); - if (!field.IsValidFieldName()) - continue; + foreach (string key in keysToCopy.Where(k => !String.IsNullOrEmpty(k) && ev.Data.ContainsKey(k))) { + string field = key.Trim().ToLowerInvariant(); - ev.Idx[field + "-r"] = (string)ev.Data[key]; + if (field.StartsWith("@ref:")) { + field = field.Substring(5); + if (!field.IsValidFieldName()) continue; - } - if (field.StartsWith("@") || ev.Data[key] == null) + ev.Idx[field + "-r"] = (string)ev.Data[key]; + continue; + } + + if (field.StartsWith("@") || ev.Data[key] == null) + continue; + + if (!field.IsValidFieldName()) + continue; + + var dataType = ev.Data[key].GetType(); + if (dataType == typeof(bool)) { + ev.Idx[field + "-b"] = ev.Data[key]; + } + else if (dataType.IsNumeric()) { + ev.Idx[field + "-n"] = ev.Data[key]; + } + else if (dataType == typeof(DateTime) || dataType == typeof(DateTimeOffset)) { + ev.Idx[field + "-d"] = ev.Data[key]; + } + else if (dataType == typeof(string)) { + string input = (string)ev.Data[key]; + if (String.IsNullOrEmpty(input) || input.Length >= 1000) continue; - if (!field.IsValidFieldName()) + if (input.GetJsonType() != JsonType.None) continue; - var dataType = ev.Data[key].GetType(); - if (dataType == typeof(bool)) { - ev.Idx[field + "-b"] = ev.Data[key]; - } else if (dataType.IsNumeric()) { - ev.Idx[field + "-n"] = ev.Data[key]; - } else if (dataType == typeof(DateTime) || dataType == typeof(DateTimeOffset)) { - ev.Idx[field + "-d"] = ev.Data[key]; - } else if (dataType == typeof(string)) { - string input = (string)ev.Data[key]; - if (String.IsNullOrEmpty(input) || input.Length >= 1000) - continue; - - if (input.GetJsonType() != JsonType.None) - continue; - - if (input[0] == '"') - input = input.TrimStart('"').TrimEnd('"'); - - if (Boolean.TryParse(input, out bool value)) - ev.Idx[field + "-b"] = value; - else if (DateTimeOffset.TryParse(input, out var dtoValue)) - ev.Idx[field + "-d"] = dtoValue; - else if (Decimal.TryParse(input, out decimal decValue)) - ev.Idx[field + "-n"] = decValue; - else if (Double.TryParse(input, out double dblValue) && !Double.IsNaN(dblValue) && !Double.IsInfinity(dblValue)) - ev.Idx[field + "-n"] = dblValue; - else - ev.Idx[field + "-s"] = input; - } + if (input[0] == '"') + input = input.TrimStart('"').TrimEnd('"'); + + if (Boolean.TryParse(input, out bool value)) + ev.Idx[field + "-b"] = value; + else if (DateTimeOffset.TryParse(input, out var dtoValue)) + ev.Idx[field + "-d"] = dtoValue; + else if (Decimal.TryParse(input, out decimal decValue)) + ev.Idx[field + "-n"] = decValue; + else if (Double.TryParse(input, out double dblValue) && !Double.IsNaN(dblValue) && !Double.IsInfinity(dblValue)) + ev.Idx[field + "-n"] = dblValue; + else + ev.Idx[field + "-s"] = input; } } + } - public static string GetEventReference(this PersistentEvent ev, string name) { - if (ev == null || String.IsNullOrEmpty(name)) - return null; + public static string GetEventReference(this PersistentEvent ev, string name) { + if (ev == null || String.IsNullOrEmpty(name)) + return null; - return ev.Data.GetString($"@ref:{name}"); - } + return ev.Data.GetString($"@ref:{name}"); + } - /// - /// Allows you to reference a parent event by it's property. This allows you to have parent and child relationships. - /// - /// The event - /// Reference name - /// The reference id that points to a specific event - public static void SetEventReference(this PersistentEvent ev, string name, string id) { - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); + /// + /// Allows you to reference a parent event by it's property. This allows you to have parent and child relationships. + /// + /// The event + /// Reference name + /// The reference id that points to a specific event + public static void SetEventReference(this PersistentEvent ev, string name, string id) { + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); - if (!IsValidIdentifier(id) || String.IsNullOrEmpty(id)) - throw new ArgumentException("Id must contain between 8 and 100 alphanumeric or '-' characters.", nameof(id)); + if (!IsValidIdentifier(id) || String.IsNullOrEmpty(id)) + throw new ArgumentException("Id must contain between 8 and 100 alphanumeric or '-' characters.", nameof(id)); - ev.Data[$"@ref:{name}"] = id; - } + ev.Data[$"@ref:{name}"] = id; + } - public static string GetSessionId(this PersistentEvent ev) { - if (ev == null) - return null; + public static string GetSessionId(this PersistentEvent ev) { + if (ev == null) + return null; - return ev.IsSessionStart() ? ev.ReferenceId : ev.GetEventReference("session"); - } + return ev.IsSessionStart() ? ev.ReferenceId : ev.GetEventReference("session"); + } - public static void SetSessionId(this PersistentEvent ev, string sessionId) { - if (ev == null) - return; + public static void SetSessionId(this PersistentEvent ev, string sessionId) { + if (ev == null) + return; - if (!IsValidIdentifier(sessionId) || String.IsNullOrEmpty(sessionId)) - throw new ArgumentException("Session Id must contain between 8 and 100 alphanumeric or '-' characters.", nameof(sessionId)); + if (!IsValidIdentifier(sessionId) || String.IsNullOrEmpty(sessionId)) + throw new ArgumentException("Session Id must contain between 8 and 100 alphanumeric or '-' characters.", nameof(sessionId)); - if (ev.IsSessionStart()) - ev.ReferenceId = sessionId; - else - ev.SetEventReference("session", sessionId); - } + if (ev.IsSessionStart()) + ev.ReferenceId = sessionId; + else + ev.SetEventReference("session", sessionId); + } - public static bool HasSessionEndTime(this PersistentEvent ev) { - if (ev == null || !ev.IsSessionStart()) - return false; + public static bool HasSessionEndTime(this PersistentEvent ev) { + if (ev == null || !ev.IsSessionStart()) + return false; - return ev.Data.ContainsKey(Event.KnownDataKeys.SessionEnd); - } + return ev.Data.ContainsKey(Event.KnownDataKeys.SessionEnd); + } - public static DateTime? GetSessionEndTime(this PersistentEvent ev) { - if (ev == null || !ev.IsSessionStart()) - return null; + public static DateTime? GetSessionEndTime(this PersistentEvent ev) { + if (ev == null || !ev.IsSessionStart()) + return null; - if (ev.Data.TryGetValue(Event.KnownDataKeys.SessionEnd, out object sessionEnd)) { - if (sessionEnd is DateTimeOffset dto) - return dto.UtcDateTime; - - if (sessionEnd is DateTime dt) - return dt; - } + if (ev.Data.TryGetValue(Event.KnownDataKeys.SessionEnd, out object sessionEnd)) { + if (sessionEnd is DateTimeOffset dto) + return dto.UtcDateTime; - return null; + if (sessionEnd is DateTime dt) + return dt; } - public static bool UpdateSessionStart(this PersistentEvent ev, DateTime lastActivityUtc, bool isSessionEnd = false) { - if (ev == null || !ev.IsSessionStart()) - return false; - - decimal duration = ev.Value.GetValueOrDefault(); - if (duration < 0) - duration = 0; - - decimal newDuration = (decimal)(lastActivityUtc - ev.Date.UtcDateTime).TotalSeconds; - if (duration >= newDuration) - lastActivityUtc = ev.Date.UtcDateTime.AddSeconds((double)duration); - else - duration = newDuration; - - ev.Value = duration; - if (isSessionEnd) { - ev.Data[Event.KnownDataKeys.SessionEnd] = lastActivityUtc; - ev.CopyDataToIndex(new [] { Event.KnownDataKeys.SessionEnd }); - } else { - ev.Data.Remove(Event.KnownDataKeys.SessionEnd); - ev.Idx.Remove(Event.KnownDataKeys.SessionEnd + "-d"); - } + return null; + } - return true; - } + public static bool UpdateSessionStart(this PersistentEvent ev, DateTime lastActivityUtc, bool isSessionEnd = false) { + if (ev == null || !ev.IsSessionStart()) + return false; - public static PersistentEvent ToSessionStartEvent(this PersistentEvent source, DateTime? lastActivityUtc = null, bool? isSessionEnd = null, bool hasPremiumFeatures = true, bool includePrivateInformation = true) { - var startEvent = new PersistentEvent { - Date = source.Date, - Geo = source.Geo, - OrganizationId = source.OrganizationId, - ProjectId = source.ProjectId, - Type = Event.KnownTypes.Session, - Value = 0 - }; - - startEvent.SetSessionId(source.GetSessionId()); - if (includePrivateInformation) - startEvent.SetUserIdentity(source.GetUserIdentity()); - startEvent.SetLocation(source.GetLocation()); - startEvent.SetVersion(source.GetVersion()); - - var ei = source.GetEnvironmentInfo(); - if (ei != null) { - startEvent.SetEnvironmentInfo(new EnvironmentInfo { - Architecture = ei.Architecture, - CommandLine = ei.CommandLine, - Data = ei.Data, - InstallId = ei.InstallId, - IpAddress = includePrivateInformation ? ei.IpAddress : null, - MachineName = includePrivateInformation ? ei.MachineName : null, - OSName = ei.OSName, - OSVersion = ei.OSVersion, - ProcessId = ei.ProcessId, - ProcessName = ei.ProcessName, - ProcessorCount = ei.ProcessorCount, - RuntimeVersion = ei.RuntimeVersion, - TotalPhysicalMemory = ei.TotalPhysicalMemory - }); - } + decimal duration = ev.Value.GetValueOrDefault(); + if (duration < 0) + duration = 0; - var ri = source.GetRequestInfo(); - if (ri != null) { - startEvent.AddRequestInfo(new RequestInfo { - ClientIpAddress = includePrivateInformation ? ri.ClientIpAddress : null, - Data = ri.Data, - Host = ri.Host, - HttpMethod = ri.HttpMethod, - IsSecure = ri.IsSecure, - Port = ri.Port, - Path = ri.Path, - Referrer = ri.Referrer, - UserAgent = ri.UserAgent - }); - } + decimal newDuration = (decimal)(lastActivityUtc - ev.Date.UtcDateTime).TotalSeconds; + if (duration >= newDuration) + lastActivityUtc = ev.Date.UtcDateTime.AddSeconds((double)duration); + else + duration = newDuration; - if (lastActivityUtc.HasValue) - startEvent.UpdateSessionStart(lastActivityUtc.Value, isSessionEnd.GetValueOrDefault()); + ev.Value = duration; + if (isSessionEnd) { + ev.Data[Event.KnownDataKeys.SessionEnd] = lastActivityUtc; + ev.CopyDataToIndex(new[] { Event.KnownDataKeys.SessionEnd }); + } + else { + ev.Data.Remove(Event.KnownDataKeys.SessionEnd); + ev.Idx.Remove(Event.KnownDataKeys.SessionEnd + "-d"); + } - if (hasPremiumFeatures) - startEvent.CopyDataToIndex(Array.Empty()); + return true; + } - return startEvent; + public static PersistentEvent ToSessionStartEvent(this PersistentEvent source, DateTime? lastActivityUtc = null, bool? isSessionEnd = null, bool hasPremiumFeatures = true, bool includePrivateInformation = true) { + var startEvent = new PersistentEvent { + Date = source.Date, + Geo = source.Geo, + OrganizationId = source.OrganizationId, + ProjectId = source.ProjectId, + Type = Event.KnownTypes.Session, + Value = 0 + }; + + startEvent.SetSessionId(source.GetSessionId()); + if (includePrivateInformation) + startEvent.SetUserIdentity(source.GetUserIdentity()); + startEvent.SetLocation(source.GetLocation()); + startEvent.SetVersion(source.GetVersion()); + + var ei = source.GetEnvironmentInfo(); + if (ei != null) { + startEvent.SetEnvironmentInfo(new EnvironmentInfo { + Architecture = ei.Architecture, + CommandLine = ei.CommandLine, + Data = ei.Data, + InstallId = ei.InstallId, + IpAddress = includePrivateInformation ? ei.IpAddress : null, + MachineName = includePrivateInformation ? ei.MachineName : null, + OSName = ei.OSName, + OSVersion = ei.OSVersion, + ProcessId = ei.ProcessId, + ProcessName = ei.ProcessName, + ProcessorCount = ei.ProcessorCount, + RuntimeVersion = ei.RuntimeVersion, + TotalPhysicalMemory = ei.TotalPhysicalMemory + }); } - public static IEnumerable GetIpAddresses(this PersistentEvent ev) { - if (ev == null) - yield break; + var ri = source.GetRequestInfo(); + if (ri != null) { + startEvent.AddRequestInfo(new RequestInfo { + ClientIpAddress = includePrivateInformation ? ri.ClientIpAddress : null, + Data = ri.Data, + Host = ri.Host, + HttpMethod = ri.HttpMethod, + IsSecure = ri.IsSecure, + Port = ri.Port, + Path = ri.Path, + Referrer = ri.Referrer, + UserAgent = ri.UserAgent + }); + } - if (!String.IsNullOrEmpty(ev.Geo) && (ev.Geo.Contains(".") || ev.Geo.Contains(":"))) - yield return ev.Geo.Trim(); + if (lastActivityUtc.HasValue) + startEvent.UpdateSessionStart(lastActivityUtc.Value, isSessionEnd.GetValueOrDefault()); - var ri = ev.GetRequestInfo(); - if (!String.IsNullOrEmpty(ri?.ClientIpAddress)) { - foreach (string ip in ri.ClientIpAddress.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - yield return ip.Trim(); - } + if (hasPremiumFeatures) + startEvent.CopyDataToIndex(Array.Empty()); - var ei = ev.GetEnvironmentInfo(); - if (!String.IsNullOrEmpty(ei?.IpAddress)) { - foreach (string ip in ei.IpAddress.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) - yield return ip.Trim(); - } + return startEvent; + } + + public static IEnumerable GetIpAddresses(this PersistentEvent ev) { + if (ev == null) + yield break; + + if (!String.IsNullOrEmpty(ev.Geo) && (ev.Geo.Contains(".") || ev.Geo.Contains(":"))) + yield return ev.Geo.Trim(); + + var ri = ev.GetRequestInfo(); + if (!String.IsNullOrEmpty(ri?.ClientIpAddress)) { + foreach (string ip in ri.ClientIpAddress.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + yield return ip.Trim(); } - public static bool HasValidReferenceId(this PersistentEvent ev) { - return IsValidIdentifier(ev.ReferenceId); + var ei = ev.GetEnvironmentInfo(); + if (!String.IsNullOrEmpty(ei?.IpAddress)) { + foreach (string ip in ei.IpAddress.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + yield return ip.Trim(); } + } - private static bool IsValidIdentifier(string value) { - if (value == null) - return true; + public static bool HasValidReferenceId(this PersistentEvent ev) { + return IsValidIdentifier(ev.ReferenceId); + } - if (value.Length < 8 || value.Length > 100) - return false; + private static bool IsValidIdentifier(string value) { + if (value == null) + return true; - return value.IsValidIdentifier(); - } + if (value.Length < 8 || value.Length > 100) + return false; + + return value.IsValidIdentifier(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/ProjectExtensions.cs b/src/Exceptionless.Core/Extensions/ProjectExtensions.cs index a2e4e062e9..248c6f695a 100644 --- a/src/Exceptionless.Core/Extensions/ProjectExtensions.cs +++ b/src/Exceptionless.Core/Extensions/ProjectExtensions.cs @@ -1,98 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using Exceptionless.Core.Models; using Exceptionless.DateTimeExtensions; using Foundatio.Utility; -namespace Exceptionless.Core.Extensions { - public static class ProjectExtensions { - /// - /// These are the default settings for the integration or user who created the project. - /// - public static void AddDefaultNotificationSettings(this Project project, string userIdOrIntegration, NotificationSettings settings = null) { - if (project.NotificationSettings.ContainsKey(userIdOrIntegration)) - return; - - project.NotificationSettings.Add(userIdOrIntegration, settings ?? new NotificationSettings { - SendDailySummary = true, - ReportNewErrors = true, - ReportCriticalErrors = true, - ReportEventRegressions = true - }); - } - - public static void SetDefaultUserAgentBotPatterns(this Project project) { - if (project.Configuration.Settings.ContainsKey(SettingsDictionary.KnownKeys.UserAgentBotPatterns)) - return; +namespace Exceptionless.Core.Extensions; + +public static class ProjectExtensions { + /// + /// These are the default settings for the integration or user who created the project. + /// + public static void AddDefaultNotificationSettings(this Project project, string userIdOrIntegration, NotificationSettings settings = null) { + if (project.NotificationSettings.ContainsKey(userIdOrIntegration)) + return; + + project.NotificationSettings.Add(userIdOrIntegration, settings ?? new NotificationSettings { + SendDailySummary = true, + ReportNewErrors = true, + ReportCriticalErrors = true, + ReportEventRegressions = true + }); + } - project.Configuration.Settings[SettingsDictionary.KnownKeys.UserAgentBotPatterns] = "*bot*,*crawler*,*spider*,*aolbuild*,*teoma*,*yahoo*"; - } + public static void SetDefaultUserAgentBotPatterns(this Project project) { + if (project.Configuration.Settings.ContainsKey(SettingsDictionary.KnownKeys.UserAgentBotPatterns)) + return; - public static string BuildFilter(this IList projects) { - var builder = new StringBuilder(); - for (int index = 0; index < projects.Count; index++) { - if (index > 0) - builder.Append(" OR "); + project.Configuration.Settings[SettingsDictionary.KnownKeys.UserAgentBotPatterns] = "*bot*,*crawler*,*spider*,*aolbuild*,*teoma*,*yahoo*"; + } - builder.AppendFormat("project:{0}", projects[index].Id); - } + public static string BuildFilter(this IList projects) { + var builder = new StringBuilder(); + for (int index = 0; index < projects.Count; index++) { + if (index > 0) + builder.Append(" OR "); - return builder.ToString(); + builder.AppendFormat("project:{0}", projects[index].Id); } - /// - /// Gets the slack token from extended data. - /// - public static SlackToken GetSlackToken(this Project project) { - return project.Data.TryGetValue(Project.KnownDataKeys.SlackToken, out object value) ? value as SlackToken : null; - } + return builder.ToString(); + } - public static int GetCurrentHourlyTotal(this Project project) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - var usageInfo = project.OverageHours.FirstOrDefault(o => o.Date == date); - return usageInfo?.Total ?? 0; - } + /// + /// Gets the slack token from extended data. + /// + public static SlackToken GetSlackToken(this Project project) { + return project.Data.TryGetValue(Project.KnownDataKeys.SlackToken, out object value) ? value as SlackToken : null; + } - public static int GetCurrentHourlyBlocked(this Project project) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - var usageInfo = project.OverageHours.FirstOrDefault(o => o.Date == date); - return usageInfo?.Blocked ?? 0; - } + public static int GetCurrentHourlyTotal(this Project project) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + var usageInfo = project.OverageHours.FirstOrDefault(o => o.Date == date); + return usageInfo?.Total ?? 0; + } - public static int GetCurrentHourlyTooBig(this Project project) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - var usageInfo = project.OverageHours.FirstOrDefault(o => o.Date == date); - return usageInfo?.TooBig ?? 0; - } + public static int GetCurrentHourlyBlocked(this Project project) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + var usageInfo = project.OverageHours.FirstOrDefault(o => o.Date == date); + return usageInfo?.Blocked ?? 0; + } - public static int GetCurrentMonthlyTotal(this Project project) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - var usageInfo = project.Usage.FirstOrDefault(o => o.Date == date); - return usageInfo?.Total ?? 0; - } + public static int GetCurrentHourlyTooBig(this Project project) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + var usageInfo = project.OverageHours.FirstOrDefault(o => o.Date == date); + return usageInfo?.TooBig ?? 0; + } - public static int GetCurrentMonthlyBlocked(this Project project) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - var usageInfo = project.Usage.FirstOrDefault(o => o.Date == date); - return usageInfo?.Blocked ?? 0; - } + public static int GetCurrentMonthlyTotal(this Project project) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var usageInfo = project.Usage.FirstOrDefault(o => o.Date == date); + return usageInfo?.Total ?? 0; + } - public static int GetCurrentMonthlyTooBig(this Project project) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - var usageInfo = project.Usage.FirstOrDefault(o => o.Date == date); - return usageInfo?.TooBig ?? 0; - } + public static int GetCurrentMonthlyBlocked(this Project project) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var usageInfo = project.Usage.FirstOrDefault(o => o.Date == date); + return usageInfo?.Blocked ?? 0; + } - public static void SetHourlyOverage(this Project project, double total, double blocked, double tooBig, int hourlyLimit) { - var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); - project.OverageHours.SetUsage(date, (int)total, (int)blocked, (int)tooBig, hourlyLimit, TimeSpan.FromDays(3)); - } + public static int GetCurrentMonthlyTooBig(this Project project) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var usageInfo = project.Usage.FirstOrDefault(o => o.Date == date); + return usageInfo?.TooBig ?? 0; + } - public static void SetMonthlyUsage(this Project project, double total, double blocked, double tooBig, int monthlyLimit) { - var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - project.Usage.SetUsage(date, (int)total, (int)blocked, (int)tooBig, monthlyLimit, TimeSpan.FromDays(366)); - } + public static void SetHourlyOverage(this Project project, double total, double blocked, double tooBig, int hourlyLimit) { + var date = SystemClock.UtcNow.Floor(TimeSpan.FromHours(1)); + project.OverageHours.SetUsage(date, (int)total, (int)blocked, (int)tooBig, hourlyLimit, TimeSpan.FromDays(3)); + } + + public static void SetMonthlyUsage(this Project project, double total, double blocked, double tooBig, int monthlyLimit) { + var date = new DateTime(SystemClock.UtcNow.Year, SystemClock.UtcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + project.Usage.SetUsage(date, (int)total, (int)blocked, (int)tooBig, monthlyLimit, TimeSpan.FromDays(366)); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/QueryNodeExtensions.cs b/src/Exceptionless.Core/Extensions/QueryNodeExtensions.cs index e7112ac842..7f2fc26157 100644 --- a/src/Exceptionless.Core/Extensions/QueryNodeExtensions.cs +++ b/src/Exceptionless.Core/Extensions/QueryNodeExtensions.cs @@ -1,23 +1,22 @@ -using System; -using Foundatio.Parsers.LuceneQueries.Nodes; +using Foundatio.Parsers.LuceneQueries.Nodes; -namespace Exceptionless.Core.Extensions { - public static class QueryNodeExtensions { - public static GroupNode GetParent(this IQueryNode node, Func condition) { - if (node == null) - return null; +namespace Exceptionless.Core.Extensions; - IQueryNode queryNode = node; - do { - GroupNode groupNode = queryNode as GroupNode; - if (groupNode != null && condition(groupNode)) - return groupNode; +public static class QueryNodeExtensions { + public static GroupNode GetParent(this IQueryNode node, Func condition) { + if (node == null) + return null; - queryNode = queryNode.Parent; - } - while (queryNode != null); + IQueryNode queryNode = node; + do { + GroupNode groupNode = queryNode as GroupNode; + if (groupNode != null && condition(groupNode)) + return groupNode; - return null; + queryNode = queryNode.Parent; } + while (queryNode != null); + + return null; } } diff --git a/src/Exceptionless.Core/Extensions/RequestInfoExtensions.cs b/src/Exceptionless.Core/Extensions/RequestInfoExtensions.cs index be67b19586..496983ce8e 100644 --- a/src/Exceptionless.Core/Extensions/RequestInfoExtensions.cs +++ b/src/Exceptionless.Core/Extensions/RequestInfoExtensions.cs @@ -1,104 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using Exceptionless.Core.Models.Data; using Newtonsoft.Json; -namespace Exceptionless.Core.Extensions { - public static class RequestInfoExtensions { - public static RequestInfo ApplyDataExclusions(this RequestInfo request, IList exclusions, int maxLength = 1000) { - if (request == null) - return null; +namespace Exceptionless.Core.Extensions; - request.Cookies = ApplyExclusions(request.Cookies, exclusions, maxLength); - request.QueryString = ApplyExclusions(request.QueryString, exclusions, maxLength); - request.PostData = ApplyPostDataExclusions(request.PostData, exclusions, maxLength); +public static class RequestInfoExtensions { + public static RequestInfo ApplyDataExclusions(this RequestInfo request, IList exclusions, int maxLength = 1000) { + if (request == null) + return null; - return request; - } + request.Cookies = ApplyExclusions(request.Cookies, exclusions, maxLength); + request.QueryString = ApplyExclusions(request.QueryString, exclusions, maxLength); + request.PostData = ApplyPostDataExclusions(request.PostData, exclusions, maxLength); + + return request; + } - private static object ApplyPostDataExclusions(object data, IEnumerable exclusions, int maxLength) { - if (data == null) - return null; + private static object ApplyPostDataExclusions(object data, IEnumerable exclusions, int maxLength) { + if (data == null) + return null; - var dictionary = data as Dictionary; - if (dictionary == null && data is string) { - string json = (string)data; - if (!json.IsJson()) - return data; + var dictionary = data as Dictionary; + if (dictionary == null && data is string) { + string json = (string)data; + if (!json.IsJson()) + return data; - try { - dictionary = JsonConvert.DeserializeObject>(json); - } catch (Exception) {} + try { + dictionary = JsonConvert.DeserializeObject>(json); } - - return dictionary != null ? ApplyExclusions(dictionary, exclusions, maxLength) : data; + catch (Exception) { } } - private static Dictionary ApplyExclusions(Dictionary dictionary, IEnumerable exclusions, int maxLength) { - if (dictionary == null || dictionary.Count == 0) - return dictionary; + return dictionary != null ? ApplyExclusions(dictionary, exclusions, maxLength) : data; + } - foreach (string key in dictionary.Keys.Where(k => String.IsNullOrEmpty(k) || StringExtensions.AnyWildcardMatches(k, exclusions, true)).ToList()) - dictionary.Remove(key); + private static Dictionary ApplyExclusions(Dictionary dictionary, IEnumerable exclusions, int maxLength) { + if (dictionary == null || dictionary.Count == 0) + return dictionary; - foreach (string key in dictionary.Where(kvp => kvp.Value != null && kvp.Value.Length > maxLength).Select(kvp => kvp.Key).ToList()) - dictionary[key] = String.Format("Value is too large to be included."); + foreach (string key in dictionary.Keys.Where(k => String.IsNullOrEmpty(k) || StringExtensions.AnyWildcardMatches(k, exclusions, true)).ToList()) + dictionary.Remove(key); - return dictionary; - } + foreach (string key in dictionary.Where(kvp => kvp.Value != null && kvp.Value.Length > maxLength).Select(kvp => kvp.Key).ToList()) + dictionary[key] = String.Format("Value is too large to be included."); - /// - /// The full path for the request including host, path and query String. - /// - public static string GetFullPath(this RequestInfo requestInfo, bool includeHttpMethod = false, bool includeHost = true, bool includeQueryString = true) { - var sb = new StringBuilder(); - if (includeHttpMethod) - sb.Append(requestInfo.HttpMethod).Append(" "); - - if (includeHost) { - sb.Append(requestInfo.IsSecure ? "https://" : "http://"); - sb.Append(requestInfo.Host); - if (requestInfo.Port != 80 && requestInfo.Port != 443) - sb.Append(":").Append(requestInfo.Port); - } + return dictionary; + } - if (!requestInfo.Path.StartsWith("/")) - sb.Append("/"); + /// + /// The full path for the request including host, path and query String. + /// + public static string GetFullPath(this RequestInfo requestInfo, bool includeHttpMethod = false, bool includeHost = true, bool includeQueryString = true) { + var sb = new StringBuilder(); + if (includeHttpMethod) + sb.Append(requestInfo.HttpMethod).Append(" "); + + if (includeHost) { + sb.Append(requestInfo.IsSecure ? "https://" : "http://"); + sb.Append(requestInfo.Host); + if (requestInfo.Port != 80 && requestInfo.Port != 443) + sb.Append(":").Append(requestInfo.Port); + } - sb.Append(requestInfo.Path); + if (!requestInfo.Path.StartsWith("/")) + sb.Append("/"); - if (includeQueryString && requestInfo.QueryString != null && requestInfo.QueryString.Count > 0) - sb.Append("?").Append(CreateQueryString(requestInfo.QueryString)); + sb.Append(requestInfo.Path); - return sb.ToString(); - } + if (includeQueryString && requestInfo.QueryString != null && requestInfo.QueryString.Count > 0) + sb.Append("?").Append(CreateQueryString(requestInfo.QueryString)); - private static string CreateQueryString(IEnumerable> args) { - if (args == null) - return String.Empty; + return sb.ToString(); + } - if (!args.Any()) - return String.Empty; + private static string CreateQueryString(IEnumerable> args) { + if (args == null) + return String.Empty; - var sb = new StringBuilder(args.Count() * 10); + if (!args.Any()) + return String.Empty; - foreach (var p in args) { - if (String.IsNullOrEmpty(p.Key) && p.Value == null) - continue; + var sb = new StringBuilder(args.Count() * 10); - if (!String.IsNullOrEmpty(p.Key)) { - sb.Append(Uri.EscapeDataString(p.Key)); - sb.Append('='); - } - if (p.Value != null) - sb.Append(p.Value); - sb.Append('&'); - } - sb.Length--; // remove trailing & + foreach (var p in args) { + if (String.IsNullOrEmpty(p.Key) && p.Value == null) + continue; - return sb.ToString(); + if (!String.IsNullOrEmpty(p.Key)) { + sb.Append(Uri.EscapeDataString(p.Key)); + sb.Append('='); + } + if (p.Value != null) + sb.Append(p.Value); + sb.Append('&'); } + sb.Length--; // remove trailing & + + return sb.ToString(); } } diff --git a/src/Exceptionless.Core/Extensions/ServiceCollectionExtensions.cs b/src/Exceptionless.Core/Extensions/ServiceCollectionExtensions.cs index 6fc7bc0e97..0bf9dfc700 100644 --- a/src/Exceptionless.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Exceptionless.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,68 +1,66 @@ -using System; -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; using Exceptionless.Core.Helpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Exceptionless.Core.Extensions { - public static class ServiceCollectionExtensions { - public static IServiceCollection AddScoped(this IServiceCollection services, Type type, params Assembly[] assemblies) { - return Add(services, type, ServiceLifetime.Scoped, assemblies); - } +namespace Exceptionless.Core.Extensions; - public static IServiceCollection AddSingleton(this IServiceCollection services, Type type, params Assembly[] assemblies) { - return Add(services, type, ServiceLifetime.Singleton, assemblies); - } +public static class ServiceCollectionExtensions { + public static IServiceCollection AddScoped(this IServiceCollection services, Type type, params Assembly[] assemblies) { + return Add(services, type, ServiceLifetime.Scoped, assemblies); + } - public static IServiceCollection AddTransient(this IServiceCollection services, Type type, params Assembly[] assemblies) { - return Add(services, type, ServiceLifetime.Transient, assemblies); - } + public static IServiceCollection AddSingleton(this IServiceCollection services, Type type, params Assembly[] assemblies) { + return Add(services, type, ServiceLifetime.Singleton, assemblies); + } - public static IServiceCollection Add(this IServiceCollection services, Type type, ServiceLifetime lifetime, params Assembly[] assemblies) { - var implementingTypes = new List(); - implementingTypes.AddRange(type.IsGenericTypeDefinition - ? TypeHelper.GetAllTypesImplementingOpenGenericType(type, assemblies) - : TypeHelper.GetDerivedTypes(type, assemblies)); + public static IServiceCollection AddTransient(this IServiceCollection services, Type type, params Assembly[] assemblies) { + return Add(services, type, ServiceLifetime.Transient, assemblies); + } - foreach (var implementingType in implementingTypes) { - var registrationType = type; - if (type.IsGenericTypeDefinition) { - if (type.IsInterface) - registrationType = type.MakeGenericType(implementingType.GetInterface(type.Name).GenericTypeArguments); - else - registrationType = type.MakeGenericType(implementingType.BaseType.GenericTypeArguments); - } + public static IServiceCollection Add(this IServiceCollection services, Type type, ServiceLifetime lifetime, params Assembly[] assemblies) { + var implementingTypes = new List(); + implementingTypes.AddRange(type.IsGenericTypeDefinition + ? TypeHelper.GetAllTypesImplementingOpenGenericType(type, assemblies) + : TypeHelper.GetDerivedTypes(type, assemblies)); - services.Add(new ServiceDescriptor(registrationType, implementingType, lifetime)); - services.Add(new ServiceDescriptor(implementingType, implementingType, lifetime)); + foreach (var implementingType in implementingTypes) { + var registrationType = type; + if (type.IsGenericTypeDefinition) { + if (type.IsInterface) + registrationType = type.MakeGenericType(implementingType.GetInterface(type.Name).GenericTypeArguments); + else + registrationType = type.MakeGenericType(implementingType.BaseType.GenericTypeArguments); } - return services; + services.Add(new ServiceDescriptor(registrationType, implementingType, lifetime)); + services.Add(new ServiceDescriptor(implementingType, implementingType, lifetime)); } - public static IServiceCollection AddSingleton(this IServiceCollection services, params Assembly[] assemblies) { - return AddSingleton(services, typeof(T), assemblies); - } + return services; + } - public static IServiceCollection ReplaceSingleton(this IServiceCollection services, T instance) { - return services.Replace(new ServiceDescriptor(typeof(T), s => instance, ServiceLifetime.Singleton)); - } + public static IServiceCollection AddSingleton(this IServiceCollection services, params Assembly[] assemblies) { + return AddSingleton(services, typeof(T), assemblies); + } - public static IServiceCollection ReplaceSingleton(this IServiceCollection services, object instance) { - return services.Replace(new ServiceDescriptor(typeof(T), s => instance, ServiceLifetime.Singleton)); - } + public static IServiceCollection ReplaceSingleton(this IServiceCollection services, T instance) { + return services.Replace(new ServiceDescriptor(typeof(T), s => instance, ServiceLifetime.Singleton)); + } - public static IServiceCollection ReplaceSingleton(this IServiceCollection services, Func factory) { - return services.Replace(new ServiceDescriptor(typeof(T), factory, ServiceLifetime.Singleton)); - } + public static IServiceCollection ReplaceSingleton(this IServiceCollection services, object instance) { + return services.Replace(new ServiceDescriptor(typeof(T), s => instance, ServiceLifetime.Singleton)); + } - public static IServiceCollection ReplaceSingleton(this IServiceCollection services, Func factory) { - return services.Replace(new ServiceDescriptor(typeof(T), s => factory(s), ServiceLifetime.Singleton)); - } + public static IServiceCollection ReplaceSingleton(this IServiceCollection services, Func factory) { + return services.Replace(new ServiceDescriptor(typeof(T), factory, ServiceLifetime.Singleton)); + } - public static IServiceCollection ReplaceSingleton(this IServiceCollection services) { - return services.Replace(new ServiceDescriptor(typeof(TService), typeof(TInstance), ServiceLifetime.Singleton)); - } + public static IServiceCollection ReplaceSingleton(this IServiceCollection services, Func factory) { + return services.Replace(new ServiceDescriptor(typeof(T), s => factory(s), ServiceLifetime.Singleton)); + } + + public static IServiceCollection ReplaceSingleton(this IServiceCollection services) { + return services.Replace(new ServiceDescriptor(typeof(TService), typeof(TInstance), ServiceLifetime.Singleton)); } } diff --git a/src/Exceptionless.Core/Extensions/StackExtensions.cs b/src/Exceptionless.Core/Extensions/StackExtensions.cs index caa4ad4a91..329d22d744 100644 --- a/src/Exceptionless.Core/Extensions/StackExtensions.cs +++ b/src/Exceptionless.Core/Extensions/StackExtensions.cs @@ -1,59 +1,58 @@ -using System; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Utility; using McSherry.SemanticVersioning; -namespace Exceptionless.Core.Extensions { - public static class StackExtensions { - public static void MarkFixed(this Stack stack, SemanticVersion version = null) { - stack.Status = StackStatus.Fixed; - stack.DateFixed = SystemClock.UtcNow; - stack.FixedInVersion = version?.ToString(); - stack.SnoozeUntilUtc = null; - } +namespace Exceptionless.Core.Extensions; - public static void MarkOpen(this Stack stack) { - stack.Status = StackStatus.Open; - stack.DateFixed = null; - stack.FixedInVersion = null; - stack.SnoozeUntilUtc = null; - } +public static class StackExtensions { + public static void MarkFixed(this Stack stack, SemanticVersion version = null) { + stack.Status = StackStatus.Fixed; + stack.DateFixed = SystemClock.UtcNow; + stack.FixedInVersion = version?.ToString(); + stack.SnoozeUntilUtc = null; + } + + public static void MarkOpen(this Stack stack) { + stack.Status = StackStatus.Open; + stack.DateFixed = null; + stack.FixedInVersion = null; + stack.SnoozeUntilUtc = null; + } - public static Stack ApplyOffset(this Stack stack, TimeSpan offset) { - if (stack == null) - return null; + public static Stack ApplyOffset(this Stack stack, TimeSpan offset) { + if (stack == null) + return null; - if (stack.DateFixed.HasValue) - stack.DateFixed = stack.DateFixed.Value.Add(offset); + if (stack.DateFixed.HasValue) + stack.DateFixed = stack.DateFixed.Value.Add(offset); - if (stack.FirstOccurrence != DateTime.MinValue) - stack.FirstOccurrence = stack.FirstOccurrence.Add(offset); + if (stack.FirstOccurrence != DateTime.MinValue) + stack.FirstOccurrence = stack.FirstOccurrence.Add(offset); - if (stack.LastOccurrence != DateTime.MinValue) - stack.LastOccurrence = stack.LastOccurrence.Add(offset); + if (stack.LastOccurrence != DateTime.MinValue) + stack.LastOccurrence = stack.LastOccurrence.Add(offset); - return stack; - } + return stack; + } - public static string GetTypeName(this Stack stack) { - if (stack.SignatureInfo.TryGetValue("ExceptionType", out string type) && !String.IsNullOrEmpty(type)) - return type.TypeName(); + public static string GetTypeName(this Stack stack) { + if (stack.SignatureInfo.TryGetValue("ExceptionType", out string type) && !String.IsNullOrEmpty(type)) + return type.TypeName(); - return type; - } + return type; + } - public static bool IsFixed(this Stack stack) { - if (stack == null) - return false; + public static bool IsFixed(this Stack stack) { + if (stack == null) + return false; - return stack.Status == StackStatus.Fixed; - } + return stack.Status == StackStatus.Fixed; + } - public static bool Is404(this Stack stack) { - if (stack?.SignatureInfo == null) - return false; + public static bool Is404(this Stack stack) { + if (stack?.SignatureInfo == null) + return false; - return stack.SignatureInfo.ContainsKey("HttpMethod") && stack.SignatureInfo.ContainsKey("Path"); - } + return stack.SignatureInfo.ContainsKey("HttpMethod") && stack.SignatureInfo.ContainsKey("Path"); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/StringExtensions.cs b/src/Exceptionless.Core/Extensions/StringExtensions.cs index dab568228a..1c6ebc36f9 100644 --- a/src/Exceptionless.Core/Extensions/StringExtensions.cs +++ b/src/Exceptionless.Core/Extensions/StringExtensions.cs @@ -1,1125 +1,1126 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Text; using System.Text.RegularExpressions; -using System.Linq; using System.Security.Cryptography; -namespace Exceptionless.Core.Extensions { - public static class StringExtensions { - public static bool IsLocalHost(this string ip) { - if (String.IsNullOrEmpty(ip)) - return false; - - return String.Equals(ip, "127.0.0.1") || String.Equals(ip, "::1"); - } +namespace Exceptionless.Core.Extensions; - /// - /// Very basic check to try and remove the port from any ipv4 or ipv6 address. - /// - /// ip address without port - public static string ToAddress(this string ip) { - if (String.IsNullOrEmpty(ip) || String.Equals(ip, "::1")) - return ip; +public static class StringExtensions { + public static bool IsLocalHost(this string ip) { + if (String.IsNullOrEmpty(ip)) + return false; - string[] parts = ip.Split(new [] {':' }, 9); - if (parts.Length == 2) // 1.2.3.4:port - return parts[0]; - if (parts.Length > 8) // 1:2:3:4:5:6:7:8:port - return String.Join(":", parts.Take(8)); + return String.Equals(ip, "127.0.0.1") || String.Equals(ip, "::1"); + } + /// + /// Very basic check to try and remove the port from any ipv4 or ipv6 address. + /// + /// ip address without port + public static string ToAddress(this string ip) { + if (String.IsNullOrEmpty(ip) || String.Equals(ip, "::1")) return ip; - } - public static bool IsPrivateNetwork(this string ip) { - if (String.IsNullOrEmpty(ip)) - return false; + string[] parts = ip.Split(new[] { ':' }, 9); + if (parts.Length == 2) // 1.2.3.4:port + return parts[0]; + if (parts.Length > 8) // 1:2:3:4:5:6:7:8:port + return String.Join(":", parts.Take(8)); - if (ip.IsLocalHost()) - return true; + return ip; + } - // 10.0.0.0 – 10.255.255.255 (Class A) - if (ip.StartsWith("10.")) - return true; + public static bool IsPrivateNetwork(this string ip) { + if (String.IsNullOrEmpty(ip)) + return false; - // 172.16.0.0 – 172.31.255.255 (Class B) - if (ip.StartsWith("172.")) { - for (int range = 16; range < 32; range++) { - if (ip.StartsWith("172." + range + ".")) - return true; - } - } + if (ip.IsLocalHost()) + return true; + + // 10.0.0.0 – 10.255.255.255 (Class A) + if (ip.StartsWith("10.")) + return true; - // 192.168.0.0 – 192.168.255.255 (Class C) - return ip.StartsWith("192.168."); + // 172.16.0.0 – 172.31.255.255 (Class B) + if (ip.StartsWith("172.")) { + for (int range = 16; range < 32; range++) { + if (ip.StartsWith("172." + range + ".")) + return true; + } } - public static string TrimScript(this string script) { - if (String.IsNullOrEmpty(script)) - return script; + // 192.168.0.0 – 192.168.255.255 (Class C) + return ip.StartsWith("192.168."); + } - return script - .Replace("\r", String.Empty) - .Replace("\n", String.Empty) - .Replace(" ", " "); - } + public static string TrimScript(this string script) { + if (String.IsNullOrEmpty(script)) + return script; - public static string GetNewToken() { - return GetRandomString(40); - } + return script + .Replace("\r", String.Empty) + .Replace("\n", String.Empty) + .Replace(" ", " "); + } - public static string GetRandomString(int length, string allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") { - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length), "length cannot be less than zero."); + public static string GetNewToken() { + return GetRandomString(40); + } - if (String.IsNullOrEmpty(allowedChars)) - throw new ArgumentException("allowedChars may not be empty."); + public static string GetRandomString(int length, string allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") { + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length), "length cannot be less than zero."); - const int byteSize = 0x100; - char[] allowedCharSet = new HashSet(allowedChars).ToArray(); - if (byteSize < allowedCharSet.Length) - throw new ArgumentException($"allowedChars may contain no more than {byteSize} characters."); + if (String.IsNullOrEmpty(allowedChars)) + throw new ArgumentException("allowedChars may not be empty."); - var result = new StringBuilder(); - byte[] buf = new byte[128]; + const int byteSize = 0x100; + char[] allowedCharSet = new HashSet(allowedChars).ToArray(); + if (byteSize < allowedCharSet.Length) + throw new ArgumentException($"allowedChars may contain no more than {byteSize} characters."); - while (result.Length < length) { - RandomNumberGenerator.Fill(buf); - for (int i = 0; i < buf.Length && result.Length < length; ++i) { - int outOfRangeStart = byteSize - (byteSize % allowedCharSet.Length); - if (outOfRangeStart <= buf[i]) - continue; - result.Append(allowedCharSet[buf[i] % allowedCharSet.Length]); - } - } + var result = new StringBuilder(); + byte[] buf = new byte[128]; - return result.ToString(); + while (result.Length < length) { + RandomNumberGenerator.Fill(buf); + for (int i = 0; i < buf.Length && result.Length < length; ++i) { + int outOfRangeStart = byteSize - (byteSize % allowedCharSet.Length); + if (outOfRangeStart <= buf[i]) + continue; + result.Append(allowedCharSet[buf[i] % allowedCharSet.Length]); + } } - // TODO: Add support for detecting the culture number separators as well as suffix (Ex. 100d) - public static bool IsNumeric(this string value) { - if (String.IsNullOrEmpty(value)) - return false; + return result.ToString(); + } - for (int i = 0; i < value.Length; i++) { - if (Char.IsNumber(value[i])) - continue; + // TODO: Add support for detecting the culture number separators as well as suffix (Ex. 100d) + public static bool IsNumeric(this string value) { + if (String.IsNullOrEmpty(value)) + return false; - if (i == 0 && value[i] == '-') - continue; + for (int i = 0; i < value.Length; i++) { + if (Char.IsNumber(value[i])) + continue; - return false; - } + if (i == 0 && value[i] == '-') + continue; - return true; + return false; } - public static bool IsValidFieldName(this string value) { - if (value == null || value.Length > 25) - return false; + return true; + } - return IsValidIdentifier(value); - } + public static bool IsValidFieldName(this string value) { + if (value == null || value.Length > 25) + return false; - public static bool IsValidIdentifier(this string value) { - if (value == null) - return false; + return IsValidIdentifier(value); + } - for (int index = 0; index < value.Length; index++) { - if (!Char.IsLetterOrDigit(value[index]) && value[index] != '-') - return false; - } + public static bool IsValidIdentifier(this string value) { + if (value == null) + return false; - return true; + for (int index = 0; index < value.Length; index++) { + if (!Char.IsLetterOrDigit(value[index]) && value[index] != '-') + return false; } - public static string ToSaltedHash(this string password, string salt) { - byte[] passwordBytes = Encoding.Unicode.GetBytes(password); - byte[] saltBytes = Convert.FromBase64String(salt); - var hashStrategy = new HMACSHA256(); - if (hashStrategy.Key.Length == saltBytes.Length) { - hashStrategy.Key = saltBytes; - } else if (hashStrategy.Key.Length < saltBytes.Length) { - byte[] keyBytes = new byte[hashStrategy.Key.Length]; - Buffer.BlockCopy(saltBytes, 0, keyBytes, 0, keyBytes.Length); - hashStrategy.Key = keyBytes; - } else { - byte[] keyBytes = new byte[hashStrategy.Key.Length]; - for (int i = 0; i < keyBytes.Length;) { - int len = Math.Min(saltBytes.Length, keyBytes.Length - i); - Buffer.BlockCopy(saltBytes, 0, keyBytes, i, len); - i += len; - } - hashStrategy.Key = keyBytes; - } + return true; + } - byte[] result = hashStrategy.ComputeHash(passwordBytes); - return Convert.ToBase64String(result); + public static string ToSaltedHash(this string password, string salt) { + byte[] passwordBytes = Encoding.Unicode.GetBytes(password); + byte[] saltBytes = Convert.FromBase64String(salt); + var hashStrategy = new HMACSHA256(); + if (hashStrategy.Key.Length == saltBytes.Length) { + hashStrategy.Key = saltBytes; + } + else if (hashStrategy.Key.Length < saltBytes.Length) { + byte[] keyBytes = new byte[hashStrategy.Key.Length]; + Buffer.BlockCopy(saltBytes, 0, keyBytes, 0, keyBytes.Length); + hashStrategy.Key = keyBytes; + } + else { + byte[] keyBytes = new byte[hashStrategy.Key.Length]; + for (int i = 0; i < keyBytes.Length;) { + int len = Math.Min(saltBytes.Length, keyBytes.Length - i); + Buffer.BlockCopy(saltBytes, 0, keyBytes, i, len); + i += len; + } + hashStrategy.Key = keyBytes; } - public static string ToDelimitedString(this IEnumerable values, string delimiter = ",") { - if (String.IsNullOrEmpty(delimiter)) - delimiter = ","; + byte[] result = hashStrategy.ComputeHash(passwordBytes); + return Convert.ToBase64String(result); + } - var sb = new StringBuilder(); - foreach (string i in values) { - if (sb.Length > 0) - sb.Append(delimiter); + public static string ToDelimitedString(this IEnumerable values, string delimiter = ",") { + if (String.IsNullOrEmpty(delimiter)) + delimiter = ","; - sb.Append(i); - } + var sb = new StringBuilder(); + foreach (string i in values) { + if (sb.Length > 0) + sb.Append(delimiter); - return sb.ToString(); + sb.Append(i); } - public static string[] FromDelimitedString(this string value, string delimiter = ",") { - if (String.IsNullOrEmpty(value)) - return null; + return sb.ToString(); + } - if (String.IsNullOrEmpty(delimiter)) - delimiter = ","; + public static string[] FromDelimitedString(this string value, string delimiter = ",") { + if (String.IsNullOrEmpty(value)) + return null; - return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).ToArray(); - } + if (String.IsNullOrEmpty(delimiter)) + delimiter = ","; - public static string ToLowerUnderscoredWords(this string value) { - var builder = new StringBuilder(value.Length + 10); - for (int index = 0; index < value.Length; index++) { - char c = value[index]; - if (Char.IsUpper(c)) { - if (index > 0 && value[index - 1] != '_') - builder.Append('_'); - - builder.Append(Char.ToLowerInvariant(c)); - } else { - builder.Append(c); - } - } + return value.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).ToArray(); + } + + public static string ToLowerUnderscoredWords(this string value) { + var builder = new StringBuilder(value.Length + 10); + for (int index = 0; index < value.Length; index++) { + char c = value[index]; + if (Char.IsUpper(c)) { + if (index > 0 && value[index - 1] != '_') + builder.Append('_'); - return builder.ToString(); + builder.Append(Char.ToLowerInvariant(c)); + } + else { + builder.Append(c); + } } - public static bool AnyWildcardMatches(this string value, IEnumerable patternsToMatch, bool ignoreCase = false) { - if (ignoreCase) - value = value.ToLowerInvariant(); + return builder.ToString(); + } - return patternsToMatch.Any(pattern => CheckForMatch(pattern, value, ignoreCase)); - } + public static bool AnyWildcardMatches(this string value, IEnumerable patternsToMatch, bool ignoreCase = false) { + if (ignoreCase) + value = value.ToLowerInvariant(); - private static bool CheckForMatch(string pattern, string value, bool ignoreCase = true) { - bool startsWithWildcard = pattern.StartsWith("*"); - if (startsWithWildcard) - pattern = pattern.Substring(1); + return patternsToMatch.Any(pattern => CheckForMatch(pattern, value, ignoreCase)); + } - bool endsWithWildcard = pattern.EndsWith("*"); - if (endsWithWildcard) - pattern = pattern.Substring(0, pattern.Length - 1); + private static bool CheckForMatch(string pattern, string value, bool ignoreCase = true) { + bool startsWithWildcard = pattern.StartsWith("*"); + if (startsWithWildcard) + pattern = pattern.Substring(1); - if (ignoreCase) - pattern = pattern.ToLowerInvariant(); + bool endsWithWildcard = pattern.EndsWith("*"); + if (endsWithWildcard) + pattern = pattern.Substring(0, pattern.Length - 1); - if (startsWithWildcard && endsWithWildcard) - return value.Contains(pattern); + if (ignoreCase) + pattern = pattern.ToLowerInvariant(); - if (startsWithWildcard) - return value.EndsWith(pattern); + if (startsWithWildcard && endsWithWildcard) + return value.Contains(pattern); - if (endsWithWildcard) - return value.StartsWith(pattern); + if (startsWithWildcard) + return value.EndsWith(pattern); - return value.Equals(pattern); - } + if (endsWithWildcard) + return value.StartsWith(pattern); - public static string ToConcatenatedString(this IEnumerable values, Func stringSelector) { - return values.ToConcatenatedString(stringSelector, String.Empty); - } + return value.Equals(pattern); + } - public static string ToConcatenatedString(this IEnumerable values, Func action, string separator) { - var sb = new StringBuilder(); - foreach (var item in values) { - if (sb.Length > 0) - sb.Append(separator); + public static string ToConcatenatedString(this IEnumerable values, Func stringSelector) { + return values.ToConcatenatedString(stringSelector, String.Empty); + } - sb.Append(action(item)); - } + public static string ToConcatenatedString(this IEnumerable values, Func action, string separator) { + var sb = new StringBuilder(); + foreach (var item in values) { + if (sb.Length > 0) + sb.Append(separator); - return sb.ToString(); + sb.Append(action(item)); } - public static string ReplaceFirst(this string input, string find, string replace) { - if (String.IsNullOrEmpty(input)) - return input; + return sb.ToString(); + } - int i = input.IndexOf(find, StringComparison.Ordinal); - if (i < 0) - return input; + public static string ReplaceFirst(this string input, string find, string replace) { + if (String.IsNullOrEmpty(input)) + return input; - string pre = input.Substring(0, i); - string post = input.Substring(i + find.Length); - return String.Concat(pre, replace, post); - } + int i = input.IndexOf(find, StringComparison.Ordinal); + if (i < 0) + return input; - public static IEnumerable SplitLines(this string text) { - return text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Where(l => !String.IsNullOrWhiteSpace(l)).Select(l => l.Trim()); - } + string pre = input.Substring(0, i); + string post = input.Substring(i + find.Length); + return String.Concat(pre, replace, post); + } - public static string StripInvisible(this string s) { - return s.Replace("\r\n", " ").Replace('\n', ' ').Replace('\t', ' '); - } + public static IEnumerable SplitLines(this string text) { + return text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Where(l => !String.IsNullOrWhiteSpace(l)).Select(l => l.Trim()); + } - public static string NormalizeLineEndings(this string text, string lineEnding = null) { - if (String.IsNullOrEmpty(lineEnding)) - lineEnding = Environment.NewLine; + public static string StripInvisible(this string s) { + return s.Replace("\r\n", " ").Replace('\n', ' ').Replace('\t', ' '); + } - text = text.Replace("\r\n", "\n"); - if (lineEnding != "\n") - text = text.Replace("\r\n", lineEnding); + public static string NormalizeLineEndings(this string text, string lineEnding = null) { + if (String.IsNullOrEmpty(lineEnding)) + lineEnding = Environment.NewLine; - return text; - } + text = text.Replace("\r\n", "\n"); + if (lineEnding != "\n") + text = text.Replace("\r\n", lineEnding); - public static string Truncate(this string text, int keep) { - if (String.IsNullOrEmpty(text)) - return String.Empty; + return text; + } - string buffer = NormalizeLineEndings(text); - if (buffer.Length <= keep) - return buffer; + public static string Truncate(this string text, int keep) { + if (String.IsNullOrEmpty(text)) + return String.Empty; - return String.Concat(buffer.Substring(0, keep - 3), "..."); - } + string buffer = NormalizeLineEndings(text); + if (buffer.Length <= keep) + return buffer; - public static string TypeName(this string typeFullName) { - return typeFullName?.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries).Last(); - } + return String.Concat(buffer.Substring(0, keep - 3), "..."); + } - public static string ToLowerFiltered(this string value, char[] charsToRemove) { - var builder = new StringBuilder(value.Length); + public static string TypeName(this string typeFullName) { + return typeFullName?.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries).Last(); + } - for (int index = 0; index < value.Length; index++) { - char c = value[index]; - if (Char.IsUpper(c)) - c = Char.ToLowerInvariant(c); + public static string ToLowerFiltered(this string value, char[] charsToRemove) { + var builder = new StringBuilder(value.Length); - bool includeChar = true; - for (int i = 0; i < charsToRemove.Length; i++) { - if (charsToRemove[i] == c) { - includeChar = false; - break; - } - } + for (int index = 0; index < value.Length; index++) { + char c = value[index]; + if (Char.IsUpper(c)) + c = Char.ToLowerInvariant(c); - if (includeChar) - builder.Append(c); + bool includeChar = true; + for (int i = 0; i < charsToRemove.Length; i++) { + if (charsToRemove[i] == c) { + includeChar = false; + break; + } } - return builder.ToString(); + if (includeChar) + builder.Append(c); } - public static string[] SplitAndTrim(this string s, char[] separator) { - if (s.IsNullOrEmpty()) - return new string[0]; + return builder.ToString(); + } - string[] result = s.Split(separator, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < result.Length; i++) - result[i] = result[i].Trim(); + public static string[] SplitAndTrim(this string s, char[] separator) { + if (s.IsNullOrEmpty()) + return new string[0]; - return result; + string[] result = s.Split(separator, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < result.Length; i++) + result[i] = result[i].Trim(); - } + return result; - public static bool IsNullOrEmpty(this string item) { - return String.IsNullOrEmpty(item); - } + } - private static readonly Regex _entityResolver = new Regex("([&][#](?'decimal'[0-9]+);)|([&][#][(x|X)](?'hex'[0-9a-fA-F]+);)|([&](?'html'\\w+);)"); + public static bool IsNullOrEmpty(this string item) { + return String.IsNullOrEmpty(item); + } - public static string HtmlEntityDecode(this string encodedText) { - return _entityResolver.Replace(encodedText, new MatchEvaluator(ResolveEntityAngleAmp)); - } + private static readonly Regex _entityResolver = new Regex("([&][#](?'decimal'[0-9]+);)|([&][#][(x|X)](?'hex'[0-9a-fA-F]+);)|([&](?'html'\\w+);)"); - private static string ResolveEntityAngleAmp(Match matchToProcess) { - return !matchToProcess.Groups["decimal"].Success ? (!matchToProcess.Groups["hex"].Success ? (!matchToProcess.Groups["html"].Success ? "Y" : EntityLookup(matchToProcess.Groups["html"].Value)) : Convert.ToChar(HexToInt(matchToProcess.Groups["hex"].Value)).ToString()) : Convert.ToChar(Convert.ToInt32(matchToProcess.Groups["decimal"].Value)).ToString(); - } + public static string HtmlEntityDecode(this string encodedText) { + return _entityResolver.Replace(encodedText, new MatchEvaluator(ResolveEntityAngleAmp)); + } - public static int HexToInt(string input) { - int num = 0; - input = input.ToUpperInvariant(); - char[] chArray = input.ToCharArray(); - for (int index = chArray.Length - 1; index >= 0; --index) { - if ((int)chArray[index] >= 48 && (int)chArray[index] <= 57) - num += ((int)chArray[index] - 48) * (int)Math.Pow(16.0, (double)(chArray.Length - 1 - index)); - else if ((int)chArray[index] >= 65 && (int)chArray[index] <= 70) { - num += ((int)chArray[index] - 55) * (int)Math.Pow(16.0, (double)(chArray.Length - 1 - index)); - } else { - num = 0; - break; - } + private static string ResolveEntityAngleAmp(Match matchToProcess) { + return !matchToProcess.Groups["decimal"].Success ? (!matchToProcess.Groups["hex"].Success ? (!matchToProcess.Groups["html"].Success ? "Y" : EntityLookup(matchToProcess.Groups["html"].Value)) : Convert.ToChar(HexToInt(matchToProcess.Groups["hex"].Value)).ToString()) : Convert.ToChar(Convert.ToInt32(matchToProcess.Groups["decimal"].Value)).ToString(); + } + + public static int HexToInt(string input) { + int num = 0; + input = input.ToUpperInvariant(); + char[] chArray = input.ToCharArray(); + for (int index = chArray.Length - 1; index >= 0; --index) { + if ((int)chArray[index] >= 48 && (int)chArray[index] <= 57) + num += ((int)chArray[index] - 48) * (int)Math.Pow(16.0, (double)(chArray.Length - 1 - index)); + else if ((int)chArray[index] >= 65 && (int)chArray[index] <= 70) { + num += ((int)chArray[index] - 55) * (int)Math.Pow(16.0, (double)(chArray.Length - 1 - index)); + } + else { + num = 0; + break; } - return num; } + return num; + } - private static string EntityLookup(string entity) { - string str = ""; - switch (entity) { - case "Aacute": - str = Convert.ToChar(193).ToString(); - break; - case "aacute": - str = Convert.ToChar(225).ToString(); - break; - case "acirc": - str = Convert.ToChar(226).ToString(); - break; - case "Acirc": - str = Convert.ToChar(194).ToString(); - break; - case "acute": - str = Convert.ToChar(180).ToString(); - break; - case "AElig": - str = Convert.ToChar(198).ToString(); - break; - case "aelig": - str = Convert.ToChar(230).ToString(); - break; - case "Agrave": - str = Convert.ToChar(192).ToString(); - break; - case "agrave": - str = Convert.ToChar(224).ToString(); - break; - case "alefsym": - str = Convert.ToChar(8501).ToString(); - break; - case "Alpha": - str = Convert.ToChar(913).ToString(); - break; - case "alpha": - str = Convert.ToChar(945).ToString(); - break; - case "amp": - str = Convert.ToChar(38).ToString(); - break; - case "and": - str = Convert.ToChar(8743).ToString(); - break; - case "ang": - str = Convert.ToChar(8736).ToString(); - break; - case "aring": - str = Convert.ToChar(229).ToString(); - break; - case "Aring": - str = Convert.ToChar(197).ToString(); - break; - case "asymp": - str = Convert.ToChar(8776).ToString(); - break; - case "Atilde": - str = Convert.ToChar(195).ToString(); - break; - case "atilde": - str = Convert.ToChar(227).ToString(); - break; - case "auml": - str = Convert.ToChar(228).ToString(); - break; - case "Auml": - str = Convert.ToChar(196).ToString(); - break; - case "bdquo": - str = Convert.ToChar(8222).ToString(); - break; - case "Beta": - str = Convert.ToChar(914).ToString(); - break; - case "beta": - str = Convert.ToChar(946).ToString(); - break; - case "brvbar": - str = Convert.ToChar(166).ToString(); - break; - case "bull": - str = Convert.ToChar(8226).ToString(); - break; - case "cap": - str = Convert.ToChar(8745).ToString(); - break; - case "Ccedil": - str = Convert.ToChar(199).ToString(); - break; - case "ccedil": - str = Convert.ToChar(231).ToString(); - break; - case "cedil": - str = Convert.ToChar(184).ToString(); - break; - case "cent": - str = Convert.ToChar(162).ToString(); - break; - case "chi": - str = Convert.ToChar(967).ToString(); - break; - case "Chi": - str = Convert.ToChar(935).ToString(); - break; - case "circ": - str = Convert.ToChar(710).ToString(); - break; - case "clubs": - str = Convert.ToChar(9827).ToString(); - break; - case "cong": - str = Convert.ToChar(8773).ToString(); - break; - case "copy": - str = Convert.ToChar(169).ToString(); - break; - case "crarr": - str = Convert.ToChar(8629).ToString(); - break; - case "cup": - str = Convert.ToChar(8746).ToString(); - break; - case "curren": - str = Convert.ToChar(164).ToString(); - break; - case "dagger": - str = Convert.ToChar(8224).ToString(); - break; - case "Dagger": - str = Convert.ToChar(8225).ToString(); - break; - case "darr": - str = Convert.ToChar(8595).ToString(); - break; - case "dArr": - str = Convert.ToChar(8659).ToString(); - break; - case "deg": - str = Convert.ToChar(176).ToString(); - break; - case "Delta": - str = Convert.ToChar(916).ToString(); - break; - case "delta": - str = Convert.ToChar(948).ToString(); - break; - case "diams": - str = Convert.ToChar(9830).ToString(); - break; - case "divide": - str = Convert.ToChar(247).ToString(); - break; - case "eacute": - str = Convert.ToChar(233).ToString(); - break; - case "Eacute": - str = Convert.ToChar(201).ToString(); - break; - case "Ecirc": - str = Convert.ToChar(202).ToString(); - break; - case "ecirc": - str = Convert.ToChar(234).ToString(); - break; - case "Egrave": - str = Convert.ToChar(200).ToString(); - break; - case "egrave": - str = Convert.ToChar(232).ToString(); - break; - case "empty": - str = Convert.ToChar(8709).ToString(); - break; - case "emsp": - str = Convert.ToChar(8195).ToString(); - break; - case "ensp": - str = Convert.ToChar(8194).ToString(); - break; - case "epsilon": - str = Convert.ToChar(949).ToString(); - break; - case "Epsilon": - str = Convert.ToChar(917).ToString(); - break; - case "equiv": - str = Convert.ToChar(8801).ToString(); - break; - case "Eta": - str = Convert.ToChar(919).ToString(); - break; - case "eta": - str = Convert.ToChar(951).ToString(); - break; - case "eth": - str = Convert.ToChar(240).ToString(); - break; - case "ETH": - str = Convert.ToChar(208).ToString(); - break; - case "Euml": - str = Convert.ToChar(203).ToString(); - break; - case "euml": - str = Convert.ToChar(235).ToString(); - break; - case "euro": - str = Convert.ToChar(8364).ToString(); - break; - case "exist": - str = Convert.ToChar(8707).ToString(); - break; - case "fnof": - str = Convert.ToChar(402).ToString(); - break; - case "forall": - str = Convert.ToChar(8704).ToString(); - break; - case "frac12": - str = Convert.ToChar(189).ToString(); - break; - case "frac14": - str = Convert.ToChar(188).ToString(); - break; - case "frac34": - str = Convert.ToChar(190).ToString(); - break; - case "frasl": - str = Convert.ToChar(8260).ToString(); - break; - case "gamma": - str = Convert.ToChar(947).ToString(); - break; - case "Gamma": - str = Convert.ToChar(915).ToString(); - break; - case "ge": - str = Convert.ToChar(8805).ToString(); - break; - case "gt": - str = Convert.ToChar(62).ToString(); - break; - case "hArr": - str = Convert.ToChar(8660).ToString(); - break; - case "harr": - str = Convert.ToChar(8596).ToString(); - break; - case "hearts": - str = Convert.ToChar(9829).ToString(); - break; - case "hellip": - str = Convert.ToChar(8230).ToString(); - break; - case "Iacute": - str = Convert.ToChar(205).ToString(); - break; - case "iacute": - str = Convert.ToChar(237).ToString(); - break; - case "icirc": - str = Convert.ToChar(238).ToString(); - break; - case "Icirc": - str = Convert.ToChar(206).ToString(); - break; - case "iexcl": - str = Convert.ToChar(161).ToString(); - break; - case "Igrave": - str = Convert.ToChar(204).ToString(); - break; - case "igrave": - str = Convert.ToChar(236).ToString(); - break; - case "image": - str = Convert.ToChar(8465).ToString(); - break; - case "infin": - str = Convert.ToChar(8734).ToString(); - break; - case "int": - str = Convert.ToChar(8747).ToString(); - break; - case "Iota": - str = Convert.ToChar(921).ToString(); - break; - case "iota": - str = Convert.ToChar(953).ToString(); - break; - case "iquest": - str = Convert.ToChar(191).ToString(); - break; - case "isin": - str = Convert.ToChar(8712).ToString(); - break; - case "iuml": - str = Convert.ToChar(239).ToString(); - break; - case "Iuml": - str = Convert.ToChar(207).ToString(); - break; - case "kappa": - str = Convert.ToChar(954).ToString(); - break; - case "Kappa": - str = Convert.ToChar(922).ToString(); - break; - case "Lambda": - str = Convert.ToChar(923).ToString(); - break; - case "lambda": - str = Convert.ToChar(955).ToString(); - break; - case "lang": - str = Convert.ToChar(9001).ToString(); - break; - case "laquo": - str = Convert.ToChar(171).ToString(); - break; - case "larr": - str = Convert.ToChar(8592).ToString(); - break; - case "lArr": - str = Convert.ToChar(8656).ToString(); - break; - case "lceil": - str = Convert.ToChar(8968).ToString(); - break; - case "ldquo": - str = Convert.ToChar(8220).ToString(); - break; - case "le": - str = Convert.ToChar(8804).ToString(); - break; - case "lfloor": - str = Convert.ToChar(8970).ToString(); - break; - case "lowast": - str = Convert.ToChar(8727).ToString(); - break; - case "loz": - str = Convert.ToChar(9674).ToString(); - break; - case "lrm": - str = Convert.ToChar(8206).ToString(); - break; - case "lsaquo": - str = Convert.ToChar(8249).ToString(); - break; - case "lsquo": - str = Convert.ToChar(8216).ToString(); - break; - case "lt": - str = Convert.ToChar(60).ToString(); - break; - case "macr": - str = Convert.ToChar(175).ToString(); - break; - case "mdash": - str = Convert.ToChar(8212).ToString(); - break; - case "micro": - str = Convert.ToChar(181).ToString(); - break; - case "middot": - str = Convert.ToChar(183).ToString(); - break; - case "minus": - str = Convert.ToChar(8722).ToString(); - break; - case "Mu": - str = Convert.ToChar(924).ToString(); - break; - case "mu": - str = Convert.ToChar(956).ToString(); - break; - case "nabla": - str = Convert.ToChar(8711).ToString(); - break; - case "nbsp": - str = Convert.ToChar(160).ToString(); - break; - case "ndash": - str = Convert.ToChar(8211).ToString(); - break; - case "ne": - str = Convert.ToChar(8800).ToString(); - break; - case "ni": - str = Convert.ToChar(8715).ToString(); - break; - case "not": - str = Convert.ToChar(172).ToString(); - break; - case "notin": - str = Convert.ToChar(8713).ToString(); - break; - case "nsub": - str = Convert.ToChar(8836).ToString(); - break; - case "ntilde": - str = Convert.ToChar(241).ToString(); - break; - case "Ntilde": - str = Convert.ToChar(209).ToString(); - break; - case "Nu": - str = Convert.ToChar(925).ToString(); - break; - case "nu": - str = Convert.ToChar(957).ToString(); - break; - case "oacute": - str = Convert.ToChar(243).ToString(); - break; - case "Oacute": - str = Convert.ToChar(211).ToString(); - break; - case "Ocirc": - str = Convert.ToChar(212).ToString(); - break; - case "ocirc": - str = Convert.ToChar(244).ToString(); - break; - case "OElig": - str = Convert.ToChar(338).ToString(); - break; - case "oelig": - str = Convert.ToChar(339).ToString(); - break; - case "ograve": - str = Convert.ToChar(242).ToString(); - break; - case "Ograve": - str = Convert.ToChar(210).ToString(); - break; - case "oline": - str = Convert.ToChar(8254).ToString(); - break; - case "Omega": - str = Convert.ToChar(937).ToString(); - break; - case "omega": - str = Convert.ToChar(969).ToString(); - break; - case "Omicron": - str = Convert.ToChar(927).ToString(); - break; - case "omicron": - str = Convert.ToChar(959).ToString(); - break; - case "oplus": - str = Convert.ToChar(8853).ToString(); - break; - case "or": - str = Convert.ToChar(8744).ToString(); - break; - case "ordf": - str = Convert.ToChar(170).ToString(); - break; - case "ordm": - str = Convert.ToChar(186).ToString(); - break; - case "Oslash": - str = Convert.ToChar(216).ToString(); - break; - case "oslash": - str = Convert.ToChar(248).ToString(); - break; - case "otilde": - str = Convert.ToChar(245).ToString(); - break; - case "Otilde": - str = Convert.ToChar(213).ToString(); - break; - case "otimes": - str = Convert.ToChar(8855).ToString(); - break; - case "Ouml": - str = Convert.ToChar(214).ToString(); - break; - case "ouml": - str = Convert.ToChar(246).ToString(); - break; - case "para": - str = Convert.ToChar(182).ToString(); - break; - case "part": - str = Convert.ToChar(8706).ToString(); - break; - case "permil": - str = Convert.ToChar(8240).ToString(); - break; - case "perp": - str = Convert.ToChar(8869).ToString(); - break; - case "Phi": - str = Convert.ToChar(934).ToString(); - break; - case "phi": - str = Convert.ToChar(966).ToString(); - break; - case "Pi": - str = Convert.ToChar(928).ToString(); - break; - case "pi": - str = Convert.ToChar(960).ToString(); - break; - case "piv": - str = Convert.ToChar(982).ToString(); - break; - case "plusmn": - str = Convert.ToChar(177).ToString(); - break; - case "pound": - str = Convert.ToChar(163).ToString(); - break; - case "Prime": - str = Convert.ToChar(8243).ToString(); - break; - case "prime": - str = Convert.ToChar(8242).ToString(); - break; - case "prod": - str = Convert.ToChar(8719).ToString(); - break; - case "prop": - str = Convert.ToChar(8733).ToString(); - break; - case "psi": - str = Convert.ToChar(968).ToString(); - break; - case "Psi": - str = Convert.ToChar(936).ToString(); - break; - case "quot": - str = Convert.ToChar(34).ToString(); - break; - case "radic": - str = Convert.ToChar(8730).ToString(); - break; - case "rang": - str = Convert.ToChar(9002).ToString(); - break; - case "raquo": - str = Convert.ToChar(187).ToString(); - break; - case "rarr": - str = Convert.ToChar(8594).ToString(); - break; - case "rArr": - str = Convert.ToChar(8658).ToString(); - break; - case "rceil": - str = Convert.ToChar(8969).ToString(); - break; - case "rdquo": - str = Convert.ToChar(8221).ToString(); - break; - case "real": - str = Convert.ToChar(8476).ToString(); - break; - case "reg": - str = Convert.ToChar(174).ToString(); - break; - case "rfloor": - str = Convert.ToChar(8971).ToString(); - break; - case "rho": - str = Convert.ToChar(961).ToString(); - break; - case "Rho": - str = Convert.ToChar(929).ToString(); - break; - case "rlm": - str = Convert.ToChar(8207).ToString(); - break; - case "rsaquo": - str = Convert.ToChar(8250).ToString(); - break; - case "rsquo": - str = Convert.ToChar(8217).ToString(); - break; - case "sbquo": - str = Convert.ToChar(8218).ToString(); - break; - case "Scaron": - str = Convert.ToChar(352).ToString(); - break; - case "scaron": - str = Convert.ToChar(353).ToString(); - break; - case "sdot": - str = Convert.ToChar(8901).ToString(); - break; - case "sect": - str = Convert.ToChar(167).ToString(); - break; - case "shy": - str = Convert.ToChar(173).ToString(); - break; - case "sigma": - str = Convert.ToChar(963).ToString(); - break; - case "Sigma": - str = Convert.ToChar(931).ToString(); - break; - case "sigmaf": - str = Convert.ToChar(962).ToString(); - break; - case "sim": - str = Convert.ToChar(8764).ToString(); - break; - case "spades": - str = Convert.ToChar(9824).ToString(); - break; - case "sub": - str = Convert.ToChar(8834).ToString(); - break; - case "sube": - str = Convert.ToChar(8838).ToString(); - break; - case "sum": - str = Convert.ToChar(8721).ToString(); - break; - case "sup": - str = Convert.ToChar(8835).ToString(); - break; - case "sup1": - str = Convert.ToChar(185).ToString(); - break; - case "sup2": - str = Convert.ToChar(178).ToString(); - break; - case "sup3": - str = Convert.ToChar(179).ToString(); - break; - case "supe": - str = Convert.ToChar(8839).ToString(); - break; - case "szlig": - str = Convert.ToChar(223).ToString(); - break; - case "Tau": - str = Convert.ToChar(932).ToString(); - break; - case "tau": - str = Convert.ToChar(964).ToString(); - break; - case "there4": - str = Convert.ToChar(8756).ToString(); - break; - case "theta": - str = Convert.ToChar(952).ToString(); - break; - case "Theta": - str = Convert.ToChar(920).ToString(); - break; - case "thetasym": - str = Convert.ToChar(977).ToString(); - break; - case "thinsp": - str = Convert.ToChar(8201).ToString(); - break; - case "thorn": - str = Convert.ToChar(254).ToString(); - break; - case "THORN": - str = Convert.ToChar(222).ToString(); - break; - case "tilde": - str = Convert.ToChar(732).ToString(); - break; - case "times": - str = Convert.ToChar(215).ToString(); - break; - case "trade": - str = Convert.ToChar(8482).ToString(); - break; - case "Uacute": - str = Convert.ToChar(218).ToString(); - break; - case "uacute": - str = Convert.ToChar(250).ToString(); - break; - case "uarr": - str = Convert.ToChar(8593).ToString(); - break; - case "uArr": - str = Convert.ToChar(8657).ToString(); - break; - case "Ucirc": - str = Convert.ToChar(219).ToString(); - break; - case "ucirc": - str = Convert.ToChar(251).ToString(); - break; - case "Ugrave": - str = Convert.ToChar(217).ToString(); - break; - case "ugrave": - str = Convert.ToChar(249).ToString(); - break; - case "uml": - str = Convert.ToChar(168).ToString(); - break; - case "upsih": - str = Convert.ToChar(978).ToString(); - break; - case "Upsilon": - str = Convert.ToChar(933).ToString(); - break; - case "upsilon": - str = Convert.ToChar(965).ToString(); - break; - case "Uuml": - str = Convert.ToChar(220).ToString(); - break; - case "uuml": - str = Convert.ToChar(252).ToString(); - break; - case "weierp": - str = Convert.ToChar(8472).ToString(); - break; - case "Xi": - str = Convert.ToChar(926).ToString(); - break; - case "xi": - str = Convert.ToChar(958).ToString(); - break; - case "yacute": - str = Convert.ToChar(253).ToString(); - break; - case "Yacute": - str = Convert.ToChar(221).ToString(); - break; - case "yen": - str = Convert.ToChar(165).ToString(); - break; - case "Yuml": - str = Convert.ToChar(376).ToString(); - break; - case "yuml": - str = Convert.ToChar((int)Byte.MaxValue).ToString(); - break; - case "zeta": - str = Convert.ToChar(950).ToString(); - break; - case "Zeta": - str = Convert.ToChar(918).ToString(); - break; - case "zwj": - str = Convert.ToChar(8205).ToString(); - break; - case "zwnj": - str = Convert.ToChar(8204).ToString(); - break; - } - return str; + private static string EntityLookup(string entity) { + string str = ""; + switch (entity) { + case "Aacute": + str = Convert.ToChar(193).ToString(); + break; + case "aacute": + str = Convert.ToChar(225).ToString(); + break; + case "acirc": + str = Convert.ToChar(226).ToString(); + break; + case "Acirc": + str = Convert.ToChar(194).ToString(); + break; + case "acute": + str = Convert.ToChar(180).ToString(); + break; + case "AElig": + str = Convert.ToChar(198).ToString(); + break; + case "aelig": + str = Convert.ToChar(230).ToString(); + break; + case "Agrave": + str = Convert.ToChar(192).ToString(); + break; + case "agrave": + str = Convert.ToChar(224).ToString(); + break; + case "alefsym": + str = Convert.ToChar(8501).ToString(); + break; + case "Alpha": + str = Convert.ToChar(913).ToString(); + break; + case "alpha": + str = Convert.ToChar(945).ToString(); + break; + case "amp": + str = Convert.ToChar(38).ToString(); + break; + case "and": + str = Convert.ToChar(8743).ToString(); + break; + case "ang": + str = Convert.ToChar(8736).ToString(); + break; + case "aring": + str = Convert.ToChar(229).ToString(); + break; + case "Aring": + str = Convert.ToChar(197).ToString(); + break; + case "asymp": + str = Convert.ToChar(8776).ToString(); + break; + case "Atilde": + str = Convert.ToChar(195).ToString(); + break; + case "atilde": + str = Convert.ToChar(227).ToString(); + break; + case "auml": + str = Convert.ToChar(228).ToString(); + break; + case "Auml": + str = Convert.ToChar(196).ToString(); + break; + case "bdquo": + str = Convert.ToChar(8222).ToString(); + break; + case "Beta": + str = Convert.ToChar(914).ToString(); + break; + case "beta": + str = Convert.ToChar(946).ToString(); + break; + case "brvbar": + str = Convert.ToChar(166).ToString(); + break; + case "bull": + str = Convert.ToChar(8226).ToString(); + break; + case "cap": + str = Convert.ToChar(8745).ToString(); + break; + case "Ccedil": + str = Convert.ToChar(199).ToString(); + break; + case "ccedil": + str = Convert.ToChar(231).ToString(); + break; + case "cedil": + str = Convert.ToChar(184).ToString(); + break; + case "cent": + str = Convert.ToChar(162).ToString(); + break; + case "chi": + str = Convert.ToChar(967).ToString(); + break; + case "Chi": + str = Convert.ToChar(935).ToString(); + break; + case "circ": + str = Convert.ToChar(710).ToString(); + break; + case "clubs": + str = Convert.ToChar(9827).ToString(); + break; + case "cong": + str = Convert.ToChar(8773).ToString(); + break; + case "copy": + str = Convert.ToChar(169).ToString(); + break; + case "crarr": + str = Convert.ToChar(8629).ToString(); + break; + case "cup": + str = Convert.ToChar(8746).ToString(); + break; + case "curren": + str = Convert.ToChar(164).ToString(); + break; + case "dagger": + str = Convert.ToChar(8224).ToString(); + break; + case "Dagger": + str = Convert.ToChar(8225).ToString(); + break; + case "darr": + str = Convert.ToChar(8595).ToString(); + break; + case "dArr": + str = Convert.ToChar(8659).ToString(); + break; + case "deg": + str = Convert.ToChar(176).ToString(); + break; + case "Delta": + str = Convert.ToChar(916).ToString(); + break; + case "delta": + str = Convert.ToChar(948).ToString(); + break; + case "diams": + str = Convert.ToChar(9830).ToString(); + break; + case "divide": + str = Convert.ToChar(247).ToString(); + break; + case "eacute": + str = Convert.ToChar(233).ToString(); + break; + case "Eacute": + str = Convert.ToChar(201).ToString(); + break; + case "Ecirc": + str = Convert.ToChar(202).ToString(); + break; + case "ecirc": + str = Convert.ToChar(234).ToString(); + break; + case "Egrave": + str = Convert.ToChar(200).ToString(); + break; + case "egrave": + str = Convert.ToChar(232).ToString(); + break; + case "empty": + str = Convert.ToChar(8709).ToString(); + break; + case "emsp": + str = Convert.ToChar(8195).ToString(); + break; + case "ensp": + str = Convert.ToChar(8194).ToString(); + break; + case "epsilon": + str = Convert.ToChar(949).ToString(); + break; + case "Epsilon": + str = Convert.ToChar(917).ToString(); + break; + case "equiv": + str = Convert.ToChar(8801).ToString(); + break; + case "Eta": + str = Convert.ToChar(919).ToString(); + break; + case "eta": + str = Convert.ToChar(951).ToString(); + break; + case "eth": + str = Convert.ToChar(240).ToString(); + break; + case "ETH": + str = Convert.ToChar(208).ToString(); + break; + case "Euml": + str = Convert.ToChar(203).ToString(); + break; + case "euml": + str = Convert.ToChar(235).ToString(); + break; + case "euro": + str = Convert.ToChar(8364).ToString(); + break; + case "exist": + str = Convert.ToChar(8707).ToString(); + break; + case "fnof": + str = Convert.ToChar(402).ToString(); + break; + case "forall": + str = Convert.ToChar(8704).ToString(); + break; + case "frac12": + str = Convert.ToChar(189).ToString(); + break; + case "frac14": + str = Convert.ToChar(188).ToString(); + break; + case "frac34": + str = Convert.ToChar(190).ToString(); + break; + case "frasl": + str = Convert.ToChar(8260).ToString(); + break; + case "gamma": + str = Convert.ToChar(947).ToString(); + break; + case "Gamma": + str = Convert.ToChar(915).ToString(); + break; + case "ge": + str = Convert.ToChar(8805).ToString(); + break; + case "gt": + str = Convert.ToChar(62).ToString(); + break; + case "hArr": + str = Convert.ToChar(8660).ToString(); + break; + case "harr": + str = Convert.ToChar(8596).ToString(); + break; + case "hearts": + str = Convert.ToChar(9829).ToString(); + break; + case "hellip": + str = Convert.ToChar(8230).ToString(); + break; + case "Iacute": + str = Convert.ToChar(205).ToString(); + break; + case "iacute": + str = Convert.ToChar(237).ToString(); + break; + case "icirc": + str = Convert.ToChar(238).ToString(); + break; + case "Icirc": + str = Convert.ToChar(206).ToString(); + break; + case "iexcl": + str = Convert.ToChar(161).ToString(); + break; + case "Igrave": + str = Convert.ToChar(204).ToString(); + break; + case "igrave": + str = Convert.ToChar(236).ToString(); + break; + case "image": + str = Convert.ToChar(8465).ToString(); + break; + case "infin": + str = Convert.ToChar(8734).ToString(); + break; + case "int": + str = Convert.ToChar(8747).ToString(); + break; + case "Iota": + str = Convert.ToChar(921).ToString(); + break; + case "iota": + str = Convert.ToChar(953).ToString(); + break; + case "iquest": + str = Convert.ToChar(191).ToString(); + break; + case "isin": + str = Convert.ToChar(8712).ToString(); + break; + case "iuml": + str = Convert.ToChar(239).ToString(); + break; + case "Iuml": + str = Convert.ToChar(207).ToString(); + break; + case "kappa": + str = Convert.ToChar(954).ToString(); + break; + case "Kappa": + str = Convert.ToChar(922).ToString(); + break; + case "Lambda": + str = Convert.ToChar(923).ToString(); + break; + case "lambda": + str = Convert.ToChar(955).ToString(); + break; + case "lang": + str = Convert.ToChar(9001).ToString(); + break; + case "laquo": + str = Convert.ToChar(171).ToString(); + break; + case "larr": + str = Convert.ToChar(8592).ToString(); + break; + case "lArr": + str = Convert.ToChar(8656).ToString(); + break; + case "lceil": + str = Convert.ToChar(8968).ToString(); + break; + case "ldquo": + str = Convert.ToChar(8220).ToString(); + break; + case "le": + str = Convert.ToChar(8804).ToString(); + break; + case "lfloor": + str = Convert.ToChar(8970).ToString(); + break; + case "lowast": + str = Convert.ToChar(8727).ToString(); + break; + case "loz": + str = Convert.ToChar(9674).ToString(); + break; + case "lrm": + str = Convert.ToChar(8206).ToString(); + break; + case "lsaquo": + str = Convert.ToChar(8249).ToString(); + break; + case "lsquo": + str = Convert.ToChar(8216).ToString(); + break; + case "lt": + str = Convert.ToChar(60).ToString(); + break; + case "macr": + str = Convert.ToChar(175).ToString(); + break; + case "mdash": + str = Convert.ToChar(8212).ToString(); + break; + case "micro": + str = Convert.ToChar(181).ToString(); + break; + case "middot": + str = Convert.ToChar(183).ToString(); + break; + case "minus": + str = Convert.ToChar(8722).ToString(); + break; + case "Mu": + str = Convert.ToChar(924).ToString(); + break; + case "mu": + str = Convert.ToChar(956).ToString(); + break; + case "nabla": + str = Convert.ToChar(8711).ToString(); + break; + case "nbsp": + str = Convert.ToChar(160).ToString(); + break; + case "ndash": + str = Convert.ToChar(8211).ToString(); + break; + case "ne": + str = Convert.ToChar(8800).ToString(); + break; + case "ni": + str = Convert.ToChar(8715).ToString(); + break; + case "not": + str = Convert.ToChar(172).ToString(); + break; + case "notin": + str = Convert.ToChar(8713).ToString(); + break; + case "nsub": + str = Convert.ToChar(8836).ToString(); + break; + case "ntilde": + str = Convert.ToChar(241).ToString(); + break; + case "Ntilde": + str = Convert.ToChar(209).ToString(); + break; + case "Nu": + str = Convert.ToChar(925).ToString(); + break; + case "nu": + str = Convert.ToChar(957).ToString(); + break; + case "oacute": + str = Convert.ToChar(243).ToString(); + break; + case "Oacute": + str = Convert.ToChar(211).ToString(); + break; + case "Ocirc": + str = Convert.ToChar(212).ToString(); + break; + case "ocirc": + str = Convert.ToChar(244).ToString(); + break; + case "OElig": + str = Convert.ToChar(338).ToString(); + break; + case "oelig": + str = Convert.ToChar(339).ToString(); + break; + case "ograve": + str = Convert.ToChar(242).ToString(); + break; + case "Ograve": + str = Convert.ToChar(210).ToString(); + break; + case "oline": + str = Convert.ToChar(8254).ToString(); + break; + case "Omega": + str = Convert.ToChar(937).ToString(); + break; + case "omega": + str = Convert.ToChar(969).ToString(); + break; + case "Omicron": + str = Convert.ToChar(927).ToString(); + break; + case "omicron": + str = Convert.ToChar(959).ToString(); + break; + case "oplus": + str = Convert.ToChar(8853).ToString(); + break; + case "or": + str = Convert.ToChar(8744).ToString(); + break; + case "ordf": + str = Convert.ToChar(170).ToString(); + break; + case "ordm": + str = Convert.ToChar(186).ToString(); + break; + case "Oslash": + str = Convert.ToChar(216).ToString(); + break; + case "oslash": + str = Convert.ToChar(248).ToString(); + break; + case "otilde": + str = Convert.ToChar(245).ToString(); + break; + case "Otilde": + str = Convert.ToChar(213).ToString(); + break; + case "otimes": + str = Convert.ToChar(8855).ToString(); + break; + case "Ouml": + str = Convert.ToChar(214).ToString(); + break; + case "ouml": + str = Convert.ToChar(246).ToString(); + break; + case "para": + str = Convert.ToChar(182).ToString(); + break; + case "part": + str = Convert.ToChar(8706).ToString(); + break; + case "permil": + str = Convert.ToChar(8240).ToString(); + break; + case "perp": + str = Convert.ToChar(8869).ToString(); + break; + case "Phi": + str = Convert.ToChar(934).ToString(); + break; + case "phi": + str = Convert.ToChar(966).ToString(); + break; + case "Pi": + str = Convert.ToChar(928).ToString(); + break; + case "pi": + str = Convert.ToChar(960).ToString(); + break; + case "piv": + str = Convert.ToChar(982).ToString(); + break; + case "plusmn": + str = Convert.ToChar(177).ToString(); + break; + case "pound": + str = Convert.ToChar(163).ToString(); + break; + case "Prime": + str = Convert.ToChar(8243).ToString(); + break; + case "prime": + str = Convert.ToChar(8242).ToString(); + break; + case "prod": + str = Convert.ToChar(8719).ToString(); + break; + case "prop": + str = Convert.ToChar(8733).ToString(); + break; + case "psi": + str = Convert.ToChar(968).ToString(); + break; + case "Psi": + str = Convert.ToChar(936).ToString(); + break; + case "quot": + str = Convert.ToChar(34).ToString(); + break; + case "radic": + str = Convert.ToChar(8730).ToString(); + break; + case "rang": + str = Convert.ToChar(9002).ToString(); + break; + case "raquo": + str = Convert.ToChar(187).ToString(); + break; + case "rarr": + str = Convert.ToChar(8594).ToString(); + break; + case "rArr": + str = Convert.ToChar(8658).ToString(); + break; + case "rceil": + str = Convert.ToChar(8969).ToString(); + break; + case "rdquo": + str = Convert.ToChar(8221).ToString(); + break; + case "real": + str = Convert.ToChar(8476).ToString(); + break; + case "reg": + str = Convert.ToChar(174).ToString(); + break; + case "rfloor": + str = Convert.ToChar(8971).ToString(); + break; + case "rho": + str = Convert.ToChar(961).ToString(); + break; + case "Rho": + str = Convert.ToChar(929).ToString(); + break; + case "rlm": + str = Convert.ToChar(8207).ToString(); + break; + case "rsaquo": + str = Convert.ToChar(8250).ToString(); + break; + case "rsquo": + str = Convert.ToChar(8217).ToString(); + break; + case "sbquo": + str = Convert.ToChar(8218).ToString(); + break; + case "Scaron": + str = Convert.ToChar(352).ToString(); + break; + case "scaron": + str = Convert.ToChar(353).ToString(); + break; + case "sdot": + str = Convert.ToChar(8901).ToString(); + break; + case "sect": + str = Convert.ToChar(167).ToString(); + break; + case "shy": + str = Convert.ToChar(173).ToString(); + break; + case "sigma": + str = Convert.ToChar(963).ToString(); + break; + case "Sigma": + str = Convert.ToChar(931).ToString(); + break; + case "sigmaf": + str = Convert.ToChar(962).ToString(); + break; + case "sim": + str = Convert.ToChar(8764).ToString(); + break; + case "spades": + str = Convert.ToChar(9824).ToString(); + break; + case "sub": + str = Convert.ToChar(8834).ToString(); + break; + case "sube": + str = Convert.ToChar(8838).ToString(); + break; + case "sum": + str = Convert.ToChar(8721).ToString(); + break; + case "sup": + str = Convert.ToChar(8835).ToString(); + break; + case "sup1": + str = Convert.ToChar(185).ToString(); + break; + case "sup2": + str = Convert.ToChar(178).ToString(); + break; + case "sup3": + str = Convert.ToChar(179).ToString(); + break; + case "supe": + str = Convert.ToChar(8839).ToString(); + break; + case "szlig": + str = Convert.ToChar(223).ToString(); + break; + case "Tau": + str = Convert.ToChar(932).ToString(); + break; + case "tau": + str = Convert.ToChar(964).ToString(); + break; + case "there4": + str = Convert.ToChar(8756).ToString(); + break; + case "theta": + str = Convert.ToChar(952).ToString(); + break; + case "Theta": + str = Convert.ToChar(920).ToString(); + break; + case "thetasym": + str = Convert.ToChar(977).ToString(); + break; + case "thinsp": + str = Convert.ToChar(8201).ToString(); + break; + case "thorn": + str = Convert.ToChar(254).ToString(); + break; + case "THORN": + str = Convert.ToChar(222).ToString(); + break; + case "tilde": + str = Convert.ToChar(732).ToString(); + break; + case "times": + str = Convert.ToChar(215).ToString(); + break; + case "trade": + str = Convert.ToChar(8482).ToString(); + break; + case "Uacute": + str = Convert.ToChar(218).ToString(); + break; + case "uacute": + str = Convert.ToChar(250).ToString(); + break; + case "uarr": + str = Convert.ToChar(8593).ToString(); + break; + case "uArr": + str = Convert.ToChar(8657).ToString(); + break; + case "Ucirc": + str = Convert.ToChar(219).ToString(); + break; + case "ucirc": + str = Convert.ToChar(251).ToString(); + break; + case "Ugrave": + str = Convert.ToChar(217).ToString(); + break; + case "ugrave": + str = Convert.ToChar(249).ToString(); + break; + case "uml": + str = Convert.ToChar(168).ToString(); + break; + case "upsih": + str = Convert.ToChar(978).ToString(); + break; + case "Upsilon": + str = Convert.ToChar(933).ToString(); + break; + case "upsilon": + str = Convert.ToChar(965).ToString(); + break; + case "Uuml": + str = Convert.ToChar(220).ToString(); + break; + case "uuml": + str = Convert.ToChar(252).ToString(); + break; + case "weierp": + str = Convert.ToChar(8472).ToString(); + break; + case "Xi": + str = Convert.ToChar(926).ToString(); + break; + case "xi": + str = Convert.ToChar(958).ToString(); + break; + case "yacute": + str = Convert.ToChar(253).ToString(); + break; + case "Yacute": + str = Convert.ToChar(221).ToString(); + break; + case "yen": + str = Convert.ToChar(165).ToString(); + break; + case "Yuml": + str = Convert.ToChar(376).ToString(); + break; + case "yuml": + str = Convert.ToChar((int)Byte.MaxValue).ToString(); + break; + case "zeta": + str = Convert.ToChar(950).ToString(); + break; + case "Zeta": + str = Convert.ToChar(918).ToString(); + break; + case "zwj": + str = Convert.ToChar(8205).ToString(); + break; + case "zwnj": + str = Convert.ToChar(8204).ToString(); + break; } + return str; } } diff --git a/src/Exceptionless.Core/Extensions/TaskExtensions.cs b/src/Exceptionless.Core/Extensions/TaskExtensions.cs index 1d4b9ac439..0c0befa257 100644 --- a/src/Exceptionless.Core/Extensions/TaskExtensions.cs +++ b/src/Exceptionless.Core/Extensions/TaskExtensions.cs @@ -1,17 +1,16 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Threading.Tasks; -namespace Exceptionless.Core.Extensions { - public static class TaskExtensions { - [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) { - return task.ConfigureAwait(continueOnCapturedContext: false); - } +namespace Exceptionless.Core.Extensions; - [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) { - return task.ConfigureAwait(continueOnCapturedContext: false); - } +public static class TaskExtensions { + [DebuggerStepThrough] + public static ConfiguredTaskAwaitable AnyContext(this Task task) { + return task.ConfigureAwait(continueOnCapturedContext: false); + } + + [DebuggerStepThrough] + public static ConfiguredTaskAwaitable AnyContext(this Task task) { + return task.ConfigureAwait(continueOnCapturedContext: false); } } diff --git a/src/Exceptionless.Core/Extensions/TypeExtensions.cs b/src/Exceptionless.Core/Extensions/TypeExtensions.cs index 24d61136a5..3ede643942 100644 --- a/src/Exceptionless.Core/Extensions/TypeExtensions.cs +++ b/src/Exceptionless.Core/Extensions/TypeExtensions.cs @@ -1,80 +1,78 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; +using System.ComponentModel; using Exceptionless.Core.Pipeline; -namespace Exceptionless.Core.Extensions { - public static class TypeExtensions { - public static IList SortByPriority(this IEnumerable types) { - return types.OrderBy(t => { - var priorityAttribute = t.GetCustomAttributes(typeof(PriorityAttribute), true).FirstOrDefault() as PriorityAttribute; - return priorityAttribute?.Priority ?? 0; - }).ToList(); - } - - public static bool IsNumeric(this Type type) { - if (type.IsArray) - return false; +namespace Exceptionless.Core.Extensions; - switch (Type.GetTypeCode(type)) { - case TypeCode.Byte: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.SByte: - case TypeCode.Single: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return true; - } +public static class TypeExtensions { + public static IList SortByPriority(this IEnumerable types) { + return types.OrderBy(t => { + var priorityAttribute = t.GetCustomAttributes(typeof(PriorityAttribute), true).FirstOrDefault() as PriorityAttribute; + return priorityAttribute?.Priority ?? 0; + }).ToList(); + } + public static bool IsNumeric(this Type type) { + if (type.IsArray) return false; + + switch (Type.GetTypeCode(type)) { + case TypeCode.Byte: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.SByte: + case TypeCode.Single: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + return true; } - public static T ToType(this object value) { - if (value == null) - throw new ArgumentNullException(nameof(value)); + return false; + } - var targetType = typeof(T); - var converter = TypeDescriptor.GetConverter(targetType); - var valueType = value.GetType(); + public static T ToType(this object value) { + if (value == null) + throw new ArgumentNullException(nameof(value)); - if (targetType.IsAssignableFrom(valueType)) - return (T)value; + var targetType = typeof(T); + var converter = TypeDescriptor.GetConverter(targetType); + var valueType = value.GetType(); - if ((valueType.IsEnum || value is string) && targetType.IsEnum) { - // attempt to match enum by name. - if (EnumHelper.TryEnumIsDefined(targetType, value.ToString())) { - object parsedValue = Enum.Parse(targetType, value.ToString(), false); - return (T)parsedValue; - } + if (targetType.IsAssignableFrom(valueType)) + return (T)value; - string message = $"The Enum value of '{value}' is not defined as a valid value for '{targetType.FullName}'."; - throw new ArgumentException(message); + if ((valueType.IsEnum || value is string) && targetType.IsEnum) { + // attempt to match enum by name. + if (EnumHelper.TryEnumIsDefined(targetType, value.ToString())) { + object parsedValue = Enum.Parse(targetType, value.ToString(), false); + return (T)parsedValue; } - if (valueType.IsNumeric() && targetType.IsEnum) - return (T)Enum.ToObject(targetType, value); + string message = $"The Enum value of '{value}' is not defined as a valid value for '{targetType.FullName}'."; + throw new ArgumentException(message); + } + + if (valueType.IsNumeric() && targetType.IsEnum) + return (T)Enum.ToObject(targetType, value); - if (converter != null && converter.CanConvertFrom(valueType)) { - object convertedValue = converter.ConvertFrom(value); + if (converter != null && converter.CanConvertFrom(valueType)) { + object convertedValue = converter.ConvertFrom(value); + return (T)convertedValue; + } + + if (value is IConvertible) { + try { + object convertedValue = Convert.ChangeType(value, targetType); return (T)convertedValue; } - - if (value is IConvertible) { - try { - object convertedValue = Convert.ChangeType(value, targetType); - return (T)convertedValue; - } catch (Exception e) { - throw new ArgumentException($"An incompatible value specified. Target Type: {targetType.FullName} Value Type: {value.GetType().FullName}", nameof(value), e); - } + catch (Exception e) { + throw new ArgumentException($"An incompatible value specified. Target Type: {targetType.FullName} Value Type: {value.GetType().FullName}", nameof(value), e); } - - throw new ArgumentException($"An incompatible value specified. Target Type: {targetType.FullName} Value Type: {value.GetType().FullName}", nameof(value)); } + + throw new ArgumentException($"An incompatible value specified. Target Type: {targetType.FullName} Value Type: {value.GetType().FullName}", nameof(value)); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/UriExtensions.cs b/src/Exceptionless.Core/Extensions/UriExtensions.cs index 7e30ad5a0e..87838ae21f 100644 --- a/src/Exceptionless.Core/Extensions/UriExtensions.cs +++ b/src/Exceptionless.Core/Extensions/UriExtensions.cs @@ -1,27 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; +using System.Collections.Specialized; -namespace Exceptionless.Core.Extensions { - public static class UriExtensions { - public static string ToQueryString(this NameValueCollection collection) { - return collection.AsKeyValuePairs().ToQueryString(); - } +namespace Exceptionless.Core.Extensions; - public static string ToQueryString(this IEnumerable> collection) { - return collection.ToConcatenatedString(pair => pair.Key == null ? pair.Value : $"{pair.Key}={Uri.EscapeDataString(pair.Value)}", "&"); - } +public static class UriExtensions { + public static string ToQueryString(this NameValueCollection collection) { + return collection.AsKeyValuePairs().ToQueryString(); + } + + public static string ToQueryString(this IEnumerable> collection) { + return collection.ToConcatenatedString(pair => pair.Key == null ? pair.Value : $"{pair.Key}={Uri.EscapeDataString(pair.Value)}", "&"); + } - /// - /// Converts the legacy NameValueCollection into a strongly-typed KeyValuePair sequence. - /// - private static IEnumerable> AsKeyValuePairs(this NameValueCollection collection) { - return collection.AllKeys.Select(key => new KeyValuePair(key, collection.Get(key))); - } + /// + /// Converts the legacy NameValueCollection into a strongly-typed KeyValuePair sequence. + /// + private static IEnumerable> AsKeyValuePairs(this NameValueCollection collection) { + return collection.AllKeys.Select(key => new KeyValuePair(key, collection.Get(key))); + } - public static string GetBaseUrl(this Uri uri) { - return uri.Scheme + "://" + uri.Authority + uri.AbsolutePath; - } + public static string GetBaseUrl(this Uri uri) { + return uri.Scheme + "://" + uri.Authority + uri.AbsolutePath; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/UsageExtensions.cs b/src/Exceptionless.Core/Extensions/UsageExtensions.cs index 151151ebbb..71dbb55d2b 100644 --- a/src/Exceptionless.Core/Extensions/UsageExtensions.cs +++ b/src/Exceptionless.Core/Extensions/UsageExtensions.cs @@ -1,36 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Exceptionless.Core.Models; using Foundatio.Utility; -namespace Exceptionless.Core.Extensions { - public static class UsageExtensions { - public static void SetUsage(this ICollection usages, DateTime dateUtc, int total, int blocked, int tooBig, int limit, TimeSpan? maxUsageAge = null) { - var usageInfo = usages.FirstOrDefault(o => o.Date == dateUtc); - if (usageInfo == null) { - usageInfo = new UsageInfo { - Date = dateUtc, - Total = total, - Blocked = blocked, - Limit = limit, - TooBig = tooBig - }; - usages.Add(usageInfo); - } - else { - usageInfo.Limit = limit; - usageInfo.Total = total; - usageInfo.Blocked = blocked; - usageInfo.TooBig = tooBig; - } +namespace Exceptionless.Core.Extensions; - if (!maxUsageAge.HasValue) - return; - - // remove old usage entries - foreach (var usage in usages.Where(u => u.Date < SystemClock.UtcNow.Subtract(maxUsageAge.Value)).ToList()) - usages.Remove(usage); +public static class UsageExtensions { + public static void SetUsage(this ICollection usages, DateTime dateUtc, int total, int blocked, int tooBig, int limit, TimeSpan? maxUsageAge = null) { + var usageInfo = usages.FirstOrDefault(o => o.Date == dateUtc); + if (usageInfo == null) { + usageInfo = new UsageInfo { + Date = dateUtc, + Total = total, + Blocked = blocked, + Limit = limit, + TooBig = tooBig + }; + usages.Add(usageInfo); + } + else { + usageInfo.Limit = limit; + usageInfo.Total = total; + usageInfo.Blocked = blocked; + usageInfo.TooBig = tooBig; } + + if (!maxUsageAge.HasValue) + return; + + // remove old usage entries + foreach (var usage in usages.Where(u => u.Date < SystemClock.UtcNow.Subtract(maxUsageAge.Value)).ToList()) + usages.Remove(usage); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/UserExtensions.cs b/src/Exceptionless.Core/Extensions/UserExtensions.cs index 18655f52cf..bb80f07dad 100644 --- a/src/Exceptionless.Core/Extensions/UserExtensions.cs +++ b/src/Exceptionless.Core/Extensions/UserExtensions.cs @@ -1,95 +1,93 @@ -using System; -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Utility; -namespace Exceptionless.Core.Extensions { - public static class UserExtensions { - public static bool IsCorrectPassword(this User user, string password) { - if (String.IsNullOrEmpty(user.Salt) || String.IsNullOrEmpty(user.Password)) - return false; +namespace Exceptionless.Core.Extensions; - string encodedPassword = password.ToSaltedHash(user.Salt); - return String.Equals(encodedPassword, user.Password); - } +public static class UserExtensions { + public static bool IsCorrectPassword(this User user, string password) { + if (String.IsNullOrEmpty(user.Salt) || String.IsNullOrEmpty(user.Password)) + return false; - public static void ResetVerifyEmailAddressToken(this User user) { - if (user == null) - return; + string encodedPassword = password.ToSaltedHash(user.Salt); + return String.Equals(encodedPassword, user.Password); + } - user.VerifyEmailAddressToken = null; - user.VerifyEmailAddressTokenExpiration = DateTime.MinValue; - } + public static void ResetVerifyEmailAddressToken(this User user) { + if (user == null) + return; - public static void CreateVerifyEmailAddressToken(this User user) { - if (user == null) - return; + user.VerifyEmailAddressToken = null; + user.VerifyEmailAddressTokenExpiration = DateTime.MinValue; + } - user.VerifyEmailAddressToken = StringExtensions.GetNewToken(); - user.VerifyEmailAddressTokenExpiration = SystemClock.UtcNow.AddMinutes(1440); - } + public static void CreateVerifyEmailAddressToken(this User user) { + if (user == null) + return; - public static bool HasValidVerifyEmailAddressTokenExpiration(this User user) { - if (user == null) - return false; + user.VerifyEmailAddressToken = StringExtensions.GetNewToken(); + user.VerifyEmailAddressTokenExpiration = SystemClock.UtcNow.AddMinutes(1440); + } - return user.VerifyEmailAddressTokenExpiration != DateTime.MinValue && user.VerifyEmailAddressTokenExpiration >= SystemClock.UtcNow; - } + public static bool HasValidVerifyEmailAddressTokenExpiration(this User user) { + if (user == null) + return false; + + return user.VerifyEmailAddressTokenExpiration != DateTime.MinValue && user.VerifyEmailAddressTokenExpiration >= SystemClock.UtcNow; + } - public static void MarkEmailAddressVerified(this User user) { - if (user == null) - return; + public static void MarkEmailAddressVerified(this User user) { + if (user == null) + return; - user.IsEmailAddressVerified = true; - user.VerifyEmailAddressToken = null; - user.VerifyEmailAddressTokenExpiration = DateTime.MinValue; - } + user.IsEmailAddressVerified = true; + user.VerifyEmailAddressToken = null; + user.VerifyEmailAddressTokenExpiration = DateTime.MinValue; + } - public static void ResetPasswordResetToken(this User user) { - if (user == null) - return; + public static void ResetPasswordResetToken(this User user) { + if (user == null) + return; - user.PasswordResetToken = null; - user.PasswordResetTokenExpiration = DateTime.MinValue; - } + user.PasswordResetToken = null; + user.PasswordResetTokenExpiration = DateTime.MinValue; + } - public static void CreatePasswordResetToken(this User user) { - if (user == null) - return; + public static void CreatePasswordResetToken(this User user) { + if (user == null) + return; - user.PasswordResetToken = StringExtensions.GetNewToken(); - user.PasswordResetTokenExpiration = SystemClock.UtcNow.AddMinutes(1440); - } + user.PasswordResetToken = StringExtensions.GetNewToken(); + user.PasswordResetTokenExpiration = SystemClock.UtcNow.AddMinutes(1440); + } - public static bool HasValidPasswordResetTokenExpiration(this User user) { - if (user == null) - return false; + public static bool HasValidPasswordResetTokenExpiration(this User user) { + if (user == null) + return false; - return user.PasswordResetTokenExpiration != DateTime.MinValue && user.PasswordResetTokenExpiration >= SystemClock.UtcNow; - } + return user.PasswordResetTokenExpiration != DateTime.MinValue && user.PasswordResetTokenExpiration >= SystemClock.UtcNow; + } - public static void AddOAuthAccount(this User user, string providerName, string providerUserId, string username, SettingsDictionary data = null) { - var account = new OAuthAccount { - Provider = providerName.ToLowerInvariant(), - ProviderUserId = providerUserId, - Username = username - }; + public static void AddOAuthAccount(this User user, string providerName, string providerUserId, string username, SettingsDictionary data = null) { + var account = new OAuthAccount { + Provider = providerName.ToLowerInvariant(), + ProviderUserId = providerUserId, + Username = username + }; - if (data != null) - account.ExtraData.Apply(data); + if (data != null) + account.ExtraData.Apply(data); - user.OAuthAccounts.Add(account); - } + user.OAuthAccounts.Add(account); + } - public static bool RemoveOAuthAccount(this User user, string providerName, string providerUserId) { - if (user.OAuthAccounts.Count <= 1 && String.IsNullOrEmpty(user.Password)) - return false; + public static bool RemoveOAuthAccount(this User user, string providerName, string providerUserId) { + if (user.OAuthAccounts.Count <= 1 && String.IsNullOrEmpty(user.Password)) + return false; - var account = user.OAuthAccounts.FirstOrDefault(o => o.Provider == providerName.ToLowerInvariant() && o.ProviderUserId == providerUserId); - if (account == null) - return true; + var account = user.OAuthAccounts.FirstOrDefault(o => o.Provider == providerName.ToLowerInvariant() && o.ProviderUserId == providerUserId); + if (account == null) + return true; - return user.OAuthAccounts.Remove(account); - } + return user.OAuthAccounts.Remove(account); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Extensions/ValidationExtensions.cs b/src/Exceptionless.Core/Extensions/ValidationExtensions.cs index 88d733c138..8c152fd3b8 100644 --- a/src/Exceptionless.Core/Extensions/ValidationExtensions.cs +++ b/src/Exceptionless.Core/Extensions/ValidationExtensions.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Validation; +using Exceptionless.Core.Validation; using FluentValidation; using FluentValidation.Results; -namespace Exceptionless.Core.Extensions { - public static class ValidationExtensions { - public static string ToErrorMessage(this IEnumerable failures) { - return failures == null ? null : String.Join(Environment.NewLine, failures.Select(f => f.ErrorMessage)); - } +namespace Exceptionless.Core.Extensions; - public static IRuleBuilderOptions IsObjectId(this IRuleBuilder ruleBuilder) { - return ruleBuilder.SetValidator(new IsObjectIdValidator()); - } +public static class ValidationExtensions { + public static string ToErrorMessage(this IEnumerable failures) { + return failures == null ? null : String.Join(Environment.NewLine, failures.Select(f => f.ErrorMessage)); + } + + public static IRuleBuilderOptions IsObjectId(this IRuleBuilder ruleBuilder) { + return ruleBuilder.SetValidator(new IsObjectIdValidator()); } } diff --git a/src/Exceptionless.Core/Geo/GeoResult.cs b/src/Exceptionless.Core/Geo/GeoResult.cs index 01f97610ab..2062f3f14e 100644 --- a/src/Exceptionless.Core/Geo/GeoResult.cs +++ b/src/Exceptionless.Core/Geo/GeoResult.cs @@ -1,83 +1,82 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Geo { - [DebuggerDisplay("{Latitude},{Longitude}, {Locality}, {Level2}, {Level1}, {Country}")] - public class GeoResult { - public double? Latitude { get; set; } +namespace Exceptionless.Core.Geo; - public double? Longitude { get; set; } - - public string Country { get; set; } +[DebuggerDisplay("{Latitude},{Longitude}, {Locality}, {Level2}, {Level1}, {Country}")] +public class GeoResult { + public double? Latitude { get; set; } - /// - /// State / Province - /// - public string Level1 { get; set; } + public double? Longitude { get; set; } - /// - /// County - /// - public string Level2 { get; set; } + public string Country { get; set; } - /// - /// City - /// - public string Locality { get; set; } + /// + /// State / Province + /// + public string Level1 { get; set; } - public bool IsValid() { - if (!Latitude.HasValue || Latitude < -90.0 || Latitude > 90.0) - return false; + /// + /// County + /// + public string Level2 { get; set; } - if (!Longitude.HasValue || Longitude < -180.0 || Longitude > 180.0) - return false; + /// + /// City + /// + public string Locality { get; set; } - return true; - } + public bool IsValid() { + if (!Latitude.HasValue || Latitude < -90.0 || Latitude > 90.0) + return false; - public static bool TryParse(string input, out GeoResult result) { - result = null; - if (String.IsNullOrEmpty(input)) - return false; + if (!Longitude.HasValue || Longitude < -180.0 || Longitude > 180.0) + return false; - string[] parts = input.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length != 2) - return false; + return true; + } - if (!Double.TryParse(parts[0].Trim(), out double latitude) || Double.IsNaN(latitude) || Double.IsInfinity(latitude)) - return false; + public static bool TryParse(string input, out GeoResult result) { + result = null; + if (String.IsNullOrEmpty(input)) + return false; - if (!Double.TryParse(parts[1].Trim(), out double longitude) || Double.IsNaN(longitude) || Double.IsInfinity(longitude)) - return false; + string[] parts = input.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + return false; - result = new GeoResult { Latitude = latitude, Longitude = longitude }; - return true; - } + if (!Double.TryParse(parts[0].Trim(), out double latitude) || Double.IsNaN(latitude) || Double.IsInfinity(latitude)) + return false; - public override string ToString() { - if (!Latitude.HasValue || !Longitude.HasValue) - return null; + if (!Double.TryParse(parts[1].Trim(), out double longitude) || Double.IsNaN(longitude) || Double.IsInfinity(longitude)) + return false; - return Latitude.GetValueOrDefault().ToString("#0.0#######", CultureInfo.InvariantCulture) + "," + Longitude.GetValueOrDefault().ToString("#0.0#######", CultureInfo.InvariantCulture); - } + result = new GeoResult { Latitude = latitude, Longitude = longitude }; + return true; } - public static class GeoResultExtensions { - public static Location ToLocation(this GeoResult result) { - if (result == null) - return null; - - if (String.IsNullOrEmpty(result.Country) && String.IsNullOrEmpty(result.Level1) && String.IsNullOrEmpty(result.Level2) && String.IsNullOrEmpty(result.Locality)) - return null; - - return new Location { - Country = result.Country?.Trim(), - Level1 = result.Level1?.Trim(), - Level2 = result.Level2?.Trim(), - Locality = result.Locality?.Trim() - }; - } + public override string ToString() { + if (!Latitude.HasValue || !Longitude.HasValue) + return null; + + return Latitude.GetValueOrDefault().ToString("#0.0#######", CultureInfo.InvariantCulture) + "," + Longitude.GetValueOrDefault().ToString("#0.0#######", CultureInfo.InvariantCulture); + } +} + +public static class GeoResultExtensions { + public static Location ToLocation(this GeoResult result) { + if (result == null) + return null; + + if (String.IsNullOrEmpty(result.Country) && String.IsNullOrEmpty(result.Level1) && String.IsNullOrEmpty(result.Level2) && String.IsNullOrEmpty(result.Locality)) + return null; + + return new Location { + Country = result.Country?.Trim(), + Level1 = result.Level1?.Trim(), + Level2 = result.Level2?.Trim(), + Locality = result.Locality?.Trim() + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Geo/IGeoIpService.cs b/src/Exceptionless.Core/Geo/IGeoIpService.cs index bcf034cb98..d3e87732ab 100644 --- a/src/Exceptionless.Core/Geo/IGeoIpService.cs +++ b/src/Exceptionless.Core/Geo/IGeoIpService.cs @@ -1,8 +1,5 @@ -using System.Threading; -using System.Threading.Tasks; +namespace Exceptionless.Core.Geo; -namespace Exceptionless.Core.Geo { - public interface IGeoIpService { - Task ResolveIpAsync(string ip, CancellationToken cancellationToken = default); - } +public interface IGeoIpService { + Task ResolveIpAsync(string ip, CancellationToken cancellationToken = default); } diff --git a/src/Exceptionless.Core/Geo/IGeocodeService.cs b/src/Exceptionless.Core/Geo/IGeocodeService.cs index 7c27426522..f08853214e 100644 --- a/src/Exceptionless.Core/Geo/IGeocodeService.cs +++ b/src/Exceptionless.Core/Geo/IGeocodeService.cs @@ -1,8 +1,5 @@ -using System.Threading; -using System.Threading.Tasks; +namespace Exceptionless.Core.Geo; -namespace Exceptionless.Core.Geo { - public interface IGeocodeService { - Task ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default); - } -} \ No newline at end of file +public interface IGeocodeService { + Task ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default); +} diff --git a/src/Exceptionless.Core/Geo/NullGeoIpService.cs b/src/Exceptionless.Core/Geo/NullGeoIpService.cs index 0580feceb6..381f874d46 100644 --- a/src/Exceptionless.Core/Geo/NullGeoIpService.cs +++ b/src/Exceptionless.Core/Geo/NullGeoIpService.cs @@ -1,10 +1,7 @@ -using System.Threading; -using System.Threading.Tasks; +namespace Exceptionless.Core.Geo; -namespace Exceptionless.Core.Geo { - public class NullGeoIpService : IGeoIpService { - public Task ResolveIpAsync(string ip, CancellationToken cancellationToken = default) { - return Task.FromResult(null); - } +public class NullGeoIpService : IGeoIpService { + public Task ResolveIpAsync(string ip, CancellationToken cancellationToken = default) { + return Task.FromResult(null); } } diff --git a/src/Exceptionless.Core/Geo/NullGeocodeService.cs b/src/Exceptionless.Core/Geo/NullGeocodeService.cs index 1dee0da822..809198cad5 100644 --- a/src/Exceptionless.Core/Geo/NullGeocodeService.cs +++ b/src/Exceptionless.Core/Geo/NullGeocodeService.cs @@ -1,10 +1,7 @@ -using System.Threading; -using System.Threading.Tasks; +namespace Exceptionless.Core.Geo; -namespace Exceptionless.Core.Geo { - public class NullGeocodeService : IGeocodeService { - public Task ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default) { - return Task.FromResult(null); - } +public class NullGeocodeService : IGeocodeService { + public Task ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default) { + return Task.FromResult(null); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs index f3a6e2f5d9..5aed1023a5 100644 --- a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; @@ -17,246 +12,251 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Deletes soft deleted data and enforces data retention.", IsContinuous = false)] - public class CleanupDataJob : JobWithLockBase, IHealthCheck { - private readonly IOrganizationRepository _organizationRepository; - private readonly OrganizationService _organizationService; - private readonly IProjectRepository _projectRepository; - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly ITokenRepository _tokenRepository; - private readonly IWebHookRepository _webHookRepository; - private readonly BillingManager _billingManager; - private readonly AppOptions _appOptions; - private readonly ILockProvider _lockProvider; - private readonly ICacheClient _cacheClient; - private DateTime? _lastRun; - - public CleanupDataJob( - IOrganizationRepository organizationRepository, - OrganizationService organizationService, - IProjectRepository projectRepository, - IStackRepository stackRepository, - IEventRepository eventRepository, - ITokenRepository tokenRepository, - IWebHookRepository webHookRepository, - ILockProvider lockProvider, - ICacheClient cacheClient, - BillingManager billingManager, - AppOptions appOptions, - ILoggerFactory loggerFactory = null - ) : base(loggerFactory) { - _organizationRepository = organizationRepository; - _organizationService = organizationService; - _projectRepository = projectRepository; - _stackRepository = stackRepository; - _eventRepository = eventRepository; - _tokenRepository = tokenRepository; - _webHookRepository = webHookRepository; - _billingManager = billingManager; - _appOptions = appOptions; - _lockProvider = lockProvider; - _cacheClient = cacheClient; - } +namespace Exceptionless.Core.Jobs; + +[Job(Description = "Deletes soft deleted data and enforces data retention.", IsContinuous = false)] +public class CleanupDataJob : JobWithLockBase, IHealthCheck { + private readonly IOrganizationRepository _organizationRepository; + private readonly OrganizationService _organizationService; + private readonly IProjectRepository _projectRepository; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly ITokenRepository _tokenRepository; + private readonly IWebHookRepository _webHookRepository; + private readonly BillingManager _billingManager; + private readonly AppOptions _appOptions; + private readonly ILockProvider _lockProvider; + private readonly ICacheClient _cacheClient; + private DateTime? _lastRun; + + public CleanupDataJob( + IOrganizationRepository organizationRepository, + OrganizationService organizationService, + IProjectRepository projectRepository, + IStackRepository stackRepository, + IEventRepository eventRepository, + ITokenRepository tokenRepository, + IWebHookRepository webHookRepository, + ILockProvider lockProvider, + ICacheClient cacheClient, + BillingManager billingManager, + AppOptions appOptions, + ILoggerFactory loggerFactory = null + ) : base(loggerFactory) { + _organizationRepository = organizationRepository; + _organizationService = organizationService; + _projectRepository = projectRepository; + _stackRepository = stackRepository; + _eventRepository = eventRepository; + _tokenRepository = tokenRepository; + _webHookRepository = webHookRepository; + _billingManager = billingManager; + _appOptions = appOptions; + _lockProvider = lockProvider; + _cacheClient = cacheClient; + } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); - } + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { + return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + } - protected override async Task RunInternalAsync(JobContext context) { - _lastRun = SystemClock.UtcNow; + protected override async Task RunInternalAsync(JobContext context) { + _lastRun = SystemClock.UtcNow; - await CleanupSoftDeletedOrganizationsAsync(context).AnyContext(); - await CleanupSoftDeletedProjectsAsync(context).AnyContext(); - await CleanupSoftDeletedStacksAsync(context).AnyContext(); + await CleanupSoftDeletedOrganizationsAsync(context).AnyContext(); + await CleanupSoftDeletedProjectsAsync(context).AnyContext(); + await CleanupSoftDeletedStacksAsync(context).AnyContext(); - await EnforceRetentionAsync(context).AnyContext(); + await EnforceRetentionAsync(context).AnyContext(); - _logger.CleanupFinished(); + _logger.CleanupFinished(); - return JobResult.Success; - } + return JobResult.Success; + } - private async Task CleanupSoftDeletedOrganizationsAsync(JobContext context) { - var organizationResults = await _organizationRepository.GetAllAsync(o => o.SoftDeleteMode(SoftDeleteQueryMode.DeletedOnly).SearchAfterPaging().PageLimit(5)).AnyContext(); - _logger.CleanupOrganizationSoftDeletes(organizationResults.Total); - - while (organizationResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - foreach (var organization in organizationResults.Documents) { - using var _ = _logger.BeginScope(new ExceptionlessState().Organization(organization.Id)); - try { - await RemoveOrganizationAsync(organization, context).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error removing soft deleted organization {OrganizationId}: {Message}", organization.Id, ex.Message); - } - - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + private async Task CleanupSoftDeletedOrganizationsAsync(JobContext context) { + var organizationResults = await _organizationRepository.GetAllAsync(o => o.SoftDeleteMode(SoftDeleteQueryMode.DeletedOnly).SearchAfterPaging().PageLimit(5)).AnyContext(); + _logger.CleanupOrganizationSoftDeletes(organizationResults.Total); + + while (organizationResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + foreach (var organization in organizationResults.Documents) { + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(organization.Id)); + try { + await RemoveOrganizationAsync(organization, context).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "Error removing soft deleted organization {OrganizationId}: {Message}", organization.Id, ex.Message); } - if (context.CancellationToken.IsCancellationRequested || !await organizationResults.NextPageAsync().AnyContext()) - break; + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); } + + if (context.CancellationToken.IsCancellationRequested || !await organizationResults.NextPageAsync().AnyContext()) + break; } + } + + private async Task CleanupSoftDeletedProjectsAsync(JobContext context) { + var projectResults = await _projectRepository.GetAllAsync(o => o.SoftDeleteMode(SoftDeleteQueryMode.DeletedOnly).SearchAfterPaging().PageLimit(5)).AnyContext(); + _logger.CleanupProjectSoftDeletes(projectResults.Total); - private async Task CleanupSoftDeletedProjectsAsync(JobContext context) { - var projectResults = await _projectRepository.GetAllAsync(o => o.SoftDeleteMode(SoftDeleteQueryMode.DeletedOnly).SearchAfterPaging().PageLimit(5)).AnyContext(); - _logger.CleanupProjectSoftDeletes(projectResults.Total); - - while (projectResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - foreach (var project in projectResults.Documents) { - using var _ = _logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id)); - try { - await RemoveProjectsAsync(project, context).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error removing soft deleted project {ProjectId}: {Message}", project.Id, ex.Message); - } - - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + while (projectResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + foreach (var project in projectResults.Documents) { + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id)); + try { + await RemoveProjectsAsync(project, context).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "Error removing soft deleted project {ProjectId}: {Message}", project.Id, ex.Message); } - if (context.CancellationToken.IsCancellationRequested || !await projectResults.NextPageAsync().AnyContext()) - break; + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); } + + if (context.CancellationToken.IsCancellationRequested || !await projectResults.NextPageAsync().AnyContext()) + break; } + } - private async Task CleanupSoftDeletedStacksAsync(JobContext context) { - var stackResults = await _stackRepository.GetSoftDeleted().AnyContext(); - _logger.CleanupStackSoftDeletes(stackResults.Total); + private async Task CleanupSoftDeletedStacksAsync(JobContext context) { + var stackResults = await _stackRepository.GetSoftDeleted().AnyContext(); + _logger.CleanupStackSoftDeletes(stackResults.Total); - while (stackResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - try { - await RemoveStacksAsync(stackResults.Documents, context).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error removing soft deleted stacks: {Message}", ex.Message); - } - - if (context.CancellationToken.IsCancellationRequested || !await stackResults.NextPageAsync().AnyContext()) - break; + while (stackResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + try { + await RemoveStacksAsync(stackResults.Documents, context).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "Error removing soft deleted stacks: {Message}", ex.Message); } + + if (context.CancellationToken.IsCancellationRequested || !await stackResults.NextPageAsync().AnyContext()) + break; } + } - private async Task RemoveOrganizationAsync(Organization organization, JobContext context) { - _logger.RemoveOrganizationStart(organization.Name, organization.Id); - await _organizationService.RemoveTokensAsync(organization).AnyContext(); - await _organizationService.RemoveWebHooksAsync(organization).AnyContext(); - await _organizationService.CancelSubscriptionsAsync(organization).AnyContext(); - await _organizationService.RemoveUsersAsync(organization, null).AnyContext(); + private async Task RemoveOrganizationAsync(Organization organization, JobContext context) { + _logger.RemoveOrganizationStart(organization.Name, organization.Id); + await _organizationService.RemoveTokensAsync(organization).AnyContext(); + await _organizationService.RemoveWebHooksAsync(organization).AnyContext(); + await _organizationService.CancelSubscriptionsAsync(organization).AnyContext(); + await _organizationService.RemoveUsersAsync(organization, null).AnyContext(); - await RenewLockAsync(context).AnyContext(); - long removedEvents = await _eventRepository.RemoveAllByOrganizationIdAsync(organization.Id).AnyContext(); + await RenewLockAsync(context).AnyContext(); + long removedEvents = await _eventRepository.RemoveAllByOrganizationIdAsync(organization.Id).AnyContext(); - await RenewLockAsync(context).AnyContext(); - long removedStacks = await _stackRepository.RemoveAllByOrganizationIdAsync(organization.Id).AnyContext(); + await RenewLockAsync(context).AnyContext(); + long removedStacks = await _stackRepository.RemoveAllByOrganizationIdAsync(organization.Id).AnyContext(); - await RenewLockAsync(context).AnyContext(); - long removedProjects = await _projectRepository.RemoveAllByOrganizationIdAsync(organization.Id).AnyContext(); + await RenewLockAsync(context).AnyContext(); + long removedProjects = await _projectRepository.RemoveAllByOrganizationIdAsync(organization.Id).AnyContext(); - await _organizationRepository.RemoveAsync(organization).AnyContext(); - _logger.RemoveOrganizationComplete(organization.Name, organization.Id, removedProjects, removedStacks, removedEvents); - } + await _organizationRepository.RemoveAsync(organization).AnyContext(); + _logger.RemoveOrganizationComplete(organization.Name, organization.Id, removedProjects, removedStacks, removedEvents); + } - private async Task RemoveProjectsAsync(Project project, JobContext context) { - _logger.RemoveProjectStart(project.Name, project.Id); - await _tokenRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); - await _webHookRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); + private async Task RemoveProjectsAsync(Project project, JobContext context) { + _logger.RemoveProjectStart(project.Name, project.Id); + await _tokenRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); + await _webHookRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); - await RenewLockAsync(context).AnyContext(); - long removedEvents = await _eventRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); + await RenewLockAsync(context).AnyContext(); + long removedEvents = await _eventRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); - await RenewLockAsync(context).AnyContext(); - long removedStacks = await _stackRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); + await RenewLockAsync(context).AnyContext(); + long removedStacks = await _stackRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id).AnyContext(); - await _projectRepository.RemoveAsync(project).AnyContext(); - _logger.RemoveProjectComplete(project.Name, project.Id, removedStacks, removedEvents); - } + await _projectRepository.RemoveAsync(project).AnyContext(); + _logger.RemoveProjectComplete(project.Name, project.Id, removedStacks, removedEvents); + } - private async Task RemoveStacksAsync(IReadOnlyCollection stacks, JobContext context) { - await RenewLockAsync(context).AnyContext(); + private async Task RemoveStacksAsync(IReadOnlyCollection stacks, JobContext context) { + await RenewLockAsync(context).AnyContext(); - string[] stackIds = stacks.Select(s => s.Id).ToArray(); - long removedEvents = await _eventRepository.RemoveAllByStackIdsAsync(stackIds).AnyContext(); - await _stackRepository.RemoveAsync(stacks).AnyContext(); - foreach (var orgGroup in stacks.GroupBy(s => (s.OrganizationId, s.ProjectId))) - await _cacheClient.RemoveByPrefixAsync(String.Concat("stack-filter:", orgGroup.Key.OrganizationId, ":", orgGroup.Key.ProjectId)); + string[] stackIds = stacks.Select(s => s.Id).ToArray(); + long removedEvents = await _eventRepository.RemoveAllByStackIdsAsync(stackIds).AnyContext(); + await _stackRepository.RemoveAsync(stacks).AnyContext(); + foreach (var orgGroup in stacks.GroupBy(s => (s.OrganizationId, s.ProjectId))) + await _cacheClient.RemoveByPrefixAsync(String.Concat("stack-filter:", orgGroup.Key.OrganizationId, ":", orgGroup.Key.ProjectId)); - _logger.RemoveStacksComplete(stackIds.Length, removedEvents); - } - - private async Task EnforceRetentionAsync(JobContext context) { - var results = await _organizationRepository.FindAsync(q => q.Include(o => o.Id, o => o.Name, o => o.RetentionDays), o => o.SearchAfterPaging().PageLimit(100)).AnyContext(); - while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - foreach (var organization in results.Documents) { - using var _ = _logger.BeginScope(new ExceptionlessState().Organization(organization.Id)); - - int retentionDays = _billingManager.GetBillingPlanByUpsellingRetentionPeriod(organization.RetentionDays)?.RetentionDays ?? _appOptions.MaximumRetentionDays; - if (retentionDays <= 0) - retentionDays = _appOptions.MaximumRetentionDays; - retentionDays = Math.Min(retentionDays, _appOptions.MaximumRetentionDays); - - try { - // adding 60 days to retention in order to keep track of whether a stack is new or not - await EnforceStackRetentionDaysAsync(organization, retentionDays + 60, context).AnyContext(); - await EnforceEventRetentionDaysAsync(organization, retentionDays, context).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error enforcing retention for Organization {OrganizationId}: {Message}", organization.Id, ex.Message); - } - - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); - } + _logger.RemoveStacksComplete(stackIds.Length, removedEvents); + } - if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) - break; - } - } + private async Task EnforceRetentionAsync(JobContext context) { + var results = await _organizationRepository.FindAsync(q => q.Include(o => o.Id, o => o.Name, o => o.RetentionDays), o => o.SearchAfterPaging().PageLimit(100)).AnyContext(); + while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + foreach (var organization in results.Documents) { + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(organization.Id)); - private async Task EnforceStackRetentionDaysAsync(Organization organization, int retentionDays, JobContext context) { - await RenewLockAsync(context).AnyContext(); + int retentionDays = _billingManager.GetBillingPlanByUpsellingRetentionPeriod(organization.RetentionDays)?.RetentionDays ?? _appOptions.MaximumRetentionDays; + if (retentionDays <= 0) + retentionDays = _appOptions.MaximumRetentionDays; + retentionDays = Math.Min(retentionDays, _appOptions.MaximumRetentionDays); - var cutoff = SystemClock.UtcNow.Date.SubtractDays(retentionDays); - var stackResults = await _stackRepository.GetStacksForCleanupAsync(organization.Id, cutoff).AnyContext(); - _logger.RetentionEnforcementStackStart(cutoff, organization.Name, organization.Id, stackResults.Total); - - while (stackResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { try { - await RemoveStacksAsync(stackResults.Documents, context).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error removing stacks: {Message}", ex.Message); + // adding 60 days to retention in order to keep track of whether a stack is new or not + await EnforceStackRetentionDaysAsync(organization, retentionDays + 60, context).AnyContext(); + await EnforceEventRetentionDaysAsync(organization, retentionDays, context).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "Error enforcing retention for Organization {OrganizationId}: {Message}", organization.Id, ex.Message); } - if (context.CancellationToken.IsCancellationRequested || !await stackResults.NextPageAsync().AnyContext()) - break; + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); } - - _logger.RetentionEnforcementStackComplete(organization.Name, organization.Id, stackResults.Documents.Count); + + if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) + break; } + } - private async Task EnforceEventRetentionDaysAsync(Organization organization, int retentionDays, JobContext context) { - await RenewLockAsync(context).AnyContext(); + private async Task EnforceStackRetentionDaysAsync(Organization organization, int retentionDays, JobContext context) { + await RenewLockAsync(context).AnyContext(); - var cutoff = SystemClock.UtcNow.Date.SubtractDays(retentionDays); - _logger.RetentionEnforcementEventStart(cutoff, organization.Name, organization.Id); + var cutoff = SystemClock.UtcNow.Date.SubtractDays(retentionDays); + var stackResults = await _stackRepository.GetStacksForCleanupAsync(organization.Id, cutoff).AnyContext(); + _logger.RetentionEnforcementStackStart(cutoff, organization.Name, organization.Id, stackResults.Total); - long removedEvents = await _eventRepository.RemoveAllAsync(organization.Id, null, null, cutoff).AnyContext(); - _logger.RetentionEnforcementEventComplete(organization.Name, organization.Id, removedEvents); - } + while (stackResults.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + try { + await RemoveStacksAsync(stackResults.Documents, context).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "Error removing stacks: {Message}", ex.Message); + } - private Task RenewLockAsync(JobContext context) { - _lastRun = SystemClock.UtcNow; - return context.RenewLockAsync(); + if (context.CancellationToken.IsCancellationRequested || !await stackResults.NextPageAsync().AnyContext()) + break; } - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + _logger.RetentionEnforcementStackComplete(organization.Name, organization.Id, stackResults.Documents.Count); + } + + private async Task EnforceEventRetentionDaysAsync(Organization organization, int retentionDays, JobContext context) { + await RenewLockAsync(context).AnyContext(); - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); + var cutoff = SystemClock.UtcNow.Date.SubtractDays(retentionDays); + _logger.RetentionEnforcementEventStart(cutoff, organization.Name, organization.Id); - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); - } + long removedEvents = await _eventRepository.RemoveAllAsync(organization.Id, null, null, cutoff).AnyContext(); + _logger.RetentionEnforcementEventComplete(organization.Name, organization.Id, removedEvents); + } + + private Task RenewLockAsync(JobContext context) { + _lastRun = SystemClock.UtcNow; + return context.RenewLockAsync(); + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); + + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs index ccce2ce62a..7a3e4b81c8 100644 --- a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; using Exceptionless.Core.Repositories.Configuration; @@ -20,333 +15,334 @@ using Nest; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Deletes orphaned data.", IsContinuous = false)] - public class CleanupOrphanedDataJob : JobWithLockBase, IHealthCheck { - private readonly ExceptionlessElasticConfiguration _config; - private readonly IElasticClient _elasticClient; - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly ICacheClient _cacheClient; - private readonly ILockProvider _lockProvider; - private DateTime? _lastRun; - - public CleanupOrphanedDataJob(ExceptionlessElasticConfiguration config, IStackRepository stackRepository, IEventRepository eventRepository, ICacheClient cacheClient, ILockProvider lockProvider, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _config = config; - _elasticClient = config.Client; - _stackRepository = stackRepository; - _eventRepository = eventRepository; - _cacheClient = cacheClient; - _lockProvider = lockProvider; - } - - protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), new CancellationToken(true)); - } +namespace Exceptionless.Core.Jobs; + +[Job(Description = "Deletes orphaned data.", IsContinuous = false)] +public class CleanupOrphanedDataJob : JobWithLockBase, IHealthCheck { + private readonly ExceptionlessElasticConfiguration _config; + private readonly IElasticClient _elasticClient; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly ICacheClient _cacheClient; + private readonly ILockProvider _lockProvider; + private DateTime? _lastRun; + + public CleanupOrphanedDataJob(ExceptionlessElasticConfiguration config, IStackRepository stackRepository, IEventRepository eventRepository, ICacheClient cacheClient, ILockProvider lockProvider, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _config = config; + _elasticClient = config.Client; + _stackRepository = stackRepository; + _eventRepository = eventRepository; + _cacheClient = cacheClient; + _lockProvider = lockProvider; + } - protected override async Task RunInternalAsync(JobContext context) { - await DeleteOrphanedEventsByStackAsync(context).AnyContext(); - await DeleteOrphanedEventsByProjectAsync(context).AnyContext(); - await DeleteOrphanedEventsByOrganizationAsync(context).AnyContext(); + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { + return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), new CancellationToken(true)); + } - await FixDuplicateStacks(context).AnyContext(); + protected override async Task RunInternalAsync(JobContext context) { + await DeleteOrphanedEventsByStackAsync(context).AnyContext(); + await DeleteOrphanedEventsByProjectAsync(context).AnyContext(); + await DeleteOrphanedEventsByOrganizationAsync(context).AnyContext(); - return JobResult.Success; - } + await FixDuplicateStacks(context).AnyContext(); - public async Task DeleteOrphanedEventsByStackAsync(JobContext context) { - // get approximate number of unique stack ids - var stackCardinality = await _elasticClient.SearchAsync(s => s.Aggregations(a => a - .Cardinality("cardinality_stack_id", c => c.Field(f => f.StackId).PrecisionThreshold(40000)))); + return JobResult.Success; + } - double? uniqueStackIdCount = stackCardinality.Aggregations.Cardinality("cardinality_stack_id")?.Value; - if (!uniqueStackIdCount.HasValue || uniqueStackIdCount.Value <= 0) - return; + public async Task DeleteOrphanedEventsByStackAsync(JobContext context) { + // get approximate number of unique stack ids + var stackCardinality = await _elasticClient.SearchAsync(s => s.Aggregations(a => a + .Cardinality("cardinality_stack_id", c => c.Field(f => f.StackId).PrecisionThreshold(40000)))); - // break into batches of 500 - const int batchSize = 500; - int buckets = (int)uniqueStackIdCount.Value / batchSize; - buckets = Math.Max(1, buckets); - int totalOrphanedEventCount = 0; - int totalStackIds = 0; + double? uniqueStackIdCount = stackCardinality.Aggregations.Cardinality("cardinality_stack_id")?.Value; + if (!uniqueStackIdCount.HasValue || uniqueStackIdCount.Value <= 0) + return; - for (int batchNumber = 0; batchNumber < buckets; batchNumber++) { - await RenewLockAsync(context); + // break into batches of 500 + const int batchSize = 500; + int buckets = (int)uniqueStackIdCount.Value / batchSize; + buckets = Math.Max(1, buckets); + int totalOrphanedEventCount = 0; + int totalStackIds = 0; - var stackIdTerms = await _elasticClient.SearchAsync(s => s.Aggregations(a => a - .Terms("terms_stack_id", c => c.Field(f => f.StackId).Include(batchNumber, buckets).Size(batchSize * 2)))); + for (int batchNumber = 0; batchNumber < buckets; batchNumber++) { + await RenewLockAsync(context); - string[] stackIds = stackIdTerms.Aggregations.Terms("terms_stack_id").Buckets.Select(b => b.Key).ToArray(); - if (stackIds.Length == 0) - continue; + var stackIdTerms = await _elasticClient.SearchAsync(s => s.Aggregations(a => a + .Terms("terms_stack_id", c => c.Field(f => f.StackId).Include(batchNumber, buckets).Size(batchSize * 2)))); - totalStackIds += stackIds.Length; + string[] stackIds = stackIdTerms.Aggregations.Terms("terms_stack_id").Buckets.Select(b => b.Key).ToArray(); + if (stackIds.Length == 0) + continue; - var stacks = await _elasticClient.MultiGetAsync(r => r.SourceEnabled(false).GetMany(stackIds)); - string[] missingStackIds = stacks.Hits.Where(h => !h.Found).Select(h => h.Id).ToArray(); + totalStackIds += stackIds.Length; + var stacks = await _elasticClient.MultiGetAsync(r => r.SourceEnabled(false).GetMany(stackIds)); + string[] missingStackIds = stacks.Hits.Where(h => !h.Found).Select(h => h.Id).ToArray(); - if (missingStackIds.Length == 0) { - _logger.LogInformation("{BatchNumber}/{BatchCount}: Did not find any missing stacks out of {StackIdCount}", batchNumber, buckets, stackIds.Length); - continue; - } - totalOrphanedEventCount += missingStackIds.Length; - _logger.LogInformation("{BatchNumber}/{BatchCount}: Found {OrphanedEventCount} orphaned events from missing stacks {MissingStackIds} out of {StackIdCount}", batchNumber, buckets, missingStackIds.Length, missingStackIds, stackIds.Length); - await _elasticClient.DeleteByQueryAsync(r => r.Query(q => q.Terms(t => t.Field(f => f.StackId).Terms(missingStackIds)))); + if (missingStackIds.Length == 0) { + _logger.LogInformation("{BatchNumber}/{BatchCount}: Did not find any missing stacks out of {StackIdCount}", batchNumber, buckets, stackIds.Length); + continue; } - _logger.LogInformation("Found {OrphanedEventCount} orphaned events from missing stacks out of {StackIdCount}", totalOrphanedEventCount, totalStackIds); + totalOrphanedEventCount += missingStackIds.Length; + _logger.LogInformation("{BatchNumber}/{BatchCount}: Found {OrphanedEventCount} orphaned events from missing stacks {MissingStackIds} out of {StackIdCount}", batchNumber, buckets, missingStackIds.Length, missingStackIds, stackIds.Length); + await _elasticClient.DeleteByQueryAsync(r => r.Query(q => q.Terms(t => t.Field(f => f.StackId).Terms(missingStackIds)))); } - public async Task DeleteOrphanedEventsByProjectAsync(JobContext context) { - // get approximate number of unique project ids - var projectCardinality = await _elasticClient.SearchAsync(s => s.Aggregations(a => a - .Cardinality("cardinality_project_id", c => c.Field(f => f.ProjectId).PrecisionThreshold(40000)))); + _logger.LogInformation("Found {OrphanedEventCount} orphaned events from missing stacks out of {StackIdCount}", totalOrphanedEventCount, totalStackIds); + } - double? uniqueProjectIdCount = projectCardinality.Aggregations.Cardinality("cardinality_project_id")?.Value; - if (!uniqueProjectIdCount.HasValue || uniqueProjectIdCount.Value <= 0) - return; + public async Task DeleteOrphanedEventsByProjectAsync(JobContext context) { + // get approximate number of unique project ids + var projectCardinality = await _elasticClient.SearchAsync(s => s.Aggregations(a => a + .Cardinality("cardinality_project_id", c => c.Field(f => f.ProjectId).PrecisionThreshold(40000)))); - // break into batches of 500 - const int batchSize = 500; - int buckets = (int)uniqueProjectIdCount.Value / batchSize; - buckets = Math.Max(1, buckets); - int totalOrphanedEventCount = 0; - int totalProjectIds = 0; + double? uniqueProjectIdCount = projectCardinality.Aggregations.Cardinality("cardinality_project_id")?.Value; + if (!uniqueProjectIdCount.HasValue || uniqueProjectIdCount.Value <= 0) + return; - for (int batchNumber = 0; batchNumber < buckets; batchNumber++) { - await RenewLockAsync(context); + // break into batches of 500 + const int batchSize = 500; + int buckets = (int)uniqueProjectIdCount.Value / batchSize; + buckets = Math.Max(1, buckets); + int totalOrphanedEventCount = 0; + int totalProjectIds = 0; - var projectIdTerms = await _elasticClient.SearchAsync(s => s.Aggregations(a => a - .Terms("terms_project_id", c => c.Field(f => f.ProjectId).Include(batchNumber, buckets).Size(batchSize * 2)))); + for (int batchNumber = 0; batchNumber < buckets; batchNumber++) { + await RenewLockAsync(context); - string[] projectIds = projectIdTerms.Aggregations.Terms("terms_project_id").Buckets.Select(b => b.Key).ToArray(); - if (projectIds.Length == 0) - continue; + var projectIdTerms = await _elasticClient.SearchAsync(s => s.Aggregations(a => a + .Terms("terms_project_id", c => c.Field(f => f.ProjectId).Include(batchNumber, buckets).Size(batchSize * 2)))); - totalProjectIds += projectIds.Length; + string[] projectIds = projectIdTerms.Aggregations.Terms("terms_project_id").Buckets.Select(b => b.Key).ToArray(); + if (projectIds.Length == 0) + continue; - var projects = await _elasticClient.MultiGetAsync(r => r.SourceEnabled(false).GetMany(projectIds)); - string[] missingProjectIds = projects.Hits.Where(h => !h.Found).Select(h => h.Id).ToArray(); + totalProjectIds += projectIds.Length; - if (missingProjectIds.Length == 0) { - _logger.LogInformation("{BatchNumber}/{BatchCount}: Did not find any missing projects out of {ProjectIdCount}", batchNumber, buckets, projectIds.Length); - continue; - } + var projects = await _elasticClient.MultiGetAsync(r => r.SourceEnabled(false).GetMany(projectIds)); + string[] missingProjectIds = projects.Hits.Where(h => !h.Found).Select(h => h.Id).ToArray(); - _logger.LogInformation("{BatchNumber}/{BatchCount}: Found {OrphanedEventCount} orphaned events from missing projects {MissingProjectIds} out of {ProjectIdCount}", batchNumber, buckets, missingProjectIds.Length, missingProjectIds, projectIds.Length); - await _elasticClient.DeleteByQueryAsync(r => r.Query(q => q.Terms(t => t.Field(f => f.ProjectId).Terms(missingProjectIds)))); + if (missingProjectIds.Length == 0) { + _logger.LogInformation("{BatchNumber}/{BatchCount}: Did not find any missing projects out of {ProjectIdCount}", batchNumber, buckets, projectIds.Length); + continue; } - _logger.LogInformation("Found {OrphanedEventCount} orphaned events from missing projects out of {ProjectIdCount}", totalOrphanedEventCount, totalProjectIds); + _logger.LogInformation("{BatchNumber}/{BatchCount}: Found {OrphanedEventCount} orphaned events from missing projects {MissingProjectIds} out of {ProjectIdCount}", batchNumber, buckets, missingProjectIds.Length, missingProjectIds, projectIds.Length); + await _elasticClient.DeleteByQueryAsync(r => r.Query(q => q.Terms(t => t.Field(f => f.ProjectId).Terms(missingProjectIds)))); } - public async Task DeleteOrphanedEventsByOrganizationAsync(JobContext context) { - // get approximate number of unique organization ids - var organizationCardinality = await _elasticClient.SearchAsync(s => s.Aggregations(a => a - .Cardinality("cardinality_organization_id", c => c.Field(f => f.OrganizationId).PrecisionThreshold(40000)))); + _logger.LogInformation("Found {OrphanedEventCount} orphaned events from missing projects out of {ProjectIdCount}", totalOrphanedEventCount, totalProjectIds); + } - double? uniqueOrganizationIdCount = organizationCardinality.Aggregations.Cardinality("cardinality_organization_id")?.Value; - if (!uniqueOrganizationIdCount.HasValue || uniqueOrganizationIdCount.Value <= 0) - return; + public async Task DeleteOrphanedEventsByOrganizationAsync(JobContext context) { + // get approximate number of unique organization ids + var organizationCardinality = await _elasticClient.SearchAsync(s => s.Aggregations(a => a + .Cardinality("cardinality_organization_id", c => c.Field(f => f.OrganizationId).PrecisionThreshold(40000)))); - // break into batches of 500 - const int batchSize = 500; - int buckets = (int)uniqueOrganizationIdCount.Value / batchSize; - buckets = Math.Max(1, buckets); - int totalOrphanedEventCount = 0; - int totalOrganizationIds = 0; + double? uniqueOrganizationIdCount = organizationCardinality.Aggregations.Cardinality("cardinality_organization_id")?.Value; + if (!uniqueOrganizationIdCount.HasValue || uniqueOrganizationIdCount.Value <= 0) + return; - for (int batchNumber = 0; batchNumber < buckets; batchNumber++) { - await RenewLockAsync(context); + // break into batches of 500 + const int batchSize = 500; + int buckets = (int)uniqueOrganizationIdCount.Value / batchSize; + buckets = Math.Max(1, buckets); + int totalOrphanedEventCount = 0; + int totalOrganizationIds = 0; - var organizationIdTerms = await _elasticClient.SearchAsync(s => s.Aggregations(a => a - .Terms("terms_organization_id", c => c.Field(f => f.OrganizationId).Include(batchNumber, buckets).Size(batchSize * 2)))); + for (int batchNumber = 0; batchNumber < buckets; batchNumber++) { + await RenewLockAsync(context); - string[] organizationIds = organizationIdTerms.Aggregations.Terms("terms_organization_id").Buckets.Select(b => b.Key).ToArray(); - if (organizationIds.Length == 0) - continue; + var organizationIdTerms = await _elasticClient.SearchAsync(s => s.Aggregations(a => a + .Terms("terms_organization_id", c => c.Field(f => f.OrganizationId).Include(batchNumber, buckets).Size(batchSize * 2)))); - totalOrganizationIds += organizationIds.Length; + string[] organizationIds = organizationIdTerms.Aggregations.Terms("terms_organization_id").Buckets.Select(b => b.Key).ToArray(); + if (organizationIds.Length == 0) + continue; - var organizations = await _elasticClient.MultiGetAsync(r => r.SourceEnabled(false).GetMany(organizationIds)); - string[] missingOrganizationIds = organizations.Hits.Where(h => !h.Found).Select(h => h.Id).ToArray(); + totalOrganizationIds += organizationIds.Length; - if (missingOrganizationIds.Length == 0) { - _logger.LogInformation("{BatchNumber}/{BatchCount}: Did not find any missing organizations out of {OrganizationIdCount}", batchNumber, buckets, organizationIds.Length); - continue; - } + var organizations = await _elasticClient.MultiGetAsync(r => r.SourceEnabled(false).GetMany(organizationIds)); + string[] missingOrganizationIds = organizations.Hits.Where(h => !h.Found).Select(h => h.Id).ToArray(); - _logger.LogInformation("{BatchNumber}/{BatchCount}: Found {OrphanedEventCount} orphaned events from missing organizations {MissingOrganizationIds} out of {OrganizationIdCount}", batchNumber, buckets, missingOrganizationIds.Length, missingOrganizationIds, organizationIds.Length); - await _elasticClient.DeleteByQueryAsync(r => r.Query(q => q.Terms(t => t.Field(f => f.OrganizationId).Terms(missingOrganizationIds)))); + if (missingOrganizationIds.Length == 0) { + _logger.LogInformation("{BatchNumber}/{BatchCount}: Did not find any missing organizations out of {OrganizationIdCount}", batchNumber, buckets, organizationIds.Length); + continue; } - _logger.LogInformation("Found {OrphanedEventCount} orphaned events from missing organizations out of {OrganizationIdCount}", totalOrphanedEventCount, totalOrganizationIds); + _logger.LogInformation("{BatchNumber}/{BatchCount}: Found {OrphanedEventCount} orphaned events from missing organizations {MissingOrganizationIds} out of {OrganizationIdCount}", batchNumber, buckets, missingOrganizationIds.Length, missingOrganizationIds, organizationIds.Length); + await _elasticClient.DeleteByQueryAsync(r => r.Query(q => q.Terms(t => t.Field(f => f.OrganizationId).Terms(missingOrganizationIds)))); } - public async Task FixDuplicateStacks(JobContext context) { - _logger.LogInformation("Getting duplicate stacks"); + _logger.LogInformation("Found {OrphanedEventCount} orphaned events from missing organizations out of {OrganizationIdCount}", totalOrphanedEventCount, totalOrganizationIds); + } - var duplicateStackAgg = await _elasticClient.SearchAsync(q => q - .QueryOnQueryString("is_deleted:false") - .Size(0) - .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); - _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); + public async Task FixDuplicateStacks(JobContext context) { + _logger.LogInformation("Getting duplicate stacks"); + + var duplicateStackAgg = await _elasticClient.SearchAsync(q => q + .QueryOnQueryString("is_deleted:false") + .Size(0) + .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); + _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); + + var buckets = duplicateStackAgg.Aggregations.Terms("stacks")?.Buckets ?? new List>(); + int total = buckets.Count; + int processed = 0; + int error = 0; + long totalUpdatedEventCount = 0; + var lastStatus = SystemClock.Now; + int batch = 1; + + while (buckets.Count > 0) { + _logger.LogInformation($"Found {buckets.Count} duplicate stacks in batch #{batch}."); + await RenewLockAsync(context); + + foreach (var duplicateSignature in buckets) { + string projectId = null; + string signature = null; + try { + string[] parts = duplicateSignature.Key.Split(':'); + if (parts.Length != 2) { + _logger.LogError("Error parsing duplicate signature {DuplicateSignature}", duplicateSignature.Key); + continue; + } + projectId = parts[0]; + signature = parts[1]; - var buckets = duplicateStackAgg.Aggregations.Terms("stacks")?.Buckets ?? new List>(); - int total = buckets.Count; - int processed = 0; - int error = 0; - long totalUpdatedEventCount = 0; - var lastStatus = SystemClock.Now; - int batch = 1; - - while (buckets.Count > 0) { - _logger.LogInformation($"Found {buckets.Count} duplicate stacks in batch #{batch}."); - await RenewLockAsync(context); - - foreach (var duplicateSignature in buckets) { - string projectId = null; - string signature = null; - try { - string[] parts = duplicateSignature.Key.Split(':'); - if (parts.Length != 2) { - _logger.LogError("Error parsing duplicate signature {DuplicateSignature}", duplicateSignature.Key); - continue; - } - projectId = parts[0]; - signature = parts[1]; - - var stacks = await _stackRepository.FindAsync(q => q.Project(projectId).FilterExpression($"signature_hash:{signature}")); - if (stacks.Documents.Count < 2) { - _logger.LogError("Did not find multiple stacks with signature {SignatureHash} and project {ProjectId}", signature, projectId); - continue; - } - - var eventCounts = await _eventRepository.CountAsync(q => q.Stack(stacks.Documents.Select(s => s.Id)).AggregationsExpression("terms:stack_id")); - var eventCountBuckets = eventCounts.Aggregations.Terms("terms_stack_id")?.Buckets ?? new List>(); - - // we only need to update events if more than one stack has events associated to it - bool shouldUpdateEvents = eventCountBuckets.Count > 1; - - // default to using the oldest stack - var targetStack = stacks.Documents.OrderBy(s => s.CreatedUtc).First(); - var duplicateStacks = stacks.Documents.OrderBy(s => s.CreatedUtc).Skip(1).ToList(); - - // use the stack that has the most events on it so we can reduce the number of updates - if (eventCountBuckets.Count > 0) { - string targetStackId = eventCountBuckets.OrderByDescending(b => b.Total).First().Key; - targetStack = stacks.Documents.Single(d => d.Id == targetStackId); - duplicateStacks = stacks.Documents.Where(d => d.Id != targetStackId).ToList(); - } - - targetStack.CreatedUtc = stacks.Documents.Min(d => d.CreatedUtc); - targetStack.Status = stacks.Documents.FirstOrDefault(d => d.Status != StackStatus.Open)?.Status ?? StackStatus.Open; - targetStack.LastOccurrence = stacks.Documents.Max(d => d.LastOccurrence); - targetStack.SnoozeUntilUtc = stacks.Documents.Max(d => d.SnoozeUntilUtc); - targetStack.DateFixed = stacks.Documents.Max(d => d.DateFixed); ; - targetStack.TotalOccurrences += duplicateStacks.Sum(d => d.TotalOccurrences); - targetStack.Tags.AddRange(duplicateStacks.SelectMany(d => d.Tags)); - targetStack.References = stacks.Documents.SelectMany(d => d.References).Distinct().ToList(); - targetStack.OccurrencesAreCritical = stacks.Documents.Any(d => d.OccurrencesAreCritical); - - duplicateStacks.ForEach(s => s.IsDeleted = true); - await _stackRepository.SaveAsync(duplicateStacks); - await _stackRepository.SaveAsync(targetStack); - processed++; - - long eventsToMove = eventCountBuckets.Where(b => b.Key != targetStack.Id).Sum(b => b.Total) ?? 0; - _logger.LogInformation("De-duped stack: Target={TargetId} Events={EventCount} Dupes={DuplicateIds} HasEvents={HasEvents}", targetStack.Id, eventsToMove, duplicateStacks.Select(s => s.Id), shouldUpdateEvents); - - if (shouldUpdateEvents) { - var response = await _elasticClient.UpdateByQueryAsync(u => u - .Query(q => q.Bool(b => b.Must(m => m - .Terms(t => t.Field(f => f.StackId).Terms(duplicateStacks.Select(s => s.Id))) - ))) - .Script(s => s.Source($"ctx._source.stack_id = '{targetStack.Id}'").Lang(ScriptLang.Painless)) - .Conflicts(Elasticsearch.Net.Conflicts.Proceed) - .WaitForCompletion(false)); - _logger.LogRequest(response, LogLevel.Trace); - - var taskStartedTime = SystemClock.Now; - var taskId = response.Task; - int attempts = 0; - long affectedRecords = 0; - do { - attempts++; - var taskStatus = await _elasticClient.Tasks.GetTaskAsync(taskId); - var status = taskStatus.Task.Status; - if (taskStatus.Completed) { - // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. - if (SystemClock.Now.Subtract(taskStartedTime) > TimeSpan.FromSeconds(30)) - _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - - affectedRecords += status.Created + status.Updated + status.Deleted; - break; - } - - if (SystemClock.Now.Subtract(taskStartedTime) > TimeSpan.FromSeconds(30)) { - await RenewLockAsync(context); - _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - } - - var delay = TimeSpan.FromMilliseconds(50); - if (attempts > 20) - delay = TimeSpan.FromSeconds(5); - else if (attempts > 10) - delay = TimeSpan.FromSeconds(1); - else if (attempts > 5) - delay = TimeSpan.FromMilliseconds(250); - - await Task.Delay(delay); - } while (true); - - _logger.LogInformation("Migrated stack events: Target={TargetId} Events={UpdatedEvents} Dupes={DuplicateIds}", targetStack.Id, affectedRecords, duplicateStacks.Select(s => s.Id)); - - totalUpdatedEventCount += affectedRecords; - } - - if (SystemClock.UtcNow.Subtract(lastStatus) > TimeSpan.FromSeconds(5)) { - lastStatus = SystemClock.UtcNow; - _logger.LogInformation("Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); - await _cacheClient.RemoveByPrefixAsync(nameof(Stack)); - } - } catch (Exception ex) { - error++; - _logger.LogError(ex, "Error fixing duplicate stack {ProjectId} {SignatureHash}", projectId, signature); + var stacks = await _stackRepository.FindAsync(q => q.Project(projectId).FilterExpression($"signature_hash:{signature}")); + if (stacks.Documents.Count < 2) { + _logger.LogError("Did not find multiple stacks with signature {SignatureHash} and project {ProjectId}", signature, projectId); + continue; } - } - await _elasticClient.Indices.RefreshAsync(_config.Stacks.VersionedName); - duplicateStackAgg = await _elasticClient.SearchAsync(q => q - .QueryOnQueryString("is_deleted:false") - .Size(0) - .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); - _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); + var eventCounts = await _eventRepository.CountAsync(q => q.Stack(stacks.Documents.Select(s => s.Id)).AggregationsExpression("terms:stack_id")); + var eventCountBuckets = eventCounts.Aggregations.Terms("terms_stack_id")?.Buckets ?? new List>(); - buckets = duplicateStackAgg.Aggregations.Terms("stacks").Buckets; - total += buckets.Count; - batch++; + // we only need to update events if more than one stack has events associated to it + bool shouldUpdateEvents = eventCountBuckets.Count > 1; - _logger.LogInformation("Done de-duping stacks: Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); - await _cacheClient.RemoveByPrefixAsync(nameof(Stack)); + // default to using the oldest stack + var targetStack = stacks.Documents.OrderBy(s => s.CreatedUtc).First(); + var duplicateStacks = stacks.Documents.OrderBy(s => s.CreatedUtc).Skip(1).ToList(); + + // use the stack that has the most events on it so we can reduce the number of updates + if (eventCountBuckets.Count > 0) { + string targetStackId = eventCountBuckets.OrderByDescending(b => b.Total).First().Key; + targetStack = stacks.Documents.Single(d => d.Id == targetStackId); + duplicateStacks = stacks.Documents.Where(d => d.Id != targetStackId).ToList(); + } + + targetStack.CreatedUtc = stacks.Documents.Min(d => d.CreatedUtc); + targetStack.Status = stacks.Documents.FirstOrDefault(d => d.Status != StackStatus.Open)?.Status ?? StackStatus.Open; + targetStack.LastOccurrence = stacks.Documents.Max(d => d.LastOccurrence); + targetStack.SnoozeUntilUtc = stacks.Documents.Max(d => d.SnoozeUntilUtc); + targetStack.DateFixed = stacks.Documents.Max(d => d.DateFixed); ; + targetStack.TotalOccurrences += duplicateStacks.Sum(d => d.TotalOccurrences); + targetStack.Tags.AddRange(duplicateStacks.SelectMany(d => d.Tags)); + targetStack.References = stacks.Documents.SelectMany(d => d.References).Distinct().ToList(); + targetStack.OccurrencesAreCritical = stacks.Documents.Any(d => d.OccurrencesAreCritical); + + duplicateStacks.ForEach(s => s.IsDeleted = true); + await _stackRepository.SaveAsync(duplicateStacks); + await _stackRepository.SaveAsync(targetStack); + processed++; + + long eventsToMove = eventCountBuckets.Where(b => b.Key != targetStack.Id).Sum(b => b.Total) ?? 0; + _logger.LogInformation("De-duped stack: Target={TargetId} Events={EventCount} Dupes={DuplicateIds} HasEvents={HasEvents}", targetStack.Id, eventsToMove, duplicateStacks.Select(s => s.Id), shouldUpdateEvents); + + if (shouldUpdateEvents) { + var response = await _elasticClient.UpdateByQueryAsync(u => u + .Query(q => q.Bool(b => b.Must(m => m + .Terms(t => t.Field(f => f.StackId).Terms(duplicateStacks.Select(s => s.Id))) + ))) + .Script(s => s.Source($"ctx._source.stack_id = '{targetStack.Id}'").Lang(ScriptLang.Painless)) + .Conflicts(Elasticsearch.Net.Conflicts.Proceed) + .WaitForCompletion(false)); + _logger.LogRequest(response, LogLevel.Trace); + + var taskStartedTime = SystemClock.Now; + var taskId = response.Task; + int attempts = 0; + long affectedRecords = 0; + do { + attempts++; + var taskStatus = await _elasticClient.Tasks.GetTaskAsync(taskId); + var status = taskStatus.Task.Status; + if (taskStatus.Completed) { + // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. + if (SystemClock.Now.Subtract(taskStartedTime) > TimeSpan.FromSeconds(30)) + _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + + affectedRecords += status.Created + status.Updated + status.Deleted; + break; + } + + if (SystemClock.Now.Subtract(taskStartedTime) > TimeSpan.FromSeconds(30)) { + await RenewLockAsync(context); + _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + } + + var delay = TimeSpan.FromMilliseconds(50); + if (attempts > 20) + delay = TimeSpan.FromSeconds(5); + else if (attempts > 10) + delay = TimeSpan.FromSeconds(1); + else if (attempts > 5) + delay = TimeSpan.FromMilliseconds(250); + + await Task.Delay(delay); + } while (true); + + _logger.LogInformation("Migrated stack events: Target={TargetId} Events={UpdatedEvents} Dupes={DuplicateIds}", targetStack.Id, affectedRecords, duplicateStacks.Select(s => s.Id)); + + totalUpdatedEventCount += affectedRecords; + } + + if (SystemClock.UtcNow.Subtract(lastStatus) > TimeSpan.FromSeconds(5)) { + lastStatus = SystemClock.UtcNow; + _logger.LogInformation("Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); + await _cacheClient.RemoveByPrefixAsync(nameof(Stack)); + } + } + catch (Exception ex) { + error++; + _logger.LogError(ex, "Error fixing duplicate stack {ProjectId} {SignatureHash}", projectId, signature); + } } - } - private Task RenewLockAsync(JobContext context) { - _lastRun = SystemClock.UtcNow; - return context.RenewLockAsync(); + await _elasticClient.Indices.RefreshAsync(_config.Stacks.VersionedName); + duplicateStackAgg = await _elasticClient.SearchAsync(q => q + .QueryOnQueryString("is_deleted:false") + .Size(0) + .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); + _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); + + buckets = duplicateStackAgg.Aggregations.Terms("stacks").Buckets; + total += buckets.Count; + batch++; + + _logger.LogInformation("Done de-duping stacks: Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); + await _cacheClient.RemoveByPrefixAsync(nameof(Stack)); } + } + + private Task RenewLockAsync(JobContext context) { + _lastRun = SystemClock.UtcNow; + return context.RenewLockAsync(); + } - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); - } + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs index 4c3f6c2885..956d53545a 100644 --- a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs +++ b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; @@ -15,131 +11,131 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Closes inactive user sessions.", InitialDelay = "30s", Interval = "30s")] - public class CloseInactiveSessionsJob : JobWithLockBase, IHealthCheck { - private readonly IEventRepository _eventRepository; - private readonly ICacheClient _cache; - private readonly ILockProvider _lockProvider; - private DateTime? _lastActivity; - - public CloseInactiveSessionsJob(IEventRepository eventRepository, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _eventRepository = eventRepository; - _cache = cacheClient; - _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromMinutes(1)); - } +namespace Exceptionless.Core.Jobs; - protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); - } +[Job(Description = "Closes inactive user sessions.", InitialDelay = "30s", Interval = "30s")] +public class CloseInactiveSessionsJob : JobWithLockBase, IHealthCheck { + private readonly IEventRepository _eventRepository; + private readonly ICacheClient _cache; + private readonly ILockProvider _lockProvider; + private DateTime? _lastActivity; + + public CloseInactiveSessionsJob(IEventRepository eventRepository, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _eventRepository = eventRepository; + _cache = cacheClient; + _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromMinutes(1)); + } - protected override async Task RunInternalAsync(JobContext context) { - _lastActivity = SystemClock.UtcNow; - var results = await _eventRepository.GetOpenSessionsAsync(SystemClock.UtcNow.SubtractMinutes(1), o => o.SearchAfterPaging().PageLimit(100)).AnyContext(); - int sessionsClosed = 0; - int totalSessions = 0; - if (results.Documents.Count == 0) - return JobResult.Success; - - while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - var inactivePeriodUtc = SystemClock.UtcNow.Subtract(DefaultInactivePeriod); - var sessionsToUpdate = new List(results.Documents.Count); - var cacheKeysToRemove = new List(results.Documents.Count * 2); - var existingSessionHeartbeatIds = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var sessionStart in results.Documents) { - var lastActivityUtc = sessionStart.Date.UtcDateTime.AddSeconds((double)sessionStart.Value.GetValueOrDefault()); - var heartbeatResult = await GetHeartbeatAsync(sessionStart).AnyContext(); - - bool closeDuplicate = heartbeatResult?.CacheKey != null && existingSessionHeartbeatIds.Contains(heartbeatResult.CacheKey); - if (heartbeatResult?.CacheKey != null && !closeDuplicate) - existingSessionHeartbeatIds.Add(heartbeatResult.CacheKey); - - if (heartbeatResult != null && (closeDuplicate || heartbeatResult.Close || heartbeatResult.ActivityUtc > lastActivityUtc)) - sessionStart.UpdateSessionStart(heartbeatResult.ActivityUtc, isSessionEnd: closeDuplicate || heartbeatResult.Close || heartbeatResult.ActivityUtc <= inactivePeriodUtc); - else if (lastActivityUtc <= inactivePeriodUtc) - sessionStart.UpdateSessionStart(lastActivityUtc, isSessionEnd: true); - else - continue; - - sessionsToUpdate.Add(sessionStart); - if (heartbeatResult != null) { - cacheKeysToRemove.Add(heartbeatResult.CacheKey); - if (heartbeatResult.Close) - cacheKeysToRemove.Add(heartbeatResult.CacheKey + "-close"); - } - - Debug.Assert(sessionStart.Value != null && sessionStart.Value >= 0, "Session start value cannot be a negative number."); + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { + return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + } + + protected override async Task RunInternalAsync(JobContext context) { + _lastActivity = SystemClock.UtcNow; + var results = await _eventRepository.GetOpenSessionsAsync(SystemClock.UtcNow.SubtractMinutes(1), o => o.SearchAfterPaging().PageLimit(100)).AnyContext(); + int sessionsClosed = 0; + int totalSessions = 0; + if (results.Documents.Count == 0) + return JobResult.Success; + + while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + var inactivePeriodUtc = SystemClock.UtcNow.Subtract(DefaultInactivePeriod); + var sessionsToUpdate = new List(results.Documents.Count); + var cacheKeysToRemove = new List(results.Documents.Count * 2); + var existingSessionHeartbeatIds = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var sessionStart in results.Documents) { + var lastActivityUtc = sessionStart.Date.UtcDateTime.AddSeconds((double)sessionStart.Value.GetValueOrDefault()); + var heartbeatResult = await GetHeartbeatAsync(sessionStart).AnyContext(); + + bool closeDuplicate = heartbeatResult?.CacheKey != null && existingSessionHeartbeatIds.Contains(heartbeatResult.CacheKey); + if (heartbeatResult?.CacheKey != null && !closeDuplicate) + existingSessionHeartbeatIds.Add(heartbeatResult.CacheKey); + + if (heartbeatResult != null && (closeDuplicate || heartbeatResult.Close || heartbeatResult.ActivityUtc > lastActivityUtc)) + sessionStart.UpdateSessionStart(heartbeatResult.ActivityUtc, isSessionEnd: closeDuplicate || heartbeatResult.Close || heartbeatResult.ActivityUtc <= inactivePeriodUtc); + else if (lastActivityUtc <= inactivePeriodUtc) + sessionStart.UpdateSessionStart(lastActivityUtc, isSessionEnd: true); + else + continue; + + sessionsToUpdate.Add(sessionStart); + if (heartbeatResult != null) { + cacheKeysToRemove.Add(heartbeatResult.CacheKey); + if (heartbeatResult.Close) + cacheKeysToRemove.Add(heartbeatResult.CacheKey + "-close"); } - totalSessions += results.Documents.Count; - sessionsClosed += sessionsToUpdate.Count; + Debug.Assert(sessionStart.Value != null && sessionStart.Value >= 0, "Session start value cannot be a negative number."); + } - if (sessionsToUpdate.Count > 0) - await _eventRepository.SaveAsync(sessionsToUpdate).AnyContext(); + totalSessions += results.Documents.Count; + sessionsClosed += sessionsToUpdate.Count; - if (cacheKeysToRemove.Count > 0) - await _cache.RemoveAllAsync(cacheKeysToRemove).AnyContext(); + if (sessionsToUpdate.Count > 0) + await _eventRepository.SaveAsync(sessionsToUpdate).AnyContext(); - _logger.LogInformation("Closing {SessionClosedCount} of {SessionCount} sessions", sessionsToUpdate.Count, results.Documents.Count); + if (cacheKeysToRemove.Count > 0) + await _cache.RemoveAllAsync(cacheKeysToRemove).AnyContext(); - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + _logger.LogInformation("Closing {SessionClosedCount} of {SessionCount} sessions", sessionsToUpdate.Count, results.Documents.Count); - if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) - break; + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); - if (results.Documents.Count > 0) { - await context.RenewLockAsync().AnyContext(); - _lastActivity = SystemClock.UtcNow; - } + if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) + break; + + if (results.Documents.Count > 0) { + await context.RenewLockAsync().AnyContext(); + _lastActivity = SystemClock.UtcNow; } - _logger.LogInformation("Done checking active sessions. Closed {SessionClosedCount} of {SessionCount} sessions", sessionsClosed, totalSessions); + } + _logger.LogInformation("Done checking active sessions. Closed {SessionClosedCount} of {SessionCount} sessions", sessionsClosed, totalSessions); - return JobResult.Success; + return JobResult.Success; + } + + private async Task GetHeartbeatAsync(PersistentEvent sessionStart) { + string sessionId = sessionStart.GetSessionId(); + if (!String.IsNullOrWhiteSpace(sessionId)) { + var result = await GetLastHeartbeatActivityUtcAsync($"Project:{sessionStart.ProjectId}:heartbeat:{sessionId.ToSHA1()}").AnyContext(); + if (result != null) + return result; } - private async Task GetHeartbeatAsync(PersistentEvent sessionStart) { - string sessionId = sessionStart.GetSessionId(); - if (!String.IsNullOrWhiteSpace(sessionId)) { - var result = await GetLastHeartbeatActivityUtcAsync($"Project:{sessionStart.ProjectId}:heartbeat:{sessionId.ToSHA1()}").AnyContext(); - if (result != null) - return result; - } + var user = sessionStart.GetUserIdentity(); + if (String.IsNullOrWhiteSpace(user?.Identity)) + return null; - var user = sessionStart.GetUserIdentity(); - if (String.IsNullOrWhiteSpace(user?.Identity)) - return null; + return await GetLastHeartbeatActivityUtcAsync($"Project:{sessionStart.ProjectId}:heartbeat:{user.Identity.ToSHA1()}").AnyContext(); + } - return await GetLastHeartbeatActivityUtcAsync($"Project:{sessionStart.ProjectId}:heartbeat:{user.Identity.ToSHA1()}").AnyContext(); + private async Task GetLastHeartbeatActivityUtcAsync(string cacheKey) { + var cacheValue = await _cache.GetAsync(cacheKey).AnyContext(); + if (cacheValue.HasValue) { + bool close = await _cache.GetAsync(cacheKey + "-close", false).AnyContext(); + return new HeartbeatResult { ActivityUtc = cacheValue.Value, Close = close, CacheKey = cacheKey }; } - private async Task GetLastHeartbeatActivityUtcAsync(string cacheKey) { - var cacheValue = await _cache.GetAsync(cacheKey).AnyContext(); - if (cacheValue.HasValue) { - bool close = await _cache.GetAsync(cacheKey + "-close", false).AnyContext(); - return new HeartbeatResult { ActivityUtc = cacheValue.Value, Close = close, CacheKey = cacheKey }; - } + return null; + } - return null; - } - - public TimeSpan DefaultInactivePeriod { get; set; } = TimeSpan.FromMinutes(5); - - private class HeartbeatResult { - public DateTime ActivityUtc { get; set; } - public string CacheKey { get; set; } - public bool Close { get; set; } - } + public TimeSpan DefaultInactivePeriod { get; set; } = TimeSpan.FromMinutes(5); - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastActivity.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + private class HeartbeatResult { + public DateTime ActivityUtc { get; set; } + public string CacheKey { get; set; } + public bool Close { get; set; } + } - if (SystemClock.UtcNow.Subtract(_lastActivity.Value) > TimeSpan.FromMinutes(5)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has no activity in the last 5 minutes.")); + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastActivity.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); - return Task.FromResult(HealthCheckResult.Healthy("Job has no activity in the last 5 minutes.")); - } + if (SystemClock.UtcNow.Subtract(_lastActivity.Value) > TimeSpan.FromMinutes(5)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has no activity in the last 5 minutes.")); + + return Task.FromResult(HealthCheckResult.Healthy("Job has no activity in the last 5 minutes.")); } } diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index c15af2d991..4db36c3e48 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -1,16 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; using Exceptionless.Core.Mail; using Exceptionless.Core.Models; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; -using Exceptionless.Core.Repositories.Configuration; using Exceptionless.Core.Repositories.Queries; using Exceptionless.DateTimeExtensions; using Foundatio.Caching; @@ -22,163 +16,164 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Sends daily summary emails.", InitialDelay = "1m", Interval = "1h")] - public class DailySummaryJob : JobWithLockBase, IHealthCheck { - private readonly EmailOptions _emailOptions; - private readonly IProjectRepository _projectRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IUserRepository _userRepository; - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly IMailer _mailer; - private readonly BillingPlans _plans; - private readonly ILockProvider _lockProvider; - private DateTime? _lastRun; - - public DailySummaryJob(EmailOptions emailOptions, IProjectRepository projectRepository, IOrganizationRepository organizationRepository, IUserRepository userRepository, IStackRepository stackRepository, IEventRepository eventRepository, IMailer mailer, ICacheClient cacheClient, BillingPlans plans, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _emailOptions = emailOptions; - _projectRepository = projectRepository; - _organizationRepository = organizationRepository; - _userRepository = userRepository; - _stackRepository = stackRepository; - _eventRepository = eventRepository; - _mailer = mailer; - _plans = plans; - _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromHours(1)); - } +namespace Exceptionless.Core.Jobs; + +[Job(Description = "Sends daily summary emails.", InitialDelay = "1m", Interval = "1h")] +public class DailySummaryJob : JobWithLockBase, IHealthCheck { + private readonly EmailOptions _emailOptions; + private readonly IProjectRepository _projectRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IUserRepository _userRepository; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly IMailer _mailer; + private readonly BillingPlans _plans; + private readonly ILockProvider _lockProvider; + private DateTime? _lastRun; + + public DailySummaryJob(EmailOptions emailOptions, IProjectRepository projectRepository, IOrganizationRepository organizationRepository, IUserRepository userRepository, IStackRepository stackRepository, IEventRepository eventRepository, IMailer mailer, ICacheClient cacheClient, BillingPlans plans, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _emailOptions = emailOptions; + _projectRepository = projectRepository; + _organizationRepository = organizationRepository; + _userRepository = userRepository; + _stackRepository = stackRepository; + _eventRepository = eventRepository; + _mailer = mailer; + _plans = plans; + _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromHours(1)); + } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), new CancellationToken(true)); - } + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { + return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), new CancellationToken(true)); + } - protected override async Task RunInternalAsync(JobContext context) { - _lastRun = SystemClock.UtcNow; - - if (!_emailOptions.EnableDailySummary || _mailer == null) - return JobResult.SuccessWithMessage("Summary notifications are disabled."); - - var results = await _projectRepository.GetByNextSummaryNotificationOffsetAsync(9).AnyContext(); - while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - _logger.LogTrace("Got {Count} projects to process. ", results.Documents.Count); - - var projectsToBulkUpdate = new List(results.Documents.Count); - var processSummariesNewerThan = SystemClock.UtcNow.Date.SubtractDays(2); - foreach (var project in results.Documents) { - using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id))) { - var utcStartTime = new DateTime(project.NextSummaryEndOfDayTicks - TimeSpan.TicksPerDay); - if (utcStartTime < processSummariesNewerThan) { - _logger.LogInformation("Skipping daily summary older than two days for project: {Name}", project.Name); - projectsToBulkUpdate.Add(project); - continue; - } - - var notification = new SummaryNotification { - Id = project.Id, - UtcStartTime = utcStartTime, - UtcEndTime = new DateTime(project.NextSummaryEndOfDayTicks - TimeSpan.TicksPerSecond) - }; - - bool summarySent = await SendSummaryNotificationAsync(project, notification).AnyContext(); - if (summarySent) { - await _projectRepository.IncrementNextSummaryEndOfDayTicksAsync(new[] { project }).AnyContext(); - - // Sleep so we are not hammering the backend as we just generated a report. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); - } else { - projectsToBulkUpdate.Add(project); - } + protected override async Task RunInternalAsync(JobContext context) { + _lastRun = SystemClock.UtcNow; + + if (!_emailOptions.EnableDailySummary || _mailer == null) + return JobResult.SuccessWithMessage("Summary notifications are disabled."); + + var results = await _projectRepository.GetByNextSummaryNotificationOffsetAsync(9).AnyContext(); + while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + _logger.LogTrace("Got {Count} projects to process. ", results.Documents.Count); + + var projectsToBulkUpdate = new List(results.Documents.Count); + var processSummariesNewerThan = SystemClock.UtcNow.Date.SubtractDays(2); + foreach (var project in results.Documents) { + using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id))) { + var utcStartTime = new DateTime(project.NextSummaryEndOfDayTicks - TimeSpan.TicksPerDay); + if (utcStartTime < processSummariesNewerThan) { + _logger.LogInformation("Skipping daily summary older than two days for project: {Name}", project.Name); + projectsToBulkUpdate.Add(project); + continue; } - } - if (projectsToBulkUpdate.Count > 0) { - await _projectRepository.IncrementNextSummaryEndOfDayTicksAsync(projectsToBulkUpdate).AnyContext(); + var notification = new SummaryNotification { + Id = project.Id, + UtcStartTime = utcStartTime, + UtcEndTime = new DateTime(project.NextSummaryEndOfDayTicks - TimeSpan.TicksPerSecond) + }; - // Sleep so we are not hammering the backend - await SystemClock.SleepAsync(TimeSpan.FromSeconds(1)).AnyContext(); - } + bool summarySent = await SendSummaryNotificationAsync(project, notification).AnyContext(); + if (summarySent) { + await _projectRepository.IncrementNextSummaryEndOfDayTicksAsync(new[] { project }).AnyContext(); - if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) - break; - - if (results.Documents.Count > 0) { - await context.RenewLockAsync().AnyContext(); - _lastRun = SystemClock.UtcNow; + // Sleep so we are not hammering the backend as we just generated a report. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + } + else { + projectsToBulkUpdate.Add(project); + } } } - return JobResult.SuccessWithMessage("Successfully sent summary notifications."); - } + if (projectsToBulkUpdate.Count > 0) { + await _projectRepository.IncrementNextSummaryEndOfDayTicksAsync(projectsToBulkUpdate).AnyContext(); - private async Task SendSummaryNotificationAsync(Project project, SummaryNotification data) { - // TODO: Add slack daily summaries - var userIds = project.NotificationSettings.Where(n => n.Value.SendDailySummary && !String.Equals(n.Key, Project.NotificationIntegrations.Slack)).Select(n => n.Key).ToList(); - if (userIds.Count == 0) { - _logger.LogInformation("Project {ProjectName} has no users to send summary to.", project.Name); - return false; + // Sleep so we are not hammering the backend + await SystemClock.SleepAsync(TimeSpan.FromSeconds(1)).AnyContext(); } - var results = await _userRepository.GetByIdsAsync(userIds, o => o.Cache()).AnyContext(); - var users = results.Where(u => u.IsEmailAddressVerified && u.EmailNotificationsEnabled && u.OrganizationIds.Contains(project.OrganizationId)).ToList(); - if (users.Count == 0) { - _logger.LogInformation("Project {ProjectName} has no users to send summary to.", project.Name); - return false; - } + if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) + break; - // TODO: What should we do about suspended organizations. - var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()).AnyContext(); - if (organization == null) { - _logger.LogInformation("The organization {organization} for project {ProjectName} may have been deleted. No summaries will be sent.", project.OrganizationId, project.Name); - return false; + if (results.Documents.Count > 0) { + await context.RenewLockAsync().AnyContext(); + _lastRun = SystemClock.UtcNow; } + } - _logger.LogInformation("Sending daily summary: users={UserCount} project={project}", users.Count, project.Id); - var sf = new AppFilter(project, organization); - var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(data.UtcStartTime, data.UtcEndTime, (PersistentEvent e) => e.Date).Index(data.UtcStartTime, data.UtcEndTime); - string filter = "type:error (status:open OR status:regressed)"; - var result = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression("terms:(first @include:true) terms:(stack_id~3) cardinality:stack_id sum:count~1")).AnyContext(); - - double total = result.Aggregations.Sum("sum_count")?.Value ?? result.Total; - double newTotal = result.Aggregations.Terms("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0; - double uniqueTotal = result.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0; - bool hasSubmittedEvents = total > 0 || project.IsConfigured.GetValueOrDefault(); - bool isFreePlan = organization.PlanId == _plans.FreePlan.Id; - - string fixedFilter = "type:error status:fixed"; - var fixedResult = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(fixedFilter).EnforceEventStackFilter().AggregationsExpression("sum:count~1")).AnyContext(); - double fixedTotal = fixedResult.Aggregations.Sum("sum_count")?.Value ?? fixedResult.Total; - - var range = new DateTimeRange(data.UtcStartTime, data.UtcEndTime); - var usages = project.OverageHours.Where(u => range.Contains(u.Date)).ToList(); - int blockedTotal = usages.Sum(u => u.Blocked); - int tooBigTotal = usages.Sum(u => u.TooBig); - - IReadOnlyCollection mostFrequent = null; - var stackTerms = result.Aggregations.Terms("terms_stack_id"); - if (stackTerms?.Buckets.Count > 0) - mostFrequent = await _stackRepository.GetByIdsAsync(stackTerms.Buckets.Select(b => b.Key).ToArray()).AnyContext(); - - IReadOnlyCollection newest = null; - if (newTotal > 0) - newest = (await _stackRepository.FindAsync(q => q.AppFilter(sf).FilterExpression(filter).SortExpression("-first").DateRange(data.UtcStartTime, data.UtcEndTime, "first"), o => o.PageLimit(3)).AnyContext()).Documents; - - foreach (var user in users) { - _logger.LogInformation("Queuing {ProjectName} daily summary email ({UtcStartTime}-{UtcEndTime}) for user {EmailAddress}.", project.Name, data.UtcStartTime, data.UtcEndTime, user.EmailAddress); - await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, newest, data.UtcStartTime, hasSubmittedEvents, total, uniqueTotal, newTotal, fixedTotal, blockedTotal, tooBigTotal, isFreePlan).AnyContext(); - } + return JobResult.SuccessWithMessage("Successfully sent summary notifications."); + } - _logger.LogInformation("Done sending daily summary: users={UserCount} project={ProjectName} events={EventCount}", users.Count, project.Name, total); - return true; + private async Task SendSummaryNotificationAsync(Project project, SummaryNotification data) { + // TODO: Add slack daily summaries + var userIds = project.NotificationSettings.Where(n => n.Value.SendDailySummary && !String.Equals(n.Key, Project.NotificationIntegrations.Slack)).Select(n => n.Key).ToList(); + if (userIds.Count == 0) { + _logger.LogInformation("Project {ProjectName} has no users to send summary to.", project.Name); + return false; } - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + var results = await _userRepository.GetByIdsAsync(userIds, o => o.Cache()).AnyContext(); + var users = results.Where(u => u.IsEmailAddressVerified && u.EmailNotificationsEnabled && u.OrganizationIds.Contains(project.OrganizationId)).ToList(); + if (users.Count == 0) { + _logger.LogInformation("Project {ProjectName} has no users to send summary to.", project.Name); + return false; + } - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); + // TODO: What should we do about suspended organizations. + var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()).AnyContext(); + if (organization == null) { + _logger.LogInformation("The organization {organization} for project {ProjectName} may have been deleted. No summaries will be sent.", project.OrganizationId, project.Name); + return false; + } - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); + _logger.LogInformation("Sending daily summary: users={UserCount} project={project}", users.Count, project.Id); + var sf = new AppFilter(project, organization); + var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(data.UtcStartTime, data.UtcEndTime, (PersistentEvent e) => e.Date).Index(data.UtcStartTime, data.UtcEndTime); + string filter = "type:error (status:open OR status:regressed)"; + var result = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression("terms:(first @include:true) terms:(stack_id~3) cardinality:stack_id sum:count~1")).AnyContext(); + + double total = result.Aggregations.Sum("sum_count")?.Value ?? result.Total; + double newTotal = result.Aggregations.Terms("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0; + double uniqueTotal = result.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0; + bool hasSubmittedEvents = total > 0 || project.IsConfigured.GetValueOrDefault(); + bool isFreePlan = organization.PlanId == _plans.FreePlan.Id; + + string fixedFilter = "type:error status:fixed"; + var fixedResult = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(fixedFilter).EnforceEventStackFilter().AggregationsExpression("sum:count~1")).AnyContext(); + double fixedTotal = fixedResult.Aggregations.Sum("sum_count")?.Value ?? fixedResult.Total; + + var range = new DateTimeRange(data.UtcStartTime, data.UtcEndTime); + var usages = project.OverageHours.Where(u => range.Contains(u.Date)).ToList(); + int blockedTotal = usages.Sum(u => u.Blocked); + int tooBigTotal = usages.Sum(u => u.TooBig); + + IReadOnlyCollection mostFrequent = null; + var stackTerms = result.Aggregations.Terms("terms_stack_id"); + if (stackTerms?.Buckets.Count > 0) + mostFrequent = await _stackRepository.GetByIdsAsync(stackTerms.Buckets.Select(b => b.Key).ToArray()).AnyContext(); + + IReadOnlyCollection newest = null; + if (newTotal > 0) + newest = (await _stackRepository.FindAsync(q => q.AppFilter(sf).FilterExpression(filter).SortExpression("-first").DateRange(data.UtcStartTime, data.UtcEndTime, "first"), o => o.PageLimit(3)).AnyContext()).Documents; + + foreach (var user in users) { + _logger.LogInformation("Queuing {ProjectName} daily summary email ({UtcStartTime}-{UtcEndTime}) for user {EmailAddress}.", project.Name, data.UtcStartTime, data.UtcEndTime, user.EmailAddress); + await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, newest, data.UtcStartTime, hasSubmittedEvents, total, uniqueTotal, newTotal, fixedTotal, blockedTotal, tooBigTotal, isFreePlan).AnyContext(); } + + _logger.LogInformation("Done sending daily summary: users={UserCount} project={ProjectName} events={EventCount}", users.Count, project.Name, total); + return true; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); + + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); } } diff --git a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs index f01ada63ff..535bb680da 100644 --- a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs +++ b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs @@ -1,8 +1,4 @@ -using System; -using System.IO.Compression; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using System.IO.Compression; using Exceptionless.Core.Extensions; using Exceptionless.DateTimeExtensions; using Foundatio.Caching; @@ -13,68 +9,69 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Downloads Geo IP database.", IsContinuous = false)] - public class DownloadGeoIPDatabaseJob : JobWithLockBase, IHealthCheck { - public const string GEO_IP_DATABASE_PATH = "GeoLite2-City.mmdb"; - private readonly AppOptions _options; - private readonly IFileStorage _storage; - private readonly ILockProvider _lockProvider; - private DateTime? _lastRun; +namespace Exceptionless.Core.Jobs; - public DownloadGeoIPDatabaseJob(AppOptions options, ICacheClient cacheClient, IFileStorage storage, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _options = options; - _storage = storage; - _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromDays(1)); - } +[Job(Description = "Downloads Geo IP database.", IsContinuous = false)] +public class DownloadGeoIPDatabaseJob : JobWithLockBase, IHealthCheck { + public const string GEO_IP_DATABASE_PATH = "GeoLite2-City.mmdb"; + private readonly AppOptions _options; + private readonly IFileStorage _storage; + private readonly ILockProvider _lockProvider; + private DateTime? _lastRun; - protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), new CancellationToken(true)); - } + public DownloadGeoIPDatabaseJob(AppOptions options, ICacheClient cacheClient, IFileStorage storage, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _options = options; + _storage = storage; + _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromDays(1)); + } - protected override async Task RunInternalAsync(JobContext context) { - _lastRun = SystemClock.UtcNow; + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { + return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), new CancellationToken(true)); + } - string licenseKey = _options.MaxMindGeoIpKey; - if (String.IsNullOrEmpty(licenseKey)) { - _logger.LogInformation("Configure {SettingKey} to download GeoIP database.", nameof(AppOptions.MaxMindGeoIpKey)); - return JobResult.Success; - } - - try { - var fi = await _storage.GetFileInfoAsync(GEO_IP_DATABASE_PATH).AnyContext(); - if (fi != null && fi.Modified.IsAfter(SystemClock.UtcNow.StartOfDay())) { - _logger.LogInformation("The GeoIP database is already up-to-date."); - return JobResult.Success; - } + protected override async Task RunInternalAsync(JobContext context) { + _lastRun = SystemClock.UtcNow; - _logger.LogInformation("Downloading GeoIP database."); - var client = new HttpClient(); - string url = $"https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key={licenseKey}&suffix=tar.gz"; - var file = await client.GetAsync(url, context.CancellationToken).AnyContext(); - if (!file.IsSuccessStatusCode) - return JobResult.FailedWithMessage("Unable to download GeoIP database."); + string licenseKey = _options.MaxMindGeoIpKey; + if (String.IsNullOrEmpty(licenseKey)) { + _logger.LogInformation("Configure {SettingKey} to download GeoIP database.", nameof(AppOptions.MaxMindGeoIpKey)); + return JobResult.Success; + } - _logger.LogInformation("Extracting GeoIP database"); - using (var decompressionStream = new GZipStream(await file.Content.ReadAsStreamAsync().AnyContext(), CompressionMode.Decompress)) - await _storage.SaveFileAsync(GEO_IP_DATABASE_PATH, decompressionStream, context.CancellationToken).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "An error occurred while downloading the GeoIP database."); - return JobResult.FromException(ex); + try { + var fi = await _storage.GetFileInfoAsync(GEO_IP_DATABASE_PATH).AnyContext(); + if (fi != null && fi.Modified.IsAfter(SystemClock.UtcNow.StartOfDay())) { + _logger.LogInformation("The GeoIP database is already up-to-date."); + return JobResult.Success; } - _logger.LogInformation("Finished downloading GeoIP database."); - return JobResult.Success; + _logger.LogInformation("Downloading GeoIP database."); + var client = new HttpClient(); + string url = $"https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key={licenseKey}&suffix=tar.gz"; + var file = await client.GetAsync(url, context.CancellationToken).AnyContext(); + if (!file.IsSuccessStatusCode) + return JobResult.FailedWithMessage("Unable to download GeoIP database."); + + _logger.LogInformation("Extracting GeoIP database"); + using (var decompressionStream = new GZipStream(await file.Content.ReadAsStreamAsync().AnyContext(), CompressionMode.Decompress)) + await _storage.SaveFileAsync(GEO_IP_DATABASE_PATH, decompressionStream, context.CancellationToken).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "An error occurred while downloading the GeoIP database."); + return JobResult.FromException(ex); } - - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromHours(25)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 25 hours.")); + _logger.LogInformation("Finished downloading GeoIP database."); + return JobResult.Success; + } - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 25 hours.")); - } + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromHours(25)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 25 hours.")); + + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 25 hours.")); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs b/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs index 86de84db15..62ad437f95 100644 --- a/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs +++ b/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Elasticsearch.Net; using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories.Configuration; @@ -15,224 +11,226 @@ using Microsoft.Extensions.Logging; using Nest; -namespace Exceptionless.Core.Jobs.Elastic { - [Job(Description = "Migrate data to new format.", IsContinuous = false)] - public class DataMigrationJob : JobBase { - private readonly ExceptionlessElasticConfiguration _configuration; - private const string MIGRATE_VERSION_SCRIPT = "if (ctx._source.version instanceof String == false) { ctx._source.version = 'v' + ctx._source.version.major; }"; - - public DataMigrationJob( - ExceptionlessElasticConfiguration configuration, - ILoggerFactory loggerFactory - ) : base(loggerFactory) { - _configuration = configuration; - } - - protected override async Task RunInternalAsync(JobContext context) { - var elasticOptions = _configuration.Options; - if (elasticOptions.ElasticsearchToMigrate == null) - return JobResult.CancelledWithMessage($"Please configure the connection string EX_{nameof(elasticOptions.ElasticsearchToMigrate)}."); - - var retentionPeriod = _configuration.Events.MaxIndexAge.GetValueOrDefault(TimeSpan.FromDays(180)); - string sourceScope = elasticOptions.ElasticsearchToMigrate.Scope; - string scope = elasticOptions.ScopePrefix; - var cutOffDate = elasticOptions.ReindexCutOffDate; - - var client = _configuration.Client; - await _configuration.ConfigureIndexesAsync().AnyContext(); - - var workItemQueue = new Queue(); - workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "organization", $"{scope}organizations-v1", "updated_utc")); - workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "project", $"{scope}projects-v1", "updated_utc")); - workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "token", $"{scope}tokens-v1", "updated_utc")); - workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "user", $"{scope}users-v1", "updated_utc")); - workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "webhook", $"{scope}webhooks-v1", "created_utc", script: MIGRATE_VERSION_SCRIPT)); - workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}stacks-v1", "stacks", $"{scope}stacks-v1", "last_occurrence")); - - // create the new indexes, don't migrate yet - foreach (var index in _configuration.Indexes.OfType()) { - for (int day = 0; day <= retentionPeriod.Days; day++) { - var date = day == 0 ? SystemClock.UtcNow : SystemClock.UtcNow.SubtractDays(day); - string indexToCreate = $"{scope}events-v1-{date:yyyy.MM.dd}"; - workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}events-v1-{date:yyyy.MM.dd}", "events", indexToCreate, "updated_utc", () => index.EnsureIndexAsync(date))); - } - } +namespace Exceptionless.Core.Jobs.Elastic; - // Reset the alias cache - var aliasCache = new ScopedCacheClient(_configuration.Cache, "alias"); - await aliasCache.RemoveAllAsync().AnyContext(); - - var started = SystemClock.UtcNow; - var lastProgress = SystemClock.UtcNow; - int retriesCount = 0; - int totalTasks = workItemQueue.Count; - var workingTasks = new List(); - var completedTasks = new List(); - var failedTasks = new List(); - while (true) { - if (workingTasks.Count == 0 && workItemQueue.Count == 0) - break; - - if (workingTasks.Count < 10 && workItemQueue.TryDequeue(out var dequeuedWorkItem)) { - if (dequeuedWorkItem.CreateIndex != null) { - try { - await dequeuedWorkItem.CreateIndex().AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Failed to create index for {TargetIndex}", dequeuedWorkItem.TargetIndex); - continue; - } - } +[Job(Description = "Migrate data to new format.", IsContinuous = false)] +public class DataMigrationJob : JobBase { + private readonly ExceptionlessElasticConfiguration _configuration; + private const string MIGRATE_VERSION_SCRIPT = "if (ctx._source.version instanceof String == false) { ctx._source.version = 'v' + ctx._source.version.major; }"; - int batchSize = 1000; - if (dequeuedWorkItem.Attempts == 1) - batchSize = 500; - else if (dequeuedWorkItem.Attempts >= 2) - batchSize = 250; - - var response = await client.ReindexOnServerAsync(r => r - .Source(s => s - .Remote(ConfigureRemoteElasticSource) - .Index(dequeuedWorkItem.SourceIndex) - .Size(batchSize) - .Query(q => { - var container = q.Term("_type", dequeuedWorkItem.SourceIndexType); - if (!String.IsNullOrEmpty(dequeuedWorkItem.DateField)) - container &= q.DateRange(d => d.Field(dequeuedWorkItem.DateField).GreaterThanOrEquals(cutOffDate)); - - return container; - })) - .Destination(d => d - .Index(dequeuedWorkItem.TargetIndex)) - .Conflicts(Conflicts.Proceed) - .WaitForCompletion(false) - .Script(s => { - if (!String.IsNullOrEmpty(dequeuedWorkItem.Script)) - return s.Source(dequeuedWorkItem.Script); - - return null; - })).AnyContext(); - - dequeuedWorkItem.Attempts += 1; - dequeuedWorkItem.TaskId = response.Task; - workingTasks.Add(dequeuedWorkItem); - - _logger.LogInformation("STARTED - {TargetIndex} A:{Attempts} ({TaskId})...", dequeuedWorkItem.TargetIndex, dequeuedWorkItem.Attempts, dequeuedWorkItem.TaskId); - - continue; - } - - double highestProgress = 0; - foreach (var workItem in workingTasks.ToArray()) { - var taskStatus = await client.Tasks.GetTaskAsync(workItem.TaskId, t => t.WaitForCompletion(false)).AnyContext(); - _logger.LogRequest(taskStatus); + public DataMigrationJob( + ExceptionlessElasticConfiguration configuration, + ILoggerFactory loggerFactory + ) : base(loggerFactory) { + _configuration = configuration; + } - var status = taskStatus?.Task?.Status; - if (status == null) { - _logger.LogWarning(taskStatus?.OriginalException, "Error getting task status for {TargetIndex} {TaskId}: {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus.GetErrorMessage()); - if (taskStatus?.ServerError?.Status == 429) - await Task.Delay(TimeSpan.FromSeconds(1)); + protected override async Task RunInternalAsync(JobContext context) { + var elasticOptions = _configuration.Options; + if (elasticOptions.ElasticsearchToMigrate == null) + return JobResult.CancelledWithMessage($"Please configure the connection string EX_{nameof(elasticOptions.ElasticsearchToMigrate)}."); + + var retentionPeriod = _configuration.Events.MaxIndexAge.GetValueOrDefault(TimeSpan.FromDays(180)); + string sourceScope = elasticOptions.ElasticsearchToMigrate.Scope; + string scope = elasticOptions.ScopePrefix; + var cutOffDate = elasticOptions.ReindexCutOffDate; + + var client = _configuration.Client; + await _configuration.ConfigureIndexesAsync().AnyContext(); + + var workItemQueue = new Queue(); + workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "organization", $"{scope}organizations-v1", "updated_utc")); + workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "project", $"{scope}projects-v1", "updated_utc")); + workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "token", $"{scope}tokens-v1", "updated_utc")); + workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "user", $"{scope}users-v1", "updated_utc")); + workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}organizations-v1", "webhook", $"{scope}webhooks-v1", "created_utc", script: MIGRATE_VERSION_SCRIPT)); + workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}stacks-v1", "stacks", $"{scope}stacks-v1", "last_occurrence")); + + // create the new indexes, don't migrate yet + foreach (var index in _configuration.Indexes.OfType()) { + for (int day = 0; day <= retentionPeriod.Days; day++) { + var date = day == 0 ? SystemClock.UtcNow : SystemClock.UtcNow.SubtractDays(day); + string indexToCreate = $"{scope}events-v1-{date:yyyy.MM.dd}"; + workItemQueue.Enqueue(new ReindexWorkItem($"{sourceScope}events-v1-{date:yyyy.MM.dd}", "events", indexToCreate, "updated_utc", () => index.EnsureIndexAsync(date))); + } + } - continue; + // Reset the alias cache + var aliasCache = new ScopedCacheClient(_configuration.Cache, "alias"); + await aliasCache.RemoveAllAsync().AnyContext(); + + var started = SystemClock.UtcNow; + var lastProgress = SystemClock.UtcNow; + int retriesCount = 0; + int totalTasks = workItemQueue.Count; + var workingTasks = new List(); + var completedTasks = new List(); + var failedTasks = new List(); + while (true) { + if (workingTasks.Count == 0 && workItemQueue.Count == 0) + break; + + if (workingTasks.Count < 10 && workItemQueue.TryDequeue(out var dequeuedWorkItem)) { + if (dequeuedWorkItem.CreateIndex != null) { + try { + await dequeuedWorkItem.CreateIndex().AnyContext(); } - - var duration = TimeSpan.FromMilliseconds(taskStatus.Task.RunningTimeInNanoseconds * 0.000001); - double progress = status.Total > 0 ? (status.Created + status.Updated + status.Deleted + status.VersionConflicts * 1.0) / status.Total : 0; - highestProgress = Math.Max(highestProgress, progress); - - if (!taskStatus.IsValid) { - _logger.LogWarning(taskStatus.OriginalException, "Error getting task status for {TargetIndex} ({TaskId}): {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus.GetErrorMessage()); - workItem.ConsecutiveStatusErrors++; - if (taskStatus.Completed || workItem.ConsecutiveStatusErrors > 5) { - workingTasks.Remove(workItem); - workItem.LastTaskInfo = taskStatus.Task; - - if (taskStatus.Completed && workItem.Attempts < 3) { - _logger.LogWarning("FAILED RETRY - {TargetIndex} in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", workItem.TargetIndex, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, workItem.Attempts, workItem.TaskId); - workItem.ConsecutiveStatusErrors = 0; - workItemQueue.Enqueue(workItem); - totalTasks++; - retriesCount++; - await Task.Delay(TimeSpan.FromSeconds(15)).AnyContext(); - } else { - _logger.LogCritical("FAILED - {TargetIndex} in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", workItem.TargetIndex, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, workItem.Attempts, workItem.TaskId); - failedTasks.Add(workItem); - } - } - + catch (Exception ex) { + _logger.LogError(ex, "Failed to create index for {TargetIndex}", dequeuedWorkItem.TargetIndex); continue; } + } - if (!taskStatus.Completed) - continue; + int batchSize = 1000; + if (dequeuedWorkItem.Attempts == 1) + batchSize = 500; + else if (dequeuedWorkItem.Attempts >= 2) + batchSize = 250; + + var response = await client.ReindexOnServerAsync(r => r + .Source(s => s + .Remote(ConfigureRemoteElasticSource) + .Index(dequeuedWorkItem.SourceIndex) + .Size(batchSize) + .Query(q => { + var container = q.Term("_type", dequeuedWorkItem.SourceIndexType); + if (!String.IsNullOrEmpty(dequeuedWorkItem.DateField)) + container &= q.DateRange(d => d.Field(dequeuedWorkItem.DateField).GreaterThanOrEquals(cutOffDate)); + + return container; + })) + .Destination(d => d + .Index(dequeuedWorkItem.TargetIndex)) + .Conflicts(Conflicts.Proceed) + .WaitForCompletion(false) + .Script(s => { + if (!String.IsNullOrEmpty(dequeuedWorkItem.Script)) + return s.Source(dequeuedWorkItem.Script); + + return null; + })).AnyContext(); + + dequeuedWorkItem.Attempts += 1; + dequeuedWorkItem.TaskId = response.Task; + workingTasks.Add(dequeuedWorkItem); + + _logger.LogInformation("STARTED - {TargetIndex} A:{Attempts} ({TaskId})...", dequeuedWorkItem.TargetIndex, dequeuedWorkItem.Attempts, dequeuedWorkItem.TaskId); + + continue; + } - workingTasks.Remove(workItem); - workItem.LastTaskInfo = taskStatus.Task; - completedTasks.Add(workItem); - var targetCount = await client.CountAsync(d => d.Index(workItem.TargetIndex)).AnyContext(); + double highestProgress = 0; + foreach (var workItem in workingTasks.ToArray()) { + var taskStatus = await client.Tasks.GetTaskAsync(workItem.TaskId, t => t.WaitForCompletion(false)).AnyContext(); + _logger.LogRequest(taskStatus); - _logger.LogInformation("COMPLETED - {TargetIndex} ({TargetCount}) in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", workItem.TargetIndex, targetCount.Count, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, workItem.Attempts, workItem.TaskId); - } - if (SystemClock.UtcNow.Subtract(lastProgress) > TimeSpan.FromMinutes(5)) { - _logger.LogInformation("STATUS - I:{Completed}/{Total} P:{Progress:F0}% T:{Duration:d\\.hh\\:mm} W:{Working} F:{Failed} R:{Retries}", completedTasks.Count, totalTasks, highestProgress * 100, SystemClock.UtcNow.Subtract(started), workingTasks.Count, failedTasks.Count, retriesCount); - lastProgress = SystemClock.UtcNow; + var status = taskStatus?.Task?.Status; + if (status == null) { + _logger.LogWarning(taskStatus?.OriginalException, "Error getting task status for {TargetIndex} {TaskId}: {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus.GetErrorMessage()); + if (taskStatus?.ServerError?.Status == 429) + await Task.Delay(TimeSpan.FromSeconds(1)); + + continue; } - await Task.Delay(TimeSpan.FromSeconds(2)); - } - _logger.LogInformation("----- REINDEX COMPLETE", completedTasks.Count, totalTasks, SystemClock.UtcNow.Subtract(started), failedTasks.Count, retriesCount); - foreach (var task in completedTasks) { - var status = task.LastTaskInfo.Status; - var duration = TimeSpan.FromMilliseconds(task.LastTaskInfo.RunningTimeInNanoseconds * 0.000001); + var duration = TimeSpan.FromMilliseconds(taskStatus.Task.RunningTimeInNanoseconds * 0.000001); double progress = status.Total > 0 ? (status.Created + status.Updated + status.Deleted + status.VersionConflicts * 1.0) / status.Total : 0; + highestProgress = Math.Max(highestProgress, progress); + + if (!taskStatus.IsValid) { + _logger.LogWarning(taskStatus.OriginalException, "Error getting task status for {TargetIndex} ({TaskId}): {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus.GetErrorMessage()); + workItem.ConsecutiveStatusErrors++; + if (taskStatus.Completed || workItem.ConsecutiveStatusErrors > 5) { + workingTasks.Remove(workItem); + workItem.LastTaskInfo = taskStatus.Task; + + if (taskStatus.Completed && workItem.Attempts < 3) { + _logger.LogWarning("FAILED RETRY - {TargetIndex} in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", workItem.TargetIndex, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, workItem.Attempts, workItem.TaskId); + workItem.ConsecutiveStatusErrors = 0; + workItemQueue.Enqueue(workItem); + totalTasks++; + retriesCount++; + await Task.Delay(TimeSpan.FromSeconds(15)).AnyContext(); + } + else { + _logger.LogCritical("FAILED - {TargetIndex} in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", workItem.TargetIndex, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, workItem.Attempts, workItem.TaskId); + failedTasks.Add(workItem); + } + } - var targetCount = await client.CountAsync(d => d.Index(task.TargetIndex)).AnyContext(); - _logger.LogInformation("SUCCESS - {TargetIndex} ({TargetCount}) in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", task.TargetIndex, targetCount.Count, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, task.Attempts, task.TaskId); - } + continue; + } - foreach (var task in failedTasks) { - var status = task.LastTaskInfo.Status; - var duration = TimeSpan.FromMilliseconds(task.LastTaskInfo.RunningTimeInNanoseconds * 0.000001); - double progress = status.Total > 0 ? (status.Created + status.Updated + status.Deleted + status.VersionConflicts * 1.0) / status.Total : 0; + if (!taskStatus.Completed) + continue; - var targetCount = await client.CountAsync(d => d.Index(task.TargetIndex)); - _logger.LogCritical("FAILED - {TargetIndex} ({TargetCount}) in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", task.TargetIndex, targetCount.Count, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, task.Attempts, task.TaskId); + workingTasks.Remove(workItem); + workItem.LastTaskInfo = taskStatus.Task; + completedTasks.Add(workItem); + var targetCount = await client.CountAsync(d => d.Index(workItem.TargetIndex)).AnyContext(); + + _logger.LogInformation("COMPLETED - {TargetIndex} ({TargetCount}) in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", workItem.TargetIndex, targetCount.Count, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, workItem.Attempts, workItem.TaskId); + } + if (SystemClock.UtcNow.Subtract(lastProgress) > TimeSpan.FromMinutes(5)) { + _logger.LogInformation("STATUS - I:{Completed}/{Total} P:{Progress:F0}% T:{Duration:d\\.hh\\:mm} W:{Working} F:{Failed} R:{Retries}", completedTasks.Count, totalTasks, highestProgress * 100, SystemClock.UtcNow.Subtract(started), workingTasks.Count, failedTasks.Count, retriesCount); + lastProgress = SystemClock.UtcNow; } - _logger.LogInformation("----- SUMMARY - I:{Completed}/{Total} T:{Duration:d\\.hh\\:mm} F:{Failed} R:{Retries}", completedTasks.Count, totalTasks, SystemClock.UtcNow.Subtract(started), failedTasks.Count, retriesCount); + await Task.Delay(TimeSpan.FromSeconds(2)); + } - _logger.LogInformation("Updating aliases"); - await _configuration.MaintainIndexesAsync(); - _logger.LogInformation("Updated aliases"); - return JobResult.Success; + _logger.LogInformation("----- REINDEX COMPLETE", completedTasks.Count, totalTasks, SystemClock.UtcNow.Subtract(started), failedTasks.Count, retriesCount); + foreach (var task in completedTasks) { + var status = task.LastTaskInfo.Status; + var duration = TimeSpan.FromMilliseconds(task.LastTaskInfo.RunningTimeInNanoseconds * 0.000001); + double progress = status.Total > 0 ? (status.Created + status.Updated + status.Deleted + status.VersionConflicts * 1.0) / status.Total : 0; + + var targetCount = await client.CountAsync(d => d.Index(task.TargetIndex)).AnyContext(); + _logger.LogInformation("SUCCESS - {TargetIndex} ({TargetCount}) in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", task.TargetIndex, targetCount.Count, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, task.Attempts, task.TaskId); } - private IRemoteSource ConfigureRemoteElasticSource(RemoteSourceDescriptor rsd) { - var elasticOptions = _configuration.Options.ElasticsearchToMigrate; - if (!String.IsNullOrEmpty(elasticOptions.UserName) && !String.IsNullOrEmpty(elasticOptions.Password)) - rsd.Username(elasticOptions.UserName).Password(elasticOptions.Password); + foreach (var task in failedTasks) { + var status = task.LastTaskInfo.Status; + var duration = TimeSpan.FromMilliseconds(task.LastTaskInfo.RunningTimeInNanoseconds * 0.000001); + double progress = status.Total > 0 ? (status.Created + status.Updated + status.Deleted + status.VersionConflicts * 1.0) / status.Total : 0; - return rsd.Host(new Uri(elasticOptions.ServerUrl)); + var targetCount = await client.CountAsync(d => d.Index(task.TargetIndex)); + _logger.LogCritical("FAILED - {TargetIndex} ({TargetCount}) in {Duration:hh\\:mm} C:{Created} U:{Updated} D:{Deleted} X:{Conflicts} T:{Total} A:{Attempts} ID:{TaskId}", task.TargetIndex, targetCount.Count, duration, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total, task.Attempts, task.TaskId); } + _logger.LogInformation("----- SUMMARY - I:{Completed}/{Total} T:{Duration:d\\.hh\\:mm} F:{Failed} R:{Retries}", completedTasks.Count, totalTasks, SystemClock.UtcNow.Subtract(started), failedTasks.Count, retriesCount); + + _logger.LogInformation("Updating aliases"); + await _configuration.MaintainIndexesAsync(); + _logger.LogInformation("Updated aliases"); + return JobResult.Success; } - public class ReindexWorkItem { - public ReindexWorkItem(string sourceIndex, string sourceIndexType, string targetIndex, string dateField, Func createIndex = null, string script = null) { - SourceIndex = sourceIndex; - SourceIndexType = sourceIndexType; - TargetIndex = targetIndex; - DateField = dateField; - CreateIndex = createIndex; - Script = script; - } + private IRemoteSource ConfigureRemoteElasticSource(RemoteSourceDescriptor rsd) { + var elasticOptions = _configuration.Options.ElasticsearchToMigrate; + if (!String.IsNullOrEmpty(elasticOptions.UserName) && !String.IsNullOrEmpty(elasticOptions.Password)) + rsd.Username(elasticOptions.UserName).Password(elasticOptions.Password); - public string SourceIndex { get; set; } - public string SourceIndexType { get; set; } - public string TargetIndex { get; set; } - public string DateField { get; set; } - public Func CreateIndex { get; set; } - public string Script { get; set; } - public TaskId TaskId { get; set; } - public TaskInfo LastTaskInfo { get; set; } - public int ConsecutiveStatusErrors { get; set; } - public int Attempts { get; set; } + return rsd.Host(new Uri(elasticOptions.ServerUrl)); } } + +public class ReindexWorkItem { + public ReindexWorkItem(string sourceIndex, string sourceIndexType, string targetIndex, string dateField, Func createIndex = null, string script = null) { + SourceIndex = sourceIndex; + SourceIndexType = sourceIndexType; + TargetIndex = targetIndex; + DateField = dateField; + CreateIndex = createIndex; + Script = script; + } + + public string SourceIndex { get; set; } + public string SourceIndexType { get; set; } + public string TargetIndex { get; set; } + public string DateField { get; set; } + public Func CreateIndex { get; set; } + public string Script { get; set; } + public TaskId TaskId { get; set; } + public TaskInfo LastTaskInfo { get; set; } + public int ConsecutiveStatusErrors { get; set; } + public int Attempts { get; set; } +} diff --git a/src/Exceptionless.Core/Jobs/Elastic/MaintainIndexesJob.cs b/src/Exceptionless.Core/Jobs/Elastic/MaintainIndexesJob.cs index 23d5cb4786..f43b7aa6ac 100644 --- a/src/Exceptionless.Core/Jobs/Elastic/MaintainIndexesJob.cs +++ b/src/Exceptionless.Core/Jobs/Elastic/MaintainIndexesJob.cs @@ -1,33 +1,30 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Repositories.Configuration; +using Exceptionless.Core.Repositories.Configuration; using Foundatio.Jobs; using Foundatio.Lock; using Foundatio.Utility; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.Elastic { - [Job(Description = "Maintains Elasticsearch index aliases and index retention", IsContinuous = false)] - public class MaintainIndexesJob : Foundatio.Repositories.Elasticsearch.Jobs.MaintainIndexesJob, IHealthCheck { - private DateTime? _lastRun; - - public MaintainIndexesJob(ExceptionlessElasticConfiguration configuration, ILockProvider lockProvider, ILoggerFactory loggerFactory) : base(configuration, lockProvider, loggerFactory) {} +namespace Exceptionless.Core.Jobs.Elastic; - public override Task RunAsync(CancellationToken cancellationToken = new CancellationToken()) { - _lastRun = SystemClock.UtcNow; - return base.RunAsync(cancellationToken); - } +[Job(Description = "Maintains Elasticsearch index aliases and index retention", IsContinuous = false)] +public class MaintainIndexesJob : Foundatio.Repositories.Elasticsearch.Jobs.MaintainIndexesJob, IHealthCheck { + private DateTime? _lastRun; - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + public MaintainIndexesJob(ExceptionlessElasticConfiguration configuration, ILockProvider lockProvider, ILoggerFactory loggerFactory) : base(configuration, lockProvider, loggerFactory) { } - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); + public override Task RunAsync(CancellationToken cancellationToken = new CancellationToken()) { + _lastRun = SystemClock.UtcNow; + return base.RunAsync(cancellationToken); + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(65)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 65 minutes.")); - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); - } + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 65 minutes.")); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/Elastic/MigrationJob.cs b/src/Exceptionless.Core/Jobs/Elastic/MigrationJob.cs index fc9be3eae6..297614991b 100644 --- a/src/Exceptionless.Core/Jobs/Elastic/MigrationJob.cs +++ b/src/Exceptionless.Core/Jobs/Elastic/MigrationJob.cs @@ -1,5 +1,3 @@ -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Jobs; @@ -7,32 +5,32 @@ using Foundatio.Repositories.Migrations; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.Elastic { - [Job(Description = "Runs any pending document migrations.", IsContinuous = false)] - public class MigrationJob : JobBase { - private readonly MigrationManager _migrationManager; - private readonly ExceptionlessElasticConfiguration _configuration; +namespace Exceptionless.Core.Jobs.Elastic; - public MigrationJob(ILoggerFactory loggerFactory, MigrationManager migrationManager, ExceptionlessElasticConfiguration configuration) - : base(loggerFactory) { +[Job(Description = "Runs any pending document migrations.", IsContinuous = false)] +public class MigrationJob : JobBase { + private readonly MigrationManager _migrationManager; + private readonly ExceptionlessElasticConfiguration _configuration; - _migrationManager = migrationManager; - _configuration = configuration; - } + public MigrationJob(ILoggerFactory loggerFactory, MigrationManager migrationManager, ExceptionlessElasticConfiguration configuration) + : base(loggerFactory) { - protected override async Task RunInternalAsync(JobContext context) { - await _configuration.ConfigureIndexesAsync(null, false).AnyContext(); - await _migrationManager.RunMigrationsAsync().AnyContext(); + _migrationManager = migrationManager; + _configuration = configuration; + } + + protected override async Task RunInternalAsync(JobContext context) { + await _configuration.ConfigureIndexesAsync(null, false).AnyContext(); + await _migrationManager.RunMigrationsAsync().AnyContext(); - var tasks = _configuration.Indexes.OfType().Select(ReindexIfNecessary); - await Task.WhenAll(tasks).AnyContext(); + var tasks = _configuration.Indexes.OfType().Select(ReindexIfNecessary); + await Task.WhenAll(tasks).AnyContext(); - return JobResult.Success; - } + return JobResult.Success; + } - private async Task ReindexIfNecessary(VersionedIndex index) { - if (index.Version != await index.GetCurrentVersionAsync().AnyContext()) - await index.ReindexAsync().AnyContext(); - } + private async Task ReindexIfNecessary(VersionedIndex index) { + if (index.Version != await index.GetCurrentVersionAsync().AnyContext()) + await index.ReindexAsync().AnyContext(); } } diff --git a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs index 95cc7ef4e3..a2c935c4fd 100644 --- a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Configuration; +using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; using Exceptionless.Core.Mail; using Exceptionless.Core.Queues.Models; @@ -19,154 +16,154 @@ #pragma warning disable 1998 -namespace Exceptionless.Core.Jobs { - [Job(Description = "Queues event notification emails.", InitialDelay = "5s")] - public class EventNotificationsJob : QueueJobBase { - private readonly SlackService _slackService; - private readonly IMailer _mailer; - private readonly IProjectRepository _projectRepository; - private readonly AppOptions _appOptions; - private readonly EmailOptions _emailOptions; - private readonly IUserRepository _userRepository; - private readonly IEventRepository _eventRepository; - private readonly ICacheClient _cache; - private readonly UserAgentParser _parser; - - public EventNotificationsJob(IQueue queue, SlackService slackService, IMailer mailer, IProjectRepository projectRepository, AppOptions appOptions, EmailOptions emailOptions, IUserRepository userRepository, IEventRepository eventRepository, ICacheClient cacheClient, UserAgentParser parser, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { - _slackService = slackService; - _mailer = mailer; - _projectRepository = projectRepository; - _appOptions = appOptions; - _emailOptions = emailOptions; - _userRepository = userRepository; - _eventRepository = eventRepository; - _cache = cacheClient; - _parser = parser; - } - - protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var wi = context.QueueEntry.Value; - var ev = await _eventRepository.GetByIdAsync(wi.EventId).AnyContext(); - if (ev == null) - return JobResult.SuccessWithMessage($"Could not load event: {wi.EventId}"); +namespace Exceptionless.Core.Jobs; + +[Job(Description = "Queues event notification emails.", InitialDelay = "5s")] +public class EventNotificationsJob : QueueJobBase { + private readonly SlackService _slackService; + private readonly IMailer _mailer; + private readonly IProjectRepository _projectRepository; + private readonly AppOptions _appOptions; + private readonly EmailOptions _emailOptions; + private readonly IUserRepository _userRepository; + private readonly IEventRepository _eventRepository; + private readonly ICacheClient _cache; + private readonly UserAgentParser _parser; + + public EventNotificationsJob(IQueue queue, SlackService slackService, IMailer mailer, IProjectRepository projectRepository, AppOptions appOptions, EmailOptions emailOptions, IUserRepository userRepository, IEventRepository eventRepository, ICacheClient cacheClient, UserAgentParser parser, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { + _slackService = slackService; + _mailer = mailer; + _projectRepository = projectRepository; + _appOptions = appOptions; + _emailOptions = emailOptions; + _userRepository = userRepository; + _eventRepository = eventRepository; + _cache = cacheClient; + _parser = parser; + } - bool shouldLog = ev.ProjectId != _appOptions.InternalProjectId; - int sent = 0; - if (shouldLog) _logger.LogTrace("Process notification: project={project} event={id} stack={stack}", ev.ProjectId, ev.Id, ev.StackId); + protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + var wi = context.QueueEntry.Value; + var ev = await _eventRepository.GetByIdAsync(wi.EventId).AnyContext(); + if (ev == null) + return JobResult.SuccessWithMessage($"Could not load event: {wi.EventId}"); - var project = await _projectRepository.GetByIdAsync(ev.ProjectId, o => o.Cache()).AnyContext(); - if (project == null) - return JobResult.SuccessWithMessage($"Could not load project: {ev.ProjectId}."); + bool shouldLog = ev.ProjectId != _appOptions.InternalProjectId; + int sent = 0; + if (shouldLog) _logger.LogTrace("Process notification: project={project} event={id} stack={stack}", ev.ProjectId, ev.Id, ev.StackId); - using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id))) { - if (shouldLog) _logger.LogTrace("Loaded project: name={ProjectName}", project.Name); + var project = await _projectRepository.GetByIdAsync(ev.ProjectId, o => o.Cache()).AnyContext(); + if (project == null) + return JobResult.SuccessWithMessage($"Could not load project: {ev.ProjectId}."); - // after the first 2 occurrences, don't send a notification for the same stack more then once every 30 minutes - var lastTimeSentUtc = await _cache.GetAsync(String.Concat("notify:stack-throttle:", ev.StackId), DateTime.MinValue).AnyContext(); - if (wi.TotalOccurrences > 2 && !wi.IsRegression && lastTimeSentUtc != DateTime.MinValue && lastTimeSentUtc > SystemClock.UtcNow.AddMinutes(-30)) { - if (shouldLog) _logger.LogInformation("Skipping message because of stack throttling: last sent={LastSentUtc} occurrences={TotalOccurrences}", lastTimeSentUtc, wi.TotalOccurrences); - return JobResult.Success; - } + using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id))) { + if (shouldLog) _logger.LogTrace("Loaded project: name={ProjectName}", project.Name); - if (context.CancellationToken.IsCancellationRequested) - return JobResult.Cancelled; + // after the first 2 occurrences, don't send a notification for the same stack more then once every 30 minutes + var lastTimeSentUtc = await _cache.GetAsync(String.Concat("notify:stack-throttle:", ev.StackId), DateTime.MinValue).AnyContext(); + if (wi.TotalOccurrences > 2 && !wi.IsRegression && lastTimeSentUtc != DateTime.MinValue && lastTimeSentUtc > SystemClock.UtcNow.AddMinutes(-30)) { + if (shouldLog) _logger.LogInformation("Skipping message because of stack throttling: last sent={LastSentUtc} occurrences={TotalOccurrences}", lastTimeSentUtc, wi.TotalOccurrences); + return JobResult.Success; + } - // don't send more than 10 notifications for a given project every 30 minutes - var projectTimeWindow = TimeSpan.FromMinutes(30); - string cacheKey = String.Concat("notify:project-throttle:", ev.ProjectId, "-", SystemClock.UtcNow.Floor(projectTimeWindow).Ticks); - double notificationCount = await _cache.IncrementAsync(cacheKey, 1, projectTimeWindow).AnyContext(); - if (notificationCount > 10 && !wi.IsRegression) { - if (shouldLog) _logger.LogInformation("Skipping message because of project throttling: count={NotificationCount}", notificationCount); - return JobResult.Success; - } + if (context.CancellationToken.IsCancellationRequested) + return JobResult.Cancelled; - foreach (var kv in project.NotificationSettings) { - var settings = kv.Value; - if (shouldLog) _logger.LogTrace("Processing notification: {Key}", kv.Key); - - bool isCritical = ev.IsCritical(); - bool shouldReportNewError = settings.ReportNewErrors && wi.IsNew && ev.IsError(); - bool shouldReportCriticalError = settings.ReportCriticalErrors && isCritical && ev.IsError(); - bool shouldReportRegression = settings.ReportEventRegressions && wi.IsRegression; - bool shouldReportNewEvent = settings.ReportNewEvents && wi.IsNew; - bool shouldReportCriticalEvent = settings.ReportCriticalEvents && isCritical; - bool shouldReport = shouldReportNewError || shouldReportCriticalError || shouldReportRegression || shouldReportNewEvent || shouldReportCriticalEvent; - - if (shouldLog) { - _logger.LogTrace("Settings: new error={ReportNewErrors} critical error={ReportCriticalErrors} regression={ReportEventRegressions} new={ReportNewEvents} critical={ReportCriticalEvents}", settings.ReportNewErrors, settings.ReportCriticalErrors, settings.ReportEventRegressions, settings.ReportNewEvents, settings.ReportCriticalEvents); - _logger.LogTrace("Should process: new error={ShouldReportNewError} critical error={ShouldReportCriticalError} regression={ShouldReportRegression} new={ShouldReportNewEvent} critical={ShouldReportCriticalEvent}", shouldReportNewError, shouldReportCriticalError, shouldReportRegression, shouldReportNewEvent, shouldReportCriticalEvent); - } - var request = ev.GetRequestInfo(); - // check for known bots if the user has elected to not report them - if (shouldReport && !String.IsNullOrEmpty(request?.UserAgent)) { - var botPatterns = project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.UserAgentBotPatterns).ToList(); - - var info = await _parser.ParseAsync(request.UserAgent).AnyContext(); - if (info != null && info.Device.IsSpider || request.UserAgent.AnyWildcardMatches(botPatterns)) { - shouldReport = false; - if (shouldLog) _logger.LogInformation("Skipping because event is from a bot {UserAgent}.", request.UserAgent); - } - } + // don't send more than 10 notifications for a given project every 30 minutes + var projectTimeWindow = TimeSpan.FromMinutes(30); + string cacheKey = String.Concat("notify:project-throttle:", ev.ProjectId, "-", SystemClock.UtcNow.Floor(projectTimeWindow).Ticks); + double notificationCount = await _cache.IncrementAsync(cacheKey, 1, projectTimeWindow).AnyContext(); + if (notificationCount > 10 && !wi.IsRegression) { + if (shouldLog) _logger.LogInformation("Skipping message because of project throttling: count={NotificationCount}", notificationCount); + return JobResult.Success; + } - if (!shouldReport) - continue; - - bool processed; - switch (kv.Key) { - case Project.NotificationIntegrations.Slack: - processed = await _slackService.SendEventNoticeAsync(ev, project, wi.IsNew, wi.IsRegression).AnyContext(); - break; - default: - processed = await SendEmailNotificationAsync(kv.Key, project, ev, wi, shouldLog).AnyContext(); - break; + foreach (var kv in project.NotificationSettings) { + var settings = kv.Value; + if (shouldLog) _logger.LogTrace("Processing notification: {Key}", kv.Key); + + bool isCritical = ev.IsCritical(); + bool shouldReportNewError = settings.ReportNewErrors && wi.IsNew && ev.IsError(); + bool shouldReportCriticalError = settings.ReportCriticalErrors && isCritical && ev.IsError(); + bool shouldReportRegression = settings.ReportEventRegressions && wi.IsRegression; + bool shouldReportNewEvent = settings.ReportNewEvents && wi.IsNew; + bool shouldReportCriticalEvent = settings.ReportCriticalEvents && isCritical; + bool shouldReport = shouldReportNewError || shouldReportCriticalError || shouldReportRegression || shouldReportNewEvent || shouldReportCriticalEvent; + + if (shouldLog) { + _logger.LogTrace("Settings: new error={ReportNewErrors} critical error={ReportCriticalErrors} regression={ReportEventRegressions} new={ReportNewEvents} critical={ReportCriticalEvents}", settings.ReportNewErrors, settings.ReportCriticalErrors, settings.ReportEventRegressions, settings.ReportNewEvents, settings.ReportCriticalEvents); + _logger.LogTrace("Should process: new error={ShouldReportNewError} critical error={ShouldReportCriticalError} regression={ShouldReportRegression} new={ShouldReportNewEvent} critical={ShouldReportCriticalEvent}", shouldReportNewError, shouldReportCriticalError, shouldReportRegression, shouldReportNewEvent, shouldReportCriticalEvent); + } + var request = ev.GetRequestInfo(); + // check for known bots if the user has elected to not report them + if (shouldReport && !String.IsNullOrEmpty(request?.UserAgent)) { + var botPatterns = project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.UserAgentBotPatterns).ToList(); + + var info = await _parser.ParseAsync(request.UserAgent).AnyContext(); + if (info != null && info.Device.IsSpider || request.UserAgent.AnyWildcardMatches(botPatterns)) { + shouldReport = false; + if (shouldLog) _logger.LogInformation("Skipping because event is from a bot {UserAgent}.", request.UserAgent); } - - if (shouldLog) _logger.LogTrace("Finished processing notification: {Key}", kv.Key); - if (processed) - sent++; } - // if we sent any notifications, mark the last time a notification for this stack was sent. - if (sent > 0) { - await _cache.SetAsync(String.Concat("notify:stack-throttle:", ev.StackId), SystemClock.UtcNow, SystemClock.UtcNow.AddMinutes(15)).AnyContext(); - if (shouldLog) _logger.LogInformation("Notifications sent: event={id} stack={stack} count={SentCount}", ev.Id, ev.StackId, sent); + if (!shouldReport) + continue; + + bool processed; + switch (kv.Key) { + case Project.NotificationIntegrations.Slack: + processed = await _slackService.SendEventNoticeAsync(ev, project, wi.IsNew, wi.IsRegression).AnyContext(); + break; + default: + processed = await SendEmailNotificationAsync(kv.Key, project, ev, wi, shouldLog).AnyContext(); + break; } - } - return JobResult.Success; - } - private async Task SendEmailNotificationAsync(string userId, Project project, PersistentEvent ev, EventNotification wi, bool shouldLog) { - var user = await _userRepository.GetByIdAsync(userId, o => o.Cache()).AnyContext(); - if (String.IsNullOrEmpty(user?.EmailAddress)) { - if (shouldLog) _logger.LogError("Could not load user {user} or blank email address {EmailAddress}.", userId, user?.EmailAddress ?? ""); - return false; + if (shouldLog) _logger.LogTrace("Finished processing notification: {Key}", kv.Key); + if (processed) + sent++; } - if (!user.IsEmailAddressVerified) { - if (shouldLog) _logger.LogInformation("User {user} with email address {EmailAddress} has not been verified.", user.Id, user.EmailAddress); - return false; + // if we sent any notifications, mark the last time a notification for this stack was sent. + if (sent > 0) { + await _cache.SetAsync(String.Concat("notify:stack-throttle:", ev.StackId), SystemClock.UtcNow, SystemClock.UtcNow.AddMinutes(15)).AnyContext(); + if (shouldLog) _logger.LogInformation("Notifications sent: event={id} stack={stack} count={SentCount}", ev.Id, ev.StackId, sent); } + } + return JobResult.Success; + } - if (!user.EmailNotificationsEnabled) { - if (shouldLog) _logger.LogInformation("User {user} with email address {EmailAddress} has email notifications disabled.", user.Id, user.EmailAddress); - return false; - } + private async Task SendEmailNotificationAsync(string userId, Project project, PersistentEvent ev, EventNotification wi, bool shouldLog) { + var user = await _userRepository.GetByIdAsync(userId, o => o.Cache()).AnyContext(); + if (String.IsNullOrEmpty(user?.EmailAddress)) { + if (shouldLog) _logger.LogError("Could not load user {user} or blank email address {EmailAddress}.", userId, user?.EmailAddress ?? ""); + return false; + } - if (!user.OrganizationIds.Contains(project.OrganizationId)) { - if (shouldLog) _logger.LogError("Unauthorized user: project={project} user={user} organization={organization} event={id}", project.Id, userId, project.OrganizationId, ev.Id); - return false; - } + if (!user.IsEmailAddressVerified) { + if (shouldLog) _logger.LogInformation("User {user} with email address {EmailAddress} has not been verified.", user.Id, user.EmailAddress); + return false; + } - if (shouldLog) _logger.LogTrace("Loaded user: email={EmailAddress}", user.EmailAddress); + if (!user.EmailNotificationsEnabled) { + if (shouldLog) _logger.LogInformation("User {user} with email address {EmailAddress} has email notifications disabled.", user.Id, user.EmailAddress); + return false; + } - // don't send notifications in non-production mode to email addresses that are not on the outbound email list. - if (_appOptions.AppMode != AppMode.Production && !_emailOptions.AllowedOutboundAddresses.Contains(v => user.EmailAddress.ToLowerInvariant().Contains(v))) { - if (shouldLog) _logger.LogInformation("Skipping because email is not on the outbound list and not in production mode."); - return false; - } + if (!user.OrganizationIds.Contains(project.OrganizationId)) { + if (shouldLog) _logger.LogError("Unauthorized user: project={project} user={user} organization={organization} event={id}", project.Id, userId, project.OrganizationId, ev.Id); + return false; + } + + if (shouldLog) _logger.LogTrace("Loaded user: email={EmailAddress}", user.EmailAddress); - if (shouldLog) _logger.LogTrace("Sending email to {EmailAddress}...", user.EmailAddress); - return await _mailer.SendEventNoticeAsync(user, ev, project, wi.IsNew, wi.IsRegression, wi.TotalOccurrences).AnyContext(); + // don't send notifications in non-production mode to email addresses that are not on the outbound email list. + if (_appOptions.AppMode != AppMode.Production && !_emailOptions.AllowedOutboundAddresses.Contains(v => user.EmailAddress.ToLowerInvariant().Contains(v))) { + if (shouldLog) _logger.LogInformation("Skipping because email is not on the outbound list and not in production mode."); + return false; } + + if (shouldLog) _logger.LogTrace("Sending email to {EmailAddress}...", user.EmailAddress); + return await _mailer.SendEventNoticeAsync(user, ev, project, wi.IsNew, wi.IsRegression, wi.TotalOccurrences).AnyContext(); } } diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 5bea5c8ce9..3118b88b16 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Text; using Exceptionless.Core.AppStats; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; @@ -24,288 +19,291 @@ #pragma warning disable 1998 -namespace Exceptionless.Core.Jobs { - [Job(Description = "Processes queued events.", InitialDelay = "2s")] - public class EventPostsJob : QueueJobBase { - private readonly long _maximumEventPostFileSize; - private readonly long _maximumUncompressedEventPostSize; - - private readonly EventPostService _eventPostService; - private readonly EventParserPluginManager _eventParserPluginManager; - private readonly EventPipeline _eventPipeline; - private readonly IMetricsClient _metrics; - private readonly UsageService _usageService; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly JsonSerializerSettings _jsonSerializerSettings; - private readonly AppOptions _appOptions; - - public EventPostsJob(IQueue queue, EventPostService eventPostService, EventParserPluginManager eventParserPluginManager, EventPipeline eventPipeline, IMetricsClient metrics, UsageService usageService, IOrganizationRepository organizationRepository, IProjectRepository projectRepository, JsonSerializerSettings jsonSerializerSettings, AppOptions appOptions, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { - _eventPostService = eventPostService; - _eventParserPluginManager = eventParserPluginManager; - _eventPipeline = eventPipeline; - _metrics = metrics; - _usageService = usageService; - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _jsonSerializerSettings = jsonSerializerSettings; - - _appOptions = appOptions; - _maximumEventPostFileSize = _appOptions.MaximumEventPostSize + 1024; - _maximumUncompressedEventPostSize = _appOptions.MaximumEventPostSize * 10; - - AutoComplete = false; +namespace Exceptionless.Core.Jobs; + +[Job(Description = "Processes queued events.", InitialDelay = "2s")] +public class EventPostsJob : QueueJobBase { + private readonly long _maximumEventPostFileSize; + private readonly long _maximumUncompressedEventPostSize; + + private readonly EventPostService _eventPostService; + private readonly EventParserPluginManager _eventParserPluginManager; + private readonly EventPipeline _eventPipeline; + private readonly IMetricsClient _metrics; + private readonly UsageService _usageService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly AppOptions _appOptions; + + public EventPostsJob(IQueue queue, EventPostService eventPostService, EventParserPluginManager eventParserPluginManager, EventPipeline eventPipeline, IMetricsClient metrics, UsageService usageService, IOrganizationRepository organizationRepository, IProjectRepository projectRepository, JsonSerializerSettings jsonSerializerSettings, AppOptions appOptions, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { + _eventPostService = eventPostService; + _eventParserPluginManager = eventParserPluginManager; + _eventPipeline = eventPipeline; + _metrics = metrics; + _usageService = usageService; + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _jsonSerializerSettings = jsonSerializerSettings; + + _appOptions = appOptions; + _maximumEventPostFileSize = _appOptions.MaximumEventPostSize + 1024; + _maximumUncompressedEventPostSize = _appOptions.MaximumEventPostSize * 10; + + AutoComplete = false; + } + + protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + var entry = context.QueueEntry; + var ep = entry.Value; + string payloadPath = Path.ChangeExtension(entry.Value.FilePath, ".payload"); + var payloadTask = _metrics.TimeAsync(() => _eventPostService.GetEventPostPayloadAsync(payloadPath), MetricNames.PostsMarkFileActiveTime); + var projectTask = _projectRepository.GetByIdAsync(ep.ProjectId, o => o.Cache()); + var organizationTask = _organizationRepository.GetByIdAsync(ep.OrganizationId, o => o.Cache()); + + byte[] payload = await payloadTask.AnyContext(); + if (payload == null) { + await Task.WhenAll(AbandonEntryAsync(entry), projectTask, organizationTask).AnyContext(); + return JobResult.FailedWithMessage($"Unable to retrieve payload '{payloadPath}'."); } - protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var entry = context.QueueEntry; - var ep = entry.Value; - string payloadPath = Path.ChangeExtension(entry.Value.FilePath, ".payload"); - var payloadTask = _metrics.TimeAsync(() => _eventPostService.GetEventPostPayloadAsync(payloadPath), MetricNames.PostsMarkFileActiveTime); - var projectTask = _projectRepository.GetByIdAsync(ep.ProjectId, o => o.Cache()); - var organizationTask = _organizationRepository.GetByIdAsync(ep.OrganizationId, o => o.Cache()); - - byte[] payload = await payloadTask.AnyContext(); - if (payload == null) { - await Task.WhenAll(AbandonEntryAsync(entry), projectTask, organizationTask).AnyContext(); - return JobResult.FailedWithMessage($"Unable to retrieve payload '{payloadPath}'."); - } + _metrics.Gauge(MetricNames.PostsMessageSize, payload.LongLength); + if (payload.LongLength > _maximumEventPostFileSize) { + await Task.WhenAll(_metrics.TimeAsync(() => entry.CompleteAsync(), MetricNames.PostsCompleteTime), projectTask, organizationTask).AnyContext(); + return JobResult.FailedWithMessage($"Unable to process payload '{payloadPath}' ({payload.LongLength} bytes): Maximum event post size limit ({_appOptions.MaximumEventPostSize} bytes) reached."); + } + + using (_logger.BeginScope(new ExceptionlessState().Organization(ep.OrganizationId).Project(ep.ProjectId))) { + _metrics.Gauge(MetricNames.PostsCompressedSize, payload.Length); - _metrics.Gauge(MetricNames.PostsMessageSize, payload.LongLength); - if (payload.LongLength > _maximumEventPostFileSize) { - await Task.WhenAll(_metrics.TimeAsync(() => entry.CompleteAsync(), MetricNames.PostsCompleteTime), projectTask, organizationTask).AnyContext(); - return JobResult.FailedWithMessage($"Unable to process payload '{payloadPath}' ({payload.LongLength} bytes): Maximum event post size limit ({_appOptions.MaximumEventPostSize} bytes) reached."); + bool isDebugLogLevelEnabled = _logger.IsEnabled(LogLevel.Debug); + bool isInternalProject = ep.ProjectId == _appOptions.InternalProjectId; + if (!isInternalProject && _logger.IsEnabled(LogLevel.Information)) { + using (_logger.BeginScope(new ExceptionlessState().Tag("processing").Tag("compressed").Tag(ep.ContentEncoding).Value(payload.Length))) + _logger.LogInformation("Processing post: id={QueueEntryId} path={FilePath} project={project} ip={IpAddress} v={ApiVersion} agent={UserAgent}", entry.Id, payloadPath, ep.ProjectId, ep.IpAddress, ep.ApiVersion, ep.UserAgent); } - using (_logger.BeginScope(new ExceptionlessState().Organization(ep.OrganizationId).Project(ep.ProjectId))) { - _metrics.Gauge(MetricNames.PostsCompressedSize, payload.Length); + var project = await projectTask.AnyContext(); + if (project == null) { + if (!isInternalProject) _logger.LogError("Unable to process EventPost {FilePath}: Unable to load project: {Project}", payloadPath, ep.ProjectId); + await Task.WhenAll(CompleteEntryAsync(entry, ep, SystemClock.UtcNow), organizationTask).AnyContext(); + return JobResult.Success; + } - bool isDebugLogLevelEnabled = _logger.IsEnabled(LogLevel.Debug); - bool isInternalProject = ep.ProjectId == _appOptions.InternalProjectId; - if (!isInternalProject && _logger.IsEnabled(LogLevel.Information)) { - using (_logger.BeginScope(new ExceptionlessState().Tag("processing").Tag("compressed").Tag(ep.ContentEncoding).Value(payload.Length))) - _logger.LogInformation("Processing post: id={QueueEntryId} path={FilePath} project={project} ip={IpAddress} v={ApiVersion} agent={UserAgent}", entry.Id, payloadPath, ep.ProjectId, ep.IpAddress, ep.ApiVersion, ep.UserAgent); + long maxEventPostSize = _appOptions.MaximumEventPostSize; + byte[] uncompressedData = payload; + if (!String.IsNullOrEmpty(ep.ContentEncoding)) { + if (!isInternalProject && isDebugLogLevelEnabled) { + using (_logger.BeginScope(new ExceptionlessState().Tag("decompressing").Tag(ep.ContentEncoding))) + _logger.LogDebug("Decompressing EventPost: {QueueEntryId} ({CompressedBytes} bytes)", entry.Id, payload.Length); } - var project = await projectTask.AnyContext(); - if (project == null) { - if (!isInternalProject) _logger.LogError("Unable to process EventPost {FilePath}: Unable to load project: {Project}", payloadPath, ep.ProjectId); + maxEventPostSize = _maximumUncompressedEventPostSize; + try { + _metrics.Time(() => { + uncompressedData = uncompressedData.Decompress(ep.ContentEncoding); + }, MetricNames.PostsDecompressionTime); + } catch (Exception ex) { + _metrics.Counter(MetricNames.PostsDecompressionErrors); await Task.WhenAll(CompleteEntryAsync(entry, ep, SystemClock.UtcNow), organizationTask).AnyContext(); - return JobResult.Success; + return JobResult.FailedWithMessage($"Unable to decompress EventPost data '{payloadPath}' ({payload.Length} bytes compressed): {ex.Message}"); } + } - long maxEventPostSize = _appOptions.MaximumEventPostSize; - byte[] uncompressedData = payload; - if (!String.IsNullOrEmpty(ep.ContentEncoding)) { - if (!isInternalProject && isDebugLogLevelEnabled) { - using (_logger.BeginScope(new ExceptionlessState().Tag("decompressing").Tag(ep.ContentEncoding))) - _logger.LogDebug("Decompressing EventPost: {QueueEntryId} ({CompressedBytes} bytes)", entry.Id, payload.Length); - } + _metrics.Gauge(MetricNames.PostsUncompressedSize, payload.LongLength); + if (uncompressedData.Length > maxEventPostSize) { + await Task.WhenAll(CompleteEntryAsync(entry, ep, SystemClock.UtcNow), organizationTask).AnyContext(); + return JobResult.FailedWithMessage($"Unable to process decompressed EventPost data '{payloadPath}' ({payload.Length} bytes compressed, {uncompressedData.Length} bytes): Maximum uncompressed event post size limit ({maxEventPostSize} bytes) reached."); + } - maxEventPostSize = _maximumUncompressedEventPostSize; - try { - _metrics.Time(() => { - uncompressedData = uncompressedData.Decompress(ep.ContentEncoding); - }, MetricNames.PostsDecompressionTime); - } catch (Exception ex) { - _metrics.Counter(MetricNames.PostsDecompressionErrors); - await Task.WhenAll(CompleteEntryAsync(entry, ep, SystemClock.UtcNow), organizationTask).AnyContext(); - return JobResult.FailedWithMessage($"Unable to decompress EventPost data '{payloadPath}' ({payload.Length} bytes compressed): {ex.Message}"); - } - } + if (!isInternalProject && isDebugLogLevelEnabled) { + using (_logger.BeginScope(new ExceptionlessState().Tag("uncompressed").Value(uncompressedData.Length))) + _logger.LogDebug("Processing uncompressed EventPost: {QueueEntryId} ({UncompressedBytes} bytes)", entry.Id, uncompressedData.Length); + } - _metrics.Gauge(MetricNames.PostsUncompressedSize, payload.LongLength); - if (uncompressedData.Length > maxEventPostSize) { - await Task.WhenAll(CompleteEntryAsync(entry, ep, SystemClock.UtcNow), organizationTask).AnyContext(); - return JobResult.FailedWithMessage($"Unable to process decompressed EventPost data '{payloadPath}' ({payload.Length} bytes compressed, {uncompressedData.Length} bytes): Maximum uncompressed event post size limit ({maxEventPostSize} bytes) reached."); - } + var createdUtc = SystemClock.UtcNow; + var events = ParseEventPost(ep, createdUtc, uncompressedData, entry.Id, isInternalProject); + if (events == null || events.Count == 0) { + await Task.WhenAll(CompleteEntryAsync(entry, ep, createdUtc), organizationTask).AnyContext(); + return JobResult.Success; + } - if (!isInternalProject && isDebugLogLevelEnabled) { - using (_logger.BeginScope(new ExceptionlessState().Tag("uncompressed").Value(uncompressedData.Length))) - _logger.LogDebug("Processing uncompressed EventPost: {QueueEntryId} ({UncompressedBytes} bytes)", entry.Id, uncompressedData.Length); - } + if (context.CancellationToken.IsCancellationRequested) { + await Task.WhenAll(AbandonEntryAsync(entry), organizationTask).AnyContext(); + return JobResult.Cancelled; + } - var createdUtc = SystemClock.UtcNow; - var events = ParseEventPost(ep, createdUtc, uncompressedData, entry.Id, isInternalProject); - if (events == null || events.Count == 0) { - await Task.WhenAll(CompleteEntryAsync(entry, ep, createdUtc), organizationTask).AnyContext(); - return JobResult.Success; - } + var organization = await organizationTask.AnyContext(); + if (organization == null) { + if (!isInternalProject) + _logger.LogError("Unable to process EventPost {FilePath}: Unable to load organization: {OrganizationId}", payloadPath, project.OrganizationId); - if (context.CancellationToken.IsCancellationRequested) { - await Task.WhenAll(AbandonEntryAsync(entry), organizationTask).AnyContext(); - return JobResult.Cancelled; - } + await CompleteEntryAsync(entry, ep, SystemClock.UtcNow).AnyContext(); + return JobResult.Success; + } - var organization = await organizationTask.AnyContext(); - if (organization == null) { - if (!isInternalProject) - _logger.LogError("Unable to process EventPost {FilePath}: Unable to load organization: {OrganizationId}", payloadPath, project.OrganizationId); + bool isSingleEvent = events.Count == 1; + if (!isSingleEvent) { + // Don't process all the events if it will put the account over its limits. + int eventsToProcess = await _usageService.GetRemainingEventLimitAsync(organization).AnyContext(); - await CompleteEntryAsync(entry, ep, SystemClock.UtcNow).AnyContext(); - return JobResult.Success; + // Add 1 because we already counted 1 against their limit when we received the event post. + if (eventsToProcess < Int32.MaxValue) + eventsToProcess += 1; + + // Discard any events over the plan limit. + if (eventsToProcess < events.Count) { + int discarded = events.Count - eventsToProcess; + events = events.Take(eventsToProcess).ToList(); + _metrics.Counter(MetricNames.EventsDiscarded, discarded); } + } - bool isSingleEvent = events.Count == 1; - if (!isSingleEvent) { - // Don't process all the events if it will put the account over its limits. - int eventsToProcess = await _usageService.GetRemainingEventLimitAsync(organization).AnyContext(); + int errorCount = 0; + var eventsToRetry = new List(); + try { + var contexts = await _eventPipeline.RunAsync(events, organization, project, ep).AnyContext(); + if (!isInternalProject && isDebugLogLevelEnabled) { + using (_logger.BeginScope(new ExceptionlessState().Value(contexts.Count))) + _logger.LogDebug("Ran {@value} events through the pipeline: id={QueueEntryId} success={SuccessCount} error={ErrorCount}", contexts.Count, entry.Id, contexts.Count(r => r.IsProcessed), contexts.Count(r => r.HasError)); + } - // Add 1 because we already counted 1 against their limit when we received the event post. - if (eventsToProcess < Int32.MaxValue) - eventsToProcess += 1; + // increment the plan usage counters (note: OverageHandler already incremented usage by 1) + int processedEvents = contexts.Count(c => c.IsProcessed); + await _usageService.IncrementUsageAsync(organization, project, false, processedEvents - 1, applyHourlyLimit: false).AnyContext(); - // Discard any events over the plan limit. - if (eventsToProcess < events.Count) { - int discarded = events.Count - eventsToProcess; - events = events.Take(eventsToProcess).ToList(); - _metrics.Counter(MetricNames.EventsDiscarded, discarded); - } - } + int discardedEvents = contexts.Count(c => c.IsDiscarded); + _metrics.Counter(MetricNames.EventsDiscarded, discardedEvents); - int errorCount = 0; - var eventsToRetry = new List(); - try { - var contexts = await _eventPipeline.RunAsync(events, organization, project, ep).AnyContext(); - if (!isInternalProject && isDebugLogLevelEnabled) { - using (_logger.BeginScope(new ExceptionlessState().Value(contexts.Count))) - _logger.LogDebug("Ran {@value} events through the pipeline: id={QueueEntryId} success={SuccessCount} error={ErrorCount}", contexts.Count, entry.Id, contexts.Count(r => r.IsProcessed), contexts.Count(r => r.HasError)); - } - - // increment the plan usage counters (note: OverageHandler already incremented usage by 1) - int processedEvents = contexts.Count(c => c.IsProcessed); - await _usageService.IncrementUsageAsync(organization, project, false, processedEvents - 1, applyHourlyLimit: false).AnyContext(); - - int discardedEvents = contexts.Count(c => c.IsDiscarded); - _metrics.Counter(MetricNames.EventsDiscarded, discardedEvents); - - foreach (var ctx in contexts) { - if (ctx.IsCancelled) - continue; - - if (!ctx.HasError) - continue; - - if (!isInternalProject) _logger.LogError(ctx.Exception, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ctx.ErrorMessage); - if (ctx.Exception is ValidationException) - continue; - - errorCount++; - if (!isSingleEvent) { - // Put this single event back into the queue so we can retry it separately. - eventsToRetry.Add(ctx.Event); - } - } - } catch (Exception ex) { - if (!isInternalProject) _logger.LogError(ex, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ex.Message); - if (ex is ArgumentException || ex is DocumentNotFoundException) { - await CompleteEntryAsync(entry, ep, createdUtc).AnyContext(); - return JobResult.Success; - } + foreach (var ctx in contexts) { + if (ctx.IsCancelled) + continue; - errorCount++; - if (!isSingleEvent) - eventsToRetry.AddRange(events); - } + if (!ctx.HasError) + continue; - if (eventsToRetry.Count > 0) - await _metrics.TimeAsync(() => RetryEventsAsync(eventsToRetry, ep, entry, project, isInternalProject), MetricNames.PostsRetryTime).AnyContext(); + if (!isInternalProject) _logger.LogError(ctx.Exception, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ctx.ErrorMessage); + if (ctx.Exception is ValidationException) + continue; - if (isSingleEvent && errorCount > 0) - await AbandonEntryAsync(entry).AnyContext(); - else + errorCount++; + if (!isSingleEvent) { + // Put this single event back into the queue so we can retry it separately. + eventsToRetry.Add(ctx.Event); + } + } + } + catch (Exception ex) { + if (!isInternalProject) _logger.LogError(ex, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ex.Message); + if (ex is ArgumentException || ex is DocumentNotFoundException) { await CompleteEntryAsync(entry, ep, createdUtc).AnyContext(); + return JobResult.Success; + } - return JobResult.Success; + errorCount++; + if (!isSingleEvent) + eventsToRetry.AddRange(events); } - } - - private List ParseEventPost(EventPostInfo ep, DateTime createdUtc, byte[] uncompressedData, string queueEntryId, bool isInternalProject) { - using (_logger.BeginScope(new ExceptionlessState().Tag("parsing"))) { - if (!isInternalProject && _logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Parsing EventPost: {QueueEntryId}", queueEntryId); - - List events = null; - try { - var encoding = Encoding.UTF8; - if (!String.IsNullOrEmpty(ep.CharSet)) - encoding = Encoding.GetEncoding(ep.CharSet); - _metrics.Time(() => { - string input = encoding.GetString(uncompressedData); - events = _eventParserPluginManager.ParseEvents(input, ep.ApiVersion, ep.UserAgent) ?? new List(0); - foreach (var ev in events) { - ev.CreatedUtc = createdUtc; - ev.OrganizationId = ep.OrganizationId; - ev.ProjectId = ep.ProjectId; - - // set the reference id to the event id if one was defined. - if (!String.IsNullOrEmpty(ev.Id) && String.IsNullOrEmpty(ev.ReferenceId)) - ev.ReferenceId = ev.Id; - - // the event id and stack id should never be set for posted events - ev.Id = ev.StackId = null; - } - }, MetricNames.PostsParsingTime); - _metrics.Counter(MetricNames.PostsParsed); - _metrics.Gauge(MetricNames.PostsEventCount, events.Count); - } catch (Exception ex) { - _metrics.Counter(MetricNames.PostsParseErrors); - if (!isInternalProject) _logger.LogError(ex, "An error occurred while processing the EventPost {QueueEntryId}: {Message}", queueEntryId, ex.Message); - } + if (eventsToRetry.Count > 0) + await _metrics.TimeAsync(() => RetryEventsAsync(eventsToRetry, ep, entry, project, isInternalProject), MetricNames.PostsRetryTime).AnyContext(); - if(!isInternalProject && _logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Parsed {ParsedCount} events from EventPost: {QueueEntryId}", events?.Count ?? 0, queueEntryId); + if (isSingleEvent && errorCount > 0) + await AbandonEntryAsync(entry).AnyContext(); + else + await CompleteEntryAsync(entry, ep, createdUtc).AnyContext(); - return events; - } + return JobResult.Success; } + } - private async Task RetryEventsAsync(List eventsToRetry, EventPostInfo ep, IQueueEntry queueEntry, Project project, bool isInternalProject) { - _metrics.Gauge(MetricNames.EventsRetryCount, eventsToRetry.Count); - foreach (var ev in eventsToRetry) { - try { - var stream = new MemoryStream(ev.GetBytes(_jsonSerializerSettings)); - - // Put this single event back into the queue so we can retry it separately. - await _eventPostService.EnqueueAsync(new EventPost(false) { - ApiVersion = ep.ApiVersion, - CharSet = ep.CharSet, - ContentEncoding = null, - IpAddress = ep.IpAddress, - MediaType = ep.MediaType, - OrganizationId = ep.OrganizationId ?? project.OrganizationId, - ProjectId = ep.ProjectId, - UserAgent = ep.UserAgent - }, stream).AnyContext(); - } catch (Exception ex) { - if (!isInternalProject && _logger.IsEnabled(LogLevel.Critical)) { - using (_logger.BeginScope(new ExceptionlessState().Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags }))) - _logger.LogCritical(ex, "Error while requeuing event post {FilePath}: {Message}", queueEntry.Value.FilePath, ex.Message); + private List ParseEventPost(EventPostInfo ep, DateTime createdUtc, byte[] uncompressedData, string queueEntryId, bool isInternalProject) { + using (_logger.BeginScope(new ExceptionlessState().Tag("parsing"))) { + if (!isInternalProject && _logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Parsing EventPost: {QueueEntryId}", queueEntryId); + + List events = null; + try { + var encoding = Encoding.UTF8; + if (!String.IsNullOrEmpty(ep.CharSet)) + encoding = Encoding.GetEncoding(ep.CharSet); + + _metrics.Time(() => { + string input = encoding.GetString(uncompressedData); + events = _eventParserPluginManager.ParseEvents(input, ep.ApiVersion, ep.UserAgent) ?? new List(0); + foreach (var ev in events) { + ev.CreatedUtc = createdUtc; + ev.OrganizationId = ep.OrganizationId; + ev.ProjectId = ep.ProjectId; + + // set the reference id to the event id if one was defined. + if (!String.IsNullOrEmpty(ev.Id) && String.IsNullOrEmpty(ev.ReferenceId)) + ev.ReferenceId = ev.Id; + + // the event id and stack id should never be set for posted events + ev.Id = ev.StackId = null; } + }, MetricNames.PostsParsingTime); + _metrics.Counter(MetricNames.PostsParsed); + _metrics.Gauge(MetricNames.PostsEventCount, events.Count); + } + catch (Exception ex) { + _metrics.Counter(MetricNames.PostsParseErrors); + if (!isInternalProject) _logger.LogError(ex, "An error occurred while processing the EventPost {QueueEntryId}: {Message}", queueEntryId, ex.Message); + } - _metrics.Counter(MetricNames.EventsRetryErrors); + if (!isInternalProject && _logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Parsed {ParsedCount} events from EventPost: {QueueEntryId}", events?.Count ?? 0, queueEntryId); + + return events; + } + } + + private async Task RetryEventsAsync(List eventsToRetry, EventPostInfo ep, IQueueEntry queueEntry, Project project, bool isInternalProject) { + _metrics.Gauge(MetricNames.EventsRetryCount, eventsToRetry.Count); + foreach (var ev in eventsToRetry) { + try { + var stream = new MemoryStream(ev.GetBytes(_jsonSerializerSettings)); + + // Put this single event back into the queue so we can retry it separately. + await _eventPostService.EnqueueAsync(new EventPost(false) { + ApiVersion = ep.ApiVersion, + CharSet = ep.CharSet, + ContentEncoding = null, + IpAddress = ep.IpAddress, + MediaType = ep.MediaType, + OrganizationId = ep.OrganizationId ?? project.OrganizationId, + ProjectId = ep.ProjectId, + UserAgent = ep.UserAgent + }, stream).AnyContext(); + } + catch (Exception ex) { + if (!isInternalProject && _logger.IsEnabled(LogLevel.Critical)) { + using (_logger.BeginScope(new ExceptionlessState().Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags }))) + _logger.LogCritical(ex, "Error while requeuing event post {FilePath}: {Message}", queueEntry.Value.FilePath, ex.Message); } + + _metrics.Counter(MetricNames.EventsRetryErrors); } } + } - private Task AbandonEntryAsync(IQueueEntry queueEntry) { - return _metrics.TimeAsync(queueEntry.AbandonAsync, MetricNames.PostsAbandonTime); - } + private Task AbandonEntryAsync(IQueueEntry queueEntry) { + return _metrics.TimeAsync(queueEntry.AbandonAsync, MetricNames.PostsAbandonTime); + } - private Task CompleteEntryAsync(IQueueEntry entry, EventPostInfo eventPostInfo, DateTime created) { - return _metrics.TimeAsync(async () => { - await entry.CompleteAsync().AnyContext(); - await _eventPostService.CompleteEventPostAsync(entry.Value.FilePath, eventPostInfo.ProjectId, created, entry.Value.ShouldArchive).AnyContext(); - }, MetricNames.PostsCompleteTime); - } + private Task CompleteEntryAsync(IQueueEntry entry, EventPostInfo eventPostInfo, DateTime created) { + return _metrics.TimeAsync(async () => { + await entry.CompleteAsync().AnyContext(); + await _eventPostService.CompleteEventPostAsync(entry.Value.FilePath, eventPostInfo.ProjectId, created, entry.Value.ShouldArchive).AnyContext(); + }, MetricNames.PostsCompleteTime); + } - protected override void LogProcessingQueueEntry(IQueueEntry entry) { - _logger.LogDebug("Processing {QueueEntryName} queue entry ({QueueEntryId}).", _queueEntryName, entry.Id); - } + protected override void LogProcessingQueueEntry(IQueueEntry entry) { + _logger.LogDebug("Processing {QueueEntryName} queue entry ({QueueEntryId}).", _queueEntryName, entry.Id); + } - protected override void LogAutoCompletedQueueEntry(IQueueEntry entry) { - _logger.LogDebug("Auto completed {QueueEntryName} queue entry ({QueueEntryId}).", _queueEntryName, entry.Id); - } + protected override void LogAutoCompletedQueueEntry(IQueueEntry entry) { + _logger.LogDebug("Auto completed {QueueEntryName} queue entry ({QueueEntryId}).", _queueEntryName, entry.Id); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs index 8abafc21a3..99569fc13a 100644 --- a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; using Exceptionless.Core.Repositories.Base; @@ -11,48 +8,50 @@ using Foundatio.Repositories.Extensions; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Processes queued event user descriptions.", InitialDelay = "3s")] - public class EventUserDescriptionsJob : QueueJobBase { - private readonly IEventRepository _eventRepository; +namespace Exceptionless.Core.Jobs; - public EventUserDescriptionsJob(IQueue queue, IEventRepository eventRepository, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { - _eventRepository = eventRepository; - } +[Job(Description = "Processes queued event user descriptions.", InitialDelay = "3s")] +public class EventUserDescriptionsJob : QueueJobBase { + private readonly IEventRepository _eventRepository; + + public EventUserDescriptionsJob(IQueue queue, IEventRepository eventRepository, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { + _eventRepository = eventRepository; + } - protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - _logger.LogTrace("Processing user description: id={0}", context.QueueEntry.Id); - - try { - await ProcessUserDescriptionAsync(context.QueueEntry.Value).AnyContext(); - _logger.LogInformation("Processed user description: id={Id}", context.QueueEntry.Id); - } catch (DocumentNotFoundException ex){ - _logger.LogError(ex, "An event with this reference id {ReferenceId} has not been processed yet or was deleted. Queue Id: {Id}", ex.Id, context.QueueEntry.Id); - return JobResult.FromException(ex); - } catch (Exception ex) { - _logger.LogError(ex, "An error occurred while processing the EventUserDescription {Id}: {Message}", context.QueueEntry.Id, ex.Message); - return JobResult.FromException(ex); - } - - return JobResult.Success; + protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + _logger.LogTrace("Processing user description: id={0}", context.QueueEntry.Id); + + try { + await ProcessUserDescriptionAsync(context.QueueEntry.Value).AnyContext(); + _logger.LogInformation("Processed user description: id={Id}", context.QueueEntry.Id); + } + catch (DocumentNotFoundException ex) { + _logger.LogError(ex, "An event with this reference id {ReferenceId} has not been processed yet or was deleted. Queue Id: {Id}", ex.Id, context.QueueEntry.Id); + return JobResult.FromException(ex); + } + catch (Exception ex) { + _logger.LogError(ex, "An error occurred while processing the EventUserDescription {Id}: {Message}", context.QueueEntry.Id, ex.Message); + return JobResult.FromException(ex); } - - private async Task ProcessUserDescriptionAsync(EventUserDescription description) { - var ev = (await _eventRepository.GetByReferenceIdAsync(description.ProjectId, description.ReferenceId).AnyContext()).Documents.FirstOrDefault(); - if (ev == null) - throw new DocumentNotFoundException(description.ReferenceId); - var ud = new UserDescription { - EmailAddress = description.EmailAddress, - Description = description.Description - }; + return JobResult.Success; + } - if (description.Data.Count > 0) - ev.Data.AddRange(description.Data); + private async Task ProcessUserDescriptionAsync(EventUserDescription description) { + var ev = (await _eventRepository.GetByReferenceIdAsync(description.ProjectId, description.ReferenceId).AnyContext()).Documents.FirstOrDefault(); + if (ev == null) + throw new DocumentNotFoundException(description.ReferenceId); - ev.SetUserDescription(ud); + var ud = new UserDescription { + EmailAddress = description.EmailAddress, + Description = description.Description + }; - await _eventRepository.SaveAsync(ev).AnyContext(); - } + if (description.Data.Count > 0) + ev.Data.AddRange(description.Data); + + ev.SetUserDescription(ud); + + await _eventRepository.SaveAsync(ev).AnyContext(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/MailMessageJob.cs b/src/Exceptionless.Core/Jobs/MailMessageJob.cs index c83a1176ec..b9ad58c78c 100644 --- a/src/Exceptionless.Core/Jobs/MailMessageJob.cs +++ b/src/Exceptionless.Core/Jobs/MailMessageJob.cs @@ -1,32 +1,31 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Mail; using Exceptionless.Core.Queues.Models; using Foundatio.Jobs; using Foundatio.Queues; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Sends queued email messages.", InitialDelay = "5s")] - public class MailMessageJob : QueueJobBase { - private readonly IMailSender _mailSender; +namespace Exceptionless.Core.Jobs; - public MailMessageJob(IQueue queue, IMailSender mailSender, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { - _mailSender = mailSender; - } +[Job(Description = "Sends queued email messages.", InitialDelay = "5s")] +public class MailMessageJob : QueueJobBase { + private readonly IMailSender _mailSender; - protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - _logger.LogTrace("Processing message {Id}.", context.QueueEntry.Id); + public MailMessageJob(IQueue queue, IMailSender mailSender, ILoggerFactory loggerFactory = null) : base(queue, loggerFactory) { + _mailSender = mailSender; + } - try { - await _mailSender.SendAsync(context.QueueEntry.Value).AnyContext(); - _logger.LogInformation("Sent message: to={To} subject={Subject}", context.QueueEntry.Value.To, context.QueueEntry.Value.Subject); - } catch (Exception ex) { - return JobResult.FromException(ex); - } + protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + _logger.LogTrace("Processing message {Id}.", context.QueueEntry.Id); - return JobResult.Success; + try { + await _mailSender.SendAsync(context.QueueEntry.Value).AnyContext(); + _logger.LogInformation("Sent message: to={To} subject={Subject}", context.QueueEntry.Value.To, context.QueueEntry.Value.Subject); } + catch (Exception ex) { + return JobResult.FromException(ex); + } + + return JobResult.Success; } } diff --git a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs index ad118a8e00..5551c0e0f5 100644 --- a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs +++ b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Services; using Foundatio.Caching; using Foundatio.Jobs; @@ -10,38 +7,38 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Update event occurrence count for stacks.", InitialDelay = "2s", Interval = "5s")] - public class StackEventCountJob : JobWithLockBase, IHealthCheck { - private readonly StackService _stackService; - private readonly ILockProvider _lockProvider; - private DateTime? _lastRun; - - public StackEventCountJob(StackService stackService, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _stackService = stackService; - _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(5)); - } - - protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), new CancellationToken(true)); - } - - protected override async Task RunInternalAsync(JobContext context) { - _lastRun = SystemClock.UtcNow; - _logger.LogTrace("Start save stack event counts."); - await _stackService.SaveStackUsagesAsync(cancellationToken: context.CancellationToken).AnyContext(); - _logger.LogTrace("Finished save stack event counts."); - return JobResult.Success; - } - - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); - - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromSeconds(15)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 15 seconds.")); - - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 15 seconds.")); - } +namespace Exceptionless.Core.Jobs; + +[Job(Description = "Update event occurrence count for stacks.", InitialDelay = "2s", Interval = "5s")] +public class StackEventCountJob : JobWithLockBase, IHealthCheck { + private readonly StackService _stackService; + private readonly ILockProvider _lockProvider; + private DateTime? _lastRun; + + public StackEventCountJob(StackService stackService, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _stackService = stackService; + _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(5)); + } + + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { + return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), new CancellationToken(true)); + } + + protected override async Task RunInternalAsync(JobContext context) { + _lastRun = SystemClock.UtcNow; + _logger.LogTrace("Start save stack event counts."); + await _stackService.SaveStackUsagesAsync(cancellationToken: context.CancellationToken).AnyContext(); + _logger.LogTrace("Finished save stack event counts."); + return JobResult.Success; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromSeconds(15)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last 15 seconds.")); + + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last 15 seconds.")); } } diff --git a/src/Exceptionless.Core/Jobs/StackStatusJob.cs b/src/Exceptionless.Core/Jobs/StackStatusJob.cs index e840a0968c..f8aad38ade 100644 --- a/src/Exceptionless.Core/Jobs/StackStatusJob.cs +++ b/src/Exceptionless.Core/Jobs/StackStatusJob.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories; using Foundatio.Caching; using Foundatio.Jobs; @@ -11,57 +8,57 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs { - [Job(Description = "Update stack statuses", InitialDelay = "10s", Interval = "30s")] - public class StackStatusJob : JobWithLockBase, IHealthCheck { - private readonly IStackRepository _stackRepository; - private readonly ILockProvider _lockProvider; - private DateTime? _lastRun; +namespace Exceptionless.Core.Jobs; - public StackStatusJob(IStackRepository stackRepository, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _stackRepository = stackRepository; - _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(10)); - } +[Job(Description = "Update stack statuses", InitialDelay = "10s", Interval = "30s")] +public class StackStatusJob : JobWithLockBase, IHealthCheck { + private readonly IStackRepository _stackRepository; + private readonly ILockProvider _lockProvider; + private DateTime? _lastRun; - protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), new CancellationToken(true)); - } + public StackStatusJob(IStackRepository stackRepository, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _stackRepository = stackRepository; + _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(10)); + } - protected override async Task RunInternalAsync(JobContext context) { - const int LIMIT = 100; - _lastRun = SystemClock.UtcNow; - _logger.LogTrace("Start save stack event counts."); - - // Get list of stacks where snooze has expired - var results = await _stackRepository.GetExpiredSnoozedStatuses(SystemClock.UtcNow, o => o.PageLimit(LIMIT)).AnyContext(); - while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - foreach (var stack in results.Documents) - stack.MarkOpen(); + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { + return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), new CancellationToken(true)); + } - await _stackRepository.SaveAsync(results.Documents).AnyContext(); - - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + protected override async Task RunInternalAsync(JobContext context) { + const int LIMIT = 100; + _lastRun = SystemClock.UtcNow; + _logger.LogTrace("Start save stack event counts."); - if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) - break; + // Get list of stacks where snooze has expired + var results = await _stackRepository.GetExpiredSnoozedStatuses(SystemClock.UtcNow, o => o.PageLimit(LIMIT)).AnyContext(); + while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + foreach (var stack in results.Documents) + stack.MarkOpen(); - if (results.Documents.Count > 0) - await context.RenewLockAsync().AnyContext(); - } - - _logger.LogTrace("Finished save stack event counts."); - return JobResult.Success; - } + await _stackRepository.SaveAsync(results.Documents).AnyContext(); - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (!_lastRun.HasValue) - return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); - if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(1)) - return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last minute.")); + if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) + break; - return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last minute.")); + if (results.Documents.Count > 0) + await context.RenewLockAsync().AnyContext(); } + + _logger.LogTrace("Finished save stack event counts."); + return JobResult.Success; + } + + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (!_lastRun.HasValue) + return Task.FromResult(HealthCheckResult.Healthy("Job has not been run yet.")); + + if (SystemClock.UtcNow.Subtract(_lastRun.Value) > TimeSpan.FromMinutes(1)) + return Task.FromResult(HealthCheckResult.Unhealthy("Job has not run in the last minute.")); + + return Task.FromResult(HealthCheckResult.Healthy("Job has run in the last minute.")); } } diff --git a/src/Exceptionless.Core/Jobs/WebHooksJob.cs b/src/Exceptionless.Core/Jobs/WebHooksJob.cs index b82b0a1d3f..d9cb70d5e5 100644 --- a/src/Exceptionless.Core/Jobs/WebHooksJob.cs +++ b/src/Exceptionless.Core/Jobs/WebHooksJob.cs @@ -1,9 +1,5 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Queues.Models; @@ -20,156 +16,162 @@ #nullable enable -namespace Exceptionless.Core.Jobs { - [Job(Description = "Processes queued web hook messages.", InitialDelay = "5s")] - public class WebHooksJob : QueueJobBase, IDisposable { - private const string ConsecutiveErrorsCacheKey = "errors"; - private const string FirstAttemptCacheKey = "first-attempt"; - private const string LastAttemptCacheKey = "last-attempt"; - private static readonly string[] _cacheKeys = { ConsecutiveErrorsCacheKey, FirstAttemptCacheKey, LastAttemptCacheKey }; - - private readonly IProjectRepository _projectRepository; - private readonly SlackService _slackService; - private readonly IWebHookRepository _webHookRepository; - private readonly ICacheClient _cacheClient; - private readonly JsonSerializerSettings _jsonSerializerSettings; - private readonly AppOptions _appOptions; - - private HttpClient? _client; - - private HttpClient Client { - get => _client ??= new HttpClient(); - } +namespace Exceptionless.Core.Jobs; - public WebHooksJob(IQueue queue, IProjectRepository projectRepository, SlackService slackService, IWebHookRepository webHookRepository, ICacheClient cacheClient, JsonSerializerSettings settings, AppOptions appOptions, ILoggerFactory? loggerFactory = null) : base(queue, loggerFactory) { - _projectRepository = projectRepository; - _slackService = slackService; - _webHookRepository = webHookRepository; - _cacheClient = cacheClient; - _jsonSerializerSettings = settings; - _appOptions = appOptions; - } +[Job(Description = "Processes queued web hook messages.", InitialDelay = "5s")] +public class WebHooksJob : QueueJobBase, IDisposable { + private const string ConsecutiveErrorsCacheKey = "errors"; + private const string FirstAttemptCacheKey = "first-attempt"; + private const string LastAttemptCacheKey = "last-attempt"; + private static readonly string[] _cacheKeys = { ConsecutiveErrorsCacheKey, FirstAttemptCacheKey, LastAttemptCacheKey }; + + private readonly IProjectRepository _projectRepository; + private readonly SlackService _slackService; + private readonly IWebHookRepository _webHookRepository; + private readonly ICacheClient _cacheClient; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly AppOptions _appOptions; + + private HttpClient? _client; + + private HttpClient Client { + get => _client ??= new HttpClient(); + } + + public WebHooksJob(IQueue queue, IProjectRepository projectRepository, SlackService slackService, IWebHookRepository webHookRepository, ICacheClient cacheClient, JsonSerializerSettings settings, AppOptions appOptions, ILoggerFactory? loggerFactory = null) : base(queue, loggerFactory) { + _projectRepository = projectRepository; + _slackService = slackService; + _webHookRepository = webHookRepository; + _cacheClient = cacheClient; + _jsonSerializerSettings = settings; + _appOptions = appOptions; + } - protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var body = context.QueueEntry.Value; - bool shouldLog = body.ProjectId != _appOptions.InternalProjectId; - using (_logger.BeginScope(new ExceptionlessState().Organization(body.OrganizationId).Project(body.ProjectId))) { - if (shouldLog) _logger.RecordWebHook(context.QueueEntry.Id, body.ProjectId, body.Url); + protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + var body = context.QueueEntry.Value; + bool shouldLog = body.ProjectId != _appOptions.InternalProjectId; + using (_logger.BeginScope(new ExceptionlessState().Organization(body.OrganizationId).Project(body.ProjectId))) { + if (shouldLog) _logger.RecordWebHook(context.QueueEntry.Id, body.ProjectId, body.Url); + + if (!await IsEnabledAsync(body).AnyContext()) { + _logger.WebHookCancelled(); + return JobResult.Cancelled; + } - if (!await IsEnabledAsync(body).AnyContext()) { - _logger.WebHookCancelled(); + var cache = new ScopedCacheClient(_cacheClient, GetCacheKeyScope(body)); + long consecutiveErrors = await cache.GetAsync(ConsecutiveErrorsCacheKey, 0).AnyContext(); + if (consecutiveErrors > 10) { + var lastAttempt = await cache.GetAsync(LastAttemptCacheKey, SystemClock.UtcNow).AnyContext(); + var nextAttemptAllowedAt = lastAttempt.AddMinutes(15); + if (nextAttemptAllowedAt >= SystemClock.UtcNow) { + _logger.WebHookCancelledBackoff(consecutiveErrors, nextAttemptAllowedAt); return JobResult.Cancelled; } - - var cache = new ScopedCacheClient(_cacheClient, GetCacheKeyScope(body)); - long consecutiveErrors = await cache.GetAsync(ConsecutiveErrorsCacheKey, 0).AnyContext(); - if (consecutiveErrors > 10) { - var lastAttempt = await cache.GetAsync(LastAttemptCacheKey, SystemClock.UtcNow).AnyContext(); - var nextAttemptAllowedAt = lastAttempt.AddMinutes(15); - if (nextAttemptAllowedAt >= SystemClock.UtcNow) { - _logger.WebHookCancelledBackoff(consecutiveErrors, nextAttemptAllowedAt); - return JobResult.Cancelled; + } + + bool successful = true; + HttpResponseMessage? response = null; + try { + using (var timeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5))) { + using (var postCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken, timeoutCancellationTokenSource.Token)) { + response = await Client.PostAsJsonAsync(body.Url, body.Data.ToJson(Formatting.Indented, _jsonSerializerSettings), postCancellationTokenSource.Token).AnyContext(); + if (!response.IsSuccessStatusCode) + successful = false; + else if (consecutiveErrors > 0) + await cache.RemoveAllAsync(_cacheKeys).AnyContext(); } } - - bool successful = true; - HttpResponseMessage? response = null; - try { - using (var timeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5))) { - using (var postCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken, timeoutCancellationTokenSource.Token)) { - response = await Client.PostAsJsonAsync(body.Url, body.Data.ToJson(Formatting.Indented, _jsonSerializerSettings), postCancellationTokenSource.Token).AnyContext(); - if (!response.IsSuccessStatusCode) - successful = false; - else if (consecutiveErrors > 0) - await cache.RemoveAllAsync(_cacheKeys).AnyContext(); - } + } + catch (OperationCanceledException ex) { + successful = false; + if (shouldLog) _logger.WebHookTimeout(response?.StatusCode, body.OrganizationId, body.ProjectId, body.Url, ex); + return JobResult.Cancelled; + } + catch (Exception ex) { + successful = false; + if (shouldLog) _logger.WebHookError(response?.StatusCode, body.OrganizationId, body.ProjectId, body.Url, ex); + return JobResult.FromException(ex); + } + finally { + if (successful) { + _logger.WebHookComplete(response?.StatusCode, body.OrganizationId, body.ProjectId, body.Url); + } + else if (response != null && (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Gone)) { + _logger.WebHookDisabledStatusCode(body.Type == WebHookType.Slack ? "Slack" : body.WebHookId, response.StatusCode, body.OrganizationId, body.ProjectId, body.Url); + await DisableIntegrationAsync(body).AnyContext(); + await cache.RemoveAllAsync(_cacheKeys).AnyContext(); + } + else { + var now = SystemClock.UtcNow; + await cache.SetAsync(LastAttemptCacheKey, now, TimeSpan.FromDays(3)).AnyContext(); + consecutiveErrors = await cache.IncrementAsync(ConsecutiveErrorsCacheKey, TimeSpan.FromDays(3)).AnyContext(); + DateTime firstAttempt; + if (consecutiveErrors == 1) { + await cache.SetAsync(FirstAttemptCacheKey, now, TimeSpan.FromDays(3)).AnyContext(); + firstAttempt = now; } - } catch (OperationCanceledException ex) { - successful = false; - if (shouldLog) _logger.WebHookTimeout(response?.StatusCode, body.OrganizationId, body.ProjectId, body.Url, ex); - return JobResult.Cancelled; - } catch (Exception ex) { - successful = false; - if (shouldLog) _logger.WebHookError(response?.StatusCode, body.OrganizationId, body.ProjectId, body.Url, ex); - return JobResult.FromException(ex); - } finally { - if (successful) { - _logger.WebHookComplete(response?.StatusCode, body.OrganizationId, body.ProjectId, body.Url); - } else if (response != null && (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Gone)) { - _logger.WebHookDisabledStatusCode(body.Type == WebHookType.Slack ? "Slack" : body.WebHookId, response.StatusCode, body.OrganizationId, body.ProjectId, body.Url); - await DisableIntegrationAsync(body).AnyContext(); - await cache.RemoveAllAsync(_cacheKeys).AnyContext(); - } else { - var now = SystemClock.UtcNow; - await cache.SetAsync(LastAttemptCacheKey, now, TimeSpan.FromDays(3)).AnyContext(); - consecutiveErrors = await cache.IncrementAsync(ConsecutiveErrorsCacheKey, TimeSpan.FromDays(3)).AnyContext(); - DateTime firstAttempt; - if (consecutiveErrors == 1) { - await cache.SetAsync(FirstAttemptCacheKey, now, TimeSpan.FromDays(3)).AnyContext(); - firstAttempt = now; - } else { - firstAttempt = await cache.GetAsync(FirstAttemptCacheKey, now).AnyContext(); - } + else { + firstAttempt = await cache.GetAsync(FirstAttemptCacheKey, now).AnyContext(); + } + + if (consecutiveErrors >= 10) { + // don't retry any more + context.QueueEntry.MarkCompleted(); - if (consecutiveErrors >= 10) { - // don't retry any more - context.QueueEntry.MarkCompleted(); - - // disable if more than 10 consecutive errors over the course of multiple days - if (firstAttempt.IsBefore(now.SubtractDays(2))) { - _logger.WebHookDisabledTooManyErrors(body.Type == WebHookType.Slack ? "Slack" : body.WebHookId); - await DisableIntegrationAsync(body).AnyContext(); - await cache.RemoveAllAsync(_cacheKeys).AnyContext(); - } + // disable if more than 10 consecutive errors over the course of multiple days + if (firstAttempt.IsBefore(now.SubtractDays(2))) { + _logger.WebHookDisabledTooManyErrors(body.Type == WebHookType.Slack ? "Slack" : body.WebHookId); + await DisableIntegrationAsync(body).AnyContext(); + await cache.RemoveAllAsync(_cacheKeys).AnyContext(); } } } } - - return JobResult.Success; } - private async Task IsEnabledAsync(WebHookNotification body) { - switch (body.Type) { - case WebHookType.General: - var webHook = await _webHookRepository.GetByIdAsync(body.WebHookId, o => o.Cache()).AnyContext(); - return webHook?.IsEnabled ?? false; - case WebHookType.Slack: - var project = await _projectRepository.GetByIdAsync(body.ProjectId, o => o.Cache()).AnyContext(); - var token = project?.GetSlackToken(); - return token != null; - } + return JobResult.Success; + } - return false; - } - - private async Task DisableIntegrationAsync(WebHookNotification body) { - switch (body.Type) { - case WebHookType.General: - await _webHookRepository.MarkDisabledAsync(body.WebHookId).AnyContext(); - break; - case WebHookType.Slack: - var project = await _projectRepository.GetByIdAsync(body.ProjectId).AnyContext(); - var token = project?.GetSlackToken(); - if (token == null) - return; - - Debug.Assert(project != null); - - await _slackService.RevokeAccessTokenAsync(token.AccessToken).AnyContext(); - if (project.NotificationSettings.Remove(Project.NotificationIntegrations.Slack) | project.Data.Remove(Project.KnownDataKeys.SlackToken)) - await _projectRepository.SaveAsync(project, o => o.Cache()); - - break; - } - } - - private static string GetCacheKeyScope(WebHookNotification body) { - return String.Concat("Project:", body.ProjectId, ":webhook:", body.Type == WebHookType.Slack ? "slack" : body.WebHookId); + private async Task IsEnabledAsync(WebHookNotification body) { + switch (body.Type) { + case WebHookType.General: + var webHook = await _webHookRepository.GetByIdAsync(body.WebHookId, o => o.Cache()).AnyContext(); + return webHook?.IsEnabled ?? false; + case WebHookType.Slack: + var project = await _projectRepository.GetByIdAsync(body.ProjectId, o => o.Cache()).AnyContext(); + var token = project?.GetSlackToken(); + return token != null; } - public void Dispose() { - _client?.Dispose(); + return false; + } + + private async Task DisableIntegrationAsync(WebHookNotification body) { + switch (body.Type) { + case WebHookType.General: + await _webHookRepository.MarkDisabledAsync(body.WebHookId).AnyContext(); + break; + case WebHookType.Slack: + var project = await _projectRepository.GetByIdAsync(body.ProjectId).AnyContext(); + var token = project?.GetSlackToken(); + if (token == null) + return; + + Debug.Assert(project != null); + + await _slackService.RevokeAccessTokenAsync(token.AccessToken).AnyContext(); + if (project.NotificationSettings.Remove(Project.NotificationIntegrations.Slack) | project.Data.Remove(Project.KnownDataKeys.SlackToken)) + await _projectRepository.SaveAsync(project, o => o.Cache()); + + break; } } -} \ No newline at end of file + + private static string GetCacheKeyScope(WebHookNotification body) { + return String.Concat("Project:", body.ProjectId, ":webhook:", body.Type == WebHookType.Slack ? "slack" : body.WebHookId); + } + + public void Dispose() { + _client?.Dispose(); + } +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs index 24d1484c3e..2941a98f38 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; @@ -15,65 +11,65 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers { - public class OrganizationMaintenanceWorkItemHandler : WorkItemHandlerBase { - private readonly IOrganizationRepository _organizationRepository; - private readonly BillingManager _billingManager; - private readonly ILockProvider _lockProvider; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public OrganizationMaintenanceWorkItemHandler(IOrganizationRepository organizationRepository, ICacheClient cacheClient, IMessageBus messageBus, BillingManager billingManager, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _organizationRepository = organizationRepository; - _billingManager = billingManager; - _lockProvider = new CacheLockProvider(cacheClient, messageBus); - } +public class OrganizationMaintenanceWorkItemHandler : WorkItemHandlerBase { + private readonly IOrganizationRepository _organizationRepository; + private readonly BillingManager _billingManager; + private readonly ILockProvider _lockProvider; - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { - return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); - } + public OrganizationMaintenanceWorkItemHandler(IOrganizationRepository organizationRepository, ICacheClient cacheClient, IMessageBus messageBus, BillingManager billingManager, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _organizationRepository = organizationRepository; + _billingManager = billingManager; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } + + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); + } - public override async Task HandleItemAsync(WorkItemContext context) { - const int LIMIT = 100; - var wi = context.GetData(); - Log.LogInformation("Received upgrade organizations work item. Upgrade Plans: {UpgradePlans}", wi.UpgradePlans); + public override async Task HandleItemAsync(WorkItemContext context) { + const int LIMIT = 100; + var wi = context.GetData(); + Log.LogInformation("Received upgrade organizations work item. Upgrade Plans: {UpgradePlans}", wi.UpgradePlans); - var results = await _organizationRepository.GetAllAsync(o => o.PageLimit(LIMIT)).AnyContext(); - while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - foreach (var organization in results.Documents) { - if (wi.UpgradePlans) - UpgradePlan(organization); + var results = await _organizationRepository.GetAllAsync(o => o.PageLimit(LIMIT)).AnyContext(); + while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + foreach (var organization in results.Documents) { + if (wi.UpgradePlans) + UpgradePlan(organization); - if (wi.RemoveOldUsageStats) { - foreach (var usage in organization.OverageHours.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(3))).ToList()) - organization.OverageHours.Remove(usage); + if (wi.RemoveOldUsageStats) { + foreach (var usage in organization.OverageHours.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(3))).ToList()) + organization.OverageHours.Remove(usage); - foreach (var usage in organization.Usage.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(366))).ToList()) - organization.Usage.Remove(usage); - } + foreach (var usage in organization.Usage.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(366))).ToList()) + organization.Usage.Remove(usage); } + } - if (wi.UpgradePlans || wi.RemoveOldUsageStats) - await _organizationRepository.SaveAsync(results.Documents).AnyContext(); + if (wi.UpgradePlans || wi.RemoveOldUsageStats) + await _organizationRepository.SaveAsync(results.Documents).AnyContext(); - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); - if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) - break; + if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) + break; - if (results.Documents.Count > 0) - await context.RenewLockAsync().AnyContext(); - } - + if (results.Documents.Count > 0) + await context.RenewLockAsync().AnyContext(); } - private void UpgradePlan(Organization organization) { - var plan = _billingManager.GetBillingPlan(organization.PlanId); - if (plan == null) { - Log.LogError("Unable to find a valid plan for organization: {organization}", organization.Id); - return; - } + } - _billingManager.ApplyBillingPlan(organization, plan, user: null, updateBillingPrice: false); + private void UpgradePlan(Organization organization) { + var plan = _billingManager.GetBillingPlan(organization.PlanId); + if (plan == null) { + Log.LogError("Unable to find a valid plan for organization: {organization}", organization.Id); + return; } + + _billingManager.ApplyBillingPlan(organization, plan, user: null, updateBillingPrice: false); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs index 1b35cf878e..5f9ac72c15 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Mail; using Exceptionless.Core.Messaging.Models; @@ -16,78 +13,78 @@ using Foundatio.Repositories; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers { - public class EnqueueOrganizationNotificationOnPlanOverage : IStartupAction { - private readonly IQueue _workItemQueue; - private readonly IMessageSubscriber _subscriber; - private readonly ILogger _logger; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public EnqueueOrganizationNotificationOnPlanOverage(IQueue workItemQueue, IMessageSubscriber subscriber, ILoggerFactory loggerFactory = null) { - _workItemQueue = workItemQueue; - _subscriber = subscriber; - _logger = loggerFactory.CreateLogger(); - } +public class EnqueueOrganizationNotificationOnPlanOverage : IStartupAction { + private readonly IQueue _workItemQueue; + private readonly IMessageSubscriber _subscriber; + private readonly ILogger _logger; - public Task RunAsync(CancellationToken token) { - return _subscriber.SubscribeAsync(overage => { - _logger.LogInformation("Enqueueing plan overage work item for organization: {OrganizationId} IsOverHourlyLimit: {IsOverHourlyLimit} IsOverMonthlyLimit: {IsOverMonthlyLimit}", overage.OrganizationId, overage.IsHourly, !overage.IsHourly); + public EnqueueOrganizationNotificationOnPlanOverage(IQueue workItemQueue, IMessageSubscriber subscriber, ILoggerFactory loggerFactory = null) { + _workItemQueue = workItemQueue; + _subscriber = subscriber; + _logger = loggerFactory.CreateLogger(); + } - return _workItemQueue.EnqueueAsync(new OrganizationNotificationWorkItem { - OrganizationId = overage.OrganizationId, - IsOverHourlyLimit = overage.IsHourly, - IsOverMonthlyLimit = !overage.IsHourly - }); - }, token); - } + public Task RunAsync(CancellationToken token) { + return _subscriber.SubscribeAsync(overage => { + _logger.LogInformation("Enqueueing plan overage work item for organization: {OrganizationId} IsOverHourlyLimit: {IsOverHourlyLimit} IsOverMonthlyLimit: {IsOverMonthlyLimit}", overage.OrganizationId, overage.IsHourly, !overage.IsHourly); + + return _workItemQueue.EnqueueAsync(new OrganizationNotificationWorkItem { + OrganizationId = overage.OrganizationId, + IsOverHourlyLimit = overage.IsHourly, + IsOverMonthlyLimit = !overage.IsHourly + }); + }, token); } +} - public class OrganizationNotificationWorkItemHandler : WorkItemHandlerBase { - private readonly IOrganizationRepository _organizationRepository; - private readonly IUserRepository _userRepository; - private readonly IMailer _mailer; - private readonly ILockProvider _lockProvider; +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, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _organizationRepository = organizationRepository; - _userRepository = userRepository; - _mailer = mailer; - _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromHours(1)); - } + public OrganizationNotificationWorkItemHandler(IOrganizationRepository organizationRepository, IUserRepository userRepository, IMailer mailer, ICacheClient cacheClient, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _organizationRepository = organizationRepository; + _userRepository = userRepository; + _mailer = mailer; + _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromHours(1)); + } - public override Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); - string cacheKey = $"{nameof(OrganizationNotificationWorkItemHandler)}:{wi.OrganizationId}"; - - return _lockProvider.TryUsingAsync(cacheKey, async () => { - Log.LogInformation("Received organization notification work item for: {organization} IsOverHourlyLimit: {IsOverHourlyLimit} IsOverMonthlyLimit: {IsOverMonthlyLimit}", wi.OrganizationId, wi.IsOverHourlyLimit, wi.IsOverMonthlyLimit); + public override Task HandleItemAsync(WorkItemContext context) { + var wi = context.GetData(); + string cacheKey = $"{nameof(OrganizationNotificationWorkItemHandler)}:{wi.OrganizationId}"; - var organization = await _organizationRepository.GetByIdAsync(wi.OrganizationId, o => o.Cache()).AnyContext(); - if (organization == null) - return; + return _lockProvider.TryUsingAsync(cacheKey, async () => { + Log.LogInformation("Received organization notification work item for: {organization} IsOverHourlyLimit: {IsOverHourlyLimit} IsOverMonthlyLimit: {IsOverMonthlyLimit}", wi.OrganizationId, wi.IsOverHourlyLimit, wi.IsOverMonthlyLimit); - if (wi.IsOverMonthlyLimit) - await SendOverageNotificationsAsync(organization, wi.IsOverHourlyLimit, wi.IsOverMonthlyLimit).AnyContext(); - }, TimeSpan.FromMinutes(15), new CancellationToken(true)); - } + var organization = await _organizationRepository.GetByIdAsync(wi.OrganizationId, o => o.Cache()).AnyContext(); + if (organization == null) + return; - 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) { - Log.LogInformation("User {user} with email address {EmailAddress} has not been verified.", user.Id, user.EmailAddress); - continue; - } + if (wi.IsOverMonthlyLimit) + await SendOverageNotificationsAsync(organization, wi.IsOverHourlyLimit, wi.IsOverMonthlyLimit).AnyContext(); + }, TimeSpan.FromMinutes(15), new CancellationToken(true)); + } - if (!user.EmailNotificationsEnabled) { - Log.LogInformation("User {user} with email address {EmailAddress} has email notifications disabled.", user.Id, user.EmailAddress); - continue; - } + 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) { + Log.LogInformation("User {user} with email address {EmailAddress} has not been verified.", user.Id, user.EmailAddress); + continue; + } - Log.LogTrace("Sending email to {EmailAddress}...", user.EmailAddress); - await _mailer.SendOrganizationNoticeAsync(user, organization, isOverMonthlyLimit, isOverHourlyLimit).AnyContext(); + if (!user.EmailNotificationsEnabled) { + Log.LogInformation("User {user} with email address {EmailAddress} has email notifications disabled.", user.Id, user.EmailAddress); + continue; } - Log.LogTrace("Done sending email."); + Log.LogTrace("Sending email to {EmailAddress}...", user.EmailAddress); + await _mailer.SendOrganizationNoticeAsync(user, organization, isOverMonthlyLimit, isOverHourlyLimit).AnyContext(); } + + Log.LogTrace("Done sending email."); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs index 14878e68d4..972ae80bb9 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Repositories; @@ -13,56 +9,56 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers { - public class ProjectMaintenanceWorkItemHandler : WorkItemHandlerBase { - private readonly IProjectRepository _projectRepository; - private readonly ILockProvider _lockProvider; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public ProjectMaintenanceWorkItemHandler(IProjectRepository projectRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _projectRepository = projectRepository; - _lockProvider = new CacheLockProvider(cacheClient, messageBus); - } +public class ProjectMaintenanceWorkItemHandler : WorkItemHandlerBase { + private readonly IProjectRepository _projectRepository; + private readonly ILockProvider _lockProvider; - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { - return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); - } + public ProjectMaintenanceWorkItemHandler(IProjectRepository projectRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _projectRepository = projectRepository; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } - public override async Task HandleItemAsync(WorkItemContext context) { - const int LIMIT = 100; + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + } - var workItem = context.GetData(); - Log.LogInformation("Received upgrade projects work item. Update Default Bot List: {UpdateDefaultBotList} IncrementConfigurationVersion: {IncrementConfigurationVersion}", workItem.UpdateDefaultBotList, workItem.IncrementConfigurationVersion); + public override async Task HandleItemAsync(WorkItemContext context) { + const int LIMIT = 100; - var results = await _projectRepository.GetAllAsync(o => o.PageLimit(LIMIT)).AnyContext(); - while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - foreach (var project in results.Documents) { - if (workItem.UpdateDefaultBotList) - project.SetDefaultUserAgentBotPatterns(); + var workItem = context.GetData(); + Log.LogInformation("Received upgrade projects work item. Update Default Bot List: {UpdateDefaultBotList} IncrementConfigurationVersion: {IncrementConfigurationVersion}", workItem.UpdateDefaultBotList, workItem.IncrementConfigurationVersion); - if (workItem.IncrementConfigurationVersion) - project.Configuration.IncrementVersion(); + var results = await _projectRepository.GetAllAsync(o => o.PageLimit(LIMIT)).AnyContext(); + while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + foreach (var project in results.Documents) { + if (workItem.UpdateDefaultBotList) + project.SetDefaultUserAgentBotPatterns(); - if (workItem.RemoveOldUsageStats) { - foreach (var usage in project.OverageHours.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(3))).ToList()) - project.OverageHours.Remove(usage); + if (workItem.IncrementConfigurationVersion) + project.Configuration.IncrementVersion(); - foreach (var usage in project.Usage.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(366))).ToList()) - project.Usage.Remove(usage); - } + if (workItem.RemoveOldUsageStats) { + foreach (var usage in project.OverageHours.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(3))).ToList()) + project.OverageHours.Remove(usage); + + foreach (var usage in project.Usage.Where(u => u.Date < SystemClock.UtcNow.Subtract(TimeSpan.FromDays(366))).ToList()) + project.Usage.Remove(usage); } + } - if (workItem.UpdateDefaultBotList || workItem.IncrementConfigurationVersion || workItem.RemoveOldUsageStats) - await _projectRepository.SaveAsync(results.Documents).AnyContext(); + if (workItem.UpdateDefaultBotList || workItem.IncrementConfigurationVersion || workItem.RemoveOldUsageStats) + await _projectRepository.SaveAsync(results.Documents).AnyContext(); - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); - if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) - break; + if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) + break; - if (results.Documents.Count > 0) - await context.RenewLockAsync().AnyContext(); - } + if (results.Documents.Count > 0) + await context.RenewLockAsync().AnyContext(); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs index 7683f1f62d..f4b81b2cfd 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Repositories; @@ -10,31 +7,30 @@ using Foundatio.Messaging; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers -{ - public class RemoveBotEventsWorkItemHandler : WorkItemHandlerBase { - private readonly IEventRepository _eventRepository; - private readonly ILockProvider _lockProvider; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _eventRepository = eventRepository; - _lockProvider = new CacheLockProvider(cacheClient, messageBus); - } +public class RemoveBotEventsWorkItemHandler : WorkItemHandlerBase { + private readonly IEventRepository _eventRepository; + private readonly ILockProvider _lockProvider; - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { - var wi = (RemoveBotEventsWorkItem)workItem; - string cacheKey = $"{nameof(RemoveBotEventsWorkItem)}:{wi.OrganizationId}:{wi.ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); - } + public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _eventRepository = eventRepository; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } + + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + var wi = (RemoveBotEventsWorkItem)workItem; + string cacheKey = $"{nameof(RemoveBotEventsWorkItem)}:{wi.OrganizationId}:{wi.ProjectId}"; + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); + } + + public override async Task HandleItemAsync(WorkItemContext context) { + var wi = context.GetData(); + using var _ = Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId)); + Log.LogInformation("Received remove bot events work item OrganizationId={OrganizationId} ProjectId={ProjectId}, ClientIpAddress={ClientIpAddress}, UtcStartDate={UtcStartDate}, UtcEndDate={UtcEndDate}", wi.OrganizationId, wi.ProjectId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); - public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); - using var _ = Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId)); - Log.LogInformation("Received remove bot events work item OrganizationId={OrganizationId} ProjectId={ProjectId}, ClientIpAddress={ClientIpAddress}, UtcStartDate={UtcStartDate}, UtcEndDate={UtcEndDate}", wi.OrganizationId, wi.ProjectId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); - - await context.ReportProgressAsync(0, $"Starting deleting of bot events... OrganizationId={wi.OrganizationId}").AnyContext(); - long deleted = await _eventRepository.RemoveAllAsync(wi.OrganizationId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate).AnyContext(); - await context.ReportProgressAsync(100, $"Bot events deleted: {deleted} OrganizationId={wi.OrganizationId}").AnyContext(); - } + await context.ReportProgressAsync(0, $"Starting deleting of bot events... OrganizationId={wi.OrganizationId}").AnyContext(); + long deleted = await _eventRepository.RemoveAllAsync(wi.OrganizationId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate).AnyContext(); + await context.ReportProgressAsync(100, $"Bot events deleted: {deleted} OrganizationId={wi.OrganizationId}").AnyContext(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs index c3504d7577..80863c5dca 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Repositories; using Foundatio.Caching; @@ -10,32 +7,32 @@ using Foundatio.Messaging; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers { - public class RemoveStacksWorkItemHandler : WorkItemHandlerBase { - private readonly IStackRepository _stackRepository; - private readonly ILockProvider _lockProvider; - private readonly ICacheClient _cacheClient; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _stackRepository = stackRepository; - _cacheClient = cacheClient; - _lockProvider = new CacheLockProvider(cacheClient, messageBus); - } +public class RemoveStacksWorkItemHandler : WorkItemHandlerBase { + private readonly IStackRepository _stackRepository; + private readonly ILockProvider _lockProvider; + private readonly ICacheClient _cacheClient; - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { - string cacheKey = $"{nameof(RemoveStacksWorkItem)}:{((RemoveStacksWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); - } + public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _stackRepository = stackRepository; + _cacheClient = cacheClient; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } + + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + string cacheKey = $"{nameof(RemoveStacksWorkItem)}:{((RemoveStacksWorkItem)workItem).ProjectId}"; + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + } - public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); - using (Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId))) { - Log.LogInformation("Received remove stacks work item for project: {ProjectId}", wi.ProjectId); - await context.ReportProgressAsync(0, "Starting soft deleting of stacks...").AnyContext(); - long deleted = await _stackRepository.SoftDeleteByProjectIdAsync(wi.OrganizationId, wi.ProjectId).AnyContext(); - await _cacheClient.RemoveByPrefixAsync(String.Concat("stack-filter:", wi.OrganizationId, ":", wi.ProjectId)); - await context.ReportProgressAsync(100, $"Stacks soft deleted: {deleted}").AnyContext(); - } + public override async Task HandleItemAsync(WorkItemContext context) { + var wi = context.GetData(); + using (Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId))) { + Log.LogInformation("Received remove stacks work item for project: {ProjectId}", wi.ProjectId); + await context.ReportProgressAsync(0, "Starting soft deleting of stacks...").AnyContext(); + long deleted = await _stackRepository.SoftDeleteByProjectIdAsync(wi.OrganizationId, wi.ProjectId).AnyContext(); + await _cacheClient.RemoveByPrefixAsync(String.Concat("stack-filter:", wi.OrganizationId, ":", wi.ProjectId)); + await context.ReportProgressAsync(100, $"Stacks soft deleted: {deleted}").AnyContext(); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs index f58a34d596..303f1aa1b6 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.AppStats; +using Exceptionless.Core.AppStats; using Exceptionless.Core.Extensions; using Exceptionless.Core.Geo; using Exceptionless.Core.Models.Data; @@ -14,55 +11,56 @@ using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers { - public class SetLocationFromGeoWorkItemHandler : WorkItemHandlerBase { - private readonly ICacheClient _cache; - private readonly IEventRepository _eventRepository; - private readonly IGeocodeService _geocodeService; - private readonly IMetricsClient _metricsClient; - private readonly ILockProvider _lockProvider; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventRepository eventRepository, IGeocodeService geocodeService, IMetricsClient metricsClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _cache = new ScopedCacheClient(cacheClient, "Geo"); - _eventRepository = eventRepository; - _geocodeService = geocodeService; - _metricsClient = metricsClient; - _lockProvider = new CacheLockProvider(cacheClient, messageBus); - } +public class SetLocationFromGeoWorkItemHandler : WorkItemHandlerBase { + private readonly ICacheClient _cache; + private readonly IEventRepository _eventRepository; + private readonly IGeocodeService _geocodeService; + private readonly IMetricsClient _metricsClient; + private readonly ILockProvider _lockProvider; - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { - string cacheKey = $"{nameof(SetLocationFromGeoWorkItemHandler)}:{((SetLocationFromGeoWorkItem)workItem).EventId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); - } + public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventRepository eventRepository, IGeocodeService geocodeService, IMetricsClient metricsClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _cache = new ScopedCacheClient(cacheClient, "Geo"); + _eventRepository = eventRepository; + _geocodeService = geocodeService; + _metricsClient = metricsClient; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } + + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + string cacheKey = $"{nameof(SetLocationFromGeoWorkItemHandler)}:{((SetLocationFromGeoWorkItem)workItem).EventId}"; + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + } - public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); + public override async Task HandleItemAsync(WorkItemContext context) { + var workItem = context.GetData(); - if (!GeoResult.TryParse(workItem.Geo, out var result)) - return; + if (!GeoResult.TryParse(workItem.Geo, out var result)) + return; - var location = await _cache.GetAsync(workItem.Geo, null).AnyContext(); - if (location == null) { - try { - result = await _geocodeService.ReverseGeocodeAsync(result.Latitude.GetValueOrDefault(), result.Longitude.GetValueOrDefault()).AnyContext(); - location = result.ToLocation(); - _metricsClient.Counter(MetricNames.UsageGeocodingApi); - } catch (Exception ex) { - Log.LogError(ex, "Error occurred looking up reverse geocode: {Geo}", workItem.Geo); - } + var location = await _cache.GetAsync(workItem.Geo, null).AnyContext(); + if (location == null) { + try { + result = await _geocodeService.ReverseGeocodeAsync(result.Latitude.GetValueOrDefault(), result.Longitude.GetValueOrDefault()).AnyContext(); + location = result.ToLocation(); + _metricsClient.Counter(MetricNames.UsageGeocodingApi); } - - if (location == null) - return; - - await _cache.SetAsync(workItem.Geo, location, TimeSpan.FromDays(3)).AnyContext(); + catch (Exception ex) { + Log.LogError(ex, "Error occurred looking up reverse geocode: {Geo}", workItem.Geo); + } + } - var ev = await _eventRepository.GetByIdAsync(workItem.EventId).AnyContext(); - if (ev == null) - return; + if (location == null) + return; - ev.SetLocation(location); - await _eventRepository.SaveAsync(ev).AnyContext(); - } + await _cache.SetAsync(workItem.Geo, location, TimeSpan.FromDays(3)).AnyContext(); + + var ev = await _eventRepository.GetByIdAsync(workItem.EventId).AnyContext(); + if (ev == null) + return; + + ev.SetLocation(location); + await _eventRepository.SaveAsync(ev).AnyContext(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs index 003f960733..291e16c418 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Repositories; using Foundatio.Caching; @@ -11,33 +8,33 @@ using Foundatio.Repositories; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers { - public class SetProjectIsConfiguredWorkItemHandler : WorkItemHandlerBase { - private readonly IProjectRepository _projectRepository; - private readonly IEventRepository _eventRepository; - private readonly ILockProvider _lockProvider; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepository, IEventRepository eventRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _projectRepository = projectRepository; - _eventRepository = eventRepository; - _lockProvider = new CacheLockProvider(cacheClient, messageBus); - } +public class SetProjectIsConfiguredWorkItemHandler : WorkItemHandlerBase { + private readonly IProjectRepository _projectRepository; + private readonly IEventRepository _eventRepository; + private readonly ILockProvider _lockProvider; - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { - string cacheKey = $"{nameof(SetProjectIsConfiguredWorkItemHandler)}:{((SetProjectIsConfiguredWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); - } + public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepository, IEventRepository eventRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _projectRepository = projectRepository; + _eventRepository = eventRepository; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } + + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + string cacheKey = $"{nameof(SetProjectIsConfiguredWorkItemHandler)}:{((SetProjectIsConfiguredWorkItem)workItem).ProjectId}"; + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + } + + public override async Task HandleItemAsync(WorkItemContext context) { + var workItem = context.GetData(); + Log.LogInformation("Setting Is Configured for project: {project}", workItem.ProjectId); - public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); - Log.LogInformation("Setting Is Configured for project: {project}", workItem.ProjectId); + var project = await _projectRepository.GetByIdAsync(workItem.ProjectId).AnyContext(); + if (project == null || project.IsConfigured.GetValueOrDefault()) + return; - var project = await _projectRepository.GetByIdAsync(workItem.ProjectId).AnyContext(); - if (project == null || project.IsConfigured.GetValueOrDefault()) - return; - - project.IsConfigured = workItem.IsConfigured || await _eventRepository.CountAsync(q => q.Project(project.Id)).AnyContext() > 0; - await _projectRepository.SaveAsync(project, o => o.Cache()).AnyContext(); - } + project.IsConfigured = workItem.IsConfigured || await _eventRepository.CountAsync(q => q.Project(project.Id)).AnyContext() > 0; + await _projectRepository.SaveAsync(project, o => o.Cache()).AnyContext(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs index 580ca9bbbc..47a0da680d 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.WorkItems; @@ -13,55 +10,55 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Jobs.WorkItemHandlers { - public class UserMaintenanceWorkItemHandler : WorkItemHandlerBase { - private readonly IUserRepository _userRepository; - private readonly ILockProvider _lockProvider; +namespace Exceptionless.Core.Jobs.WorkItemHandlers; - public UserMaintenanceWorkItemHandler(IUserRepository userRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { - _userRepository = userRepository; - _lockProvider = new CacheLockProvider(cacheClient, messageBus); - } +public class UserMaintenanceWorkItemHandler : WorkItemHandlerBase { + private readonly IUserRepository _userRepository; + private readonly ILockProvider _lockProvider; - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { - return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); - } + public UserMaintenanceWorkItemHandler(IUserRepository userRepository, ICacheClient cacheClient, IMessageBus messageBus, ILoggerFactory loggerFactory = null) : base(loggerFactory) { + _userRepository = userRepository; + _lockProvider = new CacheLockProvider(cacheClient, messageBus); + } - public override async Task HandleItemAsync(WorkItemContext context) { - const int LIMIT = 100; + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new CancellationToken()) { + return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + } - var workItem = context.GetData(); - Log.LogInformation("Received user maintenance work item. Normalize: {Normalize}", workItem.Normalize); + public override async Task HandleItemAsync(WorkItemContext context) { + const int LIMIT = 100; - var results = await _userRepository.GetAllAsync(o => o.PageLimit(LIMIT)).AnyContext(); - while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { - foreach (var user in results.Documents) { - if (workItem.Normalize) - NormalizeUser(user); - } + var workItem = context.GetData(); + Log.LogInformation("Received user maintenance work item. Normalize: {Normalize}", workItem.Normalize); + var results = await _userRepository.GetAllAsync(o => o.PageLimit(LIMIT)).AnyContext(); + while (results.Documents.Count > 0 && !context.CancellationToken.IsCancellationRequested) { + foreach (var user in results.Documents) { if (workItem.Normalize) - await _userRepository.SaveAsync(results.Documents).AnyContext(); + NormalizeUser(user); + } - // Sleep so we are not hammering the backend. - await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); + if (workItem.Normalize) + await _userRepository.SaveAsync(results.Documents).AnyContext(); - if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) - break; + // Sleep so we are not hammering the backend. + await SystemClock.SleepAsync(TimeSpan.FromSeconds(2.5)).AnyContext(); - if (results.Documents.Count > 0) - await context.RenewLockAsync().AnyContext(); - } + if (context.CancellationToken.IsCancellationRequested || !await results.NextPageAsync().AnyContext()) + break; + + if (results.Documents.Count > 0) + await context.RenewLockAsync().AnyContext(); } + } - private void NormalizeUser(User user) { - user.FullName = user.FullName?.Trim(); + private void NormalizeUser(User user) { + user.FullName = user.FullName?.Trim(); - string email = user.EmailAddress?.Trim().ToLowerInvariant(); - if (!String.Equals(user.EmailAddress, email)) { - Log.LogInformation("Normalizing user email address {EmailAddress} to {NewEmailAddress}", user.EmailAddress, email); - user.EmailAddress = email; - } + string email = user.EmailAddress?.Trim().ToLowerInvariant(); + if (!String.Equals(user.EmailAddress, email)) { + Log.LogInformation("Normalizing user email address {EmailAddress} to {NewEmailAddress}", user.EmailAddress, email); + user.EmailAddress = email; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Mail/IMailSender.cs b/src/Exceptionless.Core/Mail/IMailSender.cs index 733fb7dd07..d77d865893 100644 --- a/src/Exceptionless.Core/Mail/IMailSender.cs +++ b/src/Exceptionless.Core/Mail/IMailSender.cs @@ -1,8 +1,7 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Queues.Models; +using Exceptionless.Core.Queues.Models; -namespace Exceptionless.Core.Mail { - public interface IMailSender { - Task SendAsync(MailMessage model); - } -} \ No newline at end of file +namespace Exceptionless.Core.Mail; + +public interface IMailSender { + Task SendAsync(MailMessage model); +} diff --git a/src/Exceptionless.Core/Mail/IMailer.cs b/src/Exceptionless.Core/Mail/IMailer.cs index d65eded6bb..07a07357c4 100644 --- a/src/Exceptionless.Core/Mail/IMailer.cs +++ b/src/Exceptionless.Core/Mail/IMailer.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Mail { - public interface IMailer { - Task SendEventNoticeAsync(User user, PersistentEvent ev, Project project, bool isNew, bool isRegression, int totalOccurrences); - Task SendOrganizationAddedAsync(User sender, Organization organization, User user); - Task SendOrganizationInviteAsync(User sender, Organization organization, Invite invite); - Task SendOrganizationNoticeAsync(User user, Organization organization, bool isOverMonthlyLimit, bool isOverHourlyLimit); - Task SendOrganizationPaymentFailedAsync(User owner, Organization organization); - Task SendProjectDailySummaryAsync(User user, Project project, IEnumerable mostFrequent, IEnumerable newest, DateTime startDate, bool hasSubmittedEvents, double count, double uniqueCount, double newCount, double fixedCount, int blockedCount, int tooBigCount, bool isFreePlan); - Task SendUserEmailVerifyAsync(User user); - Task SendUserPasswordResetAsync(User user); - } -} \ No newline at end of file +namespace Exceptionless.Core.Mail; + +public interface IMailer { + Task SendEventNoticeAsync(User user, PersistentEvent ev, Project project, bool isNew, bool isRegression, int totalOccurrences); + Task SendOrganizationAddedAsync(User sender, Organization organization, User user); + Task SendOrganizationInviteAsync(User sender, Organization organization, Invite invite); + Task SendOrganizationNoticeAsync(User user, Organization organization, bool isOverMonthlyLimit, bool isOverHourlyLimit); + Task SendOrganizationPaymentFailedAsync(User owner, Organization organization); + Task SendProjectDailySummaryAsync(User user, Project project, IEnumerable mostFrequent, IEnumerable newest, DateTime startDate, bool hasSubmittedEvents, double count, double uniqueCount, double newCount, double fixedCount, int blockedCount, int tooBigCount, bool isFreePlan); + Task SendUserEmailVerifyAsync(User user); + Task SendUserPasswordResetAsync(User user); +} diff --git a/src/Exceptionless.Core/Mail/InMemoryMailSender.cs b/src/Exceptionless.Core/Mail/InMemoryMailSender.cs index c51b41e1d0..5a58d56489 100644 --- a/src/Exceptionless.Core/Mail/InMemoryMailSender.cs +++ b/src/Exceptionless.Core/Mail/InMemoryMailSender.cs @@ -1,31 +1,27 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Queues.Models; +using Exceptionless.Core.Queues.Models; -namespace Exceptionless.Core.Mail { - public class InMemoryMailSender : IMailSender { - private readonly Queue _recentMessages = new Queue(); - private readonly int _messagesToStore; - private long _totalSent; +namespace Exceptionless.Core.Mail; - public InMemoryMailSender(int messagesToStore = 25) { - _messagesToStore = messagesToStore; - } +public class InMemoryMailSender : IMailSender { + private readonly Queue _recentMessages = new Queue(); + private readonly int _messagesToStore; + private long _totalSent; - public long TotalSent => _totalSent; - public List SentMessages => _recentMessages.ToList(); - public MailMessage LastMessage => SentMessages.LastOrDefault(); + public InMemoryMailSender(int messagesToStore = 25) { + _messagesToStore = messagesToStore; + } + + public long TotalSent => _totalSent; + public List SentMessages => _recentMessages.ToList(); + public MailMessage LastMessage => SentMessages.LastOrDefault(); - public Task SendAsync(MailMessage model) { - _recentMessages.Enqueue(model); - Interlocked.Increment(ref _totalSent); + public Task SendAsync(MailMessage model) { + _recentMessages.Enqueue(model); + Interlocked.Increment(ref _totalSent); - while (_recentMessages.Count > _messagesToStore) - _recentMessages.Dequeue(); + while (_recentMessages.Count > _messagesToStore) + _recentMessages.Dequeue(); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/src/Exceptionless.Core/Mail/Mailer.cs b/src/Exceptionless.Core/Mail/Mailer.cs index c50eb0a70e..d576ec8062 100644 --- a/src/Exceptionless.Core/Mail/Mailer.cs +++ b/src/Exceptionless.Core/Mail/Mailer.cs @@ -1,9 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Plugins.Formatting; using Exceptionless.Core.Queues.Models; @@ -15,35 +10,36 @@ using HandlebarsDotNet; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Mail { - public class Mailer : IMailer { - private readonly ConcurrentDictionary> _cachedTemplates = new ConcurrentDictionary>(); - private readonly IQueue _queue; - private readonly FormattingPluginManager _pluginManager; - private readonly AppOptions _appOptions; - private readonly IMetricsClient _metrics; - private readonly ILogger _logger; - - public Mailer(IQueue queue, FormattingPluginManager pluginManager, AppOptions appOptions, IMetricsClient metrics, ILogger logger) { - _queue = queue; - _pluginManager = pluginManager; - _appOptions = appOptions; - _metrics = metrics; - _logger = logger; - } +namespace Exceptionless.Core.Mail; + +public class Mailer : IMailer { + private readonly ConcurrentDictionary> _cachedTemplates = new ConcurrentDictionary>(); + private readonly IQueue _queue; + private readonly FormattingPluginManager _pluginManager; + private readonly AppOptions _appOptions; + private readonly IMetricsClient _metrics; + private readonly ILogger _logger; + + public Mailer(IQueue queue, FormattingPluginManager pluginManager, AppOptions appOptions, IMetricsClient metrics, ILogger logger) { + _queue = queue; + _pluginManager = pluginManager; + _appOptions = appOptions; + _metrics = metrics; + _logger = logger; + } - public async Task SendEventNoticeAsync(User user, PersistentEvent ev, Project project, bool isNew, bool isRegression, int totalOccurrences) { - bool isCritical = ev.IsCritical(); - var result = _pluginManager.GetEventNotificationMailMessageData(ev, isCritical, isNew, isRegression); - if (result == null || result.Data.Count == 0) { - _logger.LogWarning("Unable to create event notification mail message for event \"{user}\". User: \"{EmailAddress}\"", ev.Id, user.EmailAddress); - return false; - } + public async Task SendEventNoticeAsync(User user, PersistentEvent ev, Project project, bool isNew, bool isRegression, int totalOccurrences) { + bool isCritical = ev.IsCritical(); + var result = _pluginManager.GetEventNotificationMailMessageData(ev, isCritical, isNew, isRegression); + if (result == null || result.Data.Count == 0) { + _logger.LogWarning("Unable to create event notification mail message for event \"{user}\". User: \"{EmailAddress}\"", ev.Id, user.EmailAddress); + return false; + } - if (String.IsNullOrEmpty(result.Subject)) - result.Subject = ev.Message ?? ev.Source ?? "(Global)"; + if (String.IsNullOrEmpty(result.Subject)) + result.Subject = ev.Message ?? ev.Source ?? "(Global)"; - var messageData = new Dictionary { + var messageData = new Dictionary { { "Subject", result.Subject }, { "BaseUrl", _appOptions.BaseURL }, { "ProjectName", project.Name }, @@ -57,98 +53,98 @@ public async Task SendEventNoticeAsync(User user, PersistentEvent ev, Proj { "Fields", result.Data } }; - AddDefaultFields(ev, result.Data); - AddUserInfo(ev, messageData); + AddDefaultFields(ev, result.Data); + AddUserInfo(ev, messageData); - const string template = "event-notice"; - await QueueMessageAsync(new MailMessage { - To = user.EmailAddress, - Subject = $"[{project.Name}] {result.Subject}", - Body = RenderTemplate(template, messageData) - }, template).AnyContext(); - return true; - } + const string template = "event-notice"; + await QueueMessageAsync(new MailMessage { + To = user.EmailAddress, + Subject = $"[{project.Name}] {result.Subject}", + Body = RenderTemplate(template, messageData) + }, template).AnyContext(); + return true; + } - private void AddUserInfo(PersistentEvent ev, Dictionary data) { - var ud = ev.GetUserDescription(); - var ui = ev.GetUserIdentity(); - if (!String.IsNullOrEmpty(ud?.Description)) - data["UserDescription"] = ud.Description; + private void AddUserInfo(PersistentEvent ev, Dictionary data) { + var ud = ev.GetUserDescription(); + var ui = ev.GetUserIdentity(); + if (!String.IsNullOrEmpty(ud?.Description)) + data["UserDescription"] = ud.Description; - if (!String.IsNullOrEmpty(ud?.EmailAddress)) - data["UserEmail"] = ud.EmailAddress; + if (!String.IsNullOrEmpty(ud?.EmailAddress)) + data["UserEmail"] = ud.EmailAddress; - string displayName = null; - if (!String.IsNullOrEmpty(ui?.Identity)) - data["UserIdentity"] = displayName = ui.Identity; + string displayName = null; + if (!String.IsNullOrEmpty(ui?.Identity)) + data["UserIdentity"] = displayName = ui.Identity; - if (!String.IsNullOrEmpty(ui?.Name)) - data["UserName"] = displayName = ui.Name; + if (!String.IsNullOrEmpty(ui?.Name)) + data["UserName"] = displayName = ui.Name; - if (!String.IsNullOrEmpty(displayName) && !String.IsNullOrEmpty(ud?.EmailAddress)) - displayName = $"{displayName} ({ud.EmailAddress})"; - else if (!String.IsNullOrEmpty(ui?.Identity) && !String.IsNullOrEmpty(ui.Name)) - displayName = $"{ui.Name} ({ui.Identity})"; + if (!String.IsNullOrEmpty(displayName) && !String.IsNullOrEmpty(ud?.EmailAddress)) + displayName = $"{displayName} ({ud.EmailAddress})"; + else if (!String.IsNullOrEmpty(ui?.Identity) && !String.IsNullOrEmpty(ui.Name)) + displayName = $"{ui.Name} ({ui.Identity})"; - if (!String.IsNullOrEmpty(displayName)) - data["UserDisplayName"] = displayName; + if (!String.IsNullOrEmpty(displayName)) + data["UserDisplayName"] = displayName; - data["HasUserInfo"] = ud != null || ui != null; - } + data["HasUserInfo"] = ud != null || ui != null; + } - private void AddDefaultFields(PersistentEvent ev, Dictionary data) { - if (ev.Tags.Count > 0) - data["Tags"] = String.Join(", ", ev.Tags); + private void AddDefaultFields(PersistentEvent ev, Dictionary data) { + if (ev.Tags.Count > 0) + data["Tags"] = String.Join(", ", ev.Tags); - if (ev.Value.GetValueOrDefault() != 0) - data["Value"] = ev.Value; + if (ev.Value.GetValueOrDefault() != 0) + data["Value"] = ev.Value; - string version = ev.GetVersion(); - if (!String.IsNullOrEmpty(version)) - data["Version"] = version; - } + string version = ev.GetVersion(); + if (!String.IsNullOrEmpty(version)) + data["Version"] = version; + } - public Task SendOrganizationAddedAsync(User sender, Organization organization, User user) { - const string template = "organization-added"; - string subject = $"{sender.FullName} added you to the organization \"{organization.Name}\" on Exceptionless"; - var data = new Dictionary { + public Task SendOrganizationAddedAsync(User sender, Organization organization, User user) { + const string template = "organization-added"; + string subject = $"{sender.FullName} added you to the organization \"{organization.Name}\" on Exceptionless"; + var data = new Dictionary { { "Subject", subject }, { "BaseUrl", _appOptions.BaseURL }, { "OrganizationId", organization.Id }, { "OrganizationName", organization.Name } }; - return QueueMessageAsync(new MailMessage { - To = user.EmailAddress, - Subject = subject, - Body = RenderTemplate(template, data) - }, template); - } + return QueueMessageAsync(new MailMessage { + To = user.EmailAddress, + Subject = subject, + Body = RenderTemplate(template, data) + }, template); + } - public Task SendOrganizationInviteAsync(User sender, Organization organization, Invite invite) { - const string template = "organization-invited"; - string subject = $"{sender.FullName} invited you to join the organization \"{organization.Name}\" on Exceptionless"; - var data = new Dictionary { + public Task SendOrganizationInviteAsync(User sender, Organization organization, Invite invite) { + const string template = "organization-invited"; + string subject = $"{sender.FullName} invited you to join the organization \"{organization.Name}\" on Exceptionless"; + var data = new Dictionary { { "Subject", subject }, { "BaseUrl", _appOptions.BaseURL }, { "InviteToken", invite.Token } }; - var body = RenderTemplate(template, data); - return QueueMessageAsync(new MailMessage { - To = invite.EmailAddress, - Subject = subject, - Body = body - }, template); - } + var body = RenderTemplate(template, data); + return QueueMessageAsync(new MailMessage { + To = invite.EmailAddress, + Subject = subject, + Body = body + }, template); + } - public Task SendOrganizationNoticeAsync(User user, Organization organization, bool isOverMonthlyLimit, bool isOverHourlyLimit) { - const string template = "organization-notice"; - string subject = isOverMonthlyLimit - ? $"[{organization.Name}] Monthly plan limit exceeded." - : $"[{organization.Name}] Events are currently being throttled."; + public Task SendOrganizationNoticeAsync(User user, Organization organization, bool isOverMonthlyLimit, bool isOverHourlyLimit) { + const string template = "organization-notice"; + string subject = isOverMonthlyLimit + ? $"[{organization.Name}] Monthly plan limit exceeded." + : $"[{organization.Name}] Events are currently being throttled."; - var data = new Dictionary { + var data = new Dictionary { { "Subject", subject }, { "BaseUrl", _appOptions.BaseURL }, { "OrganizationId", organization.Id }, @@ -158,34 +154,34 @@ public Task SendOrganizationNoticeAsync(User user, Organization organization, bo { "ThrottledUntil", SystemClock.UtcNow.StartOfHour().AddHours(1).ToShortTimeString() } }; - return QueueMessageAsync(new MailMessage { - To = user.EmailAddress, - Subject = subject, - Body = RenderTemplate(template, data) - }, template); - } + return QueueMessageAsync(new MailMessage { + To = user.EmailAddress, + Subject = subject, + Body = RenderTemplate(template, data) + }, template); + } - public Task SendOrganizationPaymentFailedAsync(User owner, Organization organization) { - const string template = "organization-payment-failed"; - string subject = $"[{organization.Name}] Payment failed! Update billing information to avoid service interruption!"; - var data = new Dictionary { + public Task SendOrganizationPaymentFailedAsync(User owner, Organization organization) { + const string template = "organization-payment-failed"; + string subject = $"[{organization.Name}] Payment failed! Update billing information to avoid service interruption!"; + var data = new Dictionary { { "Subject", subject }, { "BaseUrl", _appOptions.BaseURL }, { "OrganizationId", organization.Id }, { "OrganizationName", organization.Name } }; - return QueueMessageAsync(new MailMessage { - To = owner.EmailAddress, - Subject = subject, - Body = RenderTemplate(template, data) - }, template); - } + return QueueMessageAsync(new MailMessage { + To = owner.EmailAddress, + Subject = subject, + Body = RenderTemplate(template, data) + }, template); + } - public Task SendProjectDailySummaryAsync(User user, Project project, IEnumerable mostFrequent, IEnumerable newest, DateTime startDate, bool hasSubmittedEvents, double count, double uniqueCount, double newCount, double fixedCount, int blockedCount, int tooBigCount, bool isFreePlan) { - const string template = "project-daily-summary"; - string subject = $"[{project.Name}] Summary for {startDate.ToLongDateString()}"; - var data = new Dictionary { + public Task SendProjectDailySummaryAsync(User user, Project project, IEnumerable mostFrequent, IEnumerable newest, DateTime startDate, bool hasSubmittedEvents, double count, double uniqueCount, double newCount, double fixedCount, int blockedCount, int tooBigCount, bool isFreePlan) { + const string template = "project-daily-summary"; + string subject = $"[{project.Name}] Summary for {startDate.ToLongDateString()}"; + var data = new Dictionary { { "Subject", subject }, { "BaseUrl", _appOptions.BaseURL }, { "OrganizationId", project.OrganizationId }, @@ -204,99 +200,98 @@ public Task SendProjectDailySummaryAsync(User user, Project project, IEnumerable { "IsFreePlan", isFreePlan } }; - return QueueMessageAsync(new MailMessage { - To = user.EmailAddress, - Subject = subject, - Body = RenderTemplate(template, data) - }, template); - } + return QueueMessageAsync(new MailMessage { + To = user.EmailAddress, + Subject = subject, + Body = RenderTemplate(template, data) + }, template); + } - private static IEnumerable GetStackTemplateData(IEnumerable stacks) { - return stacks?.Select(s => new { - StackId = s.Id, - Title = s.Title.Truncate(50), - TypeName = s.GetTypeName().Truncate(50), - s.Status, - }); - } + private static IEnumerable GetStackTemplateData(IEnumerable stacks) { + return stacks?.Select(s => new { + StackId = s.Id, + Title = s.Title.Truncate(50), + TypeName = s.GetTypeName().Truncate(50), + s.Status, + }); + } - public Task SendUserEmailVerifyAsync(User user) { - if (String.IsNullOrEmpty(user?.VerifyEmailAddressToken)) - return Task.CompletedTask; + public Task SendUserEmailVerifyAsync(User user) { + if (String.IsNullOrEmpty(user?.VerifyEmailAddressToken)) + return Task.CompletedTask; - const string template = "user-email-verify"; - const string subject = "Exceptionless Account Confirmation"; - var data = new Dictionary { + const string template = "user-email-verify"; + const string subject = "Exceptionless Account Confirmation"; + var data = new Dictionary { { "Subject", subject }, { "BaseUrl", _appOptions.BaseURL }, { "UserFullName", user.FullName }, { "UserVerifyEmailAddressToken", user.VerifyEmailAddressToken } }; - return QueueMessageAsync(new MailMessage { - To = user.EmailAddress, - Subject = subject, - Body = RenderTemplate(template, data) - }, template); - } + return QueueMessageAsync(new MailMessage { + To = user.EmailAddress, + Subject = subject, + Body = RenderTemplate(template, data) + }, template); + } - public Task SendUserPasswordResetAsync(User user) { - if (String.IsNullOrEmpty(user?.PasswordResetToken)) - return Task.CompletedTask; + public Task SendUserPasswordResetAsync(User user) { + if (String.IsNullOrEmpty(user?.PasswordResetToken)) + return Task.CompletedTask; - const string template = "user-password-reset"; - const string subject = "Exceptionless Password Reset"; - var data = new Dictionary { + const string template = "user-password-reset"; + const string subject = "Exceptionless Password Reset"; + var data = new Dictionary { { "Subject", subject }, { "BaseUrl", _appOptions.BaseURL }, { "UserFullName", user.FullName }, { "UserPasswordResetToken", user.PasswordResetToken } }; - return QueueMessageAsync(new MailMessage { - To = user.EmailAddress, - Subject = subject, - Body = RenderTemplate(template, data) - }, template); - } + return QueueMessageAsync(new MailMessage { + To = user.EmailAddress, + Subject = subject, + Body = RenderTemplate(template, data) + }, template); + } - private string RenderTemplate(string name, IDictionary data) { - var template = GetCompiledTemplate(name); - var result = template(data); - return result?.ToString(); - } + private string RenderTemplate(string name, IDictionary data) { + var template = GetCompiledTemplate(name); + var result = template(data); + return result?.ToString(); + } + + private HandlebarsTemplate GetCompiledTemplate(string name) { + return _cachedTemplates.GetOrAdd(name, templateName => { + var assembly = typeof(Mailer).Assembly; + string resourceName = $"Exceptionless.Core.Mail.Templates.{templateName}.html"; - private HandlebarsTemplate GetCompiledTemplate(string name) { - return _cachedTemplates.GetOrAdd(name, templateName => { - var assembly = typeof(Mailer).Assembly; - string resourceName = $"Exceptionless.Core.Mail.Templates.{templateName}.html"; - - using (var stream = assembly.GetManifestResourceStream(resourceName)) { - using (var reader = new StreamReader(stream)) { - string template = reader.ReadToEnd(); - var compiledTemplateFunc = Handlebars.Compile(template); - return compiledTemplateFunc; - } + using (var stream = assembly.GetManifestResourceStream(resourceName)) { + using (var reader = new StreamReader(stream)) { + string template = reader.ReadToEnd(); + var compiledTemplateFunc = Handlebars.Compile(template); + return compiledTemplateFunc; } - }); - } + } + }); + } - private Task QueueMessageAsync(MailMessage message, string metricsName) { - CleanAddresses(message); - _metrics.Counter($"mailer.{metricsName}"); - return _queue.EnqueueAsync(message); - } + private Task QueueMessageAsync(MailMessage message, string metricsName) { + CleanAddresses(message); + _metrics.Counter($"mailer.{metricsName}"); + return _queue.EnqueueAsync(message); + } - private void CleanAddresses(MailMessage message) { - if (_appOptions.AppMode == AppMode.Production) - return; + private void CleanAddresses(MailMessage message) { + if (_appOptions.AppMode == AppMode.Production) + return; - string address = message.To.ToLowerInvariant(); - if (_appOptions.EmailOptions.AllowedOutboundAddresses.Any(address.Contains)) - return; + string address = message.To.ToLowerInvariant(); + if (_appOptions.EmailOptions.AllowedOutboundAddresses.Any(address.Contains)) + return; - message.Subject = $"[{message.To}] {message.Subject}".StripInvisible(); - message.To = _appOptions.EmailOptions.TestEmailAddress; - } + message.Subject = $"[{message.To}] {message.Subject}".StripInvisible(); + message.To = _appOptions.EmailOptions.TestEmailAddress; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Migrations/001_UpdateIndexMappings.cs b/src/Exceptionless.Core/Migrations/001_UpdateIndexMappings.cs index 148ffd515e..88afdd3be9 100644 --- a/src/Exceptionless.Core/Migrations/001_UpdateIndexMappings.cs +++ b/src/Exceptionless.Core/Migrations/001_UpdateIndexMappings.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Repositories.Elasticsearch.Extensions; @@ -6,73 +5,73 @@ using Microsoft.Extensions.Logging; using Nest; -namespace Exceptionless.Core.Migrations { - public sealed class UpdateIndexMappings : MigrationBase { - private readonly IElasticClient _client; - private readonly ExceptionlessElasticConfiguration _config; +namespace Exceptionless.Core.Migrations; - public UpdateIndexMappings(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(loggerFactory) { - _config = configuration; - _client = configuration.Client; +public sealed class UpdateIndexMappings : MigrationBase { + private readonly IElasticClient _client; + private readonly ExceptionlessElasticConfiguration _config; - MigrationType = MigrationType.VersionedAndResumable; - Version = 1; - } + public UpdateIndexMappings(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(loggerFactory) { + _config = configuration; + _client = configuration.Client; - public override async Task RunAsync(MigrationContext context) { - _logger.LogInformation("Start migration for adding index mappings..."); - - _logger.LogInformation("Updating Organization mappings..."); - var response = await _client.MapAsync(d => { - d.Index(_config.Organizations.VersionedName); - d.Properties(p => p - .Date(f => f.Name(s => s.LastEventDateUtc)) - .Boolean(f => f.Name(s => s.IsDeleted)).FieldAlias(a => a.Path(p1 => p1.IsDeleted).Name("deleted"))); - - return d; - }); - _logger.LogRequest(response); + MigrationType = MigrationType.VersionedAndResumable; + Version = 1; + } + + public override async Task RunAsync(MigrationContext context) { + _logger.LogInformation("Start migration for adding index mappings..."); + + _logger.LogInformation("Updating Organization mappings..."); + var response = await _client.MapAsync(d => { + d.Index(_config.Organizations.VersionedName); + d.Properties(p => p + .Date(f => f.Name(s => s.LastEventDateUtc)) + .Boolean(f => f.Name(s => s.IsDeleted)).FieldAlias(a => a.Path(p1 => p1.IsDeleted).Name("deleted"))); + + return d; + }); + _logger.LogRequest(response); + + _logger.LogInformation("Setting Organization is_deleted=false..."); + const string script = "ctx._source.is_deleted = false;"; + await _config.Client.Indices.RefreshAsync(_config.Organizations.VersionedName); + var updateResponse = await _client.UpdateByQueryAsync(x => x.QueryOnQueryString("NOT _exists_:deleted").Script(s => s.Source(script).Lang(ScriptLang.Painless))); + _logger.LogRequest(updateResponse); + + _logger.LogInformation("Updating Project mappings..."); + response = await _client.MapAsync(d => { + d.Index(_config.Projects.VersionedName); + d.Properties(p => p + .Date(f => f.Name(s => s.LastEventDateUtc)) + .Boolean(f => f.Name(s => s.IsDeleted)).FieldAlias(a => a.Path(p1 => p1.IsDeleted).Name("deleted"))); + + return d; + }); + _logger.LogRequest(response); + + _logger.LogInformation("Setting Project is_deleted=false..."); + await _config.Client.Indices.RefreshAsync(_config.Projects.VersionedName); + updateResponse = await _client.UpdateByQueryAsync(x => x.QueryOnQueryString("NOT _exists_:deleted").Script(s => s.Source(script).Lang(ScriptLang.Painless))); + _logger.LogRequest(updateResponse); + + _logger.LogInformation("Updating Stack mappings..."); + response = await _client.MapAsync(d => { + d.Index(_config.Stacks.VersionedName); + d.Properties(p => p + .Keyword(f => f.Name(s => s.Status)) + .Date(f => f.Name(s => s.SnoozeUntilUtc)) + .Boolean(f => f.Name(s => s.IsDeleted)).FieldAlias(a => a.Path(p1 => p1.IsDeleted).Name("deleted"))); + + return d; + }); + _logger.LogRequest(response); - _logger.LogInformation("Setting Organization is_deleted=false..."); - const string script = "ctx._source.is_deleted = false;"; - await _config.Client.Indices.RefreshAsync(_config.Organizations.VersionedName); - var updateResponse = await _client.UpdateByQueryAsync(x => x.QueryOnQueryString("NOT _exists_:deleted").Script(s => s.Source(script).Lang(ScriptLang.Painless))); - _logger.LogRequest(updateResponse); - - _logger.LogInformation("Updating Project mappings..."); - response = await _client.MapAsync(d => { - d.Index(_config.Projects.VersionedName); - d.Properties(p => p - .Date(f => f.Name(s => s.LastEventDateUtc)) - .Boolean(f => f.Name(s => s.IsDeleted)).FieldAlias(a => a.Path(p1 => p1.IsDeleted).Name("deleted"))); - - return d; - }); - _logger.LogRequest(response); + _logger.LogInformation("Setting Stack is_deleted=false..."); + await _config.Client.Indices.RefreshAsync(_config.Stacks.VersionedName); + updateResponse = await _client.UpdateByQueryAsync(x => x.QueryOnQueryString("NOT _exists_:deleted").Script(s => s.Source(script).Lang(ScriptLang.Painless))); + _logger.LogRequest(updateResponse); - _logger.LogInformation("Setting Project is_deleted=false..."); - await _config.Client.Indices.RefreshAsync(_config.Projects.VersionedName); - updateResponse = await _client.UpdateByQueryAsync(x => x.QueryOnQueryString("NOT _exists_:deleted").Script(s => s.Source(script).Lang(ScriptLang.Painless))); - _logger.LogRequest(updateResponse); - - _logger.LogInformation("Updating Stack mappings..."); - response = await _client.MapAsync(d => { - d.Index(_config.Stacks.VersionedName); - d.Properties(p => p - .Keyword(f => f.Name(s => s.Status)) - .Date(f => f.Name(s => s.SnoozeUntilUtc)) - .Boolean(f => f.Name(s => s.IsDeleted)).FieldAlias(a => a.Path(p1 => p1.IsDeleted).Name("deleted"))); - - return d; - }); - _logger.LogRequest(response); - - _logger.LogInformation("Setting Stack is_deleted=false..."); - await _config.Client.Indices.RefreshAsync(_config.Stacks.VersionedName); - updateResponse = await _client.UpdateByQueryAsync(x => x.QueryOnQueryString("NOT _exists_:deleted").Script(s => s.Source(script).Lang(ScriptLang.Painless))); - _logger.LogRequest(updateResponse); - - _logger.LogInformation("Finished adding mappings."); - } + _logger.LogInformation("Finished adding mappings."); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Migrations/002_SetStackStatus.cs b/src/Exceptionless.Core/Migrations/002_SetStackStatus.cs index c9b6b86f91..a9894dd1e6 100644 --- a/src/Exceptionless.Core/Migrations/002_SetStackStatus.cs +++ b/src/Exceptionless.Core/Migrations/002_SetStackStatus.cs @@ -1,6 +1,4 @@ -using System; using System.Diagnostics; -using System.Threading.Tasks; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Caching; @@ -9,61 +7,61 @@ using Microsoft.Extensions.Logging; using Nest; -namespace Exceptionless.Core.Migrations { - public sealed class SetStackStatus : MigrationBase { - private readonly IElasticClient _client; - private readonly ExceptionlessElasticConfiguration _config; - private readonly ICacheClient _cache; +namespace Exceptionless.Core.Migrations; - public SetStackStatus(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(loggerFactory) { - _config = configuration; - _client = configuration.Client; - _cache = configuration.Cache; - - MigrationType = MigrationType.VersionedAndResumable; - Version = 2; - } +public sealed class SetStackStatus : MigrationBase { + private readonly IElasticClient _client; + private readonly ExceptionlessElasticConfiguration _config; + private readonly ICacheClient _cache; - public override async Task RunAsync(MigrationContext context) { - _logger.LogInformation("Begin refreshing all indices"); - await _config.Client.Indices.RefreshAsync(Indices.All); - _logger.LogInformation("Done refreshing all indices"); - - _logger.LogInformation("Start migrating stacks status"); - var sw = Stopwatch.StartNew(); - const string script = "if (ctx._source.is_regressed == true) ctx._source.status = 'regressed'; else if (ctx._source.is_hidden == true) ctx._source.status = 'ignored'; else if (ctx._source.disable_notifications == true) ctx._source.status = 'ignored'; else if (ctx._source.is_fixed == true) ctx._source.status = 'fixed'; else if (ctx._source.containsKey('date_fixed')) ctx._source.status = 'fixed'; else ctx._source.status = 'open';"; - var stackResponse = await _client.UpdateByQueryAsync(x => x - .QueryOnQueryString("NOT _exists_:status") - .Script(s => s.Source(script).Lang(ScriptLang.Painless)) - .Conflicts(Elasticsearch.Net.Conflicts.Proceed) - .WaitForCompletion(false)); + public SetStackStatus(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(loggerFactory) { + _config = configuration; + _client = configuration.Client; + _cache = configuration.Cache; - _logger.LogRequest(stackResponse, Microsoft.Extensions.Logging.LogLevel.Information); + MigrationType = MigrationType.VersionedAndResumable; + Version = 2; + } + + public override async Task RunAsync(MigrationContext context) { + _logger.LogInformation("Begin refreshing all indices"); + await _config.Client.Indices.RefreshAsync(Indices.All); + _logger.LogInformation("Done refreshing all indices"); + + _logger.LogInformation("Start migrating stacks status"); + var sw = Stopwatch.StartNew(); + const string script = "if (ctx._source.is_regressed == true) ctx._source.status = 'regressed'; else if (ctx._source.is_hidden == true) ctx._source.status = 'ignored'; else if (ctx._source.disable_notifications == true) ctx._source.status = 'ignored'; else if (ctx._source.is_fixed == true) ctx._source.status = 'fixed'; else if (ctx._source.containsKey('date_fixed')) ctx._source.status = 'fixed'; else ctx._source.status = 'open';"; + var stackResponse = await _client.UpdateByQueryAsync(x => x + .QueryOnQueryString("NOT _exists_:status") + .Script(s => s.Source(script).Lang(ScriptLang.Painless)) + .Conflicts(Elasticsearch.Net.Conflicts.Proceed) + .WaitForCompletion(false)); + + _logger.LogRequest(stackResponse, Microsoft.Extensions.Logging.LogLevel.Information); + + var taskId = stackResponse.Task; + int attempts = 0; + long affectedRecords = 0; + do { + attempts++; + var taskStatus = await _client.Tasks.GetTaskAsync(taskId); + var status = taskStatus.Task.Status; + if (taskStatus.Completed) { + // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. + _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + affectedRecords += status.Created + status.Updated + status.Deleted; + break; + } - var taskId = stackResponse.Task; - int attempts = 0; - long affectedRecords = 0; - do { - attempts++; - var taskStatus = await _client.Tasks.GetTaskAsync(taskId); - var status = taskStatus.Task.Status; - if (taskStatus.Completed) { - // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. - _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - affectedRecords += status.Created + status.Updated + status.Deleted; - break; - } + _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + var delay = TimeSpan.FromSeconds(attempts <= 5 ? 1 : 5); + await Task.Delay(delay); + } while (true); - _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - var delay = TimeSpan.FromSeconds(attempts <= 5 ? 1 : 5); - await Task.Delay(delay); - } while (true); + _logger.LogInformation("Finished adding stack status: Time={Duration:d\\.hh\\:mm} Completed={Completed:N0} Total={Total:N0} Errors={Errors:N0}", sw.Elapsed, affectedRecords, stackResponse.Total, stackResponse.Failures.Count); - _logger.LogInformation("Finished adding stack status: Time={Duration:d\\.hh\\:mm} Completed={Completed:N0} Total={Total:N0} Errors={Errors:N0}", sw.Elapsed, affectedRecords, stackResponse.Total, stackResponse.Failures.Count); - - _logger.LogInformation("Invalidating Stack Cache"); - await _cache.RemoveByPrefixAsync(nameof(Stack)); - _logger.LogInformation("Invalidating Stack Cache"); - } + _logger.LogInformation("Invalidating Stack Cache"); + await _cache.RemoveByPrefixAsync(nameof(Stack)); + _logger.LogInformation("Invalidating Stack Cache"); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Migrations/FixDuplicateStacks.cs b/src/Exceptionless.Core/Migrations/FixDuplicateStacks.cs index b7e0d1f5f7..78b4af0abc 100644 --- a/src/Exceptionless.Core/Migrations/FixDuplicateStacks.cs +++ b/src/Exceptionless.Core/Migrations/FixDuplicateStacks.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; using Exceptionless.Core.Repositories.Configuration; @@ -16,168 +12,169 @@ using Nest; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Core.Migrations { - public sealed class FixDuplicateStacks : MigrationBase { - private readonly IElasticClient _client; - private readonly ICacheClient _cache; - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly ExceptionlessElasticConfiguration _config; - - public FixDuplicateStacks(ExceptionlessElasticConfiguration configuration, IStackRepository stackRepository, IEventRepository eventRepository, ILoggerFactory loggerFactory) : base(loggerFactory) { - _config = configuration; - _client = configuration.Client; - _cache = configuration.Cache; - _stackRepository = stackRepository; - _eventRepository = eventRepository; - - MigrationType = MigrationType.Repeatable; - } +namespace Exceptionless.Core.Migrations; - public override async Task RunAsync(MigrationContext context) { - _logger.LogInformation("Getting duplicate stacks"); +public sealed class FixDuplicateStacks : MigrationBase { + private readonly IElasticClient _client; + private readonly ICacheClient _cache; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly ExceptionlessElasticConfiguration _config; - var duplicateStackAgg = await _client.SearchAsync(q => q - .QueryOnQueryString("is_deleted:false") - .Size(0) - .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); - _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); + public FixDuplicateStacks(ExceptionlessElasticConfiguration configuration, IStackRepository stackRepository, IEventRepository eventRepository, ILoggerFactory loggerFactory) : base(loggerFactory) { + _config = configuration; + _client = configuration.Client; + _cache = configuration.Cache; + _stackRepository = stackRepository; + _eventRepository = eventRepository; + + MigrationType = MigrationType.Repeatable; + } - var buckets = duplicateStackAgg.Aggregations.Terms("stacks").Buckets; - int total = buckets.Count; - int processed = 0; - int error = 0; - long totalUpdatedEventCount = 0; - var lastStatus = SystemClock.Now; - int batch = 1; - - while (buckets.Count > 0) { - _logger.LogInformation($"Found {total} duplicate stacks in batch #{batch}."); - - foreach (var duplicateSignature in buckets) { - string projectId = null; - string signature = null; - try { - var parts = duplicateSignature.Key.Split(':'); - if (parts.Length != 2) { - _logger.LogError("Error parsing duplicate signature {DuplicateSignature}", duplicateSignature.Key); - continue; - } - projectId = parts[0]; - signature = parts[1]; - - var stacks = await _stackRepository.FindAsync(q => q.Project(projectId).FilterExpression($"signature_hash:{signature}")); - if (stacks.Documents.Count < 2) { - _logger.LogError("Did not find multiple stacks with signature {SignatureHash} and project {ProjectId}", signature, projectId); - continue; - } - - var eventCounts = await _eventRepository.CountAsync(q => q.Stack(stacks.Documents.Select(s => s.Id)).AggregationsExpression("terms:stack_id")); - var eventCountBuckets = eventCounts.Aggregations.Terms("terms_stack_id")?.Buckets ?? new List>(); - - // we only need to update events if more than one stack has events associated to it - bool shouldUpdateEvents = eventCountBuckets.Count > 1; - - // default to using the oldest stack - var targetStack = stacks.Documents.OrderBy(s => s.CreatedUtc).First(); - var duplicateStacks = stacks.Documents.OrderBy(s => s.CreatedUtc).Skip(1).ToList(); - - // use the stack that has the most events on it so we can reduce the number of updates - if (eventCountBuckets.Count > 0) { - var targetStackId = eventCountBuckets.OrderByDescending(b => b.Total).First().Key; - targetStack = stacks.Documents.Single(d => d.Id == targetStackId); - duplicateStacks = stacks.Documents.Where(d => d.Id != targetStackId).ToList(); - } - - targetStack.CreatedUtc = stacks.Documents.Min(d => d.CreatedUtc); - targetStack.Status = stacks.Documents.FirstOrDefault(d => d.Status != StackStatus.Open)?.Status ?? StackStatus.Open; - targetStack.LastOccurrence = stacks.Documents.Max(d => d.LastOccurrence); - targetStack.SnoozeUntilUtc = stacks.Documents.Max(d => d.SnoozeUntilUtc); - targetStack.DateFixed = stacks.Documents.Max(d => d.DateFixed); ; - targetStack.TotalOccurrences += duplicateStacks.Sum(d => d.TotalOccurrences); - targetStack.Tags.AddRange(duplicateStacks.SelectMany(d => d.Tags)); - targetStack.References = stacks.Documents.SelectMany(d => d.References).Distinct().ToList(); - targetStack.OccurrencesAreCritical = stacks.Documents.Any(d => d.OccurrencesAreCritical); - - duplicateStacks.ForEach(s => s.IsDeleted = true); - await _stackRepository.SaveAsync(duplicateStacks); - await _stackRepository.SaveAsync(targetStack); - processed++; - - long eventsToMove = eventCountBuckets.Where(b => b.Key != targetStack.Id).Sum(b => b.Total) ?? 0; - _logger.LogInformation("De-duped stack: Target={TargetId} Events={EventCount} Dupes={DuplicateIds} HasEvents={HasEvents}", targetStack.Id, eventsToMove, duplicateStacks.Select(s => s.Id), shouldUpdateEvents); - - if (shouldUpdateEvents) { - var response = await _client.UpdateByQueryAsync(u => u - .Query(q => q.Bool(b => b.Must(m => m - .Terms(t => t.Field(f => f.StackId).Terms(duplicateStacks.Select(s => s.Id))) - ))) - .Script(s => s.Source($"ctx._source.stack_id = '{targetStack.Id}'").Lang(ScriptLang.Painless)) - .Conflicts(Elasticsearch.Net.Conflicts.Proceed) - .WaitForCompletion(false)); - _logger.LogRequest(response, LogLevel.Trace); - - var taskStartedTime = SystemClock.Now; - var taskId = response.Task; - int attempts = 0; - long affectedRecords = 0; - do { - attempts++; - var taskStatus = await _client.Tasks.GetTaskAsync(taskId); - var status = taskStatus.Task.Status; - if (taskStatus.Completed) { - // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. - if (SystemClock.Now.Subtract(taskStartedTime) > TimeSpan.FromSeconds(30)) - _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - - affectedRecords += status.Created + status.Updated + status.Deleted; - break; - } + public override async Task RunAsync(MigrationContext context) { + _logger.LogInformation("Getting duplicate stacks"); + + var duplicateStackAgg = await _client.SearchAsync(q => q + .QueryOnQueryString("is_deleted:false") + .Size(0) + .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); + _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); + + var buckets = duplicateStackAgg.Aggregations.Terms("stacks").Buckets; + int total = buckets.Count; + int processed = 0; + int error = 0; + long totalUpdatedEventCount = 0; + var lastStatus = SystemClock.Now; + int batch = 1; + + while (buckets.Count > 0) { + _logger.LogInformation($"Found {total} duplicate stacks in batch #{batch}."); + + foreach (var duplicateSignature in buckets) { + string projectId = null; + string signature = null; + try { + var parts = duplicateSignature.Key.Split(':'); + if (parts.Length != 2) { + _logger.LogError("Error parsing duplicate signature {DuplicateSignature}", duplicateSignature.Key); + continue; + } + projectId = parts[0]; + signature = parts[1]; + + var stacks = await _stackRepository.FindAsync(q => q.Project(projectId).FilterExpression($"signature_hash:{signature}")); + if (stacks.Documents.Count < 2) { + _logger.LogError("Did not find multiple stacks with signature {SignatureHash} and project {ProjectId}", signature, projectId); + continue; + } + + var eventCounts = await _eventRepository.CountAsync(q => q.Stack(stacks.Documents.Select(s => s.Id)).AggregationsExpression("terms:stack_id")); + var eventCountBuckets = eventCounts.Aggregations.Terms("terms_stack_id")?.Buckets ?? new List>(); + + // we only need to update events if more than one stack has events associated to it + bool shouldUpdateEvents = eventCountBuckets.Count > 1; + + // default to using the oldest stack + var targetStack = stacks.Documents.OrderBy(s => s.CreatedUtc).First(); + var duplicateStacks = stacks.Documents.OrderBy(s => s.CreatedUtc).Skip(1).ToList(); + + // use the stack that has the most events on it so we can reduce the number of updates + if (eventCountBuckets.Count > 0) { + var targetStackId = eventCountBuckets.OrderByDescending(b => b.Total).First().Key; + targetStack = stacks.Documents.Single(d => d.Id == targetStackId); + duplicateStacks = stacks.Documents.Where(d => d.Id != targetStackId).ToList(); + } + targetStack.CreatedUtc = stacks.Documents.Min(d => d.CreatedUtc); + targetStack.Status = stacks.Documents.FirstOrDefault(d => d.Status != StackStatus.Open)?.Status ?? StackStatus.Open; + targetStack.LastOccurrence = stacks.Documents.Max(d => d.LastOccurrence); + targetStack.SnoozeUntilUtc = stacks.Documents.Max(d => d.SnoozeUntilUtc); + targetStack.DateFixed = stacks.Documents.Max(d => d.DateFixed); ; + targetStack.TotalOccurrences += duplicateStacks.Sum(d => d.TotalOccurrences); + targetStack.Tags.AddRange(duplicateStacks.SelectMany(d => d.Tags)); + targetStack.References = stacks.Documents.SelectMany(d => d.References).Distinct().ToList(); + targetStack.OccurrencesAreCritical = stacks.Documents.Any(d => d.OccurrencesAreCritical); + + duplicateStacks.ForEach(s => s.IsDeleted = true); + await _stackRepository.SaveAsync(duplicateStacks); + await _stackRepository.SaveAsync(targetStack); + processed++; + + long eventsToMove = eventCountBuckets.Where(b => b.Key != targetStack.Id).Sum(b => b.Total) ?? 0; + _logger.LogInformation("De-duped stack: Target={TargetId} Events={EventCount} Dupes={DuplicateIds} HasEvents={HasEvents}", targetStack.Id, eventsToMove, duplicateStacks.Select(s => s.Id), shouldUpdateEvents); + + if (shouldUpdateEvents) { + var response = await _client.UpdateByQueryAsync(u => u + .Query(q => q.Bool(b => b.Must(m => m + .Terms(t => t.Field(f => f.StackId).Terms(duplicateStacks.Select(s => s.Id))) + ))) + .Script(s => s.Source($"ctx._source.stack_id = '{targetStack.Id}'").Lang(ScriptLang.Painless)) + .Conflicts(Elasticsearch.Net.Conflicts.Proceed) + .WaitForCompletion(false)); + _logger.LogRequest(response, LogLevel.Trace); + + var taskStartedTime = SystemClock.Now; + var taskId = response.Task; + int attempts = 0; + long affectedRecords = 0; + do { + attempts++; + var taskStatus = await _client.Tasks.GetTaskAsync(taskId); + var status = taskStatus.Task.Status; + if (taskStatus.Completed) { + // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. if (SystemClock.Now.Subtract(taskStartedTime) > TimeSpan.FromSeconds(30)) - _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - - var delay = TimeSpan.FromMilliseconds(50); - if (attempts > 20) - delay = TimeSpan.FromSeconds(5); - else if (attempts > 10) - delay = TimeSpan.FromSeconds(1); - else if (attempts > 5) - delay = TimeSpan.FromMilliseconds(250); - - await Task.Delay(delay); - } while (true); - - _logger.LogInformation("Migrated stack events: Target={TargetId} Events={UpdatedEvents} Dupes={DuplicateIds}", targetStack.Id, affectedRecords, duplicateStacks.Select(s => s.Id)); - - totalUpdatedEventCount += affectedRecords; - } - - if (SystemClock.UtcNow.Subtract(lastStatus) > TimeSpan.FromSeconds(5)) { - lastStatus = SystemClock.UtcNow; - _logger.LogInformation("Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); - await _cache.RemoveByPrefixAsync(nameof(Stack)); - } - } catch (Exception ex) { - error++; - _logger.LogError(ex, "Error fixing duplicate stack {ProjectId} {SignatureHash}", projectId, signature); + _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + + affectedRecords += status.Created + status.Updated + status.Deleted; + break; + } + + if (SystemClock.Now.Subtract(taskStartedTime) > TimeSpan.FromSeconds(30)) + _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + + var delay = TimeSpan.FromMilliseconds(50); + if (attempts > 20) + delay = TimeSpan.FromSeconds(5); + else if (attempts > 10) + delay = TimeSpan.FromSeconds(1); + else if (attempts > 5) + delay = TimeSpan.FromMilliseconds(250); + + await Task.Delay(delay); + } while (true); + + _logger.LogInformation("Migrated stack events: Target={TargetId} Events={UpdatedEvents} Dupes={DuplicateIds}", targetStack.Id, affectedRecords, duplicateStacks.Select(s => s.Id)); + + totalUpdatedEventCount += affectedRecords; + } + + if (SystemClock.UtcNow.Subtract(lastStatus) > TimeSpan.FromSeconds(5)) { + lastStatus = SystemClock.UtcNow; + _logger.LogInformation("Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); + await _cache.RemoveByPrefixAsync(nameof(Stack)); } } + catch (Exception ex) { + error++; + _logger.LogError(ex, "Error fixing duplicate stack {ProjectId} {SignatureHash}", projectId, signature); + } + } - await _client.Indices.RefreshAsync(_config.Stacks.VersionedName); - duplicateStackAgg = await _client.SearchAsync(q => q - .QueryOnQueryString("is_deleted:false") - .Size(0) - .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); - _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); + await _client.Indices.RefreshAsync(_config.Stacks.VersionedName); + duplicateStackAgg = await _client.SearchAsync(q => q + .QueryOnQueryString("is_deleted:false") + .Size(0) + .Aggregations(a => a.Terms("stacks", t => t.Field(f => f.DuplicateSignature).MinimumDocumentCount(2).Size(10000)))); + _logger.LogRequest(duplicateStackAgg, LogLevel.Trace); - buckets = duplicateStackAgg.Aggregations.Terms("stacks").Buckets; - total += buckets.Count; - batch++; + buckets = duplicateStackAgg.Aggregations.Terms("stacks").Buckets; + total += buckets.Count; + batch++; - _logger.LogInformation("Done de-duping stacks: Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); - await _cache.RemoveByPrefixAsync(nameof(Stack)); - } + _logger.LogInformation("Done de-duping stacks: Total={Processed}/{Total} Errors={ErrorCount}", processed, total, error); + await _cache.RemoveByPrefixAsync(nameof(Stack)); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Migrations/SetStackDuplicateSignature.cs b/src/Exceptionless.Core/Migrations/SetStackDuplicateSignature.cs index 4616e89551..9dba3f6607 100644 --- a/src/Exceptionless.Core/Migrations/SetStackDuplicateSignature.cs +++ b/src/Exceptionless.Core/Migrations/SetStackDuplicateSignature.cs @@ -1,6 +1,4 @@ -using System; using System.Diagnostics; -using System.Threading.Tasks; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Caching; @@ -9,69 +7,69 @@ using Microsoft.Extensions.Logging; using Nest; -namespace Exceptionless.Core.Migrations { - public sealed class SetStackDuplicateSignature : MigrationBase { - private readonly IElasticClient _client; - private readonly ExceptionlessElasticConfiguration _config; - private readonly ICacheClient _cache; +namespace Exceptionless.Core.Migrations; - public SetStackDuplicateSignature(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(loggerFactory) { - _config = configuration; - _client = configuration.Client; - _cache = configuration.Cache; - - MigrationType = MigrationType.Repeatable; - } +public sealed class SetStackDuplicateSignature : MigrationBase { + private readonly IElasticClient _client; + private readonly ExceptionlessElasticConfiguration _config; + private readonly ICacheClient _cache; - public override async Task RunAsync(MigrationContext context) { - _logger.LogInformation("Begin refreshing all indices"); - await _config.Client.Indices.RefreshAsync(Indices.All); - _logger.LogInformation("Done refreshing all indices"); + public SetStackDuplicateSignature(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(loggerFactory) { + _config = configuration; + _client = configuration.Client; + _cache = configuration.Cache; - _logger.LogInformation("Updating Stack mappings..."); - var response = await _client.MapAsync(d => { - d.Index(_config.Stacks.VersionedName); - d.Properties(p => p.Keyword(f => f.Name(s => s.DuplicateSignature))); + MigrationType = MigrationType.Repeatable; + } + + public override async Task RunAsync(MigrationContext context) { + _logger.LogInformation("Begin refreshing all indices"); + await _config.Client.Indices.RefreshAsync(Indices.All); + _logger.LogInformation("Done refreshing all indices"); + + _logger.LogInformation("Updating Stack mappings..."); + var response = await _client.MapAsync(d => { + d.Index(_config.Stacks.VersionedName); + d.Properties(p => p.Keyword(f => f.Name(s => s.DuplicateSignature))); + + return d; + }); + _logger.LogRequest(response); - return d; - }); - _logger.LogRequest(response); + _logger.LogInformation("Start populating stack duplicate signature"); + var sw = Stopwatch.StartNew(); + const string script = "ctx._source.duplicate_signature = ctx._source.project_id + ':' + ctx._source.signature_hash;"; + var stackResponse = await _client.UpdateByQueryAsync(x => x + .QueryOnQueryString("NOT _exists_:duplicate_signature") + .Script(s => s.Source(script).Lang(ScriptLang.Painless)) + .Conflicts(Elasticsearch.Net.Conflicts.Proceed) + .WaitForCompletion(false)); - _logger.LogInformation("Start populating stack duplicate signature"); - var sw = Stopwatch.StartNew(); - const string script = "ctx._source.duplicate_signature = ctx._source.project_id + ':' + ctx._source.signature_hash;"; - var stackResponse = await _client.UpdateByQueryAsync(x => x - .QueryOnQueryString("NOT _exists_:duplicate_signature") - .Script(s => s.Source(script).Lang(ScriptLang.Painless)) - .Conflicts(Elasticsearch.Net.Conflicts.Proceed) - .WaitForCompletion(false)); + _logger.LogRequest(stackResponse, Microsoft.Extensions.Logging.LogLevel.Information); - _logger.LogRequest(stackResponse, Microsoft.Extensions.Logging.LogLevel.Information); + var taskId = stackResponse.Task; + int attempts = 0; + long affectedRecords = 0; + do { + attempts++; + var taskStatus = await _client.Tasks.GetTaskAsync(taskId); + var status = taskStatus.Task.Status; + if (taskStatus.Completed) { + // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. + _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + affectedRecords += status.Created + status.Updated + status.Deleted; + break; + } - var taskId = stackResponse.Task; - int attempts = 0; - long affectedRecords = 0; - do { - attempts++; - var taskStatus = await _client.Tasks.GetTaskAsync(taskId); - var status = taskStatus.Task.Status; - if (taskStatus.Completed) { - // TODO: need to check to see if the task failed or completed successfully. Throw if it failed. - _logger.LogInformation("Script operation task ({TaskId}) completed: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - affectedRecords += status.Created + status.Updated + status.Deleted; - break; - } + _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); + var delay = TimeSpan.FromSeconds(attempts <= 5 ? 1 : 5); + await Task.Delay(delay); + } while (true); - _logger.LogInformation("Checking script operation task ({TaskId}) status: Created: {Created} Updated: {Updated} Deleted: {Deleted} Conflicts: {Conflicts} Total: {Total}", taskId, status.Created, status.Updated, status.Deleted, status.VersionConflicts, status.Total); - var delay = TimeSpan.FromSeconds(attempts <= 5 ? 1 : 5); - await Task.Delay(delay); - } while (true); + _logger.LogInformation("Finished adding stack duplicate signature: Time={Duration:d\\.hh\\:mm} Completed={Completed:N0} Total={Total:N0} Errors={Errors:N0}", sw.Elapsed, affectedRecords, stackResponse.Total, stackResponse.Failures.Count); - _logger.LogInformation("Finished adding stack duplicate signature: Time={Duration:d\\.hh\\:mm} Completed={Completed:N0} Total={Total:N0} Errors={Errors:N0}", sw.Elapsed, affectedRecords, stackResponse.Total, stackResponse.Failures.Count); - - _logger.LogInformation("Invalidating Stack Cache"); - await _cache.RemoveByPrefixAsync(nameof(Stack)); - _logger.LogInformation("Invalidating Stack Cache"); - } + _logger.LogInformation("Invalidating Stack Cache"); + await _cache.RemoveByPrefixAsync(nameof(Stack)); + _logger.LogInformation("Invalidating Stack Cache"); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Billing/BillingPlan.cs b/src/Exceptionless.Core/Models/Billing/BillingPlan.cs index cfeb1b904a..0877a3f0ec 100644 --- a/src/Exceptionless.Core/Models/Billing/BillingPlan.cs +++ b/src/Exceptionless.Core/Models/Billing/BillingPlan.cs @@ -1,17 +1,17 @@ using System.Diagnostics; -namespace Exceptionless.Core.Models.Billing { - [DebuggerDisplay("Id: {Id} Name: {Name} Price: {Price}")] - public class BillingPlan { - public string Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public decimal Price { get; set; } - public int MaxProjects { get; set; } - public int MaxUsers { get; set; } - public int RetentionDays { get; set; } - public int MaxEventsPerMonth { get; set; } - public bool HasPremiumFeatures { get; set; } - public bool IsHidden { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.Billing; + +[DebuggerDisplay("Id: {Id} Name: {Name} Price: {Price}")] +public class BillingPlan { + public string Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; set; } + public int MaxProjects { get; set; } + public int MaxUsers { get; set; } + public int RetentionDays { get; set; } + public int MaxEventsPerMonth { get; set; } + public bool HasPremiumFeatures { get; set; } + public bool IsHidden { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Billing/BillingPlanStats.cs b/src/Exceptionless.Core/Models/Billing/BillingPlanStats.cs index 34637ea00b..334e0d0bb9 100644 --- a/src/Exceptionless.Core/Models/Billing/BillingPlanStats.cs +++ b/src/Exceptionless.Core/Models/Billing/BillingPlanStats.cs @@ -1,18 +1,18 @@ -namespace Exceptionless.Core.Models.Billing { - public class BillingPlanStats { - public int SmallTotal { get; set; } - public int SmallYearlyTotal { get; set; } - public int MediumTotal { get; set; } - public int MediumYearlyTotal { get; set; } - public int LargeTotal { get; set; } - public int LargeYearlyTotal { get; set; } - public decimal MonthlyTotal { get; set; } - public decimal YearlyTotal { get; set; } - public int MonthlyTotalAccounts { get; set; } - public int YearlyTotalAccounts { get; set; } - public int FreeAccounts { get; set; } - public int PaidAccounts { get; set; } - public int FreeloaderAccounts { get; set; } - public int SuspendedAccounts { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.Billing; + +public class BillingPlanStats { + public int SmallTotal { get; set; } + public int SmallYearlyTotal { get; set; } + public int MediumTotal { get; set; } + public int MediumYearlyTotal { get; set; } + public int LargeTotal { get; set; } + public int LargeYearlyTotal { get; set; } + public decimal MonthlyTotal { get; set; } + public decimal YearlyTotal { get; set; } + public int MonthlyTotalAccounts { get; set; } + public int YearlyTotalAccounts { get; set; } + public int FreeAccounts { get; set; } + public int PaidAccounts { get; set; } + public int FreeloaderAccounts { get; set; } + public int SuspendedAccounts { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Billing/ChangePlanResult.cs b/src/Exceptionless.Core/Models/Billing/ChangePlanResult.cs index 14e4c01189..f877cc1616 100644 --- a/src/Exceptionless.Core/Models/Billing/ChangePlanResult.cs +++ b/src/Exceptionless.Core/Models/Billing/ChangePlanResult.cs @@ -1,14 +1,14 @@ -namespace Exceptionless.Core.Models.Billing { - public class ChangePlanResult { - public bool Success { get; set; } - public string Message { get; set; } - - public static ChangePlanResult FailWithMessage(string message) { - return new ChangePlanResult { Message = message }; - } - - public static ChangePlanResult SuccessWithMessage(string message) { - return new ChangePlanResult { Success = true, Message = message }; - } +namespace Exceptionless.Core.Models.Billing; + +public class ChangePlanResult { + public bool Success { get; set; } + public string Message { get; set; } + + public static ChangePlanResult FailWithMessage(string message) { + return new ChangePlanResult { Message = message }; + } + + public static ChangePlanResult SuccessWithMessage(string message) { + return new ChangePlanResult { Success = true, Message = message }; } } diff --git a/src/Exceptionless.Core/Models/ClientConfiguration.cs b/src/Exceptionless.Core/Models/ClientConfiguration.cs index 4138402b79..692cfa40f8 100644 --- a/src/Exceptionless.Core/Models/ClientConfiguration.cs +++ b/src/Exceptionless.Core/Models/ClientConfiguration.cs @@ -1,14 +1,14 @@ -namespace Exceptionless.Core.Models { - public class ClientConfiguration { - public ClientConfiguration() { - Settings = new SettingsDictionary(); - } +namespace Exceptionless.Core.Models; - public int Version { get; set; } - public SettingsDictionary Settings { get; private set; } +public class ClientConfiguration { + public ClientConfiguration() { + Settings = new SettingsDictionary(); + } + + public int Version { get; set; } + public SettingsDictionary Settings { get; private set; } - public void IncrementVersion() { - Version++; - } + public void IncrementVersion() { + Version++; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Collections/DataDictionary.cs b/src/Exceptionless.Core/Models/Collections/DataDictionary.cs index 0b566a5a48..cf5759f6f2 100644 --- a/src/Exceptionless.Core/Models/Collections/DataDictionary.cs +++ b/src/Exceptionless.Core/Models/Collections/DataDictionary.cs @@ -1,46 +1,45 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models { - public class DataDictionary : Dictionary { - public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) {} +namespace Exceptionless.Core.Models; - public DataDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) { - foreach (var kvp in values) - Add(kvp.Key, kvp.Value); - } +public class DataDictionary : Dictionary { + public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) { } - public object GetValueOrDefault(string key) { - return TryGetValue(key, out object value) ? value : null; - } + public DataDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) { + foreach (var kvp in values) + Add(kvp.Key, kvp.Value); + } - public object GetValueOrDefault(string key, object defaultValue) { - return TryGetValue(key, out object value) ? value : defaultValue; - } + public object GetValueOrDefault(string key) { + return TryGetValue(key, out object value) ? value : null; + } - public object GetValueOrDefault(string key, Func defaultValueProvider) { - return TryGetValue(key, out object value) ? value : defaultValueProvider(); - } + public object GetValueOrDefault(string key, object defaultValue) { + return TryGetValue(key, out object value) ? value : defaultValue; + } - public string GetString(string name) { - return GetString(name, String.Empty); - } + public object GetValueOrDefault(string key, Func defaultValueProvider) { + return TryGetValue(key, out object value) ? value : defaultValueProvider(); + } + + public string GetString(string name) { + return GetString(name, String.Empty); + } - public string GetString(string name, string @default) { - if (!TryGetValue(name, out object value)) - return @default; + public string GetString(string name, string @default) { + if (!TryGetValue(name, out object value)) + return @default; - if (value is string s) - return s; + if (value is string s) + return s; - if (value != null) { - try { - return value.ToType(); - } catch { } + if (value != null) { + try { + return value.ToType(); } - - return String.Empty; + catch { } } + + return String.Empty; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Collections/GenericArguments.cs b/src/Exceptionless.Core/Models/Collections/GenericArguments.cs index 0e27f32a34..d9a7f525fa 100644 --- a/src/Exceptionless.Core/Models/Collections/GenericArguments.cs +++ b/src/Exceptionless.Core/Models/Collections/GenericArguments.cs @@ -1,5 +1,5 @@ using System.Collections.ObjectModel; -namespace Exceptionless.Core.Models { - public class GenericArguments : Collection {} -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public class GenericArguments : Collection { } diff --git a/src/Exceptionless.Core/Models/Collections/ModuleCollection.cs b/src/Exceptionless.Core/Models/Collections/ModuleCollection.cs index a33b592af0..3cea8e5d9d 100644 --- a/src/Exceptionless.Core/Models/Collections/ModuleCollection.cs +++ b/src/Exceptionless.Core/Models/Collections/ModuleCollection.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Models { - public class ModuleCollection : Collection { } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public class ModuleCollection : Collection { } diff --git a/src/Exceptionless.Core/Models/Collections/ObservableDictionary.cs b/src/Exceptionless.Core/Models/Collections/ObservableDictionary.cs index 5f4f126f10..e1a8d763ff 100644 --- a/src/Exceptionless.Core/Models/Collections/ObservableDictionary.cs +++ b/src/Exceptionless.Core/Models/Collections/ObservableDictionary.cs @@ -1,126 +1,124 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; -namespace Exceptionless.Core.Models.Collections { - public class ObservableDictionary : IDictionary { - private readonly IDictionary _dictionary; +namespace Exceptionless.Core.Models.Collections; - public ObservableDictionary() { - _dictionary = new Dictionary(); - } - - public ObservableDictionary(IDictionary dictionary) { - _dictionary = new Dictionary(dictionary); - } +public class ObservableDictionary : IDictionary { + private readonly IDictionary _dictionary; - public ObservableDictionary(IEqualityComparer comparer) { - _dictionary = new Dictionary(comparer); - } + public ObservableDictionary() { + _dictionary = new Dictionary(); + } - public ObservableDictionary(IDictionary dictionary, IEqualityComparer comparer) { - _dictionary = new Dictionary(dictionary, comparer); - } + public ObservableDictionary(IDictionary dictionary) { + _dictionary = new Dictionary(dictionary); + } - public void Add(TKey key, TValue value) { - _dictionary.Add(key, value); + public ObservableDictionary(IEqualityComparer comparer) { + _dictionary = new Dictionary(comparer); + } - OnChanged(new ChangedEventArgs>(new KeyValuePair(key, value), ChangedAction.Add)); - } + public ObservableDictionary(IDictionary dictionary, IEqualityComparer comparer) { + _dictionary = new Dictionary(dictionary, comparer); + } - public void Add(KeyValuePair item) { - _dictionary.Add(item); + public void Add(TKey key, TValue value) { + _dictionary.Add(key, value); - OnChanged(new ChangedEventArgs>(item, ChangedAction.Add)); - } + OnChanged(new ChangedEventArgs>(new KeyValuePair(key, value), ChangedAction.Add)); + } - public bool Remove(TKey key) { - bool success = _dictionary.Remove(key); + public void Add(KeyValuePair item) { + _dictionary.Add(item); - if (success) - OnChanged(new ChangedEventArgs>(new KeyValuePair(key, default), ChangedAction.Remove)); + OnChanged(new ChangedEventArgs>(item, ChangedAction.Add)); + } - return success; - } + public bool Remove(TKey key) { + bool success = _dictionary.Remove(key); - public bool Remove(KeyValuePair item) { - bool success = _dictionary.Remove(item); + if (success) + OnChanged(new ChangedEventArgs>(new KeyValuePair(key, default), ChangedAction.Remove)); - if (success) - OnChanged(new ChangedEventArgs>(item, ChangedAction.Remove)); + return success; + } - return success; - } + public bool Remove(KeyValuePair item) { + bool success = _dictionary.Remove(item); - public void Clear() { - _dictionary.Clear(); + if (success) + OnChanged(new ChangedEventArgs>(item, ChangedAction.Remove)); - OnChanged(new ChangedEventArgs>(new KeyValuePair(), ChangedAction.Clear)); - } + return success; + } - public bool ContainsKey(TKey key) { - return _dictionary.ContainsKey(key); - } + public void Clear() { + _dictionary.Clear(); - public bool Contains(KeyValuePair item) { - return _dictionary.Contains(item); - } + OnChanged(new ChangedEventArgs>(new KeyValuePair(), ChangedAction.Clear)); + } - public bool TryGetValue(TKey key, out TValue value) { - return _dictionary.TryGetValue(key, out value); - } + public bool ContainsKey(TKey key) { + return _dictionary.ContainsKey(key); + } - public void CopyTo(KeyValuePair[] array, int arrayIndex) { - _dictionary.CopyTo(array, arrayIndex); - } + public bool Contains(KeyValuePair item) { + return _dictionary.Contains(item); + } - public ICollection Keys => _dictionary.Keys; + public bool TryGetValue(TKey key, out TValue value) { + return _dictionary.TryGetValue(key, out value); + } - public ICollection Values => _dictionary.Values; + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + _dictionary.CopyTo(array, arrayIndex); + } - public int Count => _dictionary.Count; + public ICollection Keys => _dictionary.Keys; - public bool IsReadOnly => _dictionary.IsReadOnly; + public ICollection Values => _dictionary.Values; - public TValue this[TKey key] { - get { return _dictionary[key]; } - set { - var action = ContainsKey(key) ? ChangedAction.Update : ChangedAction.Add; + public int Count => _dictionary.Count; - _dictionary[key] = value; - OnChanged(new ChangedEventArgs>(new KeyValuePair(key, value), action)); - } - } + public bool IsReadOnly => _dictionary.IsReadOnly; - public IEnumerator> GetEnumerator() { - return _dictionary.GetEnumerator(); - } + public TValue this[TKey key] { + get { return _dictionary[key]; } + set { + var action = ContainsKey(key) ? ChangedAction.Update : ChangedAction.Add; - IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); + _dictionary[key] = value; + OnChanged(new ChangedEventArgs>(new KeyValuePair(key, value), action)); } + } - public event EventHandler>> Changed; + public IEnumerator> GetEnumerator() { + return _dictionary.GetEnumerator(); + } - private void OnChanged(ChangedEventArgs> args) { - Changed?.Invoke(this, args); - } + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); } - public class ChangedEventArgs : EventArgs { - public T Item { get; private set; } - public ChangedAction Action { get; private set; } + public event EventHandler>> Changed; - public ChangedEventArgs(T item, ChangedAction action) { - Item = item; - Action = action; - } + private void OnChanged(ChangedEventArgs> args) { + Changed?.Invoke(this, args); } +} + +public class ChangedEventArgs : EventArgs { + public T Item { get; private set; } + public ChangedAction Action { get; private set; } - public enum ChangedAction { - Add, - Remove, - Clear, - Update + public ChangedEventArgs(T item, ChangedAction action) { + Item = item; + Action = action; } } + +public enum ChangedAction { + Add, + Remove, + Clear, + Update +} diff --git a/src/Exceptionless.Core/Models/Collections/ParameterCollection.cs b/src/Exceptionless.Core/Models/Collections/ParameterCollection.cs index 4625efc17f..989e9bfce9 100644 --- a/src/Exceptionless.Core/Models/Collections/ParameterCollection.cs +++ b/src/Exceptionless.Core/Models/Collections/ParameterCollection.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Models { - public class ParameterCollection : Collection { } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public class ParameterCollection : Collection { } diff --git a/src/Exceptionless.Core/Models/Collections/SettingsDictionary.cs b/src/Exceptionless.Core/Models/Collections/SettingsDictionary.cs index 7af77947ca..368c0cb642 100644 --- a/src/Exceptionless.Core/Models/Collections/SettingsDictionary.cs +++ b/src/Exceptionless.Core/Models/Collections/SettingsDictionary.cs @@ -1,144 +1,141 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Models.Collections; - -namespace Exceptionless.Core.Models { - public class SettingsDictionary : ObservableDictionary { - public SettingsDictionary() : base(StringComparer.OrdinalIgnoreCase) {} - - public SettingsDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) { - foreach (var kvp in values) - Add(kvp.Key, kvp.Value); - } +using Exceptionless.Core.Models.Collections; - public string GetString(string name) { - return GetString(name, String.Empty); - } +namespace Exceptionless.Core.Models; - public string GetString(string name, string @default) { - if (TryGetValue(name, out string value)) - return value; +public class SettingsDictionary : ObservableDictionary { + public SettingsDictionary() : base(StringComparer.OrdinalIgnoreCase) { } - return @default; - } + public SettingsDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) { + foreach (var kvp in values) + Add(kvp.Key, kvp.Value); + } - public bool GetBoolean(string name) { - return GetBoolean(name, false); - } + public string GetString(string name) { + return GetString(name, String.Empty); + } - public bool GetBoolean(string name, bool @default) { - bool result = TryGetValue(name, out string temp); - if (!result) - return @default; + public string GetString(string name, string @default) { + if (TryGetValue(name, out string value)) + return value; - result = Boolean.TryParse(temp, out bool value); - return result ? value : @default; - } + return @default; + } - public int GetInt32(string name) { - return GetInt32(name, 0); - } + public bool GetBoolean(string name) { + return GetBoolean(name, false); + } - public int GetInt32(string name, int @default) { - bool result = TryGetValue(name, out string temp); - if (!result) - return @default; + public bool GetBoolean(string name, bool @default) { + bool result = TryGetValue(name, out string temp); + if (!result) + return @default; - result = Int32.TryParse(temp, out int value); - return result ? value : @default; - } + result = Boolean.TryParse(temp, out bool value); + return result ? value : @default; + } - public long GetInt64(string name) { - return GetInt64(name, 0L); - } + public int GetInt32(string name) { + return GetInt32(name, 0); + } - public long GetInt64(string name, long @default) { - bool result = TryGetValue(name, out string temp); - if (!result) - return @default; + public int GetInt32(string name, int @default) { + bool result = TryGetValue(name, out string temp); + if (!result) + return @default; - result = Int64.TryParse(temp, out long value); - return result ? value : @default; - } + result = Int32.TryParse(temp, out int value); + return result ? value : @default; + } - public double GetDouble(string name, double @default = 0d) { - bool result = TryGetValue(name, out string temp); - if (!result) - return @default; + public long GetInt64(string name) { + return GetInt64(name, 0L); + } - result = Double.TryParse(temp, out double value); - return result ? value : @default; - } + public long GetInt64(string name, long @default) { + bool result = TryGetValue(name, out string temp); + if (!result) + return @default; - public DateTime GetDateTime(string name) { - return GetDateTime(name, DateTime.MinValue); - } + result = Int64.TryParse(temp, out long value); + return result ? value : @default; + } - public DateTime GetDateTime(string name, DateTime @default) { - bool result = TryGetValue(name, out string temp); - if (!result) - return @default; + public double GetDouble(string name, double @default = 0d) { + bool result = TryGetValue(name, out string temp); + if (!result) + return @default; - result = DateTime.TryParse(temp, out var value); - return result ? value : @default; - } + result = Double.TryParse(temp, out double value); + return result ? value : @default; + } - public DateTimeOffset GetDateTimeOffset(string name) { - return GetDateTimeOffset(name, DateTimeOffset.MinValue); - } + public DateTime GetDateTime(string name) { + return GetDateTime(name, DateTime.MinValue); + } - public DateTimeOffset GetDateTimeOffset(string name, DateTimeOffset @default) { - bool result = TryGetValue(name, out string temp); - if (!result) - return @default; + public DateTime GetDateTime(string name, DateTime @default) { + bool result = TryGetValue(name, out string temp); + if (!result) + return @default; - result = DateTimeOffset.TryParse(temp, out var value); - return result ? value : @default; - } + result = DateTime.TryParse(temp, out var value); + return result ? value : @default; + } - public Guid GetGuid(string name) { - return GetGuid(name, Guid.Empty); - } + public DateTimeOffset GetDateTimeOffset(string name) { + return GetDateTimeOffset(name, DateTimeOffset.MinValue); + } - public Guid GetGuid(string name, Guid @default) { - bool result = TryGetValue(name, out string temp); - return result ? new Guid(temp) : @default; - } + public DateTimeOffset GetDateTimeOffset(string name, DateTimeOffset @default) { + bool result = TryGetValue(name, out string temp); + if (!result) + return @default; - public IEnumerable GetStringCollection(string name) { - return GetStringCollection(name, null); - } + result = DateTimeOffset.TryParse(temp, out var value); + return result ? value : @default; + } - public IEnumerable GetStringCollection(string name, string @default) { - string value = GetString(name, @default); + public Guid GetGuid(string name) { + return GetGuid(name, Guid.Empty); + } - if (String.IsNullOrEmpty(value)) - return Enumerable.Empty(); + public Guid GetGuid(string name, Guid @default) { + bool result = TryGetValue(name, out string temp); + return result ? new Guid(temp) : @default; + } - string[] values = value.Split(new[] { ",", ";", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + public IEnumerable GetStringCollection(string name) { + return GetStringCollection(name, null); + } - for (int i = 0; i < values.Length; i++) - values[i] = values[i].Trim(); + public IEnumerable GetStringCollection(string name, string @default) { + string value = GetString(name, @default); - var list = new List(values); - return list; - } + if (String.IsNullOrEmpty(value)) + return Enumerable.Empty(); - public void Apply(IEnumerable> values) { - if (values == null) - return; + string[] values = value.Split(new[] { ",", ";", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); - foreach (var v in values) { - if (!ContainsKey(v.Key) || v.Value != this[v.Key]) - this[v.Key] = v.Value; - } - } + for (int i = 0; i < values.Length; i++) + values[i] = values[i].Trim(); + + var list = new List(values); + return list; + } - public static class KnownKeys { - public const string DataExclusions = "@@DataExclusions"; - public const string IncludePrivateInformation = "@@IncludePrivateInformation"; - public const string UserAgentBotPatterns = "@@UserAgentBotPatterns"; + public void Apply(IEnumerable> values) { + if (values == null) + return; + + foreach (var v in values) { + if (!ContainsKey(v.Key) || v.Value != this[v.Key]) + this[v.Key] = v.Value; } } -} \ No newline at end of file + + public static class KnownKeys { + public const string DataExclusions = "@@DataExclusions"; + public const string IncludePrivateInformation = "@@IncludePrivateInformation"; + public const string UserAgentBotPatterns = "@@UserAgentBotPatterns"; + } +} diff --git a/src/Exceptionless.Core/Models/Collections/StackFrameCollection.cs b/src/Exceptionless.Core/Models/Collections/StackFrameCollection.cs index 714ce8de58..e63fdc6b6d 100644 --- a/src/Exceptionless.Core/Models/Collections/StackFrameCollection.cs +++ b/src/Exceptionless.Core/Models/Collections/StackFrameCollection.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Models { - public class StackFrameCollection : Collection { } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public class StackFrameCollection : Collection { } diff --git a/src/Exceptionless.Core/Models/Collections/TagSet.cs b/src/Exceptionless.Core/Models/Collections/TagSet.cs index db2d5d5c94..12bc93fee7 100644 --- a/src/Exceptionless.Core/Models/Collections/TagSet.cs +++ b/src/Exceptionless.Core/Models/Collections/TagSet.cs @@ -1,34 +1,31 @@ -using System; -using System.Collections.Generic; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class TagSet : HashSet { - public TagSet() : base(StringComparer.OrdinalIgnoreCase) {} +public class TagSet : HashSet { + public TagSet() : base(StringComparer.OrdinalIgnoreCase) { } - public TagSet(IEnumerable values) : base(StringComparer.OrdinalIgnoreCase) { - foreach (string value in values) - Add(value); - } + public TagSet(IEnumerable values) : base(StringComparer.OrdinalIgnoreCase) { + foreach (string value in values) + Add(value); + } - public new IDisposable Add(string item) { - base.Add(item); - return new DisposableTag(this, item); - } + public new IDisposable Add(string item) { + base.Add(item); + return new DisposableTag(this, item); + } - private class DisposableTag : IDisposable { - private readonly TagSet _items; + private class DisposableTag : IDisposable { + private readonly TagSet _items; - public DisposableTag(TagSet items, string value) { - _items = items; - Value = value; - } + public DisposableTag(TagSet items, string value) { + _items = items; + Value = value; + } - public string Value { get; private set; } + public string Value { get; private set; } - public void Dispose() { - if (_items.Contains(Value)) - _items.Remove(Value); - } + public void Dispose() { + if (_items.Contains(Value)) + _items.Remove(Value); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/CoreMappings.cs b/src/Exceptionless.Core/Models/CoreMappings.cs index ffa4d34a77..d450665d90 100644 --- a/src/Exceptionless.Core/Models/CoreMappings.cs +++ b/src/Exceptionless.Core/Models/CoreMappings.cs @@ -1,5 +1,5 @@ using AutoMapper; -namespace Exceptionless.Core.Models { - public class CoreMappings: Profile {} -} +namespace Exceptionless.Core.Models; + +public class CoreMappings : Profile { } diff --git a/src/Exceptionless.Core/Models/Data/EnvironmentInfo.cs b/src/Exceptionless.Core/Models/Data/EnvironmentInfo.cs index fa9efb6975..6bcf490093 100644 --- a/src/Exceptionless.Core/Models/Data/EnvironmentInfo.cs +++ b/src/Exceptionless.Core/Models/Data/EnvironmentInfo.cs @@ -1,137 +1,136 @@ -using System; using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - public class EnvironmentInfo : IData { - public EnvironmentInfo() { - Data = new DataDictionary(); - } +namespace Exceptionless.Core.Models.Data; - /// - /// Gets the number of processors for the current machine. - /// - /// The number of processors for the current machine. - public int ProcessorCount { get; set; } - - /// - /// Gets the amount of physical memory for the current machine. - /// - /// The amount of physical memory for the current machine. - public long TotalPhysicalMemory { get; set; } - - /// - /// Gets the amount of physical memory mapped to the process context. - /// - /// The amount of physical memory mapped to the process context. - public long AvailablePhysicalMemory { get; set; } - - /// - /// Gets the command line information used to start the process. - /// - /// The command line information used to start the process. - public string CommandLine { get; set; } - - /// - /// The name of the process that the error occurred in. - /// - public string ProcessName { get; set; } - - /// - /// Gets the process id. - /// - /// The process id. - public string ProcessId { get; set; } - - /// - /// Gets the amount of physical memory used by the process. - /// - /// The amount of physical memory used by the process. - public long ProcessMemorySize { get; set; } - - /// - /// Gets the name of the thread. - /// - /// The name of the thread. - public string ThreadName { get; set; } - - /// - /// Gets the win32 thread id. - /// - /// The win32 thread id. - public string ThreadId { get; set; } - - /// - /// Gets the OS architecture. - /// - /// The OS architecture. - public string Architecture { get; set; } - - /// - /// The OS name that the error occurred on. - /// - public string OSName { get; set; } - - /// - /// The OS version that the error occurred on. - /// - public string OSVersion { get; set; } - - /// - /// The Ip Address of the machine that the error occurred on. - /// - public string IpAddress { get; set; } - - /// - /// The name of the machine that the error occurred on. - /// - public string MachineName { get; set; } - - /// - /// A unique value identifying each Exceptionless client installation. - /// - public string InstallId { get; set; } - - /// - /// The runtime version the application was running under when the error occurred. - /// - public string RuntimeVersion { get; set; } - - /// - /// Extended data entries for this machine environment. - /// - public DataDictionary Data { get; set; } - - protected bool Equals(EnvironmentInfo other) { - return ProcessorCount == other.ProcessorCount && TotalPhysicalMemory == other.TotalPhysicalMemory && AvailablePhysicalMemory == other.AvailablePhysicalMemory && String.Equals(CommandLine, other.CommandLine) && String.Equals(ProcessName, other.ProcessName) && String.Equals(ProcessId, other.ProcessId) && ProcessMemorySize == other.ProcessMemorySize && String.Equals(ThreadName, other.ThreadName) && String.Equals(ThreadId, other.ThreadId) && String.Equals(Architecture, other.Architecture) && String.Equals(OSName, other.OSName) && String.Equals(OSVersion, other.OSVersion) && String.Equals(IpAddress, other.IpAddress) && String.Equals(MachineName, other.MachineName) && String.Equals(InstallId, other.InstallId) && String.Equals(RuntimeVersion, other.RuntimeVersion) && Equals(Data, other.Data); - } +public class EnvironmentInfo : IData { + public EnvironmentInfo() { + Data = new DataDictionary(); + } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((EnvironmentInfo)obj); - } + /// + /// Gets the number of processors for the current machine. + /// + /// The number of processors for the current machine. + public int ProcessorCount { get; set; } + + /// + /// Gets the amount of physical memory for the current machine. + /// + /// The amount of physical memory for the current machine. + public long TotalPhysicalMemory { get; set; } + + /// + /// Gets the amount of physical memory mapped to the process context. + /// + /// The amount of physical memory mapped to the process context. + public long AvailablePhysicalMemory { get; set; } + + /// + /// Gets the command line information used to start the process. + /// + /// The command line information used to start the process. + public string CommandLine { get; set; } + + /// + /// The name of the process that the error occurred in. + /// + public string ProcessName { get; set; } + + /// + /// Gets the process id. + /// + /// The process id. + public string ProcessId { get; set; } + + /// + /// Gets the amount of physical memory used by the process. + /// + /// The amount of physical memory used by the process. + public long ProcessMemorySize { get; set; } + + /// + /// Gets the name of the thread. + /// + /// The name of the thread. + public string ThreadName { get; set; } + + /// + /// Gets the win32 thread id. + /// + /// The win32 thread id. + public string ThreadId { get; set; } + + /// + /// Gets the OS architecture. + /// + /// The OS architecture. + public string Architecture { get; set; } + + /// + /// The OS name that the error occurred on. + /// + public string OSName { get; set; } + + /// + /// The OS version that the error occurred on. + /// + public string OSVersion { get; set; } + + /// + /// The Ip Address of the machine that the error occurred on. + /// + public string IpAddress { get; set; } + + /// + /// The name of the machine that the error occurred on. + /// + public string MachineName { get; set; } + + /// + /// A unique value identifying each Exceptionless client installation. + /// + public string InstallId { get; set; } + + /// + /// The runtime version the application was running under when the error occurred. + /// + public string RuntimeVersion { get; set; } + + /// + /// Extended data entries for this machine environment. + /// + public DataDictionary Data { get; set; } + + protected bool Equals(EnvironmentInfo other) { + return ProcessorCount == other.ProcessorCount && TotalPhysicalMemory == other.TotalPhysicalMemory && AvailablePhysicalMemory == other.AvailablePhysicalMemory && String.Equals(CommandLine, other.CommandLine) && String.Equals(ProcessName, other.ProcessName) && String.Equals(ProcessId, other.ProcessId) && ProcessMemorySize == other.ProcessMemorySize && String.Equals(ThreadName, other.ThreadName) && String.Equals(ThreadId, other.ThreadId) && String.Equals(Architecture, other.Architecture) && String.Equals(OSName, other.OSName) && String.Equals(OSVersion, other.OSVersion) && String.Equals(IpAddress, other.IpAddress) && String.Equals(MachineName, other.MachineName) && String.Equals(InstallId, other.InstallId) && String.Equals(RuntimeVersion, other.RuntimeVersion) && Equals(Data, other.Data); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((EnvironmentInfo)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = ProcessorCount; - hashCode = (hashCode * 397) ^ TotalPhysicalMemory.GetHashCode(); - hashCode = (hashCode * 397) ^ (CommandLine?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (ProcessName?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (ProcessId?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Architecture?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (OSName?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (OSVersion?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (IpAddress?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (MachineName?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (InstallId?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (RuntimeVersion?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = ProcessorCount; + hashCode = (hashCode * 397) ^ TotalPhysicalMemory.GetHashCode(); + hashCode = (hashCode * 397) ^ (CommandLine?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (ProcessName?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (ProcessId?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Architecture?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (OSName?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (OSVersion?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (IpAddress?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (MachineName?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (InstallId?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (RuntimeVersion?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/Error.cs b/src/Exceptionless.Core/Models/Data/Error.cs index 017ac6fd75..96cbd0243e 100644 --- a/src/Exceptionless.Core/Models/Data/Error.cs +++ b/src/Exceptionless.Core/Models/Data/Error.cs @@ -1,39 +1,39 @@ using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - public class Error : InnerError { - public Error() { - Modules = new ModuleCollection(); - } +namespace Exceptionless.Core.Models.Data; - /// - /// Any modules that were loaded / referenced when the error occurred. - /// - public ModuleCollection Modules { get; set; } +public class Error : InnerError { + public Error() { + Modules = new ModuleCollection(); + } - public static class KnownDataKeys { - public const string ExtraProperties = "@ext"; - public const string TargetInfo = "@target"; - } + /// + /// Any modules that were loaded / referenced when the error occurred. + /// + public ModuleCollection Modules { get; set; } - protected bool Equals(Error other) { - return base.Equals(other) && Modules.CollectionEquals(other.Modules); - } + public static class KnownDataKeys { + public const string ExtraProperties = "@ext"; + public const string TargetInfo = "@target"; + } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((Error)obj); - } + protected bool Equals(Error other) { + return base.Equals(other) && Modules.CollectionEquals(other.Modules); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((Error)obj); + } - public override int GetHashCode() { - unchecked { - return (base.GetHashCode() * 397) ^ (Modules?.GetCollectionHashCode() ?? 0); - } + public override int GetHashCode() { + unchecked { + return (base.GetHashCode() * 397) ^ (Modules?.GetCollectionHashCode() ?? 0); } } } diff --git a/src/Exceptionless.Core/Models/Data/InnerError.cs b/src/Exceptionless.Core/Models/Data/InnerError.cs index b2783664c8..6884db7754 100644 --- a/src/Exceptionless.Core/Models/Data/InnerError.cs +++ b/src/Exceptionless.Core/Models/Data/InnerError.cs @@ -1,73 +1,72 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - public class InnerError : IData { - public InnerError() { - Data = new DataDictionary(); - StackTrace = new StackFrameCollection(); - } +namespace Exceptionless.Core.Models.Data; - /// - /// The error message. - /// - public string Message { get; set; } +public class InnerError : IData { + public InnerError() { + Data = new DataDictionary(); + StackTrace = new StackFrameCollection(); + } - /// - /// The error type. - /// - public string Type { get; set; } + /// + /// The error message. + /// + public string Message { get; set; } - /// - /// The error code. - /// - public string Code { get; set; } + /// + /// The error type. + /// + public string Type { get; set; } - /// - /// Extended data entries for this error. - /// - public DataDictionary Data { get; set; } + /// + /// The error code. + /// + public string Code { get; set; } - /// - /// An inner (nested) error. - /// - public InnerError Inner { get; set; } + /// + /// Extended data entries for this error. + /// + public DataDictionary Data { get; set; } - /// - /// The stack trace for the error. - /// - public StackFrameCollection StackTrace { get; set; } + /// + /// An inner (nested) error. + /// + public InnerError Inner { get; set; } - /// - /// The target method. - /// - public Method TargetMethod { get; set; } + /// + /// The stack trace for the error. + /// + public StackFrameCollection StackTrace { get; set; } - protected bool Equals(InnerError other) { - return String.Equals(Message, other.Message) && String.Equals(Type, other.Type) && String.Equals(Code, other.Code) && Equals(Data, other.Data) && Equals(Inner, other.Inner) && StackTrace.CollectionEquals(other.StackTrace) && Equals(TargetMethod, other.TargetMethod); - } + /// + /// The target method. + /// + public Method TargetMethod { get; set; } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((InnerError)obj); - } + protected bool Equals(InnerError other) { + return String.Equals(Message, other.Message) && String.Equals(Type, other.Type) && String.Equals(Code, other.Code) && Equals(Data, other.Data) && Equals(Inner, other.Inner) && StackTrace.CollectionEquals(other.StackTrace) && Equals(TargetMethod, other.TargetMethod); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((InnerError)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = Message?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Type?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Code?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Inner?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (StackTrace?.GetCollectionHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (TargetMethod?.GetHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = Message?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (Type?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Code?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Inner?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (StackTrace?.GetCollectionHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (TargetMethod?.GetHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/Location.cs b/src/Exceptionless.Core/Models/Data/Location.cs index c398161629..af43a82230 100644 --- a/src/Exceptionless.Core/Models/Data/Location.cs +++ b/src/Exceptionless.Core/Models/Data/Location.cs @@ -1,23 +1,23 @@ using System.Diagnostics; -namespace Exceptionless.Core.Models.Data { - [DebuggerDisplay("{Locality}, {Level2}, {Level1}, {Country}")] - public class Location { - public string Country { get; set; } +namespace Exceptionless.Core.Models.Data; - /// - /// State / Province - /// - public string Level1 { get; set; } - - /// - /// County - /// - public string Level2 { get; set; } - - /// - /// City - /// - public string Locality { get; set; } - } -} \ No newline at end of file +[DebuggerDisplay("{Locality}, {Level2}, {Level1}, {Country}")] +public class Location { + public string Country { get; set; } + + /// + /// State / Province + /// + public string Level1 { get; set; } + + /// + /// County + /// + public string Level2 { get; set; } + + /// + /// City + /// + public string Locality { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs b/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs index ca683c0bd8..ff9cfec931 100644 --- a/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs +++ b/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs @@ -1,33 +1,31 @@ -using System; -using System.Collections.Generic; using Foundatio.Repositories.Extensions; -namespace Exceptionless.Core.Models.Data { - public class ManualStackingInfo { - public ManualStackingInfo() { - SignatureData = new Dictionary(); - } +namespace Exceptionless.Core.Models.Data; - public ManualStackingInfo(string title) : this() { - if (!String.IsNullOrWhiteSpace(title)) - Title = title.Trim(); - } - - public ManualStackingInfo(string title, IDictionary signatureData) : this(title) { - if (signatureData != null && signatureData.Count > 0) - SignatureData.AddRange(signatureData); - } - - public ManualStackingInfo(IDictionary signatureData) : this(null, signatureData) {} +public class ManualStackingInfo { + public ManualStackingInfo() { + SignatureData = new Dictionary(); + } - /// - /// Stack Title (defaults to the event message) - /// - public string Title { get; set; } + public ManualStackingInfo(string title) : this() { + if (!String.IsNullOrWhiteSpace(title)) + Title = title.Trim(); + } - /// - /// Key value pair that determines how the event is stacked. - /// - public IDictionary SignatureData { get; set; } + public ManualStackingInfo(string title, IDictionary signatureData) : this(title) { + if (signatureData != null && signatureData.Count > 0) + SignatureData.AddRange(signatureData); } -} \ No newline at end of file + + public ManualStackingInfo(IDictionary signatureData) : this(null, signatureData) { } + + /// + /// Stack Title (defaults to the event message) + /// + public string Title { get; set; } + + /// + /// Key value pair that determines how the event is stacked. + /// + public IDictionary SignatureData { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Data/Method.cs b/src/Exceptionless.Core/Models/Data/Method.cs index 76595af0f7..2fdfa4ef2b 100644 --- a/src/Exceptionless.Core/Models/Data/Method.cs +++ b/src/Exceptionless.Core/Models/Data/Method.cs @@ -1,51 +1,50 @@ -using System; -using Exceptionless.Core.Extensions; - -namespace Exceptionless.Core.Models.Data { - public class Method : IData { - public Method() { - Data = new DataDictionary(); - GenericArguments = new GenericArguments(); - Parameters = new ParameterCollection(); - } +using Exceptionless.Core.Extensions; - public bool IsSignatureTarget { get; set; } - public string DeclaringNamespace { get; set; } - public string DeclaringType { get; set; } +namespace Exceptionless.Core.Models.Data; - public string Name { get; set; } +public class Method : IData { + public Method() { + Data = new DataDictionary(); + GenericArguments = new GenericArguments(); + Parameters = new ParameterCollection(); + } - public int ModuleId { get; set; } - public DataDictionary Data { get; set; } - public GenericArguments GenericArguments { get; set; } - public ParameterCollection Parameters { get; set; } + public bool IsSignatureTarget { get; set; } + public string DeclaringNamespace { get; set; } + public string DeclaringType { get; set; } - protected bool Equals(Method other) { - return IsSignatureTarget == other.IsSignatureTarget && String.Equals(DeclaringNamespace, other.DeclaringNamespace) && String.Equals(DeclaringType, other.DeclaringType) && String.Equals(Name, other.Name) && Equals(Data, other.Data) && GenericArguments.CollectionEquals(other.GenericArguments) && Parameters.CollectionEquals(other.Parameters); - } + public string Name { get; set; } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; + public int ModuleId { get; set; } + public DataDictionary Data { get; set; } + public GenericArguments GenericArguments { get; set; } + public ParameterCollection Parameters { get; set; } - return Equals((Method)obj); - } + protected bool Equals(Method other) { + return IsSignatureTarget == other.IsSignatureTarget && String.Equals(DeclaringNamespace, other.DeclaringNamespace) && String.Equals(DeclaringType, other.DeclaringType) && String.Equals(Name, other.Name) && Equals(Data, other.Data) && GenericArguments.CollectionEquals(other.GenericArguments) && Parameters.CollectionEquals(other.Parameters); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + + return Equals((Method)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = IsSignatureTarget.GetHashCode(); - hashCode = (hashCode * 397) ^ (DeclaringNamespace?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (DeclaringType?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode(new[] { "ILOffset", "NativeOffset" }) ?? 0); - hashCode = (hashCode * 397) ^ (GenericArguments?.GetCollectionHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Parameters?.GetCollectionHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = IsSignatureTarget.GetHashCode(); + hashCode = (hashCode * 397) ^ (DeclaringNamespace?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (DeclaringType?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode(new[] { "ILOffset", "NativeOffset" }) ?? 0); + hashCode = (hashCode * 397) ^ (GenericArguments?.GetCollectionHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Parameters?.GetCollectionHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/Module.cs b/src/Exceptionless.Core/Models/Data/Module.cs index 5a14ed8a2c..d3cc56347b 100644 --- a/src/Exceptionless.Core/Models/Data/Module.cs +++ b/src/Exceptionless.Core/Models/Data/Module.cs @@ -1,57 +1,56 @@ -using System; -using System.Text; +using System.Text; using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - public class Module : IData { - public Module() { - Data = new DataDictionary(); - } +namespace Exceptionless.Core.Models.Data; - public int ModuleId { get; set; } - public string Name { get; set; } - public string Version { get; set; } - public bool IsEntry { get; set; } - public DateTime CreatedDate { get; set; } - public DateTime ModifiedDate { get; set; } - public DataDictionary Data { get; set; } - - public override string ToString() { - var sb = new StringBuilder(); - sb.Append(Name); - sb.Append(", Version="); - sb.Append(Version); - if (Data.ContainsKey("PublicKeyToken")) - sb.Append(", PublicKeyToken=").Append(Data["PublicKeyToken"]); - - return sb.ToString(); - } +public class Module : IData { + public Module() { + Data = new DataDictionary(); + } - protected bool Equals(Module other) { - return ModuleId == other.ModuleId && String.Equals(Name, other.Name) && String.Equals(Version, other.Version) && IsEntry == other.IsEntry && CreatedDate.Equals(other.CreatedDate) && ModifiedDate.Equals(other.ModifiedDate) && Equals(Data, other.Data); - } + public int ModuleId { get; set; } + public string Name { get; set; } + public string Version { get; set; } + public bool IsEntry { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime ModifiedDate { get; set; } + public DataDictionary Data { get; set; } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((Module)obj); - } + public override string ToString() { + var sb = new StringBuilder(); + sb.Append(Name); + sb.Append(", Version="); + sb.Append(Version); + if (Data.ContainsKey("PublicKeyToken")) + sb.Append(", PublicKeyToken=").Append(Data["PublicKeyToken"]); + + return sb.ToString(); + } + + protected bool Equals(Module other) { + return ModuleId == other.ModuleId && String.Equals(Name, other.Name) && String.Equals(Version, other.Version) && IsEntry == other.IsEntry && CreatedDate.Equals(other.CreatedDate) && ModifiedDate.Equals(other.ModifiedDate) && Equals(Data, other.Data); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((Module)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = ModuleId; - hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Version?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ IsEntry.GetHashCode(); - hashCode = (hashCode * 397) ^ CreatedDate.GetHashCode(); - hashCode = (hashCode * 397) ^ ModifiedDate.GetHashCode(); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = ModuleId; + hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Version?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ IsEntry.GetHashCode(); + hashCode = (hashCode * 397) ^ CreatedDate.GetHashCode(); + hashCode = (hashCode * 397) ^ ModifiedDate.GetHashCode(); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/Parameter.cs b/src/Exceptionless.Core/Models/Data/Parameter.cs index d59f017f27..f65d62dca0 100644 --- a/src/Exceptionless.Core/Models/Data/Parameter.cs +++ b/src/Exceptionless.Core/Models/Data/Parameter.cs @@ -1,43 +1,42 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - public class Parameter : IData { - public Parameter() { - Data = new DataDictionary(); - GenericArguments = new GenericArguments(); - } +namespace Exceptionless.Core.Models.Data; - public string Name { get; set; } - public string Type { get; set; } - public string TypeNamespace { get; set; } +public class Parameter : IData { + public Parameter() { + Data = new DataDictionary(); + GenericArguments = new GenericArguments(); + } - public DataDictionary Data { get; set; } - public GenericArguments GenericArguments { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string TypeNamespace { get; set; } - protected bool Equals(Parameter other) { - return String.Equals(Name, other.Name) && String.Equals(Type, other.Type) && String.Equals(TypeNamespace, other.TypeNamespace) && Equals(Data, other.Data) && GenericArguments.CollectionEquals(other.GenericArguments); - } + public DataDictionary Data { get; set; } + public GenericArguments GenericArguments { get; set; } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((Parameter)obj); - } + protected bool Equals(Parameter other) { + return String.Equals(Name, other.Name) && String.Equals(Type, other.Type) && String.Equals(TypeNamespace, other.TypeNamespace) && Equals(Data, other.Data) && GenericArguments.CollectionEquals(other.GenericArguments); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((Parameter)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = Name?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Type?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (TypeNamespace?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (GenericArguments?.GetCollectionHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = Name?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (Type?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (TypeNamespace?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (GenericArguments?.GetCollectionHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/RequestInfo.cs b/src/Exceptionless.Core/Models/Data/RequestInfo.cs index 46b28aa1f4..f23bb734ae 100644 --- a/src/Exceptionless.Core/Models/Data/RequestInfo.cs +++ b/src/Exceptionless.Core/Models/Data/RequestInfo.cs @@ -1,119 +1,117 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; - -namespace Exceptionless.Core.Models.Data { - public class RequestInfo : IData { - public RequestInfo() { - Data = new DataDictionary(); - Cookies = new Dictionary(); - QueryString = new Dictionary(); - } +using Exceptionless.Core.Extensions; - /// - /// The user agent used for the request. - /// - public string UserAgent { get; set; } - - /// - /// The HTTP method for the request. - /// - public string HttpMethod { get; set; } - - /// - /// Whether the request was secure or not. - /// - public bool IsSecure { get; set; } - - /// - /// The host of the request. - /// - public string Host { get; set; } - - /// - /// The port of the request. - /// - public int Port { get; set; } - - /// - /// The path of the request. - /// - public string Path { get; set; } - - /// - /// The referring url for the request. - /// - public string Referrer { get; set; } - - /// - /// The client's IP address when the error occurred. - /// - public string ClientIpAddress { get; set; } - - /// - /// The request cookies. - /// - public Dictionary Cookies { get; set; } - - /// - /// The data that was POSTed for the request. - /// - public object PostData { get; set; } - - /// - /// The query string values from the request. - /// - public Dictionary QueryString { get; set; } - - /// - /// Extended data entries for this request. - /// - public DataDictionary Data { get; set; } - - protected bool Equals(RequestInfo other) { - return String.Equals(UserAgent, other.UserAgent) && String.Equals(HttpMethod, other.HttpMethod) && IsSecure == other.IsSecure && String.Equals(Host, other.Host) && Port == other.Port && String.Equals(Path, other.Path) && String.Equals(Referrer, other.Referrer) && String.Equals(ClientIpAddress, other.ClientIpAddress) && Cookies.CollectionEquals(other.Cookies) && QueryString.CollectionEquals(other.QueryString) && Equals(Data, other.Data); - } +namespace Exceptionless.Core.Models.Data; - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((RequestInfo)obj); - } +public class RequestInfo : IData { + public RequestInfo() { + Data = new DataDictionary(); + Cookies = new Dictionary(); + QueryString = new Dictionary(); + } + + /// + /// The user agent used for the request. + /// + public string UserAgent { get; set; } + + /// + /// The HTTP method for the request. + /// + public string HttpMethod { get; set; } + + /// + /// Whether the request was secure or not. + /// + public bool IsSecure { get; set; } + + /// + /// The host of the request. + /// + public string Host { get; set; } + + /// + /// The port of the request. + /// + public int Port { get; set; } + + /// + /// The path of the request. + /// + public string Path { get; set; } + + /// + /// The referring url for the request. + /// + public string Referrer { get; set; } + + /// + /// The client's IP address when the error occurred. + /// + public string ClientIpAddress { get; set; } + + /// + /// The request cookies. + /// + public Dictionary Cookies { get; set; } + + /// + /// The data that was POSTed for the request. + /// + public object PostData { get; set; } + + /// + /// The query string values from the request. + /// + public Dictionary QueryString { get; set; } + + /// + /// Extended data entries for this request. + /// + public DataDictionary Data { get; set; } + + protected bool Equals(RequestInfo other) { + return String.Equals(UserAgent, other.UserAgent) && String.Equals(HttpMethod, other.HttpMethod) && IsSecure == other.IsSecure && String.Equals(Host, other.Host) && Port == other.Port && String.Equals(Path, other.Path) && String.Equals(Referrer, other.Referrer) && String.Equals(ClientIpAddress, other.ClientIpAddress) && Cookies.CollectionEquals(other.Cookies) && QueryString.CollectionEquals(other.QueryString) && Equals(Data, other.Data); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((RequestInfo)obj); + } - private static readonly List _cookieHashCodeExclusions = new List { "__LastReferenceId" }; - - public override int GetHashCode() { - unchecked { - int hashCode = UserAgent?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (HttpMethod?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ IsSecure.GetHashCode(); - hashCode = (hashCode * 397) ^ (Host?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ Port; - hashCode = (hashCode * 397) ^ (Path?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Referrer?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (ClientIpAddress?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Cookies?.GetCollectionHashCode(_cookieHashCodeExclusions) ?? 0); - hashCode = (hashCode * 397) ^ (QueryString?.GetCollectionHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - return hashCode; - } + private static readonly List _cookieHashCodeExclusions = new List { "__LastReferenceId" }; + + public override int GetHashCode() { + unchecked { + int hashCode = UserAgent?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (HttpMethod?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ IsSecure.GetHashCode(); + hashCode = (hashCode * 397) ^ (Host?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Port; + hashCode = (hashCode * 397) ^ (Path?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Referrer?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (ClientIpAddress?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Cookies?.GetCollectionHashCode(_cookieHashCodeExclusions) ?? 0); + hashCode = (hashCode * 397) ^ (QueryString?.GetCollectionHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + return hashCode; } - public static class KnownDataKeys { - public const string Browser = "@browser"; - public const string BrowserVersion = "@browser_version"; - public const string BrowserMajorVersion = "@browser_major_version"; + } + public static class KnownDataKeys { + public const string Browser = "@browser"; + public const string BrowserVersion = "@browser_version"; + public const string BrowserMajorVersion = "@browser_major_version"; - public const string Device = "@device"; + public const string Device = "@device"; - public const string OS = "@os"; - public const string OSVersion = "@os_version"; - public const string OSMajorVersion = "@os_major_version"; + public const string OS = "@os"; + public const string OSVersion = "@os_version"; + public const string OSMajorVersion = "@os_major_version"; - public const string IsBot = "@is_bot"; - } + public const string IsBot = "@is_bot"; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/SimpleError.cs b/src/Exceptionless.Core/Models/Data/SimpleError.cs index c8d2473130..ae2b5ef10c 100644 --- a/src/Exceptionless.Core/Models/Data/SimpleError.cs +++ b/src/Exceptionless.Core/Models/Data/SimpleError.cs @@ -1,64 +1,63 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - public class SimpleError : IData { - public SimpleError() { - Data = new DataDictionary(); - } - - /// - /// The error message. - /// - public string Message { get; set; } - - /// - /// The error type. - /// - public string Type { get; set; } +namespace Exceptionless.Core.Models.Data; - /// - /// The stack trace for the error. - /// - public string StackTrace { get; set; } - - /// - /// Extended data entries for this error. - /// - public DataDictionary Data { get; set; } - - /// - /// An inner (nested) error. - /// - public SimpleError Inner { get; set; } +public class SimpleError : IData { + public SimpleError() { + Data = new DataDictionary(); + } - protected bool Equals(SimpleError other) { - return String.Equals(Message, other.Message) && String.Equals(Type, other.Type) && String.Equals(StackTrace, other.StackTrace) && Equals(Data, other.Data) && Equals(Inner, other.Inner); - } + /// + /// The error message. + /// + public string Message { get; set; } + + /// + /// The error type. + /// + public string Type { get; set; } + + /// + /// The stack trace for the error. + /// + public string StackTrace { get; set; } + + /// + /// Extended data entries for this error. + /// + public DataDictionary Data { get; set; } + + /// + /// An inner (nested) error. + /// + public SimpleError Inner { get; set; } + + protected bool Equals(SimpleError other) { + return String.Equals(Message, other.Message) && String.Equals(Type, other.Type) && String.Equals(StackTrace, other.StackTrace) && Equals(Data, other.Data) && Equals(Inner, other.Inner); + } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((SimpleError)obj); - } + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((SimpleError)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = Message?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Type?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (StackTrace?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Inner?.GetHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = Message?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (Type?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (StackTrace?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Inner?.GetHashCode() ?? 0); + return hashCode; } + } - public static class KnownDataKeys { - public const string ExtraProperties = "@ext"; - } + public static class KnownDataKeys { + public const string ExtraProperties = "@ext"; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/StackFrame.cs b/src/Exceptionless.Core/Models/Data/StackFrame.cs index 844d3c9a6e..1d31577d20 100644 --- a/src/Exceptionless.Core/Models/Data/StackFrame.cs +++ b/src/Exceptionless.Core/Models/Data/StackFrame.cs @@ -1,31 +1,29 @@ -using System; +namespace Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Models.Data { - public class StackFrame : Method { - public string FileName { get; set; } - public int LineNumber { get; set; } - public int Column { get; set; } +public class StackFrame : Method { + public string FileName { get; set; } + public int LineNumber { get; set; } + public int Column { get; set; } - protected bool Equals(StackFrame other) { - return base.Equals(other) && String.Equals(FileName, other.FileName); - } + protected bool Equals(StackFrame other) { + return base.Equals(other) && String.Equals(FileName, other.FileName); + } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((StackFrame)obj); - } + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((StackFrame)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ (FileName?.GetHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ (FileName?.GetHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/SubmissionClient.cs b/src/Exceptionless.Core/Models/Data/SubmissionClient.cs index 3d990ea492..18e1f90c3c 100644 --- a/src/Exceptionless.Core/Models/Data/SubmissionClient.cs +++ b/src/Exceptionless.Core/Models/Data/SubmissionClient.cs @@ -1,28 +1,28 @@ using System.Diagnostics; -namespace Exceptionless.Core.Models.Data { - [DebuggerDisplay("Ip Address: {IpAddress}, User Agent: {UserAgent}, Version: {Version}")] - public class SubmissionClient { - public string IpAddress { get; set; } - public string UserAgent { get; set; } - public string Version { get; set; } - } +namespace Exceptionless.Core.Models.Data; - public static class SubmissionClientExtensions { - public static bool IsDotNetClient(this SubmissionClient submissionClient) { - return submissionClient?.UserAgent?.Equals("exceptionless") ?? false; - } +[DebuggerDisplay("Ip Address: {IpAddress}, User Agent: {UserAgent}, Version: {Version}")] +public class SubmissionClient { + public string IpAddress { get; set; } + public string UserAgent { get; set; } + public string Version { get; set; } +} - public static bool IsJavaScriptClient(this SubmissionClient submissionClient) { - return submissionClient?.UserAgent?.Equals("exceptionless-js") ?? false; - } +public static class SubmissionClientExtensions { + public static bool IsDotNetClient(this SubmissionClient submissionClient) { + return submissionClient?.UserAgent?.Equals("exceptionless") ?? false; + } - public static bool IsJavaScriptUniversalClient(this SubmissionClient submissionClient) { - return submissionClient?.UserAgent?.Equals("exceptionless-universal-js") ?? false; - } + public static bool IsJavaScriptClient(this SubmissionClient submissionClient) { + return submissionClient?.UserAgent?.Equals("exceptionless-js") ?? false; + } + + public static bool IsJavaScriptUniversalClient(this SubmissionClient submissionClient) { + return submissionClient?.UserAgent?.Equals("exceptionless-universal-js") ?? false; + } - public static bool IsJavaScriptNodeClient(this SubmissionClient submissionClient) { - return submissionClient?.UserAgent?.Equals("exceptionless-node") ?? false; - } + public static bool IsJavaScriptNodeClient(this SubmissionClient submissionClient) { + return submissionClient?.UserAgent?.Equals("exceptionless-node") ?? false; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/UserDescription.cs b/src/Exceptionless.Core/Models/Data/UserDescription.cs index ce9553591e..e5a3cc4af7 100644 --- a/src/Exceptionless.Core/Models/Data/UserDescription.cs +++ b/src/Exceptionless.Core/Models/Data/UserDescription.cs @@ -1,49 +1,48 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - public class UserDescription : IData { - public UserDescription() { - Data = new DataDictionary(); - } +namespace Exceptionless.Core.Models.Data; - public UserDescription(string emailAddress, string description) : this() { - if (!String.IsNullOrWhiteSpace(emailAddress)) - EmailAddress = emailAddress.Trim(); +public class UserDescription : IData { + public UserDescription() { + Data = new DataDictionary(); + } - if (!String.IsNullOrWhiteSpace(description)) - Description = description.Trim(); - } + public UserDescription(string emailAddress, string description) : this() { + if (!String.IsNullOrWhiteSpace(emailAddress)) + EmailAddress = emailAddress.Trim(); - public string EmailAddress { get; set; } - public string Description { get; set; } + if (!String.IsNullOrWhiteSpace(description)) + Description = description.Trim(); + } - /// - /// Extended data entries for this user description. - /// - public DataDictionary Data { get; set; } + public string EmailAddress { get; set; } + public string Description { get; set; } - protected bool Equals(UserDescription other) { - return String.Equals(EmailAddress, other.EmailAddress) && String.Equals(Description, other.Description) && Equals(Data, other.Data); - } + /// + /// Extended data entries for this user description. + /// + public DataDictionary Data { get; set; } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((UserDescription)obj); - } + protected bool Equals(UserDescription other) { + return String.Equals(EmailAddress, other.EmailAddress) && String.Equals(Description, other.Description) && Equals(Data, other.Data); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((UserDescription)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = EmailAddress?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Description?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = EmailAddress?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (Description?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Data/UserInfo.cs b/src/Exceptionless.Core/Models/Data/UserInfo.cs index b35bcde8bb..2f9f653054 100644 --- a/src/Exceptionless.Core/Models/Data/UserInfo.cs +++ b/src/Exceptionless.Core/Models/Data/UserInfo.cs @@ -1,60 +1,59 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models.Data { - [DebuggerDisplay("{Identity}, {Name}")] - public class UserInfo : IData { - public UserInfo() { - Data = new DataDictionary(); - } +namespace Exceptionless.Core.Models.Data; - public UserInfo(string identity) : this() { - if (!String.IsNullOrWhiteSpace(identity)) - Identity = identity.Trim(); - } +[DebuggerDisplay("{Identity}, {Name}")] +public class UserInfo : IData { + public UserInfo() { + Data = new DataDictionary(); + } - public UserInfo(string identity, string name) : this(identity) { - if (!String.IsNullOrWhiteSpace(name)) - Name = name.Trim(); - } + public UserInfo(string identity) : this() { + if (!String.IsNullOrWhiteSpace(identity)) + Identity = identity.Trim(); + } - /// - /// Uniquely identifies the user. - /// - public string Identity { get; set; } + public UserInfo(string identity, string name) : this(identity) { + if (!String.IsNullOrWhiteSpace(name)) + Name = name.Trim(); + } - /// - /// The Friendly name of the user. - /// - public string Name { get; set; } + /// + /// Uniquely identifies the user. + /// + public string Identity { get; set; } - /// - /// Extended data entries for this user. - /// - public DataDictionary Data { get; set; } + /// + /// The Friendly name of the user. + /// + public string Name { get; set; } - protected bool Equals(UserInfo other) { - return String.Equals(Identity, other.Identity) && String.Equals(Name, other.Name) && Equals(Data, other.Data); - } + /// + /// Extended data entries for this user. + /// + public DataDictionary Data { get; set; } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((UserInfo)obj); - } + protected bool Equals(UserInfo other) { + return String.Equals(Identity, other.Identity) && String.Equals(Name, other.Name) && Equals(Data, other.Data); + } + + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((UserInfo)obj); + } - public override int GetHashCode() { - unchecked { - int hashCode = Identity?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); - return hashCode; - } + public override int GetHashCode() { + unchecked { + int hashCode = Identity?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode() ?? 0); + return hashCode; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Enums/NotificationMode.cs b/src/Exceptionless.Core/Models/Enums/NotificationMode.cs index 2bf2c609a3..08d5dfcd39 100644 --- a/src/Exceptionless.Core/Models/Enums/NotificationMode.cs +++ b/src/Exceptionless.Core/Models/Enums/NotificationMode.cs @@ -1,7 +1,7 @@ -namespace Exceptionless.Core.Models { - public enum NotificationMode { - None = 0, - New = 1, - All = 2 - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public enum NotificationMode { + None = 0, + New = 1, + All = 2 +} diff --git a/src/Exceptionless.Core/Models/Enums/ResponseStatusType.cs b/src/Exceptionless.Core/Models/Enums/ResponseStatusType.cs index 6d4810e537..c17f0f5677 100644 --- a/src/Exceptionless.Core/Models/Enums/ResponseStatusType.cs +++ b/src/Exceptionless.Core/Models/Enums/ResponseStatusType.cs @@ -1,9 +1,9 @@ -namespace Exceptionless.Core.Models { - public enum ResponseStatusType { - Successful = 0, - Queued = 1, - Error = 2, - Discarded = 3, - Rejected = 4 - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public enum ResponseStatusType { + Successful = 0, + Queued = 1, + Error = 2, + Discarded = 3, + Rejected = 4 +} diff --git a/src/Exceptionless.Core/Models/Enums/SuspensionCode.cs b/src/Exceptionless.Core/Models/Enums/SuspensionCode.cs index a7164f38b0..58d117a49e 100644 --- a/src/Exceptionless.Core/Models/Enums/SuspensionCode.cs +++ b/src/Exceptionless.Core/Models/Enums/SuspensionCode.cs @@ -1,8 +1,8 @@ -namespace Exceptionless.Core.Models { - public enum SuspensionCode { - Billing, - Overage, - Abuse, - Other = 100 - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public enum SuspensionCode { + Billing, + Overage, + Abuse, + Other = 100 +} diff --git a/src/Exceptionless.Core/Models/Event.cs b/src/Exceptionless.Core/Models/Event.cs index a95dfbff84..544278103e 100644 --- a/src/Exceptionless.Core/Models/Event.cs +++ b/src/Exceptionless.Core/Models/Event.cs @@ -1,125 +1,123 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Models { - [DebuggerDisplay("Type: {Type}, Date: {Date}, Message: {Message}, Value: {Value}, Count: {Count}")] - public class Event : IData { - public Event() { - Tags = new TagSet(); - Data = new DataDictionary(); - } +namespace Exceptionless.Core.Models; - /// - /// The event type (ie. error, log message, feature usage). Check Event.KnownTypes for standard event types. - /// - public string Type { get; set; } - - /// - /// The event source (ie. machine name, log name, feature name). - /// - public string Source { get; set; } - - /// - /// The date that the event occurred on. - /// - public DateTimeOffset Date { get; set; } - - /// - /// A list of tags used to categorize this event. - /// - public TagSet Tags { get; set; } - - /// - /// The event message. - /// - public string Message { get; set; } - - /// - /// The geo coordinates where the event happened. - /// - public string Geo { get; set; } - - /// - /// The value of the event if any. - /// - public decimal? Value { get; set; } - - /// - /// The number of duplicated events. - /// - public int? Count { get; set; } - - /// - /// Optional data entries that contain additional information about this event. - /// - public DataDictionary Data { get; set; } - - /// - /// An optional identifier to be used for referencing this event instance at a later time. - /// - public string ReferenceId { get; set; } - - protected bool Equals(Event other) { - return String.Equals(Type, other.Type) && String.Equals(Source, other.Source) && Tags.CollectionEquals(other.Tags) && String.Equals(Message, other.Message) && String.Equals(Geo, other.Geo) && Value == other.Value && Equals(Data, other.Data); - } +[DebuggerDisplay("Type: {Type}, Date: {Date}, Message: {Message}, Value: {Value}, Count: {Count}")] +public class Event : IData { + public Event() { + Tags = new TagSet(); + Data = new DataDictionary(); + } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((Event)obj); - } + /// + /// The event type (ie. error, log message, feature usage). Check Event.KnownTypes for standard event types. + /// + public string Type { get; set; } + + /// + /// The event source (ie. machine name, log name, feature name). + /// + public string Source { get; set; } + + /// + /// The date that the event occurred on. + /// + public DateTimeOffset Date { get; set; } + + /// + /// A list of tags used to categorize this event. + /// + public TagSet Tags { get; set; } + + /// + /// The event message. + /// + public string Message { get; set; } + + /// + /// The geo coordinates where the event happened. + /// + public string Geo { get; set; } + + /// + /// The value of the event if any. + /// + public decimal? Value { get; set; } + + /// + /// The number of duplicated events. + /// + public int? Count { get; set; } + + /// + /// Optional data entries that contain additional information about this event. + /// + public DataDictionary Data { get; set; } + + /// + /// An optional identifier to be used for referencing this event instance at a later time. + /// + public string ReferenceId { get; set; } + + protected bool Equals(Event other) { + return String.Equals(Type, other.Type) && String.Equals(Source, other.Source) && Tags.CollectionEquals(other.Tags) && String.Equals(Message, other.Message) && String.Equals(Geo, other.Geo) && Value == other.Value && Equals(Data, other.Data); + } - private static readonly List _exclusions = new List { KnownDataKeys.TraceLog }; - public override int GetHashCode() { - unchecked { - int hashCode = Type?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Source?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Tags?.GetCollectionHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Message?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ (Geo?.GetHashCode() ?? 0); - hashCode = (hashCode * 397) ^ Value.GetHashCode(); - hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode(_exclusions) ?? 0); - return hashCode; - } - } + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((Event)obj); + } - public static class KnownTypes { - public const string Error = "error"; - public const string FeatureUsage = "usage"; - public const string Log = "log"; - public const string NotFound = "404"; - public const string Session = "session"; - public const string SessionEnd = "sessionend"; - public const string SessionHeartbeat = "heartbeat"; + private static readonly List _exclusions = new List { KnownDataKeys.TraceLog }; + public override int GetHashCode() { + unchecked { + int hashCode = Type?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (Source?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Tags?.GetCollectionHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Message?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Geo?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Value.GetHashCode(); + hashCode = (hashCode * 397) ^ (Data?.GetCollectionHashCode(_exclusions) ?? 0); + return hashCode; } + } - public static class KnownTags { - public const string Critical = "Critical"; - public const string Internal = "Internal"; - } + public static class KnownTypes { + public const string Error = "error"; + public const string FeatureUsage = "usage"; + public const string Log = "log"; + public const string NotFound = "404"; + public const string Session = "session"; + public const string SessionEnd = "sessionend"; + public const string SessionHeartbeat = "heartbeat"; + } - public static class KnownDataKeys { - public const string Error = "@error"; - public const string SimpleError = "@simple_error"; - public const string RequestInfo = "@request"; - public const string TraceLog = "@trace"; - public const string EnvironmentInfo = "@environment"; - public const string UserInfo = "@user"; - public const string UserDescription = "@user_description"; - public const string Version = "@version"; - public const string Level = "@level"; - public const string Location = "@location"; - public const string SubmissionMethod = "@submission_method"; - public const string SubmissionClient = "@submission_client"; - public const string SessionEnd = "sessionend"; - public const string SessionHasError = "haserror"; - public const string ManualStackingInfo = "@stack"; - } + public static class KnownTags { + public const string Critical = "Critical"; + public const string Internal = "Internal"; + } + + public static class KnownDataKeys { + public const string Error = "@error"; + public const string SimpleError = "@simple_error"; + public const string RequestInfo = "@request"; + public const string TraceLog = "@trace"; + public const string EnvironmentInfo = "@environment"; + public const string UserInfo = "@user"; + public const string UserDescription = "@user_description"; + public const string Version = "@version"; + public const string Level = "@level"; + public const string Location = "@location"; + public const string SubmissionMethod = "@submission_method"; + public const string SubmissionClient = "@submission_client"; + public const string SessionEnd = "sessionend"; + public const string SessionHasError = "haserror"; + public const string ManualStackingInfo = "@stack"; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/EventPreviousAndNextIdResult.cs b/src/Exceptionless.Core/Models/EventPreviousAndNextIdResult.cs index 85fa264338..6535162813 100644 --- a/src/Exceptionless.Core/Models/EventPreviousAndNextIdResult.cs +++ b/src/Exceptionless.Core/Models/EventPreviousAndNextIdResult.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Core.Models { - public class PreviousAndNextEventIdResult { - public string Previous { get; set; } - public string Next { get; set; } - } +namespace Exceptionless.Core.Models; + +public class PreviousAndNextEventIdResult { + public string Previous { get; set; } + public string Next { get; set; } } diff --git a/src/Exceptionless.Core/Models/EventSummaryModel.cs b/src/Exceptionless.Core/Models/EventSummaryModel.cs index d9c374a63d..4ea5eb66ab 100644 --- a/src/Exceptionless.Core/Models/EventSummaryModel.cs +++ b/src/Exceptionless.Core/Models/EventSummaryModel.cs @@ -1,8 +1,6 @@ -using System; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class EventSummaryModel: SummaryData { - public string Id { get; set; } - public DateTimeOffset Date { get; set; } - } -} \ No newline at end of file +public class EventSummaryModel : SummaryData { + public string Id { get; set; } + public DateTimeOffset Date { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Exceptions/RateLimitException.cs b/src/Exceptionless.Core/Models/Exceptions/RateLimitException.cs index 390db28182..0a011846e5 100644 --- a/src/Exceptionless.Core/Models/Exceptions/RateLimitException.cs +++ b/src/Exceptionless.Core/Models/Exceptions/RateLimitException.cs @@ -1,7 +1,5 @@ -using System; +namespace Exceptionless.Core.Models.Exceptions; -namespace Exceptionless.Core.Models.Exceptions { - public class RateLimitException : Exception { - public DateTime RetryAfter { get; set; } - } +public class RateLimitException : Exception { + public DateTime RetryAfter { get; set; } } diff --git a/src/Exceptionless.Core/Models/Exceptions/WebHookException.cs b/src/Exceptionless.Core/Models/Exceptions/WebHookException.cs index b7c06505c4..213bf2d9e2 100644 --- a/src/Exceptionless.Core/Models/Exceptions/WebHookException.cs +++ b/src/Exceptionless.Core/Models/Exceptions/WebHookException.cs @@ -1,10 +1,8 @@ -using System; +namespace Exceptionless.Core.Models.Exceptions; -namespace Exceptionless.Core.Models.Exceptions { - public class WebHookException : Exception { - public WebHookException(string message, Exception inner = null) : base(message, inner) { } +public class WebHookException : Exception { + public WebHookException(string message, Exception inner = null) : base(message, inner) { } - public int StatusCode { get; set; } - public bool Unauthorized { get; set; } - } -} \ No newline at end of file + public int StatusCode { get; set; } + public bool Unauthorized { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Interfaces/IData.cs b/src/Exceptionless.Core/Models/Interfaces/IData.cs index ef9e59a039..8aa46d57fa 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IData.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IData.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Core.Models { - public interface IData { - DataDictionary Data { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public interface IData { + DataDictionary Data { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganization.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganization.cs index 5afc552100..6f8b643cf6 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganization.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganization.cs @@ -1,8 +1,8 @@ -namespace Exceptionless.Core.Models { - public interface IOwnedByOrganization { - /// - /// The organization that the document belongs to. - /// - string OrganizationId { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public interface IOwnedByOrganization { + /// + /// The organization that the document belongs to. + /// + string OrganizationId { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProject.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProject.cs index fd2db2d02d..374885066f 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProject.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProject.cs @@ -1,4 +1,4 @@ -namespace Exceptionless.Core.Models { - public interface IOwnedByOrganizationAndProject : IOwnedByOrganization, IOwnedByProject { - } +namespace Exceptionless.Core.Models; + +public interface IOwnedByOrganizationAndProject : IOwnedByOrganization, IOwnedByProject { } diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStack.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStack.cs index 8f2fa1eec5..9c63642114 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStack.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStack.cs @@ -1,4 +1,4 @@ -namespace Exceptionless.Core.Models { - public interface IOwnedByOrganizationAndProjectAndStack : IOwnedByOrganization, IOwnedByProject, IOwnedByStack { - } +namespace Exceptionless.Core.Models; + +public interface IOwnedByOrganizationAndProjectAndStack : IOwnedByOrganization, IOwnedByProject, IOwnedByStack { } diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStackWithIdentity.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStackWithIdentity.cs index 9b9d2980d7..8619fb91a5 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStackWithIdentity.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectAndStackWithIdentity.cs @@ -1,4 +1,4 @@ -namespace Exceptionless.Core.Models { - public interface IOwnedByOrganizationAndProjectAndStackWithIdentity : IOwnedByOrganizationAndProjectWithIdentity, IOwnedByStack { - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public interface IOwnedByOrganizationAndProjectAndStackWithIdentity : IOwnedByOrganizationAndProjectWithIdentity, IOwnedByStack { +} diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectWithIdentity.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectWithIdentity.cs index d8c8396c44..78849dddcf 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectWithIdentity.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationAndProjectWithIdentity.cs @@ -1,5 +1,5 @@ using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - public interface IOwnedByOrganizationAndProjectWithIdentity : IOwnedByOrganization, IOwnedByProject, IIdentity {} -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public interface IOwnedByOrganizationAndProjectWithIdentity : IOwnedByOrganization, IOwnedByProject, IIdentity { } diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationWithIdentity.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationWithIdentity.cs index 3620d52c87..6e285e7992 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationWithIdentity.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByOrganizationWithIdentity.cs @@ -1,5 +1,5 @@ using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - public interface IOwnedByOrganizationWithIdentity : IOwnedByOrganization, IIdentity {} -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public interface IOwnedByOrganizationWithIdentity : IOwnedByOrganization, IIdentity { } diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByProject.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByProject.cs index db3827e5a5..9b04a849c6 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByProject.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByProject.cs @@ -1,8 +1,8 @@ -namespace Exceptionless.Core.Models { - public interface IOwnedByProject { - /// - /// The project that the document belongs to. - /// - string ProjectId { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public interface IOwnedByProject { + /// + /// The project that the document belongs to. + /// + string ProjectId { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Interfaces/IOwnedByStack.cs b/src/Exceptionless.Core/Models/Interfaces/IOwnedByStack.cs index 26e0edf34a..db3b6d6e08 100644 --- a/src/Exceptionless.Core/Models/Interfaces/IOwnedByStack.cs +++ b/src/Exceptionless.Core/Models/Interfaces/IOwnedByStack.cs @@ -1,8 +1,8 @@ -namespace Exceptionless.Core.Models { - public interface IOwnedByStack { - /// - /// The stack that the document belongs to. - /// - string StackId { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public interface IOwnedByStack { + /// + /// The stack that the document belongs to. + /// + string StackId { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Invite.cs b/src/Exceptionless.Core/Models/Invite.cs index 06e2b5e2fc..cbfc925106 100644 --- a/src/Exceptionless.Core/Models/Invite.cs +++ b/src/Exceptionless.Core/Models/Invite.cs @@ -1,9 +1,7 @@ -using System; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class Invite { - public string Token { get; set; } - public string EmailAddress { get; set; } - public DateTime DateAdded { get; set; } - } -} \ No newline at end of file +public class Invite { + public string Token { get; set; } + public string EmailAddress { get; set; } + public DateTime DateAdded { get; set; } +} diff --git a/src/Exceptionless.Core/Models/MailMessageData.cs b/src/Exceptionless.Core/Models/MailMessageData.cs index 332f7cdb01..82391c7f9b 100644 --- a/src/Exceptionless.Core/Models/MailMessageData.cs +++ b/src/Exceptionless.Core/Models/MailMessageData.cs @@ -1,8 +1,6 @@ -using System.Collections.Generic; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class MailMessageData { - public string Subject { get; set; } - public Dictionary Data { get; set; } - } -} \ No newline at end of file +public class MailMessageData { + public string Subject { get; set; } + public Dictionary Data { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs b/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs index f8fffa28d1..d4b9d80179 100644 --- a/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs +++ b/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs @@ -1,50 +1,50 @@ using System.Diagnostics; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Messaging.Models { - [DebuggerDisplay("{Type} {ChangeType}: Id={Id}, OrganizationId={OrganizationId}, ProjectId={ProjectId}, StackId={StackId}")] - public class ExtendedEntityChanged : EntityChanged { - private ExtendedEntityChanged() { } // Ensure create is used. - - public string OrganizationId { get; private set; } - public string ProjectId { get; private set; } - public string StackId { get; private set; } - - public static ExtendedEntityChanged Create(EntityChanged entityChanged, bool removeWhenSettingProperties = true) { - var model = new ExtendedEntityChanged { - Id = entityChanged.Id, - Type = entityChanged.Type, - ChangeType = entityChanged.ChangeType, - Data = entityChanged.Data - }; - - if (model.Data.TryGetValue(KnownKeys.OrganizationId, out object organizationId)) { - model.OrganizationId = organizationId.ToString(); - if (removeWhenSettingProperties) - model.Data.Remove(KnownKeys.OrganizationId); - } - - if (model.Data.TryGetValue(KnownKeys.ProjectId, out object projectId)) { - model.ProjectId = projectId.ToString(); - if (removeWhenSettingProperties) - model.Data.Remove(KnownKeys.ProjectId); - } - - if (model.Data.TryGetValue(KnownKeys.StackId, out object stackId)) { - model.StackId = stackId.ToString(); - if (removeWhenSettingProperties) - model.Data.Remove(KnownKeys.StackId); - } - - return model; +namespace Exceptionless.Core.Messaging.Models; + +[DebuggerDisplay("{Type} {ChangeType}: Id={Id}, OrganizationId={OrganizationId}, ProjectId={ProjectId}, StackId={StackId}")] +public class ExtendedEntityChanged : EntityChanged { + private ExtendedEntityChanged() { } // Ensure create is used. + + public string OrganizationId { get; private set; } + public string ProjectId { get; private set; } + public string StackId { get; private set; } + + public static ExtendedEntityChanged Create(EntityChanged entityChanged, bool removeWhenSettingProperties = true) { + var model = new ExtendedEntityChanged { + Id = entityChanged.Id, + Type = entityChanged.Type, + ChangeType = entityChanged.ChangeType, + Data = entityChanged.Data + }; + + if (model.Data.TryGetValue(KnownKeys.OrganizationId, out object organizationId)) { + model.OrganizationId = organizationId.ToString(); + if (removeWhenSettingProperties) + model.Data.Remove(KnownKeys.OrganizationId); } - public class KnownKeys { - public const string OrganizationId = nameof(OrganizationId); - public const string ProjectId = nameof(ProjectId); - public const string StackId = nameof(StackId); - public const string UserId = nameof(UserId); - public const string IsAuthenticationToken = nameof(IsAuthenticationToken); + if (model.Data.TryGetValue(KnownKeys.ProjectId, out object projectId)) { + model.ProjectId = projectId.ToString(); + if (removeWhenSettingProperties) + model.Data.Remove(KnownKeys.ProjectId); } + + if (model.Data.TryGetValue(KnownKeys.StackId, out object stackId)) { + model.StackId = stackId.ToString(); + if (removeWhenSettingProperties) + model.Data.Remove(KnownKeys.StackId); + } + + return model; + } + + public class KnownKeys { + public const string OrganizationId = nameof(OrganizationId); + public const string ProjectId = nameof(ProjectId); + public const string StackId = nameof(StackId); + public const string UserId = nameof(UserId); + public const string IsAuthenticationToken = nameof(IsAuthenticationToken); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Messaging/PlanChanged.cs b/src/Exceptionless.Core/Models/Messaging/PlanChanged.cs index 4cac5c74fe..d5c9339695 100644 --- a/src/Exceptionless.Core/Models/Messaging/PlanChanged.cs +++ b/src/Exceptionless.Core/Models/Messaging/PlanChanged.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Core.Messaging.Models { - public class PlanChanged { - public string OrganizationId { get; set; } - } +namespace Exceptionless.Core.Messaging.Models; + +public class PlanChanged { + public string OrganizationId { get; set; } } diff --git a/src/Exceptionless.Core/Models/Messaging/PlanOverage.cs b/src/Exceptionless.Core/Models/Messaging/PlanOverage.cs index 324d5e29ce..bf947cfb66 100644 --- a/src/Exceptionless.Core/Models/Messaging/PlanOverage.cs +++ b/src/Exceptionless.Core/Models/Messaging/PlanOverage.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Core.Messaging.Models { - public class PlanOverage { - public string OrganizationId { get; set; } - public bool IsHourly { get; set; } - } +namespace Exceptionless.Core.Messaging.Models; + +public class PlanOverage { + public string OrganizationId { get; set; } + public bool IsHourly { get; set; } } diff --git a/src/Exceptionless.Core/Models/Messaging/ReleaseNotification.cs b/src/Exceptionless.Core/Models/Messaging/ReleaseNotification.cs index f4f6e38b26..677c41c8ae 100644 --- a/src/Exceptionless.Core/Models/Messaging/ReleaseNotification.cs +++ b/src/Exceptionless.Core/Models/Messaging/ReleaseNotification.cs @@ -1,9 +1,7 @@ -using System; +namespace Exceptionless.Core.Messaging.Models; -namespace Exceptionless.Core.Messaging.Models { - public class ReleaseNotification { - public bool Critical { get; set; } - public DateTime Date { get; set; } - public string Message { get; set; } - } -} \ No newline at end of file +public class ReleaseNotification { + public bool Critical { get; set; } + public DateTime Date { get; set; } + public string Message { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Messaging/SystemNotification.cs b/src/Exceptionless.Core/Models/Messaging/SystemNotification.cs index be7a2a40a6..5e0666a462 100644 --- a/src/Exceptionless.Core/Models/Messaging/SystemNotification.cs +++ b/src/Exceptionless.Core/Models/Messaging/SystemNotification.cs @@ -1,8 +1,6 @@ -using System; +namespace Exceptionless.Core.Messaging.Models; -namespace Exceptionless.Core.Messaging.Models { - public class SystemNotification { - public DateTime Date { get; set; } - public string Message { get; set; } - } -} \ No newline at end of file +public class SystemNotification { + public DateTime Date { get; set; } + public string Message { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Messaging/UserMembershipChanged.cs b/src/Exceptionless.Core/Models/Messaging/UserMembershipChanged.cs index d683295d7d..c27cde6d4a 100644 --- a/src/Exceptionless.Core/Models/Messaging/UserMembershipChanged.cs +++ b/src/Exceptionless.Core/Models/Messaging/UserMembershipChanged.cs @@ -1,9 +1,9 @@ using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Messaging.Models { - public class UserMembershipChanged { - public ChangeType ChangeType { get; set; } - public string UserId { get; set; } - public string OrganizationId { get; set; } - } +namespace Exceptionless.Core.Messaging.Models; + +public class UserMembershipChanged { + public ChangeType ChangeType { get; set; } + public string UserId { get; set; } + public string OrganizationId { get; set; } } diff --git a/src/Exceptionless.Core/Models/NotificationSettings.cs b/src/Exceptionless.Core/Models/NotificationSettings.cs index 5e203733f8..a004954103 100644 --- a/src/Exceptionless.Core/Models/NotificationSettings.cs +++ b/src/Exceptionless.Core/Models/NotificationSettings.cs @@ -1,10 +1,10 @@ -namespace Exceptionless.Core.Models { - public class NotificationSettings { - public bool SendDailySummary { get; set; } - public bool ReportNewErrors { get; set; } - public bool ReportCriticalErrors { get; set; } - public bool ReportEventRegressions { get; set; } - public bool ReportNewEvents { get; set; } - public bool ReportCriticalEvents { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public class NotificationSettings { + public bool SendDailySummary { get; set; } + public bool ReportNewErrors { get; set; } + public bool ReportCriticalErrors { get; set; } + public bool ReportEventRegressions { get; set; } + public bool ReportNewEvents { get; set; } + public bool ReportCriticalEvents { get; set; } +} diff --git a/src/Exceptionless.Core/Models/OAuthAccount.cs b/src/Exceptionless.Core/Models/OAuthAccount.cs index e812481d2e..a20c1a2b8c 100644 --- a/src/Exceptionless.Core/Models/OAuthAccount.cs +++ b/src/Exceptionless.Core/Models/OAuthAccount.cs @@ -1,64 +1,61 @@ -using System; -using System.Linq; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class OAuthAccount : IEquatable { - public OAuthAccount() { - ExtraData = new SettingsDictionary(); - } +public class OAuthAccount : IEquatable { + public OAuthAccount() { + ExtraData = new SettingsDictionary(); + } - public string Provider { get; set; } - public string ProviderUserId { get; set; } - public string Username { get; set; } + public string Provider { get; set; } + public string ProviderUserId { get; set; } + public string Username { get; set; } - public SettingsDictionary ExtraData { get; private set; } + public SettingsDictionary ExtraData { get; private set; } - public bool Equals(OAuthAccount other) { - if (other is null) - return false; - if (ReferenceEquals(this, other)) - return true; - return other.Provider.Equals(Provider) && other.ProviderUserId.Equals(ProviderUserId); - } + public bool Equals(OAuthAccount other) { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return other.Provider.Equals(Provider) && other.ProviderUserId.Equals(ProviderUserId); + } - public override bool Equals(object obj) { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != typeof(OAuthAccount)) - return false; - return Equals((OAuthAccount)obj); - } + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != typeof(OAuthAccount)) + return false; + return Equals((OAuthAccount)obj); + } - public override int GetHashCode() { - unchecked { - int hash = 2153; - if (Provider != null) - hash = hash * 9929 + Provider.GetHashCode(); - if (ProviderUserId != null) - hash = hash * 9929 + ProviderUserId.GetHashCode(); - return hash; - } + public override int GetHashCode() { + unchecked { + int hash = 2153; + if (Provider != null) + hash = hash * 9929 + Provider.GetHashCode(); + if (ProviderUserId != null) + hash = hash * 9929 + ProviderUserId.GetHashCode(); + return hash; } + } - public string EmailAddress() { - if (!String.IsNullOrEmpty(Username) && Username.Contains("@")) - return Username; - - foreach (var kvp in ExtraData) { - if ((String.Equals(kvp.Key, "email") || String.Equals(kvp.Key, "account_email") || String.Equals(kvp.Key, "preferred_email") || String.Equals(kvp.Key, "personal_email")) && !String.IsNullOrEmpty(kvp.Value)) - return kvp.Value; - } + public string EmailAddress() { + if (!String.IsNullOrEmpty(Username) && Username.Contains("@")) + return Username; - return null; + foreach (var kvp in ExtraData) { + if ((String.Equals(kvp.Key, "email") || String.Equals(kvp.Key, "account_email") || String.Equals(kvp.Key, "preferred_email") || String.Equals(kvp.Key, "personal_email")) && !String.IsNullOrEmpty(kvp.Value)) + return kvp.Value; } - public string FullName() { - foreach (var kvp in ExtraData.Where(kvp => String.Equals(kvp.Key, "name") && !String.IsNullOrEmpty(kvp.Value))) - return kvp.Value; + return null; + } - return !String.IsNullOrEmpty(Username) && Username.Contains(" ") ? Username : null; - } + public string FullName() { + foreach (var kvp in ExtraData.Where(kvp => String.Equals(kvp.Key, "name") && !String.IsNullOrEmpty(kvp.Value))) + return kvp.Value; + + return !String.IsNullOrEmpty(Username) && Username.Contains(" ") ? Username : null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Organization.cs b/src/Exceptionless.Core/Models/Organization.cs index 9ef713aad1..24696181e5 100644 --- a/src/Exceptionless.Core/Models/Organization.cs +++ b/src/Exceptionless.Core/Models/Organization.cs @@ -1,173 +1,171 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Diagnostics; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - [DebuggerDisplay("{Id}, {Name}, {PlanName}")] - public class Organization : IData, IOwnedByOrganizationWithIdentity, IHaveDates, ISupportSoftDeletes { - public Organization() { - Invites = new Collection(); - BillingStatus = BillingStatus.Trialing; - Usage = new Collection(); - OverageHours = new Collection(); - Data = new DataDictionary(); - } - - /// - /// Unique id that identifies the organization. - /// - public string Id { get; set; } - - /// - /// Name of the organization. - /// - public string Name { get; set; } - - /// - /// Stripe customer id that will be charged. - /// - public string StripeCustomerId { get; set; } - - /// - /// Billing plan id that the organization belongs to. - /// - public string PlanId { get; set; } - - /// - /// Billing plan name that the organization belongs to. - /// - public string PlanName { get; set; } - - /// - /// Billing plan description that the organization belongs to. - /// - public string PlanDescription { get; set; } - - /// - /// Last 4 digits of the credit card used for billing. - /// - public string CardLast4 { get; set; } - - /// - /// Date the organization first subscribed to a paid plan. - /// - public DateTime? SubscribeDate { get; set; } - - /// - /// Date the billing information was last changed. - /// - public DateTime? BillingChangeDate { get; set; } - - /// - /// User id that the billing information was last changed by. - /// - public string BillingChangedByUserId { get; set; } - - /// - /// Organization's current billing status. - /// - public BillingStatus BillingStatus { get; set; } - - /// - /// The price of the plan that this organization is currently on. - /// - public decimal BillingPrice { get; set; } - - /// - /// Maximum number of event occurrences allowed per month. - /// - public int MaxEventsPerMonth { get; set; } - - /// - /// Bonus number of event occurrences allowed per month. - /// - public int BonusEventsPerMonth { get; set; } - - /// - /// Date that the bonus events expire. - /// - public DateTime? BonusExpiration { get; set; } - - /// - /// Number of days event data is retained. - /// - public int RetentionDays { get; set; } - - /// - /// If true, the account is suspended and can't be used. - /// - public bool IsSuspended { get; set; } - - /// - /// The code indicating why the account was suspended. - /// - public SuspensionCode? SuspensionCode { get; set; } - - /// - /// Any notes on why the account was suspended. - /// - public string SuspensionNotes { get; set; } - - /// - /// The reason the account was suspended. - /// - public DateTime? SuspensionDate { get; set; } - - /// - /// User id that suspended the account. - /// - public string SuspendedByUserId { get; set; } - - /// - /// If true, premium features will be enabled. - /// - public bool HasPremiumFeatures { get; set; } - - /// - /// Maximum number of users allowed by the current plan. - /// - public int MaxUsers { get; set; } - - /// - /// Maximum number of projects allowed by the current plan. - /// - public int MaxProjects { get; set; } - - /// - /// Organization invites. - /// - public ICollection Invites { get; set; } - - /// - /// Hours over event limit. - /// - public ICollection OverageHours { get; set; } - - /// - /// Account event usage information. - /// - public ICollection Usage { get; set; } - public DateTime? LastEventDateUtc { get; set; } - - /// - /// Optional data entries that contain additional configuration information for this organization. - /// - public DataDictionary Data { get; set; } - - public DateTime CreatedUtc { get; set; } - public DateTime UpdatedUtc { get; set; } - public bool IsDeleted { get; set; } - - string IOwnedByOrganization.OrganizationId { get { return Id; } set { Id = value; } } - } +namespace Exceptionless.Core.Models; - public enum BillingStatus { - Trialing = 0, - Active = 1, - PastDue = 2, - Canceled = 3, - Unpaid = 4 +[DebuggerDisplay("{Id}, {Name}, {PlanName}")] +public class Organization : IData, IOwnedByOrganizationWithIdentity, IHaveDates, ISupportSoftDeletes { + public Organization() { + Invites = new Collection(); + BillingStatus = BillingStatus.Trialing; + Usage = new Collection(); + OverageHours = new Collection(); + Data = new DataDictionary(); } + + /// + /// Unique id that identifies the organization. + /// + public string Id { get; set; } + + /// + /// Name of the organization. + /// + public string Name { get; set; } + + /// + /// Stripe customer id that will be charged. + /// + public string StripeCustomerId { get; set; } + + /// + /// Billing plan id that the organization belongs to. + /// + public string PlanId { get; set; } + + /// + /// Billing plan name that the organization belongs to. + /// + public string PlanName { get; set; } + + /// + /// Billing plan description that the organization belongs to. + /// + public string PlanDescription { get; set; } + + /// + /// Last 4 digits of the credit card used for billing. + /// + public string CardLast4 { get; set; } + + /// + /// Date the organization first subscribed to a paid plan. + /// + public DateTime? SubscribeDate { get; set; } + + /// + /// Date the billing information was last changed. + /// + public DateTime? BillingChangeDate { get; set; } + + /// + /// User id that the billing information was last changed by. + /// + public string BillingChangedByUserId { get; set; } + + /// + /// Organization's current billing status. + /// + public BillingStatus BillingStatus { get; set; } + + /// + /// The price of the plan that this organization is currently on. + /// + public decimal BillingPrice { get; set; } + + /// + /// Maximum number of event occurrences allowed per month. + /// + public int MaxEventsPerMonth { get; set; } + + /// + /// Bonus number of event occurrences allowed per month. + /// + public int BonusEventsPerMonth { get; set; } + + /// + /// Date that the bonus events expire. + /// + public DateTime? BonusExpiration { get; set; } + + /// + /// Number of days event data is retained. + /// + public int RetentionDays { get; set; } + + /// + /// If true, the account is suspended and can't be used. + /// + public bool IsSuspended { get; set; } + + /// + /// The code indicating why the account was suspended. + /// + public SuspensionCode? SuspensionCode { get; set; } + + /// + /// Any notes on why the account was suspended. + /// + public string SuspensionNotes { get; set; } + + /// + /// The reason the account was suspended. + /// + public DateTime? SuspensionDate { get; set; } + + /// + /// User id that suspended the account. + /// + public string SuspendedByUserId { get; set; } + + /// + /// If true, premium features will be enabled. + /// + public bool HasPremiumFeatures { get; set; } + + /// + /// Maximum number of users allowed by the current plan. + /// + public int MaxUsers { get; set; } + + /// + /// Maximum number of projects allowed by the current plan. + /// + public int MaxProjects { get; set; } + + /// + /// Organization invites. + /// + public ICollection Invites { get; set; } + + /// + /// Hours over event limit. + /// + public ICollection OverageHours { get; set; } + + /// + /// Account event usage information. + /// + public ICollection Usage { get; set; } + public DateTime? LastEventDateUtc { get; set; } + + /// + /// Optional data entries that contain additional configuration information for this organization. + /// + public DataDictionary Data { get; set; } + + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } + public bool IsDeleted { get; set; } + + string IOwnedByOrganization.OrganizationId { get { return Id; } set { Id = value; } } +} + +public enum BillingStatus { + Trialing = 0, + Active = 1, + PastDue = 2, + Canceled = 3, + Unpaid = 4 } diff --git a/src/Exceptionless.Core/Models/PersistentEvent.cs b/src/Exceptionless.Core/Models/PersistentEvent.cs index 4c1cdd83f0..a1548a2597 100644 --- a/src/Exceptionless.Core/Models/PersistentEvent.cs +++ b/src/Exceptionless.Core/Models/PersistentEvent.cs @@ -1,47 +1,46 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - [DebuggerDisplay("Id: {Id}, Type: {Type}, Date: {Date}, Message: {Message}, Value: {Value}, Count: {Count}")] - public class PersistentEvent : Event, IOwnedByOrganizationAndProjectAndStackWithIdentity, IHaveCreatedDate { - public PersistentEvent() { - Idx = new DataDictionary(); - } - - /// - /// Unique id that identifies an event. - /// - public string Id { get; set; } - - /// - /// The organization that the event belongs to. - /// - public string OrganizationId { get; set; } - - /// - /// The project that the event belongs to. - /// - public string ProjectId { get; set; } - - /// - /// The stack that the event belongs to. - /// - public string StackId { get; set; } - - /// - /// Whether the event resulted in the creation of a new stack. - /// - public bool IsFirstOccurrence { get; set; } - - /// - /// The date that the event was created in the system. - /// - public DateTime CreatedUtc { get; set; } - - /// - /// Used to store primitive data type custom data values for searching the event. - /// - public DataDictionary Idx { get; set; } +namespace Exceptionless.Core.Models; + +[DebuggerDisplay("Id: {Id}, Type: {Type}, Date: {Date}, Message: {Message}, Value: {Value}, Count: {Count}")] +public class PersistentEvent : Event, IOwnedByOrganizationAndProjectAndStackWithIdentity, IHaveCreatedDate { + public PersistentEvent() { + Idx = new DataDictionary(); } + + /// + /// Unique id that identifies an event. + /// + public string Id { get; set; } + + /// + /// The organization that the event belongs to. + /// + public string OrganizationId { get; set; } + + /// + /// The project that the event belongs to. + /// + public string ProjectId { get; set; } + + /// + /// The stack that the event belongs to. + /// + public string StackId { get; set; } + + /// + /// Whether the event resulted in the creation of a new stack. + /// + public bool IsFirstOccurrence { get; set; } + + /// + /// The date that the event was created in the system. + /// + public DateTime CreatedUtc { get; set; } + + /// + /// Used to store primitive data type custom data values for searching the event. + /// + public DataDictionary Idx { get; set; } } diff --git a/src/Exceptionless.Core/Models/Project.cs b/src/Exceptionless.Core/Models/Project.cs index a8b088dea7..d02453ec01 100644 --- a/src/Exceptionless.Core/Models/Project.cs +++ b/src/Exceptionless.Core/Models/Project.cs @@ -1,78 +1,76 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Diagnostics; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - [DebuggerDisplay("Id: {Id}, Name: {Name}, NextSummaryEndOfDayTicks: {NextSummaryEndOfDayTicks}")] - public class Project : IOwnedByOrganizationWithIdentity, IData, IHaveDates, ISupportSoftDeletes { - public Project() { - Configuration = new ClientConfiguration(); - NotificationSettings = new Dictionary(); - PromotedTabs = new HashSet(); - DeleteBotDataEnabled = false; - Usage = new Collection(); - OverageHours = new Collection(); - Data = new DataDictionary(); - } +namespace Exceptionless.Core.Models; + +[DebuggerDisplay("Id: {Id}, Name: {Name}, NextSummaryEndOfDayTicks: {NextSummaryEndOfDayTicks}")] +public class Project : IOwnedByOrganizationWithIdentity, IData, IHaveDates, ISupportSoftDeletes { + public Project() { + Configuration = new ClientConfiguration(); + NotificationSettings = new Dictionary(); + PromotedTabs = new HashSet(); + DeleteBotDataEnabled = false; + Usage = new Collection(); + OverageHours = new Collection(); + Data = new DataDictionary(); + } - /// - /// Unique id that identifies an project. - /// - public string Id { get; set; } + /// + /// Unique id that identifies an project. + /// + public string Id { get; set; } - public string OrganizationId { get; set; } + public string OrganizationId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - /// - /// Returns true if we've detected that the project has received data. - /// - public bool? IsConfigured { get; set; } + /// + /// Returns true if we've detected that the project has received data. + /// + public bool? IsConfigured { get; set; } - public ClientConfiguration Configuration { get; set; } + public ClientConfiguration Configuration { get; set; } - public Dictionary NotificationSettings { get; set; } + public Dictionary NotificationSettings { get; set; } - /// - /// Hours over event limit. - /// - public ICollection OverageHours { get; set; } + /// + /// Hours over event limit. + /// + public ICollection OverageHours { get; set; } - /// - /// Account event usage information. - /// - public ICollection Usage { get; set; } - public DateTime? LastEventDateUtc { get; set; } + /// + /// Account event usage information. + /// + public ICollection Usage { get; set; } + public DateTime? LastEventDateUtc { get; set; } - /// - /// Optional data entries that contain additional configuration information for this project. - /// - public DataDictionary Data { get; set; } + /// + /// Optional data entries that contain additional configuration information for this project. + /// + public DataDictionary Data { get; set; } - public HashSet PromotedTabs { get; set; } + public HashSet PromotedTabs { get; set; } - public string CustomContent { get; set; } + public string CustomContent { get; set; } - public bool DeleteBotDataEnabled { get; set; } + public bool DeleteBotDataEnabled { get; set; } - /// - /// The tick count that represents the next time the daily summary job should run. This time is set to midnight of the - /// projects local time. - /// - public long NextSummaryEndOfDayTicks { get; set; } + /// + /// The tick count that represents the next time the daily summary job should run. This time is set to midnight of the + /// projects local time. + /// + public long NextSummaryEndOfDayTicks { get; set; } - public DateTime CreatedUtc { get; set; } - public DateTime UpdatedUtc { get; set; } - public bool IsDeleted { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } + public bool IsDeleted { get; set; } - public static class NotificationIntegrations { - public const string Slack = "slack"; - } + public static class NotificationIntegrations { + public const string Slack = "slack"; + } - public static class KnownDataKeys { - public const string SlackToken = "-@slack"; - } + public static class KnownDataKeys { + public const string SlackToken = "-@slack"; } } diff --git a/src/Exceptionless.Core/Models/Queues/EventNotification.cs b/src/Exceptionless.Core/Models/Queues/EventNotification.cs index 77b459004e..143130e4b3 100644 --- a/src/Exceptionless.Core/Models/Queues/EventNotification.cs +++ b/src/Exceptionless.Core/Models/Queues/EventNotification.cs @@ -1,8 +1,8 @@ -namespace Exceptionless.Core.Queues.Models { - public class EventNotification { - public string EventId { get; set; } - public bool IsNew { get; set; } - public bool IsRegression { get; set; } - public int TotalOccurrences { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Queues.Models; + +public class EventNotification { + public string EventId { get; set; } + public bool IsNew { get; set; } + public bool IsRegression { get; set; } + public int TotalOccurrences { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Queues/EventPostInfo.cs b/src/Exceptionless.Core/Models/Queues/EventPostInfo.cs index ba5d8b7264..ce26e117ad 100644 --- a/src/Exceptionless.Core/Models/Queues/EventPostInfo.cs +++ b/src/Exceptionless.Core/Models/Queues/EventPostInfo.cs @@ -1,21 +1,21 @@ -namespace Exceptionless.Core.Queues.Models { - public class EventPostInfo { - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string CharSet { get; set; } - public string MediaType { get; set; } - public int ApiVersion { get; set; } - public string UserAgent { get; set; } - public string ContentEncoding { get; set; } - public string IpAddress { get; set; } - } +namespace Exceptionless.Core.Queues.Models; - public class EventPost : EventPostInfo { - public EventPost(bool enableArchive) { - ShouldArchive = enableArchive; - } +public class EventPostInfo { + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string CharSet { get; set; } + public string MediaType { get; set; } + public int ApiVersion { get; set; } + public string UserAgent { get; set; } + public string ContentEncoding { get; set; } + public string IpAddress { get; set; } +} - public bool ShouldArchive { get; set; } - public string FilePath { get; set; } +public class EventPost : EventPostInfo { + public EventPost(bool enableArchive) { + ShouldArchive = enableArchive; } + + public bool ShouldArchive { get; set; } + public string FilePath { get; set; } } diff --git a/src/Exceptionless.Core/Models/Queues/EventUserDescription.cs b/src/Exceptionless.Core/Models/Queues/EventUserDescription.cs index ea08c19cd9..39f533ab2b 100644 --- a/src/Exceptionless.Core/Models/Queues/EventUserDescription.cs +++ b/src/Exceptionless.Core/Models/Queues/EventUserDescription.cs @@ -1,8 +1,8 @@ using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Queues.Models { - public class EventUserDescription : UserDescription { - public string ReferenceId { get; set; } - public string ProjectId { get; set; } - } +namespace Exceptionless.Core.Queues.Models; + +public class EventUserDescription : UserDescription { + public string ReferenceId { get; set; } + public string ProjectId { get; set; } } diff --git a/src/Exceptionless.Core/Models/Queues/MailMessage.cs b/src/Exceptionless.Core/Models/Queues/MailMessage.cs index 4f239af8ea..a31cd303f0 100644 --- a/src/Exceptionless.Core/Models/Queues/MailMessage.cs +++ b/src/Exceptionless.Core/Models/Queues/MailMessage.cs @@ -1,8 +1,8 @@ -namespace Exceptionless.Core.Queues.Models { - public class MailMessage { - public string To { get; set; } - public string From { get; set; } - public string Subject { get; set; } - public string Body { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Queues.Models; + +public class MailMessage { + public string To { get; set; } + public string From { get; set; } + public string Subject { get; set; } + public string Body { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Queues/SummaryNotification.cs b/src/Exceptionless.Core/Models/Queues/SummaryNotification.cs index 41ffb95ddd..6dda0a02d4 100644 --- a/src/Exceptionless.Core/Models/Queues/SummaryNotification.cs +++ b/src/Exceptionless.Core/Models/Queues/SummaryNotification.cs @@ -1,9 +1,7 @@ -using System; +namespace Exceptionless.Core.Queues.Models; -namespace Exceptionless.Core.Queues.Models { - public class SummaryNotification { - public string Id { get; set; } - public DateTime UtcStartTime { get; set; } - public DateTime UtcEndTime { get; set; } - } -} \ No newline at end of file +public class SummaryNotification { + public string Id { get; set; } + public DateTime UtcStartTime { get; set; } + public DateTime UtcEndTime { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Queues/WebHookNotification.cs b/src/Exceptionless.Core/Models/Queues/WebHookNotification.cs index 35bccfa2a0..e643d61850 100644 --- a/src/Exceptionless.Core/Models/Queues/WebHookNotification.cs +++ b/src/Exceptionless.Core/Models/Queues/WebHookNotification.cs @@ -1,15 +1,15 @@ -namespace Exceptionless.Core.Queues.Models { - public class WebHookNotification { - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string WebHookId { get; set; } - public WebHookType Type { get; set; } = WebHookType.General; - public string Url { get; set; } - public object Data { get; set; } - } +namespace Exceptionless.Core.Queues.Models; - public enum WebHookType { - General = 0, - Slack = 1 - } -} \ No newline at end of file +public class WebHookNotification { + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string WebHookId { get; set; } + public WebHookType Type { get; set; } = WebHookType.General; + public string Url { get; set; } + public object Data { get; set; } +} + +public enum WebHookType { + General = 0, + Slack = 1 +} diff --git a/src/Exceptionless.Core/Models/SessionInfo.cs b/src/Exceptionless.Core/Models/SessionInfo.cs index bd00f96e62..d34373a031 100644 --- a/src/Exceptionless.Core/Models/SessionInfo.cs +++ b/src/Exceptionless.Core/Models/SessionInfo.cs @@ -1,28 +1,28 @@ -namespace Exceptionless.Core.Models { - public class SessionInfo { - /// - /// The application version during the time of the session. - /// - public string Version { get; set; } +namespace Exceptionless.Core.Models; - /// - /// A unique identifier for the user that the event happened to (ie. email address, user name or database id). - /// - public string UserId { get; set; } +public class SessionInfo { + /// + /// The application version during the time of the session. + /// + public string Version { get; set; } - /// - /// The IP address of the user that the event happened to. - /// - public string IpAddress { get; set; } + /// + /// A unique identifier for the user that the event happened to (ie. email address, user name or database id). + /// + public string UserId { get; set; } - /// - /// A unique identifier for the machine that the event happened on (ie. machine name or ip address). - /// - public string MachineId { get; set; } + /// + /// The IP address of the user that the event happened to. + /// + public string IpAddress { get; set; } - /// - /// A unique identifier for this installation of the Exceptionless client. - /// - public string InstallId { get; set; } - } + /// + /// A unique identifier for the machine that the event happened on (ie. machine name or ip address). + /// + public string MachineId { get; set; } + + /// + /// A unique identifier for this installation of the Exceptionless client. + /// + public string InstallId { get; set; } } diff --git a/src/Exceptionless.Core/Models/SlackToken.cs b/src/Exceptionless.Core/Models/SlackToken.cs index 4501a48ec6..bb58cb9829 100644 --- a/src/Exceptionless.Core/Models/SlackToken.cs +++ b/src/Exceptionless.Core/Models/SlackToken.cs @@ -1,91 +1,89 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Exceptionless.Core.Models { - public class SlackToken { - public string AccessToken { get; set; } - public string[] Scopes { get; set; } - public string UserId { get; set; } - public string TeamId { get; set; } - public string TeamName { get; set; } - public IncomingWebHook IncomingWebhook { get; set; } +namespace Exceptionless.Core.Models; - public class IncomingWebHook { - public string Channel { get; set; } - public string ChannelId { get; set; } - public string ConfigurationUrl { get; set; } - public string Url { get; set; } - } +public class SlackToken { + public string AccessToken { get; set; } + public string[] Scopes { get; set; } + public string UserId { get; set; } + public string TeamId { get; set; } + public string TeamName { get; set; } + public IncomingWebHook IncomingWebhook { get; set; } + + public class IncomingWebHook { + public string Channel { get; set; } + public string ChannelId { get; set; } + public string ConfigurationUrl { get; set; } + public string Url { get; set; } } +} - public class SlackMessage { - public SlackMessage(string text) { - Text = text; - } +public class SlackMessage { + public SlackMessage(string text) { + Text = text; + } - [JsonProperty("text")] - public string Text { get; set; } - [JsonProperty("attachments")] - public List Attachments { get; set; } = new List(); + [JsonProperty("text")] + public string Text { get; set; } + [JsonProperty("attachments")] + public List Attachments { get; set; } = new List(); - public class SlackAttachment { - public SlackAttachment(PersistentEvent ev) { - TimeStamp = ev.Date.ToUnixTimeSeconds(); + public class SlackAttachment { + public SlackAttachment(PersistentEvent ev) { + TimeStamp = ev.Date.ToUnixTimeSeconds(); - var ud = ev.GetUserDescription(); - var ui = ev.GetUserIdentity(); - Text = ud?.Description; + var ud = ev.GetUserDescription(); + var ui = ev.GetUserIdentity(); + Text = ud?.Description; - string displayName = null; - if (!String.IsNullOrEmpty(ui?.Identity)) - displayName = ui.Identity; + string displayName = null; + if (!String.IsNullOrEmpty(ui?.Identity)) + displayName = ui.Identity; - if (!String.IsNullOrEmpty(ui?.Name)) - displayName = ui.Name; + if (!String.IsNullOrEmpty(ui?.Name)) + displayName = ui.Name; - if (!String.IsNullOrEmpty(displayName) && !String.IsNullOrEmpty(ud?.EmailAddress)) - displayName = $"{displayName} ({ud.EmailAddress})"; - else if (!String.IsNullOrEmpty(ui?.Identity) && !String.IsNullOrEmpty(ui.Name)) - displayName = $"{ui.Name} ({ui.Identity})"; + if (!String.IsNullOrEmpty(displayName) && !String.IsNullOrEmpty(ud?.EmailAddress)) + displayName = $"{displayName} ({ud.EmailAddress})"; + else if (!String.IsNullOrEmpty(ui?.Identity) && !String.IsNullOrEmpty(ui.Name)) + displayName = $"{ui.Name} ({ui.Identity})"; - if (!String.IsNullOrEmpty(displayName)) { - AuthorName = displayName; + if (!String.IsNullOrEmpty(displayName)) { + AuthorName = displayName; - if (!String.IsNullOrEmpty(ud?.EmailAddress)) { - AuthorLink = $"mailto:{ud.EmailAddress}?body={ud.Description}"; - //AuthorIcon = $"https://www.gravatar.com/avatar/{ud.EmailAddress.ToMD5()}", - } + if (!String.IsNullOrEmpty(ud?.EmailAddress)) { + AuthorLink = $"mailto:{ud.EmailAddress}?body={ud.Description}"; + //AuthorIcon = $"https://www.gravatar.com/avatar/{ud.EmailAddress.ToMD5()}", } } - - [JsonProperty("title")] - public string Title { get; set; } - [JsonProperty("text")] - public string Text { get; set; } - [JsonProperty("author_name")] - public string AuthorName { get; set; } - [JsonProperty("author_link")] - public string AuthorLink { get; set; } - [JsonProperty("author_icon")] - public string AuthorIcon { get; set; } - [JsonProperty("color")] - public string Color { get; set; } = "#5E9A00"; - [JsonProperty("fields")] - public List Fields { get; set; } = new List(); - [JsonProperty("mrkdwn_in")] - public string[] SupportedMarkdownFields { get; set; } = { "text", "fields" }; - [JsonProperty("ts")] - public long TimeStamp { get; set; } } - public class SlackAttachmentFields { - [JsonProperty("title")] - public string Title { get; set; } - [JsonProperty("value")] - public string Value { get; set; } - [JsonProperty("short")] - public bool Short { get; set; } - } + [JsonProperty("title")] + public string Title { get; set; } + [JsonProperty("text")] + public string Text { get; set; } + [JsonProperty("author_name")] + public string AuthorName { get; set; } + [JsonProperty("author_link")] + public string AuthorLink { get; set; } + [JsonProperty("author_icon")] + public string AuthorIcon { get; set; } + [JsonProperty("color")] + public string Color { get; set; } = "#5E9A00"; + [JsonProperty("fields")] + public List Fields { get; set; } = new List(); + [JsonProperty("mrkdwn_in")] + public string[] SupportedMarkdownFields { get; set; } = { "text", "fields" }; + [JsonProperty("ts")] + public long TimeStamp { get; set; } + } + + public class SlackAttachmentFields { + [JsonProperty("title")] + public string Title { get; set; } + [JsonProperty("value")] + public string Value { get; set; } + [JsonProperty("short")] + public bool Short { get; set; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Models/Stack.cs b/src/Exceptionless.Core/Models/Stack.cs index b7465f4df1..24fb6b75d3 100644 --- a/src/Exceptionless.Core/Models/Stack.cs +++ b/src/Exceptionless.Core/Models/Stack.cs @@ -1,140 +1,138 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.Serialization; using Foundatio.Repositories.Models; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace Exceptionless.Core.Models { - [DebuggerDisplay("Id={Id} Type={Type} Status={Status} IsDeleted={IsDeleted} Title={Title} TotalOccurrences={TotalOccurrences}")] - public class Stack : IOwnedByOrganizationAndProjectWithIdentity, IHaveDates, ISupportSoftDeletes { - public Stack() { - Tags = new TagSet(); - References = new Collection(); - SignatureInfo = new SettingsDictionary(); - } - - /// - /// Unique id that identifies a stack. - /// - public string Id { get; set; } - - /// - /// The organization that the stack belongs to. - /// - public string OrganizationId { get; set; } - - /// - /// The project that the stack belongs to. - /// - public string ProjectId { get; set; } - - /// - /// The stack type (ie. error, log message, feature usage). Check Stack.KnownTypes for standard stack types. - /// - public string Type { get; set; } - - /// - /// The stack status (ie. open, fixed, regressed, - /// - public StackStatus Status { get; set; } - - /// - /// The date that the stack should be snoozed until. - /// - public DateTime? SnoozeUntilUtc { get; set; } - - /// - /// The signature used for stacking future occurrences. - /// - public string SignatureHash { get; set; } - - /// - /// The collection of information that went into creating the signature hash for the stack. - /// - public SettingsDictionary SignatureInfo { get; set; } - - /// - /// The version the stack was fixed in. - /// - public string FixedInVersion { get; set; } - - /// - /// The date the stack was fixed. - /// - public DateTime? DateFixed { get; set; } - - /// - /// The stack title. - /// - public string Title { get; set; } - - /// - /// The total number of occurrences in the stack. - /// - public int TotalOccurrences { get; set; } - - /// - /// The date of the 1st occurrence of this stack in UTC time. - /// - public DateTime FirstOccurrence { get; set; } - - /// - /// The date of the last occurrence of this stack in UTC time. - /// - public DateTime LastOccurrence { get; set; } - - /// - /// The stack description. - /// - public string Description { get; set; } - - /// - /// If true, all future occurrences will be marked as critical. - /// - public bool OccurrencesAreCritical { get; set; } - - /// - /// A list of references. - /// - public ICollection References { get; set; } - - /// - /// A list of tags used to categorize this stack. - /// - public TagSet Tags { get; set; } - - /// - /// The signature used for finding duplicate stacks. (ProjectId, SignatureHash) - /// - public string DuplicateSignature { get; set; } - - public DateTime CreatedUtc { get; set; } - public DateTime UpdatedUtc { get; set; } - public bool IsDeleted { get; set; } - - public bool AllowNotifications => Status != StackStatus.Fixed && Status != StackStatus.Ignored && Status != StackStatus.Discarded && Status != StackStatus.Snoozed; - - public static class KnownTypes { - public const string Error = "error"; - public const string FeatureUsage = "usage"; - public const string SessionHeartbeat = "heartbeat"; - public const string Log = "log"; - public const string NotFound = "404"; - public const string Session = "session"; - public const string SessionEnd = "sessionend"; - } +namespace Exceptionless.Core.Models; + +[DebuggerDisplay("Id={Id} Type={Type} Status={Status} IsDeleted={IsDeleted} Title={Title} TotalOccurrences={TotalOccurrences}")] +public class Stack : IOwnedByOrganizationAndProjectWithIdentity, IHaveDates, ISupportSoftDeletes { + public Stack() { + Tags = new TagSet(); + References = new Collection(); + SignatureInfo = new SettingsDictionary(); } - [JsonConverter(typeof(StringEnumConverter))] - public enum StackStatus { - [EnumMember(Value = "open")] Open, - [EnumMember(Value = "fixed")] Fixed, - [EnumMember(Value = "regressed")] Regressed, - [EnumMember(Value = "snoozed")] Snoozed, - [EnumMember(Value = "ignored")] Ignored, - [EnumMember(Value = "discarded")] Discarded + /// + /// Unique id that identifies a stack. + /// + public string Id { get; set; } + + /// + /// The organization that the stack belongs to. + /// + public string OrganizationId { get; set; } + + /// + /// The project that the stack belongs to. + /// + public string ProjectId { get; set; } + + /// + /// The stack type (ie. error, log message, feature usage). Check Stack.KnownTypes for standard stack types. + /// + public string Type { get; set; } + + /// + /// The stack status (ie. open, fixed, regressed, + /// + public StackStatus Status { get; set; } + + /// + /// The date that the stack should be snoozed until. + /// + public DateTime? SnoozeUntilUtc { get; set; } + + /// + /// The signature used for stacking future occurrences. + /// + public string SignatureHash { get; set; } + + /// + /// The collection of information that went into creating the signature hash for the stack. + /// + public SettingsDictionary SignatureInfo { get; set; } + + /// + /// The version the stack was fixed in. + /// + public string FixedInVersion { get; set; } + + /// + /// The date the stack was fixed. + /// + public DateTime? DateFixed { get; set; } + + /// + /// The stack title. + /// + public string Title { get; set; } + + /// + /// The total number of occurrences in the stack. + /// + public int TotalOccurrences { get; set; } + + /// + /// The date of the 1st occurrence of this stack in UTC time. + /// + public DateTime FirstOccurrence { get; set; } + + /// + /// The date of the last occurrence of this stack in UTC time. + /// + public DateTime LastOccurrence { get; set; } + + /// + /// The stack description. + /// + public string Description { get; set; } + + /// + /// If true, all future occurrences will be marked as critical. + /// + public bool OccurrencesAreCritical { get; set; } + + /// + /// A list of references. + /// + public ICollection References { get; set; } + + /// + /// A list of tags used to categorize this stack. + /// + public TagSet Tags { get; set; } + + /// + /// The signature used for finding duplicate stacks. (ProjectId, SignatureHash) + /// + public string DuplicateSignature { get; set; } + + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } + public bool IsDeleted { get; set; } + + public bool AllowNotifications => Status != StackStatus.Fixed && Status != StackStatus.Ignored && Status != StackStatus.Discarded && Status != StackStatus.Snoozed; + + public static class KnownTypes { + public const string Error = "error"; + public const string FeatureUsage = "usage"; + public const string SessionHeartbeat = "heartbeat"; + public const string Log = "log"; + public const string NotFound = "404"; + public const string Session = "session"; + public const string SessionEnd = "sessionend"; } -} \ No newline at end of file +} + +[JsonConverter(typeof(StringEnumConverter))] +public enum StackStatus { + [EnumMember(Value = "open")] Open, + [EnumMember(Value = "fixed")] Fixed, + [EnumMember(Value = "regressed")] Regressed, + [EnumMember(Value = "snoozed")] Snoozed, + [EnumMember(Value = "ignored")] Ignored, + [EnumMember(Value = "discarded")] Discarded +} diff --git a/src/Exceptionless.Core/Models/StackSummaryModel.cs b/src/Exceptionless.Core/Models/StackSummaryModel.cs index 06f2716ebf..9cc97734b5 100644 --- a/src/Exceptionless.Core/Models/StackSummaryModel.cs +++ b/src/Exceptionless.Core/Models/StackSummaryModel.cs @@ -1,17 +1,16 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; -namespace Exceptionless.Core.Models { - [DebuggerDisplay("Id: {Id}, Status: {Status}, Title: {Title}, First: {FirstOccurrence}, Last: {LastOccurrence}")] - public class StackSummaryModel : SummaryData { - public string Id { get; set; } - public string Title { get; set; } - public StackStatus Status { get; set; } - public DateTime FirstOccurrence { get; set; } - public DateTime LastOccurrence { get; set; } - public long Total { get; set; } +namespace Exceptionless.Core.Models; - public double Users { get; set; } - public double TotalUsers { get; set; } - } -} \ No newline at end of file +[DebuggerDisplay("Id: {Id}, Status: {Status}, Title: {Title}, First: {FirstOccurrence}, Last: {LastOccurrence}")] +public class StackSummaryModel : SummaryData { + public string Id { get; set; } + public string Title { get; set; } + public StackStatus Status { get; set; } + public DateTime FirstOccurrence { get; set; } + public DateTime LastOccurrence { get; set; } + public long Total { get; set; } + + public double Users { get; set; } + public double TotalUsers { get; set; } +} diff --git a/src/Exceptionless.Core/Models/SummaryData.cs b/src/Exceptionless.Core/Models/SummaryData.cs index 40e19e3a37..499ef596c8 100644 --- a/src/Exceptionless.Core/Models/SummaryData.cs +++ b/src/Exceptionless.Core/Models/SummaryData.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Core.Models { - public class SummaryData { - public string TemplateKey { get; set; } - public object Data { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models; + +public class SummaryData { + public string TemplateKey { get; set; } + public object Data { get; set; } +} diff --git a/src/Exceptionless.Core/Models/Token.cs b/src/Exceptionless.Core/Models/Token.cs index d59e18e0ea..f898ea533c 100644 --- a/src/Exceptionless.Core/Models/Token.cs +++ b/src/Exceptionless.Core/Models/Token.cs @@ -1,31 +1,29 @@ -using System; -using System.Collections.Generic; -using Foundatio.Repositories.Models; +using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - public class Token : IOwnedByOrganizationAndProjectWithIdentity, IHaveDates { - public Token() { - Scopes = new HashSet(); - } +namespace Exceptionless.Core.Models; - public string Id { get; set; } - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string UserId { get; set; } - public string DefaultProjectId { get; set; } - public string Refresh { get; set; } - public TokenType Type { get; set; } - public HashSet Scopes { get; set; } - public DateTime? ExpiresUtc { get; set; } - public string Notes { get; set; } - public bool IsDisabled { get; set; } - public string CreatedBy { get; set; } - public DateTime CreatedUtc { get; set; } - public DateTime UpdatedUtc { get; set; } +public class Token : IOwnedByOrganizationAndProjectWithIdentity, IHaveDates { + public Token() { + Scopes = new HashSet(); } - public enum TokenType { - Authentication, - Access - } + public string Id { get; set; } + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string UserId { get; set; } + public string DefaultProjectId { get; set; } + public string Refresh { get; set; } + public TokenType Type { get; set; } + public HashSet Scopes { get; set; } + public DateTime? ExpiresUtc { get; set; } + public string Notes { get; set; } + public bool IsDisabled { get; set; } + public string CreatedBy { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } +} + +public enum TokenType { + Authentication, + Access } diff --git a/src/Exceptionless.Core/Models/UsageInfo.cs b/src/Exceptionless.Core/Models/UsageInfo.cs index 375e74848c..e9ca53e52a 100644 --- a/src/Exceptionless.Core/Models/UsageInfo.cs +++ b/src/Exceptionless.Core/Models/UsageInfo.cs @@ -1,11 +1,9 @@ -using System; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class UsageInfo { - public DateTime Date { get; set; } - public int Total { get; set; } - public int Blocked { get; set; } - public int Limit { get; set; } - public int TooBig { get; set; } - } -} \ No newline at end of file +public class UsageInfo { + public DateTime Date { get; set; } + public int Total { get; set; } + public int Blocked { get; set; } + public int Limit { get; set; } + public int TooBig { get; set; } +} diff --git a/src/Exceptionless.Core/Models/User.cs b/src/Exceptionless.Core/Models/User.cs index d1cc8fa8a3..01e1b75706 100644 --- a/src/Exceptionless.Core/Models/User.cs +++ b/src/Exceptionless.Core/Models/User.cs @@ -1,53 +1,51 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - public class User : IIdentity, IHaveDates { - public User() { - IsActive = true; - OAuthAccounts = new Collection(); - Roles = new Collection(); - OrganizationIds = new Collection(); - EmailNotificationsEnabled = true; - } - - /// - /// Unique id that identifies an user. - /// - public string Id { get; set; } - - /// - /// The organizations that the user has access to. - /// - public ICollection OrganizationIds { get; set; } - - public string Password { get; set; } - public string Salt { get; set; } - public string PasswordResetToken { get; set; } - public DateTime PasswordResetTokenExpiration { get; set; } - public ICollection OAuthAccounts { get; set; } - - /// - /// Gets or sets the users Full Name. - /// - public string FullName { get; set; } - - public string EmailAddress { get; set; } - public bool EmailNotificationsEnabled { get; set; } - public bool IsEmailAddressVerified { get; set; } - public string VerifyEmailAddressToken { get; set; } - public DateTime VerifyEmailAddressTokenExpiration { get; set; } - - /// - /// Gets or sets the users active state. - /// - public bool IsActive { get; set; } - - public ICollection Roles { get; set; } - - public DateTime CreatedUtc { get; set; } - public DateTime UpdatedUtc { get; set; } +namespace Exceptionless.Core.Models; + +public class User : IIdentity, IHaveDates { + public User() { + IsActive = true; + OAuthAccounts = new Collection(); + Roles = new Collection(); + OrganizationIds = new Collection(); + EmailNotificationsEnabled = true; } + + /// + /// Unique id that identifies an user. + /// + public string Id { get; set; } + + /// + /// The organizations that the user has access to. + /// + public ICollection OrganizationIds { get; set; } + + public string Password { get; set; } + public string Salt { get; set; } + public string PasswordResetToken { get; set; } + public DateTime PasswordResetTokenExpiration { get; set; } + public ICollection OAuthAccounts { get; set; } + + /// + /// Gets or sets the users Full Name. + /// + public string FullName { get; set; } + + public string EmailAddress { get; set; } + public bool EmailNotificationsEnabled { get; set; } + public bool IsEmailAddressVerified { get; set; } + public string VerifyEmailAddressToken { get; set; } + public DateTime VerifyEmailAddressTokenExpiration { get; set; } + + /// + /// Gets or sets the users active state. + /// + public bool IsActive { get; set; } + + public ICollection Roles { get; set; } + + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } } diff --git a/src/Exceptionless.Core/Models/WebHook.cs b/src/Exceptionless.Core/Models/WebHook.cs index 8ea125aecc..cae977e001 100644 --- a/src/Exceptionless.Core/Models/WebHook.cs +++ b/src/Exceptionless.Core/Models/WebHook.cs @@ -1,26 +1,25 @@ -using System; -using Foundatio.Repositories.Models; +using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Models { - public class WebHook : IOwnedByOrganizationAndProjectWithIdentity, IHaveCreatedDate { - public string Id { get; set; } - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string Url { get; set; } - public string[] EventTypes { get; set; } - - public bool IsEnabled { get; set; } = true; +namespace Exceptionless.Core.Models; - /// - /// The schema version that should be used. - /// - public string Version { get; set; } +public class WebHook : IOwnedByOrganizationAndProjectWithIdentity, IHaveCreatedDate { + public string Id { get; set; } + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string Url { get; set; } + public string[] EventTypes { get; set; } - public DateTime CreatedUtc { get; set; } - - public static class KnownVersions { - public const string Version1 = "v1"; - public const string Version2 = "v2"; - } + public bool IsEnabled { get; set; } = true; + + /// + /// The schema version that should be used. + /// + public string Version { get; set; } + + public DateTime CreatedUtc { get; set; } + + public static class KnownVersions { + public const string Version1 = "v1"; + public const string Version2 = "v2"; } } diff --git a/src/Exceptionless.Core/Models/WebHookEvent.cs b/src/Exceptionless.Core/Models/WebHookEvent.cs index 68cd798a39..a791e21543 100644 --- a/src/Exceptionless.Core/Models/WebHookEvent.cs +++ b/src/Exceptionless.Core/Models/WebHookEvent.cs @@ -1,35 +1,33 @@ -using System; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class WebHookEvent { - private readonly string _baseUrl; +public class WebHookEvent { + private readonly string _baseUrl; - public WebHookEvent(string baseUrl) { - _baseUrl = baseUrl; - } - - public string Id { get; set; } - public string Url => String.Concat(_baseUrl, "/event/", Id); - public DateTimeOffset OccurrenceDate { get; set; } - public TagSet Tags { get; set; } - public string Type { get; set; } - public string Source { get; set; } - public string Message { get; set; } - public string ProjectId { get; set; } - public string ProjectName { get; set; } - public string OrganizationId { get; set; } - public string OrganizationName { get; set; } - public string StackId { get; set; } - public string StackUrl => String.Concat(_baseUrl, "/stack/", StackId); - public string StackTitle { get; set; } - public string StackDescription { get; set; } - public TagSet StackTags { get; set; } - public int TotalOccurrences { get; set; } - public DateTime FirstOccurrence { get; set; } - public DateTime LastOccurrence { get; set; } - public DateTime? DateFixed { get; set; } - public bool IsNew { get; set; } - public bool IsRegression { get; set; } - public bool IsCritical => Tags != null && Tags.Contains("Critical"); + public WebHookEvent(string baseUrl) { + _baseUrl = baseUrl; } -} \ No newline at end of file + + public string Id { get; set; } + public string Url => String.Concat(_baseUrl, "/event/", Id); + public DateTimeOffset OccurrenceDate { get; set; } + public TagSet Tags { get; set; } + public string Type { get; set; } + public string Source { get; set; } + public string Message { get; set; } + public string ProjectId { get; set; } + public string ProjectName { get; set; } + public string OrganizationId { get; set; } + public string OrganizationName { get; set; } + public string StackId { get; set; } + public string StackUrl => String.Concat(_baseUrl, "/stack/", StackId); + public string StackTitle { get; set; } + public string StackDescription { get; set; } + public TagSet StackTags { get; set; } + public int TotalOccurrences { get; set; } + public DateTime FirstOccurrence { get; set; } + public DateTime LastOccurrence { get; set; } + public DateTime? DateFixed { get; set; } + public bool IsNew { get; set; } + public bool IsRegression { get; set; } + public bool IsCritical => Tags != null && Tags.Contains("Critical"); +} diff --git a/src/Exceptionless.Core/Models/WebHookStack.cs b/src/Exceptionless.Core/Models/WebHookStack.cs index 7d4bc91c9c..975195db95 100644 --- a/src/Exceptionless.Core/Models/WebHookStack.cs +++ b/src/Exceptionless.Core/Models/WebHookStack.cs @@ -1,32 +1,30 @@ -using System; +namespace Exceptionless.Core.Models; -namespace Exceptionless.Core.Models { - public class WebHookStack { - private readonly string _baseUrl; +public class WebHookStack { + private readonly string _baseUrl; - public WebHookStack(string baseUrl) { - _baseUrl = baseUrl; - } - - public string Id { get; set; } - public string Url => String.Concat(_baseUrl, "/stack/", Id); - public string Title { get; set; } - public string Description { get; set; } - - public TagSet Tags { get; set; } - public string RequestPath { get; set; } - public string Type { get; set; } - public string TargetMethod { get; set; } - public string ProjectId { get; set; } - public string ProjectName { get; set; } - public string OrganizationId { get; set; } - public string OrganizationName { get; set; } - public int TotalOccurrences { get; set; } - public DateTime FirstOccurrence { get; set; } - public DateTime LastOccurrence { get; set; } - public DateTime? DateFixed { get; set; } - public string FixedInVersion { get; set; } - public bool IsRegression { get; set; } - public bool IsCritical { get; set; } + public WebHookStack(string baseUrl) { + _baseUrl = baseUrl; } -} \ No newline at end of file + + public string Id { get; set; } + public string Url => String.Concat(_baseUrl, "/stack/", Id); + public string Title { get; set; } + public string Description { get; set; } + + public TagSet Tags { get; set; } + public string RequestPath { get; set; } + public string Type { get; set; } + public string TargetMethod { get; set; } + public string ProjectId { get; set; } + public string ProjectName { get; set; } + public string OrganizationId { get; set; } + public string OrganizationName { get; set; } + public int TotalOccurrences { get; set; } + public DateTime FirstOccurrence { get; set; } + public DateTime LastOccurrence { get; set; } + public DateTime? DateFixed { get; set; } + public string FixedInVersion { get; set; } + public bool IsRegression { get; set; } + public bool IsCritical { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/OrganizationMaintenanceWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/OrganizationMaintenanceWorkItem.cs index 2cf5fe8df9..261c4b7289 100644 --- a/src/Exceptionless.Core/Models/WorkItems/OrganizationMaintenanceWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/OrganizationMaintenanceWorkItem.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Core.Models.WorkItems { - public class OrganizationMaintenanceWorkItem { - public bool UpgradePlans { get; set; } - public bool RemoveOldUsageStats { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.WorkItems; + +public class OrganizationMaintenanceWorkItem { + public bool UpgradePlans { get; set; } + public bool RemoveOldUsageStats { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/OrganizationNotificationWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/OrganizationNotificationWorkItem.cs index 4db2411236..329cde9579 100644 --- a/src/Exceptionless.Core/Models/WorkItems/OrganizationNotificationWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/OrganizationNotificationWorkItem.cs @@ -1,7 +1,7 @@ -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 +namespace Exceptionless.Core.Models.WorkItems; + +public class OrganizationNotificationWorkItem { + public string OrganizationId { get; set; } + public bool IsOverHourlyLimit { get; set; } + public bool IsOverMonthlyLimit { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/ProjectMaintenanceWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/ProjectMaintenanceWorkItem.cs index a37d522763..502b116ad3 100644 --- a/src/Exceptionless.Core/Models/WorkItems/ProjectMaintenanceWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/ProjectMaintenanceWorkItem.cs @@ -1,7 +1,7 @@ -namespace Exceptionless.Core.Models.WorkItems { - public class ProjectMaintenanceWorkItem { - public bool UpdateDefaultBotList { get; set; } - public bool IncrementConfigurationVersion { get; set; } - public bool RemoveOldUsageStats { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.WorkItems; + +public class ProjectMaintenanceWorkItem { + public bool UpdateDefaultBotList { get; set; } + public bool IncrementConfigurationVersion { get; set; } + public bool RemoveOldUsageStats { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/RemoveBotEventsWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/RemoveBotEventsWorkItem.cs index ac784ee22e..cb64fbd031 100644 --- a/src/Exceptionless.Core/Models/WorkItems/RemoveBotEventsWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/RemoveBotEventsWorkItem.cs @@ -1,12 +1,9 @@ -using System; +namespace Exceptionless.Core.Models.WorkItems; -namespace Exceptionless.Core.Models.WorkItems -{ - public class RemoveBotEventsWorkItem { - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string ClientIpAddress { get; set; } - public DateTime UtcStartDate { get; set; } - public DateTime UtcEndDate { get; set; } - } -} \ No newline at end of file +public class RemoveBotEventsWorkItem { + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string ClientIpAddress { get; set; } + public DateTime UtcStartDate { get; set; } + public DateTime UtcEndDate { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/RemoveStacksWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/RemoveStacksWorkItem.cs index eb063d562c..a812f550c9 100644 --- a/src/Exceptionless.Core/Models/WorkItems/RemoveStacksWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/RemoveStacksWorkItem.cs @@ -1,7 +1,6 @@ -namespace Exceptionless.Core.Models.WorkItems -{ - public class RemoveStacksWorkItem { - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.WorkItems; + +public class RemoveStacksWorkItem { + public string OrganizationId { get; set; } + public string ProjectId { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/SetLocationFromGeoWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/SetLocationFromGeoWorkItem.cs index 80fca1d0ed..40f67570d3 100644 --- a/src/Exceptionless.Core/Models/WorkItems/SetLocationFromGeoWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/SetLocationFromGeoWorkItem.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Core.Models.WorkItems { - public class SetLocationFromGeoWorkItem { - public string EventId { get; set; } - public string Geo { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.WorkItems; + +public class SetLocationFromGeoWorkItem { + public string EventId { get; set; } + public string Geo { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/SetProjectIsConfiguredWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/SetProjectIsConfiguredWorkItem.cs index a2f11097c9..801784079a 100644 --- a/src/Exceptionless.Core/Models/WorkItems/SetProjectIsConfiguredWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/SetProjectIsConfiguredWorkItem.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Core.Models.WorkItems { - public class SetProjectIsConfiguredWorkItem { - public string ProjectId { get; set; } - public bool IsConfigured { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.WorkItems; + +public class SetProjectIsConfiguredWorkItem { + public string ProjectId { get; set; } + public bool IsConfigured { get; set; } +} diff --git a/src/Exceptionless.Core/Models/WorkItems/UserMaintenanceWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/UserMaintenanceWorkItem.cs index 6cb7622280..711529225a 100644 --- a/src/Exceptionless.Core/Models/WorkItems/UserMaintenanceWorkItem.cs +++ b/src/Exceptionless.Core/Models/WorkItems/UserMaintenanceWorkItem.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Core.Models.WorkItems { - public class UserMaintenanceWorkItem { - public bool Normalize { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Core.Models.WorkItems; + +public class UserMaintenanceWorkItem { + public bool Normalize { get; set; } +} diff --git a/src/Exceptionless.Core/Pipeline/001_CheckEventDateAction.cs b/src/Exceptionless.Core/Pipeline/001_CheckEventDateAction.cs index 227d1fa316..063907d87d 100644 --- a/src/Exceptionless.Core/Pipeline/001_CheckEventDateAction.cs +++ b/src/Exceptionless.Core/Pipeline/001_CheckEventDateAction.cs @@ -1,31 +1,30 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Plugins.EventProcessor; +using Exceptionless.Core.Plugins.EventProcessor; using Exceptionless.DateTimeExtensions; using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(1)] - public class CheckEventDateAction : EventPipelineActionBase { - public CheckEventDateAction(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - ContinueOnError = true; - } +namespace Exceptionless.Core.Pipeline; + +[Priority(1)] +public class CheckEventDateAction : EventPipelineActionBase { + public CheckEventDateAction(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + ContinueOnError = true; + } - public override Task ProcessAsync(EventContext ctx) { - // If the date is in the future, set it to now using the same offset. - if (SystemClock.UtcNow.IsBefore(ctx.Event.Date.UtcDateTime)) - ctx.Event.Date = ctx.Event.Date.Subtract(ctx.Event.Date.UtcDateTime - SystemClock.OffsetUtcNow); + public override Task ProcessAsync(EventContext ctx) { + // If the date is in the future, set it to now using the same offset. + if (SystemClock.UtcNow.IsBefore(ctx.Event.Date.UtcDateTime)) + ctx.Event.Date = ctx.Event.Date.Subtract(ctx.Event.Date.UtcDateTime - SystemClock.OffsetUtcNow); - // Discard events that are being submitted outside of the plan retention limit. - double eventAgeInDays = SystemClock.UtcNow.Subtract(ctx.Event.Date.UtcDateTime).TotalDays; - if (eventAgeInDays > 3 || ctx.Organization.RetentionDays > 0 && eventAgeInDays > ctx.Organization.RetentionDays) { - _logger.LogInformation("Discarding event that occurred more than three days ago or outside of organization retention limit."); - - ctx.IsCancelled = true; - ctx.IsDiscarded = true; - } + // Discard events that are being submitted outside of the plan retention limit. + double eventAgeInDays = SystemClock.UtcNow.Subtract(ctx.Event.Date.UtcDateTime).TotalDays; + if (eventAgeInDays > 3 || ctx.Organization.RetentionDays > 0 && eventAgeInDays > ctx.Organization.RetentionDays) { + _logger.LogInformation("Discarding event that occurred more than three days ago or outside of organization retention limit."); - return Task.CompletedTask; + ctx.IsCancelled = true; + ctx.IsDiscarded = true; } + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/005_RunEventProcessingPluginsAction.cs b/src/Exceptionless.Core/Pipeline/005_RunEventProcessingPluginsAction.cs index bb1e047d0b..8f2bb73ec2 100644 --- a/src/Exceptionless.Core/Pipeline/005_RunEventProcessingPluginsAction.cs +++ b/src/Exceptionless.Core/Pipeline/005_RunEventProcessingPluginsAction.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Plugins.EventProcessor; +using Exceptionless.Core.Plugins.EventProcessor; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(5)] - public class RunEventProcessingPluginsAction : EventPipelineActionBase { - private readonly EventPluginManager _pluginManager; +namespace Exceptionless.Core.Pipeline; - public RunEventProcessingPluginsAction(EventPluginManager pluginManager, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _pluginManager = pluginManager; - ContinueOnError = true; - } +[Priority(5)] +public class RunEventProcessingPluginsAction : EventPipelineActionBase { + private readonly EventPluginManager _pluginManager; - public override Task ProcessBatchAsync(ICollection contexts) { - return _pluginManager.EventBatchProcessingAsync(contexts); - } + public RunEventProcessingPluginsAction(EventPluginManager pluginManager, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _pluginManager = pluginManager; + ContinueOnError = true; } -} \ No newline at end of file + + public override Task ProcessBatchAsync(ICollection contexts) { + return _pluginManager.EventBatchProcessingAsync(contexts); + } +} diff --git a/src/Exceptionless.Core/Pipeline/006_TruncateFieldsAction.cs b/src/Exceptionless.Core/Pipeline/006_TruncateFieldsAction.cs index 77a3182957..5e718c9366 100644 --- a/src/Exceptionless.Core/Pipeline/006_TruncateFieldsAction.cs +++ b/src/Exceptionless.Core/Pipeline/006_TruncateFieldsAction.cs @@ -1,35 +1,33 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Plugins.EventProcessor; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(6)] - public class TruncateFieldsAction : EventPipelineActionBase { - public TruncateFieldsAction(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Pipeline; - protected override bool IsCritical => true; +[Priority(6)] +public class TruncateFieldsAction : EventPipelineActionBase { + public TruncateFieldsAction(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - public override Task ProcessAsync(EventContext ctx) { - ctx.Event.Tags?.RemoveExcessTags(); + protected override bool IsCritical => true; - if (ctx.Event.Message != null && ctx.Event.Message.Length > 2000) - ctx.Event.Message = ctx.Event.Message.Truncate(2000); - else if (String.IsNullOrEmpty(ctx.Event.Message)) - ctx.Event.Message = null; + public override Task ProcessAsync(EventContext ctx) { + ctx.Event.Tags?.RemoveExcessTags(); - if (ctx.Event.Source != null && ctx.Event.Source.Length > 2000) - ctx.Event.Source = ctx.Event.Source.Truncate(2000); - else if (String.IsNullOrEmpty(ctx.Event.Source)) - ctx.Event.Source = null; + if (ctx.Event.Message != null && ctx.Event.Message.Length > 2000) + ctx.Event.Message = ctx.Event.Message.Truncate(2000); + else if (String.IsNullOrEmpty(ctx.Event.Message)) + ctx.Event.Message = null; - if (!ctx.Event.HasValidReferenceId()) { - ctx.Event.Data["InvalidReferenceId"] = ctx.Event.ReferenceId; - ctx.Event.ReferenceId = "invalid-reference-id"; - } + if (ctx.Event.Source != null && ctx.Event.Source.Length > 2000) + ctx.Event.Source = ctx.Event.Source.Truncate(2000); + else if (String.IsNullOrEmpty(ctx.Event.Source)) + ctx.Event.Source = null; - return Task.CompletedTask; + if (!ctx.Event.HasValidReferenceId()) { + ctx.Event.Data["InvalidReferenceId"] = ctx.Event.ReferenceId; + ctx.Event.ReferenceId = "invalid-reference-id"; } + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/010_AssignToStackAction.cs b/src/Exceptionless.Core/Pipeline/010_AssignToStackAction.cs index 646bc7b899..196a62f54c 100644 --- a/src/Exceptionless.Core/Pipeline/010_AssignToStackAction.cs +++ b/src/Exceptionless.Core/Pipeline/010_AssignToStackAction.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Messaging.Models; using Exceptionless.Core.Plugins.EventProcessor; using Exceptionless.Core.Plugins.Formatting; @@ -15,159 +11,161 @@ using Foundatio.Lock; using Foundatio.Repositories.Extensions; -namespace Exceptionless.Core.Pipeline { - [Priority(10)] - public class AssignToStackAction : EventPipelineActionBase { - private static readonly string StackTypeName = nameof(Stack); - private readonly IStackRepository _stackRepository; - private readonly FormattingPluginManager _formattingPluginManager; - private readonly IMessagePublisher _publisher; - private readonly ILockProvider _lockProvider; - - public AssignToStackAction(IStackRepository stackRepository, FormattingPluginManager formattingPluginManager, IMessagePublisher publisher, AppOptions options, ILockProvider lockProvider, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _stackRepository = stackRepository ?? throw new ArgumentNullException(nameof(stackRepository)); - _formattingPluginManager = formattingPluginManager ?? throw new ArgumentNullException(nameof(formattingPluginManager)); - _publisher = publisher; - _lockProvider = lockProvider; - } +namespace Exceptionless.Core.Pipeline; + +[Priority(10)] +public class AssignToStackAction : EventPipelineActionBase { + private static readonly string StackTypeName = nameof(Stack); + private readonly IStackRepository _stackRepository; + private readonly FormattingPluginManager _formattingPluginManager; + private readonly IMessagePublisher _publisher; + private readonly ILockProvider _lockProvider; + + public AssignToStackAction(IStackRepository stackRepository, FormattingPluginManager formattingPluginManager, IMessagePublisher publisher, AppOptions options, ILockProvider lockProvider, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _stackRepository = stackRepository ?? throw new ArgumentNullException(nameof(stackRepository)); + _formattingPluginManager = formattingPluginManager ?? throw new ArgumentNullException(nameof(formattingPluginManager)); + _publisher = publisher; + _lockProvider = lockProvider; + } - protected override bool IsCritical => true; + protected override bool IsCritical => true; - public override async Task ProcessBatchAsync(ICollection contexts) { - var stacks = new Dictionary(); + public override async Task ProcessBatchAsync(ICollection contexts) { + var stacks = new Dictionary(); - foreach (var ctx in contexts) { - if (String.IsNullOrEmpty(ctx.Event.StackId)) { - // only add default signature info if no other signature info has been added - if (ctx.StackSignatureData.Count == 0) { - ctx.StackSignatureData.AddItemIfNotEmpty("Type", ctx.Event.Type); - ctx.StackSignatureData.AddItemIfNotEmpty("Source", ctx.Event.Source); - } + foreach (var ctx in contexts) { + if (String.IsNullOrEmpty(ctx.Event.StackId)) { + // only add default signature info if no other signature info has been added + if (ctx.StackSignatureData.Count == 0) { + ctx.StackSignatureData.AddItemIfNotEmpty("Type", ctx.Event.Type); + ctx.StackSignatureData.AddItemIfNotEmpty("Source", ctx.Event.Source); + } - string signatureHash = ctx.StackSignatureData.Values.ToSHA1(); - ctx.SignatureHash = signatureHash; + string signatureHash = ctx.StackSignatureData.Values.ToSHA1(); + ctx.SignatureHash = signatureHash; - if (stacks.TryGetValue(signatureHash, out var value)) { - ctx.Stack = value.Stack; - } else { - ctx.Stack = await _stackRepository.GetStackBySignatureHashAsync(ctx.Event.ProjectId, signatureHash).AnyContext(); - if (ctx.Stack != null) - stacks.Add(signatureHash, new StackInfo { IsNew = false, ShouldSave = false, Stack = ctx.Stack }); - } + if (stacks.TryGetValue(signatureHash, out var value)) { + ctx.Stack = value.Stack; + } + else { + ctx.Stack = await _stackRepository.GetStackBySignatureHashAsync(ctx.Event.ProjectId, signatureHash).AnyContext(); + if (ctx.Stack != null) + stacks.Add(signatureHash, new StackInfo { IsNew = false, ShouldSave = false, Stack = ctx.Stack }); + } - // create new stack in distributed lock - if (ctx.Stack == null) { - var success = await _lockProvider.TryUsingAsync($"new-stack:{ctx.Event.ProjectId}:{signatureHash}", async () => { - // double check in case another process just created the stack - var newStack = await _stackRepository.GetStackBySignatureHashAsync(ctx.Event.ProjectId, signatureHash).AnyContext(); - if (newStack != null) { - ctx.Stack = newStack; - return; - } - - _logger.LogTrace("Creating new event stack."); - ctx.IsNew = true; - - string title = _formattingPluginManager.GetStackTitle(ctx.Event); - var stack = new Stack { - OrganizationId = ctx.Event.OrganizationId, - ProjectId = ctx.Event.ProjectId, - SignatureInfo = new SettingsDictionary(ctx.StackSignatureData), - SignatureHash = signatureHash, - DuplicateSignature = ctx.Event.ProjectId + ":" + signatureHash, - Title = title?.Truncate(1000), - Tags = ctx.Event.Tags ?? new TagSet(), - Type = ctx.Event.Type, - TotalOccurrences = 1, - FirstOccurrence = ctx.Event.Date.UtcDateTime, - LastOccurrence = ctx.Event.Date.UtcDateTime - }; - - if (ctx.Event.Type == Event.KnownTypes.Session) - stack.Status = StackStatus.Ignored; - - ctx.Stack = stack; - await _stackRepository.AddAsync(stack, o => o.Cache()).AnyContext(); - - stacks.Add(signatureHash, new StackInfo { IsNew = true, ShouldSave = false, Stack = ctx.Stack }); - }, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)); - - if (!success) { - ctx.SetError($"Unable to create new stack: project={ctx.Event.ProjectId} signature={signatureHash}"); - continue; + // create new stack in distributed lock + if (ctx.Stack == null) { + var success = await _lockProvider.TryUsingAsync($"new-stack:{ctx.Event.ProjectId}:{signatureHash}", async () => { + // double check in case another process just created the stack + var newStack = await _stackRepository.GetStackBySignatureHashAsync(ctx.Event.ProjectId, signatureHash).AnyContext(); + if (newStack != null) { + ctx.Stack = newStack; + return; } - } - } else { - ctx.Stack = await _stackRepository.GetByIdAsync(ctx.Event.StackId, o => o.Cache()).AnyContext(); - if (ctx.Stack == null || ctx.Stack.ProjectId != ctx.Event.ProjectId) { - ctx.SetError("Invalid StackId."); + + _logger.LogTrace("Creating new event stack."); + ctx.IsNew = true; + + string title = _formattingPluginManager.GetStackTitle(ctx.Event); + var stack = new Stack { + OrganizationId = ctx.Event.OrganizationId, + ProjectId = ctx.Event.ProjectId, + SignatureInfo = new SettingsDictionary(ctx.StackSignatureData), + SignatureHash = signatureHash, + DuplicateSignature = ctx.Event.ProjectId + ":" + signatureHash, + Title = title?.Truncate(1000), + Tags = ctx.Event.Tags ?? new TagSet(), + Type = ctx.Event.Type, + TotalOccurrences = 1, + FirstOccurrence = ctx.Event.Date.UtcDateTime, + LastOccurrence = ctx.Event.Date.UtcDateTime + }; + + if (ctx.Event.Type == Event.KnownTypes.Session) + stack.Status = StackStatus.Ignored; + + ctx.Stack = stack; + await _stackRepository.AddAsync(stack, o => o.Cache()).AnyContext(); + + stacks.Add(signatureHash, new StackInfo { IsNew = true, ShouldSave = false, Stack = ctx.Stack }); + }, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)); + + if (!success) { + ctx.SetError($"Unable to create new stack: project={ctx.Event.ProjectId} signature={signatureHash}"); continue; } - - ctx.SignatureHash = ctx.Stack.SignatureHash; - - if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) - stacks.Add(ctx.Stack.SignatureHash, new StackInfo { IsNew = false, ShouldSave = false, Stack = ctx.Stack }); - else - stacks[ctx.Stack.SignatureHash].Stack = ctx.Stack; } - - if (ctx.Stack.Status == StackStatus.Discarded) { - ctx.IsDiscarded = true; - ctx.IsCancelled = true; + } + else { + ctx.Stack = await _stackRepository.GetByIdAsync(ctx.Event.StackId, o => o.Cache()).AnyContext(); + if (ctx.Stack == null || ctx.Stack.ProjectId != ctx.Event.ProjectId) { + ctx.SetError("Invalid StackId."); continue; } - if (!ctx.IsNew && ctx.Event.Tags != null && ctx.Event.Tags.Count > 0) { - if (ctx.Stack.Tags == null) - ctx.Stack.Tags = new TagSet(); + ctx.SignatureHash = ctx.Stack.SignatureHash; - var newTags = ctx.Event.Tags.Where(t => !ctx.Stack.Tags.Contains(t)).ToList(); - if (newTags.Count > 0 || ctx.Stack.Tags.Count > 50 || ctx.Stack.Tags.Any(t => t.Length > 100)) { - ctx.Stack.Tags.AddRange(newTags); - ctx.Stack.Tags.RemoveExcessTags(); + if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) + stacks.Add(ctx.Stack.SignatureHash, new StackInfo { IsNew = false, ShouldSave = false, Stack = ctx.Stack }); + else + stacks[ctx.Stack.SignatureHash].Stack = ctx.Stack; + } - if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) - stacks.Add(ctx.Stack.SignatureHash, new StackInfo { IsNew = false, ShouldSave = true, Stack = ctx.Stack }); - else - stacks[ctx.Stack.SignatureHash].ShouldSave = true; - } - } + if (ctx.Stack.Status == StackStatus.Discarded) { + ctx.IsDiscarded = true; + ctx.IsCancelled = true; + continue; + } + + if (!ctx.IsNew && ctx.Event.Tags != null && ctx.Event.Tags.Count > 0) { + if (ctx.Stack.Tags == null) + ctx.Stack.Tags = new TagSet(); + + var newTags = ctx.Event.Tags.Where(t => !ctx.Stack.Tags.Contains(t)).ToList(); + if (newTags.Count > 0 || ctx.Stack.Tags.Count > 50 || ctx.Stack.Tags.Any(t => t.Length > 100)) { + ctx.Stack.Tags.AddRange(newTags); + ctx.Stack.Tags.RemoveExcessTags(); - ctx.Event.IsFirstOccurrence = ctx.IsNew; + if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) + stacks.Add(ctx.Stack.SignatureHash, new StackInfo { IsNew = false, ShouldSave = true, Stack = ctx.Stack }); + else + stacks[ctx.Stack.SignatureHash].ShouldSave = true; + } } - var addedStacks = stacks.Where(s => s.Value.IsNew).Select(kvp => kvp.Value.Stack).ToList(); - if (addedStacks.Count > 0) { - await _publisher.PublishAsync(new EntityChanged { - ChangeType = ChangeType.Added, - Type = StackTypeName, - Id = addedStacks.Count == 1 ? addedStacks.First().Id : null, - Data = { + ctx.Event.IsFirstOccurrence = ctx.IsNew; + } + + var addedStacks = stacks.Where(s => s.Value.IsNew).Select(kvp => kvp.Value.Stack).ToList(); + if (addedStacks.Count > 0) { + await _publisher.PublishAsync(new EntityChanged { + ChangeType = ChangeType.Added, + Type = StackTypeName, + Id = addedStacks.Count == 1 ? addedStacks.First().Id : null, + Data = { { ExtendedEntityChanged.KnownKeys.OrganizationId, contexts.First().Organization.Id }, { ExtendedEntityChanged.KnownKeys.ProjectId, contexts.First().Project.Id } } - }).AnyContext(); - } + }).AnyContext(); + } - var stacksToSave = stacks.Where(s => s.Value.ShouldSave).Select(kvp => kvp.Value.Stack).ToList(); - if (stacksToSave.Count > 0) - await _stackRepository.SaveAsync(stacksToSave, o => o.Cache().Notifications(false)).AnyContext(); // notification will get sent later in the update stats step + var stacksToSave = stacks.Where(s => s.Value.ShouldSave).Select(kvp => kvp.Value.Stack).ToList(); + if (stacksToSave.Count > 0) + await _stackRepository.SaveAsync(stacksToSave, o => o.Cache().Notifications(false)).AnyContext(); // notification will get sent later in the update stats step - // Set stack ids after they have been saved and created - contexts.ForEach(ctx => { - ctx.Event.StackId = ctx.Stack?.Id; - }); - } + // Set stack ids after they have been saved and created + contexts.ForEach(ctx => { + ctx.Event.StackId = ctx.Stack?.Id; + }); + } - public override Task ProcessAsync(EventContext ctx) { - return Task.CompletedTask; - } + public override Task ProcessAsync(EventContext ctx) { + return Task.CompletedTask; + } - private class StackInfo { - public bool IsNew { get; set; } - public bool ShouldSave { get; set; } - public Stack Stack { get; set; } - } + private class StackInfo { + public bool IsNew { get; set; } + public bool ShouldSave { get; set; } + public Stack Stack { get; set; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/020_MarkAsCriticalAction.cs b/src/Exceptionless.Core/Pipeline/020_MarkAsCriticalAction.cs index 895f3e9ead..761e053521 100644 --- a/src/Exceptionless.Core/Pipeline/020_MarkAsCriticalAction.cs +++ b/src/Exceptionless.Core/Pipeline/020_MarkAsCriticalAction.cs @@ -1,22 +1,21 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Plugins.EventProcessor; +using Exceptionless.Core.Plugins.EventProcessor; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(20)] - public class MarkAsCriticalAction : EventPipelineActionBase { - public MarkAsCriticalAction(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - ContinueOnError = true; - } +namespace Exceptionless.Core.Pipeline; - public override Task ProcessAsync(EventContext ctx) { - if (ctx.Stack == null || !ctx.Stack.OccurrencesAreCritical) - return Task.CompletedTask; - - _logger.LogTrace("Marking error as critical."); - ctx.Event.MarkAsCritical(); +[Priority(20)] +public class MarkAsCriticalAction : EventPipelineActionBase { + public MarkAsCriticalAction(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + ContinueOnError = true; + } + public override Task ProcessAsync(EventContext ctx) { + if (ctx.Stack == null || !ctx.Stack.OccurrencesAreCritical) return Task.CompletedTask; - } + + _logger.LogTrace("Marking error as critical."); + ctx.Event.MarkAsCritical(); + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/030_CheckForRegressionAction.cs b/src/Exceptionless.Core/Pipeline/030_CheckForRegressionAction.cs index 7f2fec688c..a5d4c51853 100644 --- a/src/Exceptionless.Core/Pipeline/030_CheckForRegressionAction.cs +++ b/src/Exceptionless.Core/Pipeline/030_CheckForRegressionAction.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.EventProcessor; using Exceptionless.Core.Repositories; @@ -12,69 +8,72 @@ using McSherry.SemanticVersioning; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(30)] - public class CheckForRegressionAction : EventPipelineActionBase { - private readonly IStackRepository _stackRepository; - private readonly IQueue _workItemQueue; - private readonly SemanticVersionParser _semanticVersionParser; +namespace Exceptionless.Core.Pipeline; - public CheckForRegressionAction(IStackRepository stackRepository, IQueue workItemQueue, SemanticVersionParser semanticVersionParser, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _stackRepository = stackRepository; - _workItemQueue = workItemQueue; - _semanticVersionParser = semanticVersionParser; - ContinueOnError = true; - } +[Priority(30)] +public class CheckForRegressionAction : EventPipelineActionBase { + private readonly IStackRepository _stackRepository; + private readonly IQueue _workItemQueue; + private readonly SemanticVersionParser _semanticVersionParser; - public override async Task ProcessBatchAsync(ICollection contexts) { - var stacks = contexts.Where(c => c.Stack.Status != StackStatus.Regressed && c.Stack.DateFixed.HasValue).OrderBy(c => c.Event.Date).GroupBy(c => c.Event.StackId); - foreach (var stackGroup in stacks) { - try { - var stack = stackGroup.First().Stack; + public CheckForRegressionAction(IStackRepository stackRepository, IQueue workItemQueue, SemanticVersionParser semanticVersionParser, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _stackRepository = stackRepository; + _workItemQueue = workItemQueue; + _semanticVersionParser = semanticVersionParser; + ContinueOnError = true; + } - EventContext regressedContext = null; - SemanticVersion regressedVersion = null; - if (String.IsNullOrEmpty(stack.FixedInVersion)) { - regressedContext = stackGroup.FirstOrDefault(c => stack.DateFixed < c.Event.Date.UtcDateTime); - } else { - var fixedInVersion = await _semanticVersionParser.ParseAsync(stack.FixedInVersion).AnyContext(); - var versions = stackGroup.GroupBy(c => c.Event.GetVersion()); - foreach (var versionGroup in versions) { - var version = await _semanticVersionParser.ParseAsync(versionGroup.Key).AnyContext() ?? _semanticVersionParser.Default; - if (version < fixedInVersion) - continue; + public override async Task ProcessBatchAsync(ICollection contexts) { + var stacks = contexts.Where(c => c.Stack.Status != StackStatus.Regressed && c.Stack.DateFixed.HasValue).OrderBy(c => c.Event.Date).GroupBy(c => c.Event.StackId); + foreach (var stackGroup in stacks) { + try { + var stack = stackGroup.First().Stack; - regressedVersion = version; - regressedContext = versionGroup.First(); - break; - } - } + EventContext regressedContext = null; + SemanticVersion regressedVersion = null; + if (String.IsNullOrEmpty(stack.FixedInVersion)) { + regressedContext = stackGroup.FirstOrDefault(c => stack.DateFixed < c.Event.Date.UtcDateTime); + } + else { + var fixedInVersion = await _semanticVersionParser.ParseAsync(stack.FixedInVersion).AnyContext(); + var versions = stackGroup.GroupBy(c => c.Event.GetVersion()); + foreach (var versionGroup in versions) { + var version = await _semanticVersionParser.ParseAsync(versionGroup.Key).AnyContext() ?? _semanticVersionParser.Default; + if (version < fixedInVersion) + continue; - if (regressedContext == null) - return; + regressedVersion = version; + regressedContext = versionGroup.First(); + break; + } + } - _logger.LogTrace("Marking stack and events as regressed in version: {Version}", regressedVersion); - stack.Status = StackStatus.Regressed; - await _stackRepository.MarkAsRegressedAsync(stack.Id).AnyContext(); + if (regressedContext == null) + return; - foreach (var ctx in stackGroup) - ctx.IsRegression = ctx == regressedContext; - } catch (Exception ex) { - foreach (var context in stackGroup) { - bool cont = false; - try { - cont = HandleError(ex, context); - } catch {} + _logger.LogTrace("Marking stack and events as regressed in version: {Version}", regressedVersion); + stack.Status = StackStatus.Regressed; + await _stackRepository.MarkAsRegressedAsync(stack.Id).AnyContext(); - if (!cont) - context.SetError(ex.Message, ex); + foreach (var ctx in stackGroup) + ctx.IsRegression = ctx == regressedContext; + } + catch (Exception ex) { + foreach (var context in stackGroup) { + bool cont = false; + try { + cont = HandleError(ex, context); } + catch { } + + if (!cont) + context.SetError(ex.Message, ex); } } } + } - public override Task ProcessAsync(EventContext ctx) { - return Task.CompletedTask; - } + public override Task ProcessAsync(EventContext ctx) { + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/035_CopySimpleDataToIdxAction.cs b/src/Exceptionless.Core/Pipeline/035_CopySimpleDataToIdxAction.cs index 7a233b8a5b..a8b7f9aa31 100644 --- a/src/Exceptionless.Core/Pipeline/035_CopySimpleDataToIdxAction.cs +++ b/src/Exceptionless.Core/Pipeline/035_CopySimpleDataToIdxAction.cs @@ -1,34 +1,32 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.AppStats; +using Exceptionless.Core.AppStats; using Exceptionless.Core.Plugins.EventProcessor; using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(40)] - public class CopySimpleDataToIdxAction : EventPipelineActionBase { - private readonly IMetricsClient _metricsClient; +namespace Exceptionless.Core.Pipeline; - public CopySimpleDataToIdxAction(IMetricsClient metricsClient, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _metricsClient = metricsClient; - } - - public override Task ProcessAsync(EventContext ctx) { - if (!ctx.Organization.HasPremiumFeatures) - return Task.CompletedTask; +[Priority(40)] +public class CopySimpleDataToIdxAction : EventPipelineActionBase { + private readonly IMetricsClient _metricsClient; - // TODO: Do we need a pipeline action to trim keys and remove null values that may be sent by other native clients. - ctx.Event.CopyDataToIndex(Array.Empty()); - int fieldCount = ctx.Event.Idx.Count; - _metricsClient.Gauge(MetricNames.EventsFieldCount, fieldCount); - if (fieldCount > 20 && _logger.IsEnabled(LogLevel.Warning)) { - var ev = ctx.Event; - using (_logger.BeginScope(new ExceptionlessState().Organization(ctx.Organization.Id).Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags, ev.Idx }))) - _logger.LogWarning("Event has {FieldCount} indexed fields.", fieldCount); - } + public CopySimpleDataToIdxAction(IMetricsClient metricsClient, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _metricsClient = metricsClient; + } + public override Task ProcessAsync(EventContext ctx) { + if (!ctx.Organization.HasPremiumFeatures) return Task.CompletedTask; + + // TODO: Do we need a pipeline action to trim keys and remove null values that may be sent by other native clients. + ctx.Event.CopyDataToIndex(Array.Empty()); + int fieldCount = ctx.Event.Idx.Count; + _metricsClient.Gauge(MetricNames.EventsFieldCount, fieldCount); + if (fieldCount > 20 && _logger.IsEnabled(LogLevel.Warning)) { + var ev = ctx.Event; + using (_logger.BeginScope(new ExceptionlessState().Organization(ctx.Organization.Id).Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags, ev.Idx }))) + _logger.LogWarning("Event has {FieldCount} indexed fields.", fieldCount); } + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/040_SaveEventAction.cs b/src/Exceptionless.Core/Pipeline/040_SaveEventAction.cs index 3fb1d978b2..3e2fa71830 100644 --- a/src/Exceptionless.Core/Pipeline/040_SaveEventAction.cs +++ b/src/Exceptionless.Core/Pipeline/040_SaveEventAction.cs @@ -1,41 +1,39 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Plugins.EventProcessor; using Exceptionless.Core.Repositories; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(40)] - public class SaveEventAction : EventPipelineActionBase { - private readonly IEventRepository _eventRepository; +namespace Exceptionless.Core.Pipeline; - public SaveEventAction(IEventRepository eventRepository, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _eventRepository = eventRepository; - } +[Priority(40)] +public class SaveEventAction : EventPipelineActionBase { + private readonly IEventRepository _eventRepository; - protected override bool IsCritical => true; + public SaveEventAction(IEventRepository eventRepository, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _eventRepository = eventRepository; + } - public override async Task ProcessBatchAsync(ICollection contexts) { - try { - await _eventRepository.AddAsync(contexts.Select(c => c.Event).ToList()).AnyContext(); - } catch (Exception ex) { - foreach (var context in contexts) { - bool cont = false; - try { - cont = HandleError(ex, context); - } catch {} + protected override bool IsCritical => true; - if (!cont) - context.SetError(ex.Message, ex); + public override async Task ProcessBatchAsync(ICollection contexts) { + try { + await _eventRepository.AddAsync(contexts.Select(c => c.Event).ToList()).AnyContext(); + } + catch (Exception ex) { + foreach (var context in contexts) { + bool cont = false; + try { + cont = HandleError(ex, context); } + catch { } + + if (!cont) + context.SetError(ex.Message, ex); } } + } - public override Task ProcessAsync(EventContext ctx) { - return Task.CompletedTask; - } + public override Task ProcessAsync(EventContext ctx) { + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/050_MarkProjectConfiguredAction.cs b/src/Exceptionless.Core/Pipeline/050_MarkProjectConfiguredAction.cs index 6ac9e76617..53661cb899 100644 --- a/src/Exceptionless.Core/Pipeline/050_MarkProjectConfiguredAction.cs +++ b/src/Exceptionless.Core/Pipeline/050_MarkProjectConfiguredAction.cs @@ -1,51 +1,49 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Plugins.EventProcessor; using Foundatio.Jobs; using Foundatio.Queues; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(50)] - public class MarkProjectConfiguredAction : EventPipelineActionBase { - private readonly IQueue _workItemQueue; +namespace Exceptionless.Core.Pipeline; - public MarkProjectConfiguredAction(IQueue workItemQueue, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _workItemQueue = workItemQueue; - ContinueOnError = true; - } +[Priority(50)] +public class MarkProjectConfiguredAction : EventPipelineActionBase { + private readonly IQueue _workItemQueue; - public override async Task ProcessBatchAsync(ICollection contexts) { - var projectIds = contexts.Where(c => !c.Project.IsConfigured.GetValueOrDefault()).Select(c => c.Project.Id).Distinct().ToList(); - if (projectIds.Count == 0) - return; + public MarkProjectConfiguredAction(IQueue workItemQueue, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _workItemQueue = workItemQueue; + ContinueOnError = true; + } - try { - foreach (string projectId in projectIds) { - await _workItemQueue.EnqueueAsync(new SetProjectIsConfiguredWorkItem { - ProjectId = projectId, - IsConfigured = true - }).AnyContext(); - } - } catch (Exception ex) { - foreach (var context in contexts) { - bool cont = false; - try { - cont = HandleError(ex, context); - } catch {} + public override async Task ProcessBatchAsync(ICollection contexts) { + var projectIds = contexts.Where(c => !c.Project.IsConfigured.GetValueOrDefault()).Select(c => c.Project.Id).Distinct().ToList(); + if (projectIds.Count == 0) + return; - if (!cont) - context.SetError(ex.Message, ex); - } + try { + foreach (string projectId in projectIds) { + await _workItemQueue.EnqueueAsync(new SetProjectIsConfiguredWorkItem { + ProjectId = projectId, + IsConfigured = true + }).AnyContext(); } } + catch (Exception ex) { + foreach (var context in contexts) { + bool cont = false; + try { + cont = HandleError(ex, context); + } + catch { } - public override Task ProcessAsync(EventContext ctx) { - return Task.CompletedTask; + if (!cont) + context.SetError(ex.Message, ex); + } } } -} \ No newline at end of file + + public override Task ProcessAsync(EventContext ctx) { + return Task.CompletedTask; + } +} diff --git a/src/Exceptionless.Core/Pipeline/060_UpdateStatsAction.cs b/src/Exceptionless.Core/Pipeline/060_UpdateStatsAction.cs index 3b2218dd98..87223a9c46 100644 --- a/src/Exceptionless.Core/Pipeline/060_UpdateStatsAction.cs +++ b/src/Exceptionless.Core/Pipeline/060_UpdateStatsAction.cs @@ -1,64 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Plugins.EventProcessor; using Exceptionless.Core.Services; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(60)] - public class UpdateStatsAction : EventPipelineActionBase { - private readonly StackService _stackService; +namespace Exceptionless.Core.Pipeline; - public UpdateStatsAction(StackService stackService, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _stackService = stackService; - } +[Priority(60)] +public class UpdateStatsAction : EventPipelineActionBase { + private readonly StackService _stackService; - protected override bool IsCritical => true; + public UpdateStatsAction(StackService stackService, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _stackService = stackService; + } - public override Task ProcessAsync(EventContext ctx) { - return Task.CompletedTask; - } + protected override bool IsCritical => true; - public override async Task ProcessBatchAsync(ICollection contexts) { - var stacks = contexts.Where(c => !c.IsNew).GroupBy(c => c.Event.StackId); + public override Task ProcessAsync(EventContext ctx) { + return Task.CompletedTask; + } - foreach (var stackGroup in stacks) - await IncrementEventCountersAsync(stackGroup).AnyContext(); - } + public override async Task ProcessBatchAsync(ICollection contexts) { + var stacks = contexts.Where(c => !c.IsNew).GroupBy(c => c.Event.StackId); - private async Task IncrementEventCountersAsync(IGrouping stackGroup) { - var stackContexts = stackGroup.ToList(); + foreach (var stackGroup in stacks) + await IncrementEventCountersAsync(stackGroup).AnyContext(); + } - try { - int count = stackContexts.Count; - var minDate = stackContexts.Min(s => s.Event.Date.UtcDateTime); - var maxDate = stackContexts.Max(s => s.Event.Date.UtcDateTime); - await _stackService.IncrementStackUsageAsync(stackContexts[0].Event.OrganizationId, stackContexts[0].Event.ProjectId, stackGroup.Key, minDate, maxDate, count).AnyContext(); + private async Task IncrementEventCountersAsync(IGrouping stackGroup) { + var stackContexts = stackGroup.ToList(); - // Update stacks in memory since they are used in notifications. - foreach (var ctx in stackContexts) { - if (ctx.Stack.FirstOccurrence > minDate) - ctx.Stack.FirstOccurrence = minDate; + try { + int count = stackContexts.Count; + var minDate = stackContexts.Min(s => s.Event.Date.UtcDateTime); + var maxDate = stackContexts.Max(s => s.Event.Date.UtcDateTime); + await _stackService.IncrementStackUsageAsync(stackContexts[0].Event.OrganizationId, stackContexts[0].Event.ProjectId, stackGroup.Key, minDate, maxDate, count).AnyContext(); - if (ctx.Stack.LastOccurrence < maxDate) - ctx.Stack.LastOccurrence = maxDate; + // Update stacks in memory since they are used in notifications. + foreach (var ctx in stackContexts) { + if (ctx.Stack.FirstOccurrence > minDate) + ctx.Stack.FirstOccurrence = minDate; - ctx.Stack.TotalOccurrences += count; - } - } catch (Exception ex) { - foreach (var context in stackContexts) { - bool cont = false; - try { - cont = HandleError(ex, context); - } catch { } + if (ctx.Stack.LastOccurrence < maxDate) + ctx.Stack.LastOccurrence = maxDate; - if (!cont) - context.SetError(ex.Message, ex); + ctx.Stack.TotalOccurrences += count; + } + } + catch (Exception ex) { + foreach (var context in stackContexts) { + bool cont = false; + try { + cont = HandleError(ex, context); } + catch { } + + if (!cont) + context.SetError(ex.Message, ex); } } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/070_QueueNotificationAction.cs b/src/Exceptionless.Core/Pipeline/070_QueueNotificationAction.cs index 18e71c930f..6848f8605b 100644 --- a/src/Exceptionless.Core/Pipeline/070_QueueNotificationAction.cs +++ b/src/Exceptionless.Core/Pipeline/070_QueueNotificationAction.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Plugins.EventProcessor; using Exceptionless.Core.Plugins.WebHook; using Exceptionless.Core.Queues.Models; @@ -11,104 +7,104 @@ using Foundatio.Queues; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(70)] - public class QueueNotificationAction : EventPipelineActionBase { - private readonly IQueue _notificationQueue; - private readonly IQueue _webHookNotificationQueue; - private readonly IWebHookRepository _webHookRepository; - private readonly WebHookDataPluginManager _webHookDataPluginManager; - - public QueueNotificationAction(IQueue notificationQueue, IQueue webHookNotificationQueue, IWebHookRepository webHookRepository, WebHookDataPluginManager webHookDataPluginManager, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _notificationQueue = notificationQueue; - _webHookNotificationQueue = webHookNotificationQueue; - _webHookRepository = webHookRepository; - _webHookDataPluginManager = webHookDataPluginManager; - ContinueOnError = true; - } +namespace Exceptionless.Core.Pipeline; + +[Priority(70)] +public class QueueNotificationAction : EventPipelineActionBase { + private readonly IQueue _notificationQueue; + private readonly IQueue _webHookNotificationQueue; + private readonly IWebHookRepository _webHookRepository; + private readonly WebHookDataPluginManager _webHookDataPluginManager; + + public QueueNotificationAction(IQueue notificationQueue, IQueue webHookNotificationQueue, IWebHookRepository webHookRepository, WebHookDataPluginManager webHookDataPluginManager, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _notificationQueue = notificationQueue; + _webHookNotificationQueue = webHookNotificationQueue; + _webHookRepository = webHookRepository; + _webHookDataPluginManager = webHookDataPluginManager; + ContinueOnError = true; + } - public override async Task ProcessAsync(EventContext ctx) { - // if they don't have premium features, then we don't need to queue notifications - if (!ctx.Organization.HasPremiumFeatures) - return; - - if (!ctx.Stack.AllowNotifications) - return; - - if (ShouldQueueNotification(ctx)) - await _notificationQueue.EnqueueAsync(new EventNotification { - EventId = ctx.Event.Id, - IsNew = ctx.IsNew, - IsRegression = ctx.IsRegression, - TotalOccurrences = ctx.Stack.TotalOccurrences - }).AnyContext(); - - var webHooks = await _webHookRepository.GetByOrganizationIdOrProjectIdAsync(ctx.Event.OrganizationId, ctx.Event.ProjectId).AnyContext(); - foreach (var hook in webHooks.Documents) { - if (!ShouldCallWebHook(hook, ctx)) - continue; - - var context = new WebHookDataContext(hook.Version, ctx.Event, ctx.Organization, ctx.Project, ctx.Stack, ctx.IsNew, ctx.IsRegression); - var notification = new WebHookNotification { - OrganizationId = ctx.Event.OrganizationId, - ProjectId = ctx.Event.ProjectId, - WebHookId = hook.Id, - Url = hook.Url, - Type = WebHookType.General, - Data = await _webHookDataPluginManager.CreateFromEventAsync(context).AnyContext() - }; - - await _webHookNotificationQueue.EnqueueAsync(notification).AnyContext(); - using (_logger.BeginScope(new Dictionary { { "Web Hook Notification", notification } })) - _logger.LogTrace("Web hook queued: project={project} url={Url}", ctx.Event.ProjectId, hook.Url); - } + public override async Task ProcessAsync(EventContext ctx) { + // if they don't have premium features, then we don't need to queue notifications + if (!ctx.Organization.HasPremiumFeatures) + return; + + if (!ctx.Stack.AllowNotifications) + return; + + if (ShouldQueueNotification(ctx)) + await _notificationQueue.EnqueueAsync(new EventNotification { + EventId = ctx.Event.Id, + IsNew = ctx.IsNew, + IsRegression = ctx.IsRegression, + TotalOccurrences = ctx.Stack.TotalOccurrences + }).AnyContext(); + + var webHooks = await _webHookRepository.GetByOrganizationIdOrProjectIdAsync(ctx.Event.OrganizationId, ctx.Event.ProjectId).AnyContext(); + foreach (var hook in webHooks.Documents) { + if (!ShouldCallWebHook(hook, ctx)) + continue; + + var context = new WebHookDataContext(hook.Version, ctx.Event, ctx.Organization, ctx.Project, ctx.Stack, ctx.IsNew, ctx.IsRegression); + var notification = new WebHookNotification { + OrganizationId = ctx.Event.OrganizationId, + ProjectId = ctx.Event.ProjectId, + WebHookId = hook.Id, + Url = hook.Url, + Type = WebHookType.General, + Data = await _webHookDataPluginManager.CreateFromEventAsync(context).AnyContext() + }; + + await _webHookNotificationQueue.EnqueueAsync(notification).AnyContext(); + using (_logger.BeginScope(new Dictionary { { "Web Hook Notification", notification } })) + _logger.LogTrace("Web hook queued: project={project} url={Url}", ctx.Event.ProjectId, hook.Url); } + } - private bool ShouldCallWebHook(WebHook hook, EventContext ctx) { - if (!hook.IsEnabled) - return false; - - if (!String.IsNullOrEmpty(hook.ProjectId) && !String.Equals(ctx.Project.Id, hook.ProjectId)) - return false; + private bool ShouldCallWebHook(WebHook hook, EventContext ctx) { + if (!hook.IsEnabled) + return false; - if (ctx.IsNew && ctx.Event.IsError() && hook.EventTypes.Contains(WebHookRepository.EventTypes.NewError)) - return true; + if (!String.IsNullOrEmpty(hook.ProjectId) && !String.Equals(ctx.Project.Id, hook.ProjectId)) + return false; - if (ctx.Event.IsCritical() && ctx.Event.IsError() && hook.EventTypes.Contains(WebHookRepository.EventTypes.CriticalError)) - return true; + if (ctx.IsNew && ctx.Event.IsError() && hook.EventTypes.Contains(WebHookRepository.EventTypes.NewError)) + return true; - if (ctx.IsRegression && hook.EventTypes.Contains(WebHookRepository.EventTypes.StackRegression)) - return true; + if (ctx.Event.IsCritical() && ctx.Event.IsError() && hook.EventTypes.Contains(WebHookRepository.EventTypes.CriticalError)) + return true; - if (ctx.IsNew && hook.EventTypes.Contains(WebHookRepository.EventTypes.NewEvent)) - return true; + if (ctx.IsRegression && hook.EventTypes.Contains(WebHookRepository.EventTypes.StackRegression)) + return true; - if (ctx.Event.IsCritical() && hook.EventTypes.Contains(WebHookRepository.EventTypes.CriticalEvent)) - return true; + if (ctx.IsNew && hook.EventTypes.Contains(WebHookRepository.EventTypes.NewEvent)) + return true; - return false; - } + if (ctx.Event.IsCritical() && hook.EventTypes.Contains(WebHookRepository.EventTypes.CriticalEvent)) + return true; + + return false; + } - private bool ShouldQueueNotification(EventContext ctx) { - if (ctx.Project.NotificationSettings.Count == 0) - return false; + private bool ShouldQueueNotification(EventContext ctx) { + if (ctx.Project.NotificationSettings.Count == 0) + return false; - if (ctx.IsNew && ctx.Event.IsError() && ctx.Project.NotificationSettings.Any(n => n.Value.ReportNewErrors)) - return true; + if (ctx.IsNew && ctx.Event.IsError() && ctx.Project.NotificationSettings.Any(n => n.Value.ReportNewErrors)) + return true; - if (ctx.Event.IsCritical() && ctx.Event.IsError() && ctx.Project.NotificationSettings.Any(n => n.Value.ReportCriticalErrors)) - return true; + if (ctx.Event.IsCritical() && ctx.Event.IsError() && ctx.Project.NotificationSettings.Any(n => n.Value.ReportCriticalErrors)) + return true; - if (ctx.IsRegression && ctx.Project.NotificationSettings.Any(n => n.Value.ReportEventRegressions)) - return true; + if (ctx.IsRegression && ctx.Project.NotificationSettings.Any(n => n.Value.ReportEventRegressions)) + return true; - if (ctx.IsNew && ctx.Project.NotificationSettings.Any(n => n.Value.ReportNewEvents)) - return true; + if (ctx.IsNew && ctx.Project.NotificationSettings.Any(n => n.Value.ReportNewEvents)) + return true; - if (ctx.Event.IsCritical() && ctx.Project.NotificationSettings.Any(n => n.Value.ReportCriticalEvents)) - return true; + if (ctx.Event.IsCritical() && ctx.Project.NotificationSettings.Any(n => n.Value.ReportCriticalEvents)) + return true; - return false; - } + return false; } } diff --git a/src/Exceptionless.Core/Pipeline/090_IncrementCountersAction.cs b/src/Exceptionless.Core/Pipeline/090_IncrementCountersAction.cs index e192e97c12..032c11a8ab 100644 --- a/src/Exceptionless.Core/Pipeline/090_IncrementCountersAction.cs +++ b/src/Exceptionless.Core/Pipeline/090_IncrementCountersAction.cs @@ -1,48 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.AppStats; +using Exceptionless.Core.AppStats; using Exceptionless.Core.Billing; using Exceptionless.Core.Plugins.EventProcessor; using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(90)] - public class IncrementCountersAction : EventPipelineActionBase { - private readonly IMetricsClient _metricsClient; - private readonly BillingPlans _plans; +namespace Exceptionless.Core.Pipeline; - public IncrementCountersAction(IMetricsClient metricsClient, BillingPlans plans, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _metricsClient = metricsClient; - _plans = plans; - ContinueOnError = true; - } +[Priority(90)] +public class IncrementCountersAction : EventPipelineActionBase { + private readonly IMetricsClient _metricsClient; + private readonly BillingPlans _plans; - public override Task ProcessBatchAsync(ICollection contexts) { - try { - _metricsClient.Counter(MetricNames.EventsProcessed, contexts.Count); - - if (contexts.First().Organization.PlanId != _plans.FreePlan.Id) - _metricsClient.Counter(MetricNames.EventsPaidProcessed, contexts.Count); - } catch (Exception ex) { - foreach (var context in contexts) { - bool cont = false; - try { - cont = HandleError(ex, context); - } catch {} - - if (!cont) - context.SetError(ex.Message, ex); - } - } + public IncrementCountersAction(IMetricsClient metricsClient, BillingPlans plans, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _metricsClient = metricsClient; + _plans = plans; + ContinueOnError = true; + } + + public override Task ProcessBatchAsync(ICollection contexts) { + try { + _metricsClient.Counter(MetricNames.EventsProcessed, contexts.Count); - return Task.CompletedTask; + if (contexts.First().Organization.PlanId != _plans.FreePlan.Id) + _metricsClient.Counter(MetricNames.EventsPaidProcessed, contexts.Count); } + catch (Exception ex) { + foreach (var context in contexts) { + bool cont = false; + try { + cont = HandleError(ex, context); + } + catch { } - public override Task ProcessAsync(EventContext ctx) { - return Task.CompletedTask; + if (!cont) + context.SetError(ex.Message, ex); + } } + + return Task.CompletedTask; + } + + public override Task ProcessAsync(EventContext ctx) { + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/100_RunEventProcessedPluginsAction.cs b/src/Exceptionless.Core/Pipeline/100_RunEventProcessedPluginsAction.cs index 1d18e8f91a..2801ad628e 100644 --- a/src/Exceptionless.Core/Pipeline/100_RunEventProcessedPluginsAction.cs +++ b/src/Exceptionless.Core/Pipeline/100_RunEventProcessedPluginsAction.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Plugins.EventProcessor; +using Exceptionless.Core.Plugins.EventProcessor; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - [Priority(100)] - public class RunEventProcessedPluginsAction : EventPipelineActionBase { - private readonly EventPluginManager _pluginManager; +namespace Exceptionless.Core.Pipeline; - public RunEventProcessedPluginsAction(EventPluginManager pluginManager, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _pluginManager = pluginManager; - ContinueOnError = true; - } +[Priority(100)] +public class RunEventProcessedPluginsAction : EventPipelineActionBase { + private readonly EventPluginManager _pluginManager; - public override Task ProcessBatchAsync(ICollection contexts) { - return _pluginManager.EventBatchProcessedAsync(contexts); - } + public RunEventProcessedPluginsAction(EventPluginManager pluginManager, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _pluginManager = pluginManager; + ContinueOnError = true; } -} \ No newline at end of file + + public override Task ProcessBatchAsync(ICollection contexts) { + return _pluginManager.EventBatchProcessedAsync(contexts); + } +} diff --git a/src/Exceptionless.Core/Pipeline/Base/PipelineActionBase.cs b/src/Exceptionless.Core/Pipeline/Base/PipelineActionBase.cs index e2ea81cb91..5b18b46ea4 100644 --- a/src/Exceptionless.Core/Pipeline/Base/PipelineActionBase.cs +++ b/src/Exceptionless.Core/Pipeline/Base/PipelineActionBase.cs @@ -1,94 +1,92 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - public interface IPipelineAction where TContext : IPipelineContext { - string Name { get; } - bool Enabled { get; } +namespace Exceptionless.Core.Pipeline; - /// - /// Processes this action using the specified pipeline context. - /// - /// The pipeline context. - Task ProcessAsync(TContext context); +public interface IPipelineAction where TContext : IPipelineContext { + string Name { get; } + bool Enabled { get; } - /// - /// Processes this action using the specified pipeline context. - /// - /// The pipeline context. - Task ProcessBatchAsync(ICollection contexts); + /// + /// Processes this action using the specified pipeline context. + /// + /// The pipeline context. + Task ProcessAsync(TContext context); - /// - /// Handle exceptions thrown by this action. - /// - /// The exception that occurred while processing the action. - /// The pipeline context. - /// Return true if processing should continue or false if processing should halt. - bool HandleError(Exception exception, TContext context); - } + /// + /// Processes this action using the specified pipeline context. + /// + /// The pipeline context. + Task ProcessBatchAsync(ICollection contexts); /// - /// The base class for pipeline actions + /// Handle exceptions thrown by this action. /// - /// The type of the pipeline context. - public abstract class PipelineActionBase : IPipelineAction where TContext : class, IPipelineContext { - private readonly AppOptions _options; - protected readonly ILogger _logger; + /// The exception that occurred while processing the action. + /// The pipeline context. + /// Return true if processing should continue or false if processing should halt. + bool HandleError(Exception exception, TContext context); +} - public PipelineActionBase(AppOptions options, ILoggerFactory loggerFactory = null) { - _options = options; - var type = GetType(); - Name = type.Name; - Enabled = !_options.DisabledPipelineActions.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase); - _logger = loggerFactory?.CreateLogger(type); - } +/// +/// The base class for pipeline actions +/// +/// The type of the pipeline context. +public abstract class PipelineActionBase : IPipelineAction where TContext : class, IPipelineContext { + private readonly AppOptions _options; + protected readonly ILogger _logger; - public string Name { get; } + public PipelineActionBase(AppOptions options, ILoggerFactory loggerFactory = null) { + _options = options; + var type = GetType(); + Name = type.Name; + Enabled = !_options.DisabledPipelineActions.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase); + _logger = loggerFactory?.CreateLogger(type); + } - public bool Enabled { get; } + public string Name { get; } - protected bool ContinueOnError { get; set; } + public bool Enabled { get; } - /// - /// Processes this action using the specified pipeline context. - /// - /// The pipeline context. - public virtual Task ProcessAsync(TContext context) { - throw new NotImplementedException(); - } + protected bool ContinueOnError { get; set; } - /// - /// Processes this action using the specified pipeline context. - /// - /// The pipeline context. - public virtual async Task ProcessBatchAsync(ICollection contexts) { - foreach (var ctx in contexts) { - try { - await ProcessAsync(ctx).AnyContext(); - } catch (Exception ex) { - bool cont = false; - try { - cont = HandleError(ex, ctx); - } catch { } + /// + /// Processes this action using the specified pipeline context. + /// + /// The pipeline context. + public virtual Task ProcessAsync(TContext context) { + throw new NotImplementedException(); + } - if (!cont) - ctx.SetError(ex.Message, ex); + /// + /// Processes this action using the specified pipeline context. + /// + /// The pipeline context. + public virtual async Task ProcessBatchAsync(ICollection contexts) { + foreach (var ctx in contexts) { + try { + await ProcessAsync(ctx).AnyContext(); + } + catch (Exception ex) { + bool cont = false; + try { + cont = HandleError(ex, ctx); } + catch { } + + if (!cont) + ctx.SetError(ex.Message, ex); } } + } - /// - /// Handle exceptions thrown by this action. - /// - /// The exception that occurred while processing the action. - /// The pipeline context. - /// Return true if processing should continue or false if processing should halt. - public virtual bool HandleError(Exception exception, TContext context) { - return ContinueOnError; - } + /// + /// Handle exceptions thrown by this action. + /// + /// The exception that occurred while processing the action. + /// The pipeline context. + /// Return true if processing should continue or false if processing should halt. + public virtual bool HandleError(Exception exception, TContext context) { + return ContinueOnError; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/Base/PipelineBase.cs b/src/Exceptionless.Core/Pipeline/Base/PipelineBase.cs index 282d21d008..44acc4e8a3 100644 --- a/src/Exceptionless.Core/Pipeline/Base/PipelineBase.cs +++ b/src/Exceptionless.Core/Pipeline/Base/PipelineBase.cs @@ -1,114 +1,111 @@ -using System; -using System.Collections.Generic; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Concurrent; using Exceptionless.Core.Extensions; using Exceptionless.Core.Helpers; using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { +namespace Exceptionless.Core.Pipeline; + +/// +/// The base class for a pipeline service. +/// +/// The type used as the context for the pipeline. +/// The base type of the pipeline action to run in this pipeline. +/// +/// The pipeline works by executing actions (classes) that have a common base class in a series. +/// To setup a pipeline, you have to have a context class that will hold all the common data for the pipeline. +/// You also have to have a common base class that inherits for all your actions. +/// The pipeline looks for all types that inherit that action base class to run. +/// +public abstract class PipelineBase where TAction : class, IPipelineAction where TContext : IPipelineContext { + protected static readonly ConcurrentDictionary> _actionTypeCache = new ConcurrentDictionary>(); + private readonly IServiceProvider _serviceProvider; + private readonly AppOptions _options; + private readonly IList> _actions; + protected readonly string _metricPrefix; + protected readonly IMetricsClient _metricsClient; + protected readonly ILogger _logger; + + public PipelineBase(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) { + _serviceProvider = serviceProvider; + _options = options; + + var type = GetType(); + _metricPrefix = String.Concat(type.Name.ToLower(), "."); + _metricsClient = metricsClient ?? new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = loggerFactory }); + _logger = loggerFactory?.CreateLogger(type); + + _actions = LoadDefaultActions(); + } + /// - /// The base class for a pipeline service. + /// Runs all the actions of the pipeline with the specified context. /// - /// The type used as the context for the pipeline. - /// The base type of the pipeline action to run in this pipeline. - /// - /// The pipeline works by executing actions (classes) that have a common base class in a series. - /// To setup a pipeline, you have to have a context class that will hold all the common data for the pipeline. - /// You also have to have a common base class that inherits for all your actions. - /// The pipeline looks for all types that inherit that action base class to run. - /// - public abstract class PipelineBase where TAction : class, IPipelineAction where TContext : IPipelineContext { - protected static readonly ConcurrentDictionary> _actionTypeCache = new ConcurrentDictionary>(); - private readonly IServiceProvider _serviceProvider; - private readonly AppOptions _options; - private readonly IList> _actions; - protected readonly string _metricPrefix; - protected readonly IMetricsClient _metricsClient; - protected readonly ILogger _logger; - - public PipelineBase(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) { - _serviceProvider = serviceProvider; - _options = options; - - var type = GetType(); - _metricPrefix = String.Concat(type.Name.ToLower(), "."); - _metricsClient = metricsClient ?? new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = loggerFactory }); - _logger = loggerFactory?.CreateLogger(type); - - _actions = LoadDefaultActions(); - } + /// The context to run the actions with. + public virtual async Task RunAsync(TContext context) { + await RunAsync(new[] { context }).AnyContext(); + return context; + } + + /// + /// Runs all the specified actions with the specified context. + /// + /// The context to run the actions with. + public virtual async Task> RunAsync(ICollection contexts) { + PipelineRunning(contexts); - /// - /// Runs all the actions of the pipeline with the specified context. - /// - /// The context to run the actions with. - public virtual async Task RunAsync(TContext context) { - await RunAsync(new[] { context }).AnyContext(); - return context; + string metricPrefix = String.Concat(_metricPrefix, nameof(RunAsync).ToLower(), "."); + foreach (var action in _actions) { + string metricName = String.Concat(metricPrefix, action.Name.ToLower()); + var contextsToProcess = contexts.Where(c => c.IsCancelled == false && !c.HasError).ToList(); + await _metricsClient.TimeAsync(() => action.ProcessBatchAsync(contextsToProcess), metricName).AnyContext(); + if (contextsToProcess.All(c => c.IsCancelled || c.HasError)) + break; } - /// - /// Runs all the specified actions with the specified context. - /// - /// The context to run the actions with. - public virtual async Task> RunAsync(ICollection contexts) { - PipelineRunning(contexts); - - string metricPrefix = String.Concat(_metricPrefix, nameof(RunAsync).ToLower(), "."); - foreach (var action in _actions) { - string metricName = String.Concat(metricPrefix, action.Name.ToLower()); - var contextsToProcess = contexts.Where(c => c.IsCancelled == false && !c.HasError).ToList(); - await _metricsClient.TimeAsync(() => action.ProcessBatchAsync(contextsToProcess), metricName).AnyContext(); - if (contextsToProcess.All(c => c.IsCancelled || c.HasError)) - break; - } + contexts.ForEach(c => c.IsProcessed = c.IsCancelled == false && c.HasError == false); + PipelineCompleted(contexts); + + return contexts; + } - contexts.ForEach(c => c.IsProcessed = c.IsCancelled == false && c.HasError == false); - PipelineCompleted(contexts); + /// + /// Called before any pipeline modules are run. + /// + /// The context the modules will run with. + protected virtual void PipelineRunning(ICollection contexts) { } - return contexts; - } + /// + /// Called after all pipeline modules have run. + /// + /// The context the modules ran with. + protected virtual void PipelineCompleted(ICollection contexts) { } - /// - /// Called before any pipeline modules are run. - /// - /// The context the modules will run with. - protected virtual void PipelineRunning(ICollection contexts) { } - - /// - /// Called after all pipeline modules have run. - /// - /// The context the modules ran with. - protected virtual void PipelineCompleted(ICollection contexts) { } - - /// - /// Gets the types that are subclasses of . - /// - /// An enumerable list of action types in priority order to run for the pipeline. - protected virtual IList GetActionTypes() { - return _actionTypeCache.GetOrAdd(typeof(TAction), t => TypeHelper.GetDerivedTypes().SortByPriority()); - } + /// + /// Gets the types that are subclasses of . + /// + /// An enumerable list of action types in priority order to run for the pipeline. + protected virtual IList GetActionTypes() { + return _actionTypeCache.GetOrAdd(typeof(TAction), t => TypeHelper.GetDerivedTypes().SortByPriority()); + } - private List> LoadDefaultActions() { - var actions = new List>(); - foreach (var type in GetActionTypes()) { - if (_options.DisabledPipelineActions.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase)) { - _logger.LogWarning("Pipeline Action {Name} is currently disabled and won't be executed.", type.Name); - continue; - } - - try { - actions.Add((IPipelineAction)_serviceProvider.GetService(type)); - } catch (Exception ex) { - _logger.LogError(ex, "Unable to instantiate Pipeline Action of type {TypeFullName}: {Message}", type.FullName, ex.Message); - throw; - } + private List> LoadDefaultActions() { + var actions = new List>(); + foreach (var type in GetActionTypes()) { + if (_options.DisabledPipelineActions.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase)) { + _logger.LogWarning("Pipeline Action {Name} is currently disabled and won't be executed.", type.Name); + continue; } - return actions; + try { + actions.Add((IPipelineAction)_serviceProvider.GetService(type)); + } + catch (Exception ex) { + _logger.LogError(ex, "Unable to instantiate Pipeline Action of type {TypeFullName}: {Message}", type.FullName, ex.Message); + throw; + } } + + return actions; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Pipeline/Base/PipelineContextBase.cs b/src/Exceptionless.Core/Pipeline/Base/PipelineContextBase.cs index abfb81cc6e..7b5bc39aca 100644 --- a/src/Exceptionless.Core/Pipeline/Base/PipelineContextBase.cs +++ b/src/Exceptionless.Core/Pipeline/Base/PipelineContextBase.cs @@ -1,99 +1,96 @@ -using System; +namespace Exceptionless.Core.Pipeline; -namespace Exceptionless.Core.Pipeline -{ +/// +/// The interface for pipeline context data +/// +public interface IPipelineContext { /// - /// The interface for pipeline context data + /// Gets or sets a value indicating whether this pipeline is cancelled. /// - public interface IPipelineContext { - /// - /// Gets or sets a value indicating whether this pipeline is cancelled. - /// - /// - /// true if this pipeline is cancelled; otherwise, false. - /// - bool IsCancelled { get; set; } + /// + /// true if this pipeline is cancelled; otherwise, false. + /// + bool IsCancelled { get; set; } - /// - /// Gets or sets a value indicating whether this pipeline context is processed. - /// - /// - /// true if this pipeline context is processed; otherwise, false. - /// - bool IsProcessed { get; set; } + /// + /// Gets or sets a value indicating whether this pipeline context is processed. + /// + /// + /// true if this pipeline context is processed; otherwise, false. + /// + bool IsProcessed { get; set; } - /// - /// Gets a value indicating whether or not this context has gotten an error during processing. - /// - bool HasError { get; } + /// + /// Gets a value indicating whether or not this context has gotten an error during processing. + /// + bool HasError { get; } - /// - /// Used to set the context into an errored state with an error message and possibly exception with details. - /// - /// The error message. - /// The Exception that occurred. - void SetError(string message, Exception ex = null); + /// + /// Used to set the context into an errored state with an error message and possibly exception with details. + /// + /// The error message. + /// The Exception that occurred. + void SetError(string message, Exception ex = null); - /// - /// The error message that occurred during processing. - /// - string ErrorMessage { get; } + /// + /// The error message that occurred during processing. + /// + string ErrorMessage { get; } - /// - /// Gets or sets the exception that occurred during processing of this context. - /// - /// - /// Exception if an error occurred during processing; otherwise, null. - /// - Exception Exception { get; } - } + /// + /// Gets or sets the exception that occurred during processing of this context. + /// + /// + /// Exception if an error occurred during processing; otherwise, null. + /// + Exception Exception { get; } +} +/// +/// The base class for pipeline context data +/// +public abstract class PipelineContextBase : IPipelineContext { /// - /// The base class for pipeline context data + /// Gets or sets a value indicating whether this pipeline is cancelled. /// - public abstract class PipelineContextBase : IPipelineContext { - /// - /// Gets or sets a value indicating whether this pipeline is cancelled. - /// - /// - /// true if this pipeline is cancelled; otherwise, false. - /// - public bool IsCancelled { get; set; } + /// + /// true if this pipeline is cancelled; otherwise, false. + /// + public bool IsCancelled { get; set; } - /// - /// Gets or sets a value indicating whether this pipeline context is processed. - /// - /// - /// true if this pipeline context is processed; otherwise, false. - /// - public bool IsProcessed { get; set; } + /// + /// Gets or sets a value indicating whether this pipeline context is processed. + /// + /// + /// true if this pipeline context is processed; otherwise, false. + /// + public bool IsProcessed { get; set; } - /// - /// Gets a value indicating whether or not this context has gotten an error during processing. - /// - public bool HasError => ErrorMessage != null || Exception != null; + /// + /// Gets a value indicating whether or not this context has gotten an error during processing. + /// + public bool HasError => ErrorMessage != null || Exception != null; - /// - /// Used to set the context into an errored state with an error message and possibly exception with details. - /// - /// The error message. - /// The Exception that occurred. - public void SetError(string message, Exception ex = null) { - ErrorMessage = message; - Exception = ex; - } + /// + /// Used to set the context into an errored state with an error message and possibly exception with details. + /// + /// The error message. + /// The Exception that occurred. + public void SetError(string message, Exception ex = null) { + ErrorMessage = message; + Exception = ex; + } - /// - /// The error message that occurred during processing. - /// - public string ErrorMessage { get; private set; } + /// + /// The error message that occurred during processing. + /// + public string ErrorMessage { get; private set; } - /// - /// Gets or sets the exception that occurred during processing of this context. - /// - /// - /// Exception if an error occurred during processing; otherwise, null. - /// - public Exception Exception { get; private set; } - } -} \ No newline at end of file + /// + /// Gets or sets the exception that occurred during processing of this context. + /// + /// + /// Exception if an error occurred during processing; otherwise, null. + /// + public Exception Exception { get; private set; } +} diff --git a/src/Exceptionless.Core/Pipeline/Base/PriorityAttribute.cs b/src/Exceptionless.Core/Pipeline/Base/PriorityAttribute.cs index 62c5eb4de7..c5db95eee3 100644 --- a/src/Exceptionless.Core/Pipeline/Base/PriorityAttribute.cs +++ b/src/Exceptionless.Core/Pipeline/Base/PriorityAttribute.cs @@ -1,14 +1,12 @@ -using System; +namespace Exceptionless.Core.Pipeline; -namespace Exceptionless.Core.Pipeline { - /// - /// Used to determine action priority. - /// - public class PriorityAttribute : Attribute { - public PriorityAttribute(int priority) { - Priority = priority; - } - - public int Priority { get; private set; } +/// +/// Used to determine action priority. +/// +public class PriorityAttribute : Attribute { + public PriorityAttribute(int priority) { + Priority = priority; } + + public int Priority { get; private set; } } diff --git a/src/Exceptionless.Core/Pipeline/EventPipeline.cs b/src/Exceptionless.Core/Pipeline/EventPipeline.cs index 4898f80ab3..33b727b41e 100644 --- a/src/Exceptionless.Core/Pipeline/EventPipeline.cs +++ b/src/Exceptionless.Core/Pipeline/EventPipeline.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.AppStats; +using Exceptionless.Core.AppStats; using Exceptionless.Core.Extensions; using Exceptionless.Core.Plugins.EventProcessor; using Exceptionless.Core.Models; @@ -11,67 +7,68 @@ using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - public class EventPipeline : PipelineBase { - public EventPipeline(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) {} +namespace Exceptionless.Core.Pipeline; - public Task RunAsync(PersistentEvent ev, Organization organization, Project project, EventPostInfo epi = null) { - return RunAsync(new EventContext(ev, organization, project, epi)); - } +public class EventPipeline : PipelineBase { + public EventPipeline(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } - public Task> RunAsync(IEnumerable events, Organization organization, Project project, EventPostInfo epi = null) { - return RunAsync(events.Select(ev => new EventContext(ev, organization, project, epi)).ToList()); - } + public Task RunAsync(PersistentEvent ev, Organization organization, Project project, EventPostInfo epi = null) { + return RunAsync(new EventContext(ev, organization, project, epi)); + } - public override async Task> RunAsync(ICollection contexts) { - if (contexts == null || contexts.Count == 0) - return contexts ?? new List(); + public Task> RunAsync(IEnumerable events, Organization organization, Project project, EventPostInfo epi = null) { + return RunAsync(events.Select(ev => new EventContext(ev, organization, project, epi)).ToList()); + } - _metricsClient.Counter(MetricNames.EventsSubmitted, contexts.Count); - try { - if (contexts.Any(c => !String.IsNullOrEmpty(c.Event.Id))) - throw new ArgumentException("All Event Ids should not be populated."); + public override async Task> RunAsync(ICollection contexts) { + if (contexts == null || contexts.Count == 0) + return contexts ?? new List(); - var project = contexts.First().Project; - if (String.IsNullOrEmpty(project.Id)) - throw new ArgumentException("All Project Ids must be populated."); + _metricsClient.Counter(MetricNames.EventsSubmitted, contexts.Count); + try { + if (contexts.Any(c => !String.IsNullOrEmpty(c.Event.Id))) + throw new ArgumentException("All Event Ids should not be populated."); - if (contexts.Any(c => c.Event.ProjectId != project.Id)) - throw new ArgumentException("All Project Ids must be the same for a batch of events."); + var project = contexts.First().Project; + if (String.IsNullOrEmpty(project.Id)) + throw new ArgumentException("All Project Ids must be populated."); - // load organization settings into the context - var organization = contexts.First().Organization; - foreach (string key in organization.Data.Keys) - contexts.ForEach(c => c.SetProperty(key, organization.Data[key])); + if (contexts.Any(c => c.Event.ProjectId != project.Id)) + throw new ArgumentException("All Project Ids must be the same for a batch of events."); - // load project settings into the context, overriding any organization settings with the same name - foreach (string key in project.Data.Keys) - contexts.ForEach(c => c.SetProperty(key, project.Data[key])); + // load organization settings into the context + var organization = contexts.First().Organization; + foreach (string key in organization.Data.Keys) + contexts.ForEach(c => c.SetProperty(key, organization.Data[key])); - await _metricsClient.TimeAsync(() => base.RunAsync(contexts), MetricNames.EventsProcessingTime).AnyContext(); + // load project settings into the context, overriding any organization settings with the same name + foreach (string key in project.Data.Keys) + contexts.ForEach(c => c.SetProperty(key, project.Data[key])); - int cancelled = contexts.Count(c => c.IsCancelled); - if (cancelled > 0) - _metricsClient.Counter(MetricNames.EventsProcessCancelled, cancelled); + await _metricsClient.TimeAsync(() => base.RunAsync(contexts), MetricNames.EventsProcessingTime).AnyContext(); - int discarded = contexts.Count(c => c.IsDiscarded); - if (discarded > 0) - _metricsClient.Counter(MetricNames.EventsDiscarded, discarded); + int cancelled = contexts.Count(c => c.IsCancelled); + if (cancelled > 0) + _metricsClient.Counter(MetricNames.EventsProcessCancelled, cancelled); - // TODO: Log the errors out to the events project id. - int errors = contexts.Count(c => c.HasError); - if (errors > 0) - _metricsClient.Counter(MetricNames.EventsProcessErrors, errors); - } catch (Exception) { - _metricsClient.Counter(MetricNames.EventsProcessErrors, contexts.Count); - throw; - } + int discarded = contexts.Count(c => c.IsDiscarded); + if (discarded > 0) + _metricsClient.Counter(MetricNames.EventsDiscarded, discarded); - return contexts; + // TODO: Log the errors out to the events project id. + int errors = contexts.Count(c => c.HasError); + if (errors > 0) + _metricsClient.Counter(MetricNames.EventsProcessErrors, errors); } - - protected override IList GetActionTypes() { - return _actionTypeCache.GetOrAdd(typeof(EventPipelineActionBase), t => TypeHelper.GetDerivedTypes().SortByPriority()); + catch (Exception) { + _metricsClient.Counter(MetricNames.EventsProcessErrors, contexts.Count); + throw; } + + return contexts; + } + + protected override IList GetActionTypes() { + return _actionTypeCache.GetOrAdd(typeof(EventPipelineActionBase), t => TypeHelper.GetDerivedTypes().SortByPriority()); } } diff --git a/src/Exceptionless.Core/Pipeline/EventPipelineActionBase.cs b/src/Exceptionless.Core/Pipeline/EventPipelineActionBase.cs index f63ae1c2be..280c661f56 100644 --- a/src/Exceptionless.Core/Pipeline/EventPipelineActionBase.cs +++ b/src/Exceptionless.Core/Pipeline/EventPipelineActionBase.cs @@ -1,26 +1,24 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Plugins.EventProcessor; +using Exceptionless.Core.Plugins.EventProcessor; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Pipeline { - public abstract class EventPipelineActionBase : PipelineActionBase { - public EventPipelineActionBase(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) {} +namespace Exceptionless.Core.Pipeline; - protected virtual bool IsCritical => false; - protected virtual string[] ErrorTags => new string[0]; - protected virtual string ErrorMessage => null; +public abstract class EventPipelineActionBase : PipelineActionBase { + public EventPipelineActionBase(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - public override bool HandleError(Exception ex, EventContext ctx) { - var ev = new { ctx.Event.Date, ctx.Event.StackId, ctx.Event.Type, ctx.Event.Source, ctx.Event.Message, ctx.Event.Value, ctx.Event.Geo, ctx.Event.ReferenceId, ctx.Event.Tags }; - using (_logger.BeginScope(new Dictionary { { "Event", ev }, { "Tags", ErrorTags }})) { - if (IsCritical) - _logger.LogCritical(ex, "Error processing action: {TypeName} Message: {Message}", GetType().Name, ex.Message); - else - _logger.LogError(ex, "Error processing action: {TypeName} Message: {Message}", GetType().Name, ex.Message); - } + protected virtual bool IsCritical => false; + protected virtual string[] ErrorTags => new string[0]; + protected virtual string ErrorMessage => null; - return ContinueOnError; + public override bool HandleError(Exception ex, EventContext ctx) { + var ev = new { ctx.Event.Date, ctx.Event.StackId, ctx.Event.Type, ctx.Event.Source, ctx.Event.Message, ctx.Event.Value, ctx.Event.Geo, ctx.Event.ReferenceId, ctx.Event.Tags }; + using (_logger.BeginScope(new Dictionary { { "Event", ev }, { "Tags", ErrorTags } })) { + if (IsCritical) + _logger.LogCritical(ex, "Error processing action: {TypeName} Message: {Message}", GetType().Name, ex.Message); + else + _logger.LogError(ex, "Error processing action: {TypeName} Message: {Message}", GetType().Name, ex.Message); } + + return ContinueOnError; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventParser/Default/FallbackEventParserPlugin.cs b/src/Exceptionless.Core/Plugins/EventParser/Default/FallbackEventParserPlugin.cs index 347a570292..458651b56b 100644 --- a/src/Exceptionless.Core/Plugins/EventParser/Default/FallbackEventParserPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventParser/Default/FallbackEventParserPlugin.cs @@ -1,25 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Pipeline; using Exceptionless.Core.Models; using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventParser { - [Priority(Int32.MaxValue)] - public class FallbackEventParserPlugin : PluginBase, IEventParserPlugin { - public FallbackEventParserPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventParser; - public List ParseEvents(string input, int apiVersion, string userAgent) { - var events = input.SplitLines().Select(entry => new PersistentEvent { - Date = SystemClock.OffsetNow, - Type = "log", - Message = entry - }).ToList(); +[Priority(Int32.MaxValue)] +public class FallbackEventParserPlugin : PluginBase, IEventParserPlugin { + public FallbackEventParserPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - return events.Count > 0 ? events : null; - } + public List ParseEvents(string input, int apiVersion, string userAgent) { + var events = input.SplitLines().Select(entry => new PersistentEvent { + Date = SystemClock.OffsetNow, + Type = "log", + Message = entry + }).ToList(); + + return events.Count > 0 ? events : null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventParser/Default/JsonEventParserPlugin.cs b/src/Exceptionless.Core/Plugins/EventParser/Default/JsonEventParserPlugin.cs index d44186abd0..a49bfb8bc2 100644 --- a/src/Exceptionless.Core/Plugins/EventParser/Default/JsonEventParserPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventParser/Default/JsonEventParserPlugin.cs @@ -1,38 +1,37 @@ -using System.Collections.Generic; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Newtonsoft.Json; -namespace Exceptionless.Core.Plugins.EventParser { - [Priority(0)] - public class JsonEventParserPlugin : PluginBase, IEventParserPlugin { - private readonly JsonSerializerSettings _settings; +namespace Exceptionless.Core.Plugins.EventParser; - public JsonEventParserPlugin(AppOptions options, JsonSerializerSettings settings) : base(options) { - _settings = settings; - } +[Priority(0)] +public class JsonEventParserPlugin : PluginBase, IEventParserPlugin { + private readonly JsonSerializerSettings _settings; + + public JsonEventParserPlugin(AppOptions options, JsonSerializerSettings settings) : base(options) { + _settings = settings; + } - public List ParseEvents(string input, int apiVersion, string userAgent) { - if (apiVersion < 2) - return null; + public List ParseEvents(string input, int apiVersion, string userAgent) { + if (apiVersion < 2) + return null; - var events = new List(); - switch (input.GetJsonType()) { - case JsonType.Object: { + var events = new List(); + switch (input.GetJsonType()) { + case JsonType.Object: { if (input.TryFromJson(out PersistentEvent ev, _settings)) events.Add(ev); break; } - case JsonType.Array: { + case JsonType.Array: { if (input.TryFromJson(out PersistentEvent[] parsedEvents, _settings)) events.AddRange(parsedEvents); break; } - } - - return events.Count > 0 ? events : null; } + + return events.Count > 0 ? events : null; } } diff --git a/src/Exceptionless.Core/Plugins/EventParser/Default/LegacyErrorParserPlugin.cs b/src/Exceptionless.Core/Plugins/EventParser/Default/LegacyErrorParserPlugin.cs index 2a3152a589..8bf4cdee2f 100644 --- a/src/Exceptionless.Core/Plugins/EventParser/Default/LegacyErrorParserPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventParser/Default/LegacyErrorParserPlugin.cs @@ -1,36 +1,35 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Plugins.EventUpgrader; using Exceptionless.Core.Models; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Exceptionless.Core.Plugins.EventParser { - [Priority(10)] - public class LegacyErrorParserPlugin : PluginBase, IEventParserPlugin { - private readonly EventUpgraderPluginManager _manager; - private readonly JsonSerializerSettings _settings; +namespace Exceptionless.Core.Plugins.EventParser; - public LegacyErrorParserPlugin(EventUpgraderPluginManager manager, JsonSerializerSettings settings, AppOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) { - _manager = manager; - _settings = settings; - } +[Priority(10)] +public class LegacyErrorParserPlugin : PluginBase, IEventParserPlugin { + private readonly EventUpgraderPluginManager _manager; + private readonly JsonSerializerSettings _settings; + + public LegacyErrorParserPlugin(EventUpgraderPluginManager manager, JsonSerializerSettings settings, AppOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) { + _manager = manager; + _settings = settings; + } - public List ParseEvents(string input, int apiVersion, string userAgent) { - if (apiVersion != 1) - return null; + public List ParseEvents(string input, int apiVersion, string userAgent) { + if (apiVersion != 1) + return null; - try { - var ctx = new EventUpgraderContext(input); - _manager.Upgrade(ctx); + try { + var ctx = new EventUpgraderContext(input); + _manager.Upgrade(ctx); - return ctx.Documents.FromJson(_settings); - } catch (Exception ex) { - _logger.LogError(ex, "Error parsing event: {Message}", ex.Message); - return null; - } + return ctx.Documents.FromJson(_settings); + } + catch (Exception ex) { + _logger.LogError(ex, "Error parsing event: {Message}", ex.Message); + return null; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventParser/EventParserPluginManager.cs b/src/Exceptionless.Core/Plugins/EventParser/EventParserPluginManager.cs index 313c8d9cac..6cd41f2c72 100644 --- a/src/Exceptionless.Core/Plugins/EventParser/EventParserPluginManager.cs +++ b/src/Exceptionless.Core/Plugins/EventParser/EventParserPluginManager.cs @@ -1,45 +1,43 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Metrics; using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventParser { - public class EventParserPluginManager : PluginManagerBase { - public EventParserPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory){} - - /// - /// Runs through the formatting plugins to calculate an html summary for the stack based on the event data. - /// - public List ParseEvents(string input, int apiVersion, string userAgent) { - string metricPrefix = String.Concat(_metricPrefix, nameof(ParseEvents).ToLower(), "."); - foreach (var plugin in Plugins.Values.ToList()) { - string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); - - try { - List events = null; - _metricsClient.Time(() => events = plugin.ParseEvents(input, apiVersion, userAgent), metricName); - if (events == null) - continue; - - // Set required event properties - events.ForEach(e => { - if (e.Date == DateTimeOffset.MinValue) - e.Date = SystemClock.OffsetNow; - - if (String.IsNullOrWhiteSpace(e.Type)) - e.Type = e.Data.ContainsKey(Event.KnownDataKeys.Error) || e.Data.ContainsKey(Event.KnownDataKeys.SimpleError) ? Event.KnownTypes.Error : Event.KnownTypes.Log; - }); - - return events; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling ParseEvents in plugin {PluginName}: {Message}", plugin.Name, ex.Message); - } - } +namespace Exceptionless.Core.Plugins.EventParser; + +public class EventParserPluginManager : PluginManagerBase { + public EventParserPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } + + /// + /// Runs through the formatting plugins to calculate an html summary for the stack based on the event data. + /// + public List ParseEvents(string input, int apiVersion, string userAgent) { + string metricPrefix = String.Concat(_metricPrefix, nameof(ParseEvents).ToLower(), "."); + foreach (var plugin in Plugins.Values.ToList()) { + string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); + + try { + List events = null; + _metricsClient.Time(() => events = plugin.ParseEvents(input, apiVersion, userAgent), metricName); + if (events == null) + continue; + + // Set required event properties + events.ForEach(e => { + if (e.Date == DateTimeOffset.MinValue) + e.Date = SystemClock.OffsetNow; - return new List(); + if (String.IsNullOrWhiteSpace(e.Type)) + e.Type = e.Data.ContainsKey(Event.KnownDataKeys.Error) || e.Data.ContainsKey(Event.KnownDataKeys.SimpleError) ? Event.KnownTypes.Error : Event.KnownTypes.Log; + }); + + return events; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling ParseEvents in plugin {PluginName}: {Message}", plugin.Name, ex.Message); + } } + + return new List(); } } diff --git a/src/Exceptionless.Core/Plugins/EventParser/IEventParserPlugin.cs b/src/Exceptionless.Core/Plugins/EventParser/IEventParserPlugin.cs index db2d6c7b72..c1c7e0fd72 100644 --- a/src/Exceptionless.Core/Plugins/EventParser/IEventParserPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventParser/IEventParserPlugin.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.EventParser { - public interface IEventParserPlugin : IPlugin { - List ParseEvents(string input, int apiVersion, string userAgent); - } -} \ No newline at end of file +namespace Exceptionless.Core.Plugins.EventParser; + +public interface IEventParserPlugin : IPlugin { + List ParseEvents(string input, int apiVersion, string userAgent); +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/03_ManualStackingPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/03_ManualStackingPlugin.cs index 59ed647b28..17ccee82b4 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/03_ManualStackingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/03_ManualStackingPlugin.cs @@ -1,21 +1,20 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(3)] - public sealed class ManualStackingPlugin : EventProcessorPluginBase { - public ManualStackingPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventProcessor; - public override Task EventProcessingAsync(EventContext context) { - var msi = context.Event.GetManualStackingInfo(); - if (msi?.SignatureData != null) { - foreach (var kvp in msi.SignatureData) - context.StackSignatureData.AddItemIfNotEmpty(kvp.Key, kvp.Value); - } +[Priority(3)] +public sealed class ManualStackingPlugin : EventProcessorPluginBase { + public ManualStackingPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - return Task.CompletedTask; + public override Task EventProcessingAsync(EventContext context) { + var msi = context.Event.GetManualStackingInfo(); + if (msi?.SignatureData != null) { + foreach (var kvp in msi.SignatureData) + context.StackSignatureData.AddItemIfNotEmpty(kvp.Key, kvp.Value); } + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/05_CheckForDuplicateReferenceIdPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/05_CheckForDuplicateReferenceIdPlugin.cs index 1530072e36..10a01bb251 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/05_CheckForDuplicateReferenceIdPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/05_CheckForDuplicateReferenceIdPlugin.cs @@ -1,44 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Pipeline; using Foundatio.Caching; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(5)] - public sealed class CheckForDuplicateReferenceIdPlugin : EventProcessorPluginBase { - private readonly ICacheClient _cacheClient; +namespace Exceptionless.Core.Plugins.EventProcessor; - public CheckForDuplicateReferenceIdPlugin(ICacheClient cacheClient, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _cacheClient = cacheClient; - } +[Priority(5)] +public sealed class CheckForDuplicateReferenceIdPlugin : EventProcessorPluginBase { + private readonly ICacheClient _cacheClient; - public override async Task EventProcessingAsync(EventContext context) { - if (String.IsNullOrEmpty(context.Event.ReferenceId)) - return; + public CheckForDuplicateReferenceIdPlugin(ICacheClient cacheClient, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _cacheClient = cacheClient; + } - if (await _cacheClient.AddAsync(GetCacheKey(context), true, TimeSpan.FromDays(1)).AnyContext()) { - context.SetProperty("AddedReferenceId", true); - return; - } + public override async Task EventProcessingAsync(EventContext context) { + if (String.IsNullOrEmpty(context.Event.ReferenceId)) + return; - _logger.LogInformation("Discarding event due to duplicate reference id: {ReferenceId}", context.Event.ReferenceId); - context.IsCancelled = true; + if (await _cacheClient.AddAsync(GetCacheKey(context), true, TimeSpan.FromDays(1)).AnyContext()) { + context.SetProperty("AddedReferenceId", true); + return; } - public override Task EventBatchProcessedAsync(ICollection contexts) { - var values = contexts.Where(c => !String.IsNullOrEmpty(c.Event.ReferenceId) && c.GetProperty("AddedReferenceId") == null).ToDictionary(GetCacheKey, v => true); - if (values.Count == 0) - return Task.CompletedTask; + _logger.LogInformation("Discarding event due to duplicate reference id: {ReferenceId}", context.Event.ReferenceId); + context.IsCancelled = true; + } - return _cacheClient.SetAllAsync(values, TimeSpan.FromDays(1)); - } + public override Task EventBatchProcessedAsync(ICollection contexts) { + var values = contexts.Where(c => !String.IsNullOrEmpty(c.Event.ReferenceId) && c.GetProperty("AddedReferenceId") == null).ToDictionary(GetCacheKey, v => true); + if (values.Count == 0) + return Task.CompletedTask; - private string GetCacheKey(EventContext context) { - return String.Concat("Project:", context.Project.Id, ":", context.Event.ReferenceId); - } + return _cacheClient.SetAllAsync(values, TimeSpan.FromDays(1)); + } + + private string GetCacheKey(EventContext context) { + return String.Concat("Project:", context.Project.Id, ":", context.Event.ReferenceId); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/07_SubmissionClientPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/07_SubmissionClientPlugin.cs index 47e08cb6ad..ed3838aa32 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/07_SubmissionClientPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/07_SubmissionClientPlugin.cs @@ -1,47 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Pipeline; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor.Default { - [Priority(7)] - public sealed class SubmissionClientPlugin : EventProcessorPluginBase { - public SubmissionClientPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - - public override Task EventBatchProcessingAsync(ICollection contexts) { - contexts.ForEach(c => c.Event.Data.Remove(Event.KnownDataKeys.SubmissionClient)); - - var firstContext = contexts.FirstOrDefault(); - var epi = firstContext?.EventPostInfo; - if (epi == null) - return Task.CompletedTask; - - bool hasIpAddress = firstContext.IncludePrivateInformation && !String.IsNullOrEmpty(epi.IpAddress); - bool hasUserAgent = !String.IsNullOrEmpty(epi.UserAgent); - if (!hasIpAddress && !hasUserAgent) - return Task.CompletedTask; - - var submissionClient = new SubmissionClient(); - if (hasIpAddress) - submissionClient.IpAddress = !epi.IpAddress.IsLocalHost() ? epi.IpAddress.Trim() : "127.0.0.1"; - - if (hasUserAgent) { - string[] parts = epi.UserAgent.Split(new [] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 2 && Version.TryParse(parts[1], out var version)) { - submissionClient.UserAgent = parts[0].Trim().ToLowerInvariant(); - submissionClient.Version = version.ToString(); - } else { - submissionClient.UserAgent = epi.UserAgent.Trim().ToLowerInvariant(); - } - } +namespace Exceptionless.Core.Plugins.EventProcessor.Default; + +[Priority(7)] +public sealed class SubmissionClientPlugin : EventProcessorPluginBase { + public SubmissionClientPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } + + public override Task EventBatchProcessingAsync(ICollection contexts) { + contexts.ForEach(c => c.Event.Data.Remove(Event.KnownDataKeys.SubmissionClient)); + + var firstContext = contexts.FirstOrDefault(); + var epi = firstContext?.EventPostInfo; + if (epi == null) + return Task.CompletedTask; - contexts.ForEach(c => c.Event.SetSubmissionClient(submissionClient)); + bool hasIpAddress = firstContext.IncludePrivateInformation && !String.IsNullOrEmpty(epi.IpAddress); + bool hasUserAgent = !String.IsNullOrEmpty(epi.UserAgent); + if (!hasIpAddress && !hasUserAgent) return Task.CompletedTask; + + var submissionClient = new SubmissionClient(); + if (hasIpAddress) + submissionClient.IpAddress = !epi.IpAddress.IsLocalHost() ? epi.IpAddress.Trim() : "127.0.0.1"; + + if (hasUserAgent) { + string[] parts = epi.UserAgent.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2 && Version.TryParse(parts[1], out var version)) { + submissionClient.UserAgent = parts[0].Trim().ToLowerInvariant(); + submissionClient.Version = version.ToString(); + } + else { + submissionClient.UserAgent = epi.UserAgent.Trim().ToLowerInvariant(); + } } + + contexts.ForEach(c => c.Event.SetSubmissionClient(submissionClient)); + return Task.CompletedTask; } } diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/0_ThrottleBotsPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/0_ThrottleBotsPlugin.cs index 58d6f113f7..a746acb8b7 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/0_ThrottleBotsPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/0_ThrottleBotsPlugin.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Pipeline; -using Exceptionless.Core.Queues.Models; using Exceptionless.DateTimeExtensions; using Foundatio.Caching; using Foundatio.Jobs; @@ -13,62 +8,63 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(0)] - public sealed class ThrottleBotsPlugin : EventProcessorPluginBase { - private readonly ICacheClient _cache; - private readonly IQueue _workItemQueue; - private readonly TimeSpan _throttlingPeriod = TimeSpan.FromMinutes(5); +namespace Exceptionless.Core.Plugins.EventProcessor; - public ThrottleBotsPlugin(ICacheClient cacheClient, IQueue workItemQueue, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _cache = cacheClient; - _workItemQueue = workItemQueue; - } +[Priority(0)] +public sealed class ThrottleBotsPlugin : EventProcessorPluginBase { + private readonly ICacheClient _cache; + private readonly IQueue _workItemQueue; + private readonly TimeSpan _throttlingPeriod = TimeSpan.FromMinutes(5); + + public ThrottleBotsPlugin(ICacheClient cacheClient, IQueue workItemQueue, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _cache = cacheClient; + _workItemQueue = workItemQueue; + } - public override async Task EventBatchProcessingAsync(ICollection contexts) { - if (_options.AppMode == AppMode.Development) - return; + public override async Task EventBatchProcessingAsync(ICollection contexts) { + if (_options.AppMode == AppMode.Development) + return; - var firstContext = contexts.First(); - if (!firstContext.Project.DeleteBotDataEnabled || !firstContext.IncludePrivateInformation) - return; + var firstContext = contexts.First(); + if (!firstContext.Project.DeleteBotDataEnabled || !firstContext.IncludePrivateInformation) + return; - // Throttle errors by client ip address to no more than X every 5 minutes. - var clientIpAddressGroups = contexts.GroupBy(c => c.Event.GetRequestInfo()?.ClientIpAddress); - foreach (var clientIpAddressGroup in clientIpAddressGroups) { - if (String.IsNullOrEmpty(clientIpAddressGroup.Key) || clientIpAddressGroup.Key.IsPrivateNetwork()) - continue; + // Throttle errors by client ip address to no more than X every 5 minutes. + var clientIpAddressGroups = contexts.GroupBy(c => c.Event.GetRequestInfo()?.ClientIpAddress); + foreach (var clientIpAddressGroup in clientIpAddressGroups) { + if (String.IsNullOrEmpty(clientIpAddressGroup.Key) || clientIpAddressGroup.Key.IsPrivateNetwork()) + continue; - var clientIpContexts = clientIpAddressGroup.ToList(); - string throttleCacheKey = String.Concat("bot:", clientIpAddressGroup.Key, ":", SystemClock.UtcNow.Floor(_throttlingPeriod).Ticks); - int? requestCount = await _cache.GetAsync(throttleCacheKey, null).AnyContext(); - if (requestCount.HasValue) { - await _cache.IncrementAsync(throttleCacheKey, clientIpContexts.Count).AnyContext(); - requestCount += clientIpContexts.Count; - } else { - await _cache.SetAsync(throttleCacheKey, clientIpContexts.Count, SystemClock.UtcNow.Ceiling(_throttlingPeriod)).AnyContext(); - requestCount = clientIpContexts.Count; - } + var clientIpContexts = clientIpAddressGroup.ToList(); + string throttleCacheKey = String.Concat("bot:", clientIpAddressGroup.Key, ":", SystemClock.UtcNow.Floor(_throttlingPeriod).Ticks); + int? requestCount = await _cache.GetAsync(throttleCacheKey, null).AnyContext(); + if (requestCount.HasValue) { + await _cache.IncrementAsync(throttleCacheKey, clientIpContexts.Count).AnyContext(); + requestCount += clientIpContexts.Count; + } + else { + await _cache.SetAsync(throttleCacheKey, clientIpContexts.Count, SystemClock.UtcNow.Ceiling(_throttlingPeriod)).AnyContext(); + requestCount = clientIpContexts.Count; + } - if (requestCount < _options.BotThrottleLimit) - continue; + if (requestCount < _options.BotThrottleLimit) + continue; - _logger.LogInformation("Bot throttle triggered. IP: {IP} Time: {ThrottlingPeriod} Project: {project}", clientIpAddressGroup.Key, SystemClock.UtcNow.Floor(_throttlingPeriod), firstContext.Event.ProjectId); + _logger.LogInformation("Bot throttle triggered. IP: {IP} Time: {ThrottlingPeriod} Project: {project}", clientIpAddressGroup.Key, SystemClock.UtcNow.Floor(_throttlingPeriod), firstContext.Event.ProjectId); - // The throttle was triggered, go and delete all the errors that triggered the throttle to reduce bot noise in the system - await _workItemQueue.EnqueueAsync(new RemoveBotEventsWorkItem { - OrganizationId = firstContext.Event.OrganizationId, - ProjectId = firstContext.Event.ProjectId, - ClientIpAddress = clientIpAddressGroup.Key, - UtcStartDate = SystemClock.UtcNow.Floor(_throttlingPeriod), - UtcEndDate = SystemClock.UtcNow.Ceiling(_throttlingPeriod) - }).AnyContext(); + // The throttle was triggered, go and delete all the errors that triggered the throttle to reduce bot noise in the system + await _workItemQueue.EnqueueAsync(new RemoveBotEventsWorkItem { + OrganizationId = firstContext.Event.OrganizationId, + ProjectId = firstContext.Event.ProjectId, + ClientIpAddress = clientIpAddressGroup.Key, + UtcStartDate = SystemClock.UtcNow.Floor(_throttlingPeriod), + UtcEndDate = SystemClock.UtcNow.Ceiling(_throttlingPeriod) + }).AnyContext(); - clientIpContexts.ForEach(c => { - c.IsDiscarded = true; - c.IsCancelled = true; - }); - } + clientIpContexts.ForEach(c => { + c.IsDiscarded = true; + c.IsCancelled = true; + }); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/10_NotFoundPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/10_NotFoundPlugin.cs index 1d123a9882..82092a6a8c 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/10_NotFoundPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/10_NotFoundPlugin.cs @@ -1,35 +1,33 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Pipeline; using Exceptionless.Core.Models; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(10)] - public sealed class NotFoundPlugin : EventProcessorPluginBase { - public NotFoundPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) {} +namespace Exceptionless.Core.Plugins.EventProcessor; - public override Task EventProcessingAsync(EventContext context) { - if (context.Event.Type != Event.KnownTypes.NotFound) - return Task.CompletedTask; +[Priority(10)] +public sealed class NotFoundPlugin : EventProcessorPluginBase { + public NotFoundPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - context.Event.Data.Remove(Event.KnownDataKeys.EnvironmentInfo); - context.Event.Data.Remove(Event.KnownDataKeys.TraceLog); - - var req = context.Event.GetRequestInfo(); - if (req == null) - return Task.CompletedTask; - - if (String.IsNullOrWhiteSpace(context.Event.Source)) { - context.Event.Message = null; - context.Event.Source = req.GetFullPath(includeHttpMethod: true, includeHost: false, includeQueryString: false); - } + public override Task EventProcessingAsync(EventContext context) { + if (context.Event.Type != Event.KnownTypes.NotFound) + return Task.CompletedTask; - context.Event.Data.Remove(Event.KnownDataKeys.Error); - context.Event.Data.Remove(Event.KnownDataKeys.SimpleError); + context.Event.Data.Remove(Event.KnownDataKeys.EnvironmentInfo); + context.Event.Data.Remove(Event.KnownDataKeys.TraceLog); + var req = context.Event.GetRequestInfo(); + if (req == null) return Task.CompletedTask; + + if (String.IsNullOrWhiteSpace(context.Event.Source)) { + context.Event.Message = null; + context.Event.Source = req.GetFullPath(includeHttpMethod: true, includeHost: false, includeQueryString: false); } + + context.Event.Data.Remove(Event.KnownDataKeys.Error); + context.Event.Data.Remove(Event.KnownDataKeys.SimpleError); + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/20_ErrorPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/20_ErrorPlugin.cs index a672d6f67b..0d63f2d4db 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/20_ErrorPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/20_ErrorPlugin.cs @@ -1,54 +1,52 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Utility; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(20)] - public sealed class ErrorPlugin : EventProcessorPluginBase { - public ErrorPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) {} +namespace Exceptionless.Core.Plugins.EventProcessor; - public override Task EventProcessingAsync(EventContext context) { - if (!context.Event.IsError()) - return Task.CompletedTask; +[Priority(20)] +public sealed class ErrorPlugin : EventProcessorPluginBase { + public ErrorPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - var error = context.Event.GetError(); - if (error == null) - return Task.CompletedTask; + public override Task EventProcessingAsync(EventContext context) { + if (!context.Event.IsError()) + return Task.CompletedTask; + + var error = context.Event.GetError(); + if (error == null) + return Task.CompletedTask; - if (String.IsNullOrWhiteSpace(context.Event.Message)) - context.Event.Message = error.Message; + if (String.IsNullOrWhiteSpace(context.Event.Message)) + context.Event.Message = error.Message; - if (context.StackSignatureData.Count > 0) - return Task.CompletedTask; + if (context.StackSignatureData.Count > 0) + return Task.CompletedTask; - string[] commonUserMethods = { "DataContext.SubmitChanges", "Entities.SaveChanges" }; - if (context.HasProperty("CommonMethods")) - commonUserMethods = context.GetProperty("CommonMethods").SplitAndTrim(new [] { ',' }); + string[] commonUserMethods = { "DataContext.SubmitChanges", "Entities.SaveChanges" }; + if (context.HasProperty("CommonMethods")) + commonUserMethods = context.GetProperty("CommonMethods").SplitAndTrim(new[] { ',' }); - string[] userNamespaces = null; - if (context.HasProperty("UserNamespaces")) - userNamespaces = context.GetProperty("UserNamespaces").SplitAndTrim(new [] { ',' }); + string[] userNamespaces = null; + if (context.HasProperty("UserNamespaces")) + userNamespaces = context.GetProperty("UserNamespaces").SplitAndTrim(new[] { ',' }); - var signature = new ErrorSignature(error, userCommonMethods: commonUserMethods, userNamespaces: userNamespaces); - if (signature.SignatureInfo.Count <= 0) - return Task.CompletedTask; + var signature = new ErrorSignature(error, userCommonMethods: commonUserMethods, userNamespaces: userNamespaces); + if (signature.SignatureInfo.Count <= 0) + return Task.CompletedTask; - var targetInfo = new SettingsDictionary(signature.SignatureInfo); - var stackingTarget = error.GetStackingTarget(); - if (stackingTarget?.Error?.StackTrace?.Count > 0 && !targetInfo.ContainsKey("Message")) - targetInfo["Message"] = stackingTarget.Error.Message; + var targetInfo = new SettingsDictionary(signature.SignatureInfo); + var stackingTarget = error.GetStackingTarget(); + if (stackingTarget?.Error?.StackTrace?.Count > 0 && !targetInfo.ContainsKey("Message")) + targetInfo["Message"] = stackingTarget.Error.Message; - error.Data[Error.KnownDataKeys.TargetInfo] = targetInfo; + error.Data[Error.KnownDataKeys.TargetInfo] = targetInfo; - foreach (string key in signature.SignatureInfo.Keys) - context.StackSignatureData.Add(key, signature.SignatureInfo[key]); + foreach (string key in signature.SignatureInfo.Keys) + context.StackSignatureData.Add(key, signature.SignatureInfo[key]); - return Task.CompletedTask; - } + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/30_SimpleErrorPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/30_SimpleErrorPlugin.cs index c12e5c205b..25d1729be5 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/30_SimpleErrorPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/30_SimpleErrorPlugin.cs @@ -1,39 +1,37 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Pipeline; using Exceptionless.Core.Models.Data; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(30)] - public sealed class SimpleErrorPlugin : EventProcessorPluginBase { - public SimpleErrorPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) {} +namespace Exceptionless.Core.Plugins.EventProcessor; - public override Task EventProcessingAsync(EventContext context) { - if (!context.Event.IsError()) - return Task.CompletedTask; +[Priority(30)] +public sealed class SimpleErrorPlugin : EventProcessorPluginBase { + public SimpleErrorPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - var error = context.Event.GetSimpleError(); - if (error == null) - return Task.CompletedTask; + public override Task EventProcessingAsync(EventContext context) { + if (!context.Event.IsError()) + return Task.CompletedTask; - if (String.IsNullOrWhiteSpace(context.Event.Message)) - context.Event.Message = error.Message; + var error = context.Event.GetSimpleError(); + if (error == null) + return Task.CompletedTask; - if (context.StackSignatureData.Count > 0) - return Task.CompletedTask; + if (String.IsNullOrWhiteSpace(context.Event.Message)) + context.Event.Message = error.Message; - // TODO: Parse the stack trace and upgrade this to a full error. - if (!String.IsNullOrEmpty(error.Type)) - context.StackSignatureData.Add("ExceptionType", error.Type); + if (context.StackSignatureData.Count > 0) + return Task.CompletedTask; - if (!String.IsNullOrEmpty(error.StackTrace)) - context.StackSignatureData.Add("StackTrace", error.StackTrace.ToSHA1()); + // TODO: Parse the stack trace and upgrade this to a full error. + if (!String.IsNullOrEmpty(error.Type)) + context.StackSignatureData.Add("ExceptionType", error.Type); - error.Data[Error.KnownDataKeys.TargetInfo] = new SettingsDictionary(context.StackSignatureData); - return Task.CompletedTask; - } + if (!String.IsNullOrEmpty(error.StackTrace)) + context.StackSignatureData.Add("StackTrace", error.StackTrace.ToSHA1()); + + error.Data[Error.KnownDataKeys.TargetInfo] = new SettingsDictionary(context.StackSignatureData); + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/40_RequestInfoPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/40_RequestInfoPlugin.cs index 67a008d112..a218003a0a 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/40_RequestInfoPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/40_RequestInfoPlugin.cs @@ -1,19 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(40)] - public sealed class RequestInfoPlugin : EventProcessorPluginBase { - public const int MAX_VALUE_LENGTH = 1000; - public static readonly List DefaultExclusions = new List { +namespace Exceptionless.Core.Plugins.EventProcessor; + +[Priority(40)] +public sealed class RequestInfoPlugin : EventProcessorPluginBase { + public const int MAX_VALUE_LENGTH = 1000; + public static readonly List DefaultExclusions = new List { "*VIEWSTATE*", "*EVENTVALIDATION*", "*ASPX*", @@ -24,75 +21,75 @@ public sealed class RequestInfoPlugin : EventProcessorPluginBase { "ARRAffinity" }; - private readonly UserAgentParser _parser; - - public RequestInfoPlugin(UserAgentParser parser, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _parser = parser; - } + private readonly UserAgentParser _parser; - public override async Task EventBatchProcessingAsync(ICollection contexts) { - var project = contexts.First().Project; - var exclusions = DefaultExclusions.Union(project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.DataExclusions)).ToList(); - foreach (var context in contexts) { - var request = context.Event.GetRequestInfo(); - if (request == null) - continue; + public RequestInfoPlugin(UserAgentParser parser, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _parser = parser; + } - if (context.IncludePrivateInformation) { - var submissionClient = context.Event.GetSubmissionClient(); - AddClientIpAddress(request, submissionClient); - } else { - request.ClientIpAddress = null; - request.Cookies?.Clear(); - request.PostData = null; - request.QueryString?.Clear(); - } + public override async Task EventBatchProcessingAsync(ICollection contexts) { + var project = contexts.First().Project; + var exclusions = DefaultExclusions.Union(project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.DataExclusions)).ToList(); + foreach (var context in contexts) { + var request = context.Event.GetRequestInfo(); + if (request == null) + continue; - await SetBrowserOsAndDeviceFromUserAgent(request, context).AnyContext(); - context.Event.AddRequestInfo(request.ApplyDataExclusions(exclusions, MAX_VALUE_LENGTH)); + if (context.IncludePrivateInformation) { + var submissionClient = context.Event.GetSubmissionClient(); + AddClientIpAddress(request, submissionClient); + } + else { + request.ClientIpAddress = null; + request.Cookies?.Clear(); + request.PostData = null; + request.QueryString?.Clear(); } - } - private void AddClientIpAddress(RequestInfo request, SubmissionClient submissionClient) { - var ips = (request.ClientIpAddress ?? String.Empty) - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(ip => ip.Trim()) - .ToList(); + await SetBrowserOsAndDeviceFromUserAgent(request, context).AnyContext(); + context.Event.AddRequestInfo(request.ApplyDataExclusions(exclusions, MAX_VALUE_LENGTH)); + } + } - if (!String.IsNullOrEmpty(submissionClient?.IpAddress) && submissionClient.IsJavaScriptClient()) { - bool requestIpIsLocal = submissionClient.IpAddress.IsLocalHost(); - if (ips.Count == 0 || !requestIpIsLocal && ips.Count(ip => !ip.IsLocalHost()) == 0) - ips.Add(submissionClient.IpAddress); - } + private void AddClientIpAddress(RequestInfo request, SubmissionClient submissionClient) { + var ips = (request.ClientIpAddress ?? String.Empty) + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(ip => ip.Trim()) + .ToList(); - request.ClientIpAddress = ips.Distinct().ToDelimitedString(); + if (!String.IsNullOrEmpty(submissionClient?.IpAddress) && submissionClient.IsJavaScriptClient()) { + bool requestIpIsLocal = submissionClient.IpAddress.IsLocalHost(); + if (ips.Count == 0 || !requestIpIsLocal && ips.Count(ip => !ip.IsLocalHost()) == 0) + ips.Add(submissionClient.IpAddress); } - private async Task SetBrowserOsAndDeviceFromUserAgent(RequestInfo request, EventContext context) { - var info = await _parser.ParseAsync(request.UserAgent).AnyContext(); - if (info != null) { - if (!String.Equals(info.UA.Family, "Other")) { - request.Data[RequestInfo.KnownDataKeys.Browser] = info.UA.Family; - if (!String.IsNullOrEmpty(info.UA.Major)) { - request.Data[RequestInfo.KnownDataKeys.BrowserVersion] = String.Join(".", new[] { info.UA.Major, info.UA.Minor, info.UA.Patch }.Where(v => !String.IsNullOrEmpty(v))); - request.Data[RequestInfo.KnownDataKeys.BrowserMajorVersion] = info.UA.Major; - } + request.ClientIpAddress = ips.Distinct().ToDelimitedString(); + } + + private async Task SetBrowserOsAndDeviceFromUserAgent(RequestInfo request, EventContext context) { + var info = await _parser.ParseAsync(request.UserAgent).AnyContext(); + if (info != null) { + if (!String.Equals(info.UA.Family, "Other")) { + request.Data[RequestInfo.KnownDataKeys.Browser] = info.UA.Family; + if (!String.IsNullOrEmpty(info.UA.Major)) { + request.Data[RequestInfo.KnownDataKeys.BrowserVersion] = String.Join(".", new[] { info.UA.Major, info.UA.Minor, info.UA.Patch }.Where(v => !String.IsNullOrEmpty(v))); + request.Data[RequestInfo.KnownDataKeys.BrowserMajorVersion] = info.UA.Major; } + } - if (!String.Equals(info.Device.Family, "Other")) - request.Data[RequestInfo.KnownDataKeys.Device] = info.Device.Family; + if (!String.Equals(info.Device.Family, "Other")) + request.Data[RequestInfo.KnownDataKeys.Device] = info.Device.Family; - if (!String.Equals(info.OS.Family, "Other")) { - request.Data[RequestInfo.KnownDataKeys.OS] = info.OS.Family; - if (!String.IsNullOrEmpty(info.OS.Major)) { - request.Data[RequestInfo.KnownDataKeys.OSVersion] = String.Join(".", new[] { info.OS.Major, info.OS.Minor, info.OS.Patch }.Where(v => !String.IsNullOrEmpty(v))); - request.Data[RequestInfo.KnownDataKeys.OSMajorVersion] = info.OS.Major; - } + if (!String.Equals(info.OS.Family, "Other")) { + request.Data[RequestInfo.KnownDataKeys.OS] = info.OS.Family; + if (!String.IsNullOrEmpty(info.OS.Major)) { + request.Data[RequestInfo.KnownDataKeys.OSVersion] = String.Join(".", new[] { info.OS.Major, info.OS.Minor, info.OS.Patch }.Where(v => !String.IsNullOrEmpty(v))); + request.Data[RequestInfo.KnownDataKeys.OSMajorVersion] = info.OS.Major; } - - var botPatterns = context.Project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.UserAgentBotPatterns).ToList(); - request.Data[RequestInfo.KnownDataKeys.IsBot] = info.Device.IsSpider || request.UserAgent.AnyWildcardMatches(botPatterns); } + + var botPatterns = context.Project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.UserAgentBotPatterns).ToList(); + request.Data[RequestInfo.KnownDataKeys.IsBot] = info.Device.IsSpider || request.UserAgent.AnyWildcardMatches(botPatterns); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/45_EnvironmentInfoPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/45_EnvironmentInfoPlugin.cs index fdf782b369..8f13b6ae12 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/45_EnvironmentInfoPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/45_EnvironmentInfoPlugin.cs @@ -1,46 +1,44 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Pipeline; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor.Default { - [Priority(45)] - public sealed class EnvironmentInfoPlugin : EventProcessorPluginBase { - public EnvironmentInfoPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - - public override Task EventProcessingAsync(EventContext context) { - var environment = context.Event.GetEnvironmentInfo(); - if (environment == null) - return Task.CompletedTask; - - if (context.IncludePrivateInformation) { - var submissionClient = context.Event.GetSubmissionClient(); - AddClientIpAddress(environment, submissionClient); - } else { - environment.IpAddress = null; - environment.MachineName = null; - } - - context.Event.SetEnvironmentInfo(environment); +namespace Exceptionless.Core.Plugins.EventProcessor.Default; + +[Priority(45)] +public sealed class EnvironmentInfoPlugin : EventProcessorPluginBase { + public EnvironmentInfoPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } + + public override Task EventProcessingAsync(EventContext context) { + var environment = context.Event.GetEnvironmentInfo(); + if (environment == null) return Task.CompletedTask; + + if (context.IncludePrivateInformation) { + var submissionClient = context.Event.GetSubmissionClient(); + AddClientIpAddress(environment, submissionClient); + } + else { + environment.IpAddress = null; + environment.MachineName = null; } - private void AddClientIpAddress(EnvironmentInfo environment, SubmissionClient submissionClient) { - var ips = (environment.IpAddress ?? String.Empty) - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(ip => ip.Trim()) - .ToList(); + context.Event.SetEnvironmentInfo(environment); + return Task.CompletedTask; + } - if (!String.IsNullOrEmpty(submissionClient?.IpAddress) && !submissionClient.IsJavaScriptClient()) { - bool requestIpIsLocal = submissionClient.IpAddress.IsLocalHost(); - if (ips.Count == 0 || !requestIpIsLocal && ips.Count(ip => !ip.IsLocalHost()) == 0) - ips.Add(submissionClient.IpAddress); - } + private void AddClientIpAddress(EnvironmentInfo environment, SubmissionClient submissionClient) { + var ips = (environment.IpAddress ?? String.Empty) + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(ip => ip.Trim()) + .ToList(); - environment.IpAddress = ips.Distinct().ToDelimitedString(); + if (!String.IsNullOrEmpty(submissionClient?.IpAddress) && !submissionClient.IsJavaScriptClient()) { + bool requestIpIsLocal = submissionClient.IpAddress.IsLocalHost(); + if (ips.Count == 0 || !requestIpIsLocal && ips.Count(ip => !ip.IsLocalHost()) == 0) + ips.Add(submissionClient.IpAddress); } + + environment.IpAddress = ips.Distinct().ToDelimitedString(); } } diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/50_GeoPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/50_GeoPlugin.cs index bd3c435267..8a9d39c65e 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/50_GeoPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/50_GeoPlugin.cs @@ -1,84 +1,80 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Geo; using Exceptionless.Core.Pipeline; using Exceptionless.Core.Models; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor.Default { - [Priority(50)] - public sealed class GeoPlugin : EventProcessorPluginBase { - private readonly IGeoIpService _geoIpService; +namespace Exceptionless.Core.Plugins.EventProcessor.Default; - public GeoPlugin(IGeoIpService geoIpService, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _geoIpService = geoIpService; - } +[Priority(50)] +public sealed class GeoPlugin : EventProcessorPluginBase { + private readonly IGeoIpService _geoIpService; + + public GeoPlugin(IGeoIpService geoIpService, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _geoIpService = geoIpService; + } + + public override Task EventBatchProcessingAsync(ICollection contexts) { + var geoGroups = contexts.GroupBy(c => c.Event.Geo); - public override Task EventBatchProcessingAsync(ICollection contexts) { - var geoGroups = contexts.GroupBy(c => c.Event.Geo); - - var tasks = new List(); - foreach (var group in geoGroups) { - if (GeoResult.TryParse(group.Key, out var result) && result.IsValid()) { - group.ForEach(c => UpdateGeoAndLocation(c.Event, result, false)); - continue; - } - - // The geo coordinates are all the same, set the location from the result of any of the ip addresses. - if (!String.IsNullOrEmpty(group.Key)) { - var ips = group.SelectMany(c => c.Event.GetIpAddresses()).Union(new[] { group.First().EventPostInfo?.IpAddress }).Distinct().ToList(); - if (ips.Count > 0) - tasks.Add(UpdateGeoInformationAsync(group, ips)); - continue; - } - - // Each event in the group could be a different user; - foreach (var context in group) { - var ips = context.Event.GetIpAddresses().Union(new[] { context.EventPostInfo?.IpAddress }).ToList(); - if (ips.Count > 0) - tasks.Add(UpdateGeoInformationAsync(context, ips)); - } + var tasks = new List(); + foreach (var group in geoGroups) { + if (GeoResult.TryParse(group.Key, out var result) && result.IsValid()) { + group.ForEach(c => UpdateGeoAndLocation(c.Event, result, false)); + continue; } - return Task.WhenAll(tasks); + // The geo coordinates are all the same, set the location from the result of any of the ip addresses. + if (!String.IsNullOrEmpty(group.Key)) { + var ips = group.SelectMany(c => c.Event.GetIpAddresses()).Union(new[] { group.First().EventPostInfo?.IpAddress }).Distinct().ToList(); + if (ips.Count > 0) + tasks.Add(UpdateGeoInformationAsync(group, ips)); + continue; + } + + // Each event in the group could be a different user; + foreach (var context in group) { + var ips = context.Event.GetIpAddresses().Union(new[] { context.EventPostInfo?.IpAddress }).ToList(); + if (ips.Count > 0) + tasks.Add(UpdateGeoInformationAsync(context, ips)); + } } + return Task.WhenAll(tasks); + } - private async Task UpdateGeoInformationAsync(EventContext context, IEnumerable ips) { - var result = await GetGeoFromIpAddressesAsync(ips).AnyContext(); - UpdateGeoAndLocation(context.Event, result); - } - private async Task UpdateGeoInformationAsync(IEnumerable contexts, IEnumerable ips) { - var result = await GetGeoFromIpAddressesAsync(ips).AnyContext(); - contexts.ForEach(c => UpdateGeoAndLocation(c.Event, result)); - } + private async Task UpdateGeoInformationAsync(EventContext context, IEnumerable ips) { + var result = await GetGeoFromIpAddressesAsync(ips).AnyContext(); + UpdateGeoAndLocation(context.Event, result); + } - private void UpdateGeoAndLocation(PersistentEvent ev, GeoResult result, bool isValidLocation = true) { - ev.Geo = result?.ToString(); + private async Task UpdateGeoInformationAsync(IEnumerable contexts, IEnumerable ips) { + var result = await GetGeoFromIpAddressesAsync(ips).AnyContext(); + contexts.ForEach(c => UpdateGeoAndLocation(c.Event, result)); + } - if (result != null && isValidLocation) - ev.SetLocation(result.ToLocation()); - else - ev.Data.Remove(Event.KnownDataKeys.Location); - } + private void UpdateGeoAndLocation(PersistentEvent ev, GeoResult result, bool isValidLocation = true) { + ev.Geo = result?.ToString(); - private async Task GetGeoFromIpAddressesAsync(IEnumerable ips) { - foreach (string ip in ips) { - if (String.IsNullOrEmpty(ip)) - continue; + if (result != null && isValidLocation) + ev.SetLocation(result.ToLocation()); + else + ev.Data.Remove(Event.KnownDataKeys.Location); + } - var result = await _geoIpService.ResolveIpAsync(ip.ToAddress()).AnyContext(); - if (result == null || !result.IsValid()) - continue; + private async Task GetGeoFromIpAddressesAsync(IEnumerable ips) { + foreach (string ip in ips) { + if (String.IsNullOrEmpty(ip)) + continue; - return result; - } + var result = await _geoIpService.ResolveIpAsync(ip.ToAddress()).AnyContext(); + if (result == null || !result.IsValid()) + continue; - return null; + return result; } + + return null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs index bd4505c584..6de5f29fc0 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Models.WorkItems; @@ -12,44 +8,44 @@ using Foundatio.Queues; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor.Default { - [Priority(60)] - public sealed class LocationPlugin : EventProcessorPluginBase { - private readonly ICacheClient _cacheClient; - private readonly IQueue _workItemQueue; +namespace Exceptionless.Core.Plugins.EventProcessor.Default; - public LocationPlugin(ICacheClient cacheClient, IQueue workItemQueue, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _cacheClient = new ScopedCacheClient(cacheClient, "Geo"); - _workItemQueue = workItemQueue; - } +[Priority(60)] +public sealed class LocationPlugin : EventProcessorPluginBase { + private readonly ICacheClient _cacheClient; + private readonly IQueue _workItemQueue; - public override Task EventBatchProcessingAsync(ICollection contexts) { - var geoGroups = contexts.Where(c => c.Organization.HasPremiumFeatures && !String.IsNullOrEmpty(c.Event.Geo) && !c.Event.Data.ContainsKey(Event.KnownDataKeys.Location)).GroupBy(c => c.Event.Geo); + public LocationPlugin(ICacheClient cacheClient, IQueue workItemQueue, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _cacheClient = new ScopedCacheClient(cacheClient, "Geo"); + _workItemQueue = workItemQueue; + } + + public override Task EventBatchProcessingAsync(ICollection contexts) { + var geoGroups = contexts.Where(c => c.Organization.HasPremiumFeatures && !String.IsNullOrEmpty(c.Event.Geo) && !c.Event.Data.ContainsKey(Event.KnownDataKeys.Location)).GroupBy(c => c.Event.Geo); - var tasks = new List(); - foreach (var geoGroup in geoGroups) - tasks.Add(GetGeoLocationFromCacheAsync(geoGroup)); + var tasks = new List(); + foreach (var geoGroup in geoGroups) + tasks.Add(GetGeoLocationFromCacheAsync(geoGroup)); - return Task.WhenAll(tasks); - } + return Task.WhenAll(tasks); + } - public override Task EventBatchProcessedAsync(ICollection contexts) { - var contextsToProcess = contexts.Where(c => c.Organization.HasPremiumFeatures && !String.IsNullOrEmpty(c.Event.Geo) && !c.Event.Data.ContainsKey(Event.KnownDataKeys.Location)); + public override Task EventBatchProcessedAsync(ICollection contexts) { + var contextsToProcess = contexts.Where(c => c.Organization.HasPremiumFeatures && !String.IsNullOrEmpty(c.Event.Geo) && !c.Event.Data.ContainsKey(Event.KnownDataKeys.Location)); - var tasks = new List(); - foreach (var ctx in contextsToProcess) - tasks.Add(_workItemQueue.EnqueueAsync(new SetLocationFromGeoWorkItem { EventId = ctx.Event.Id, Geo = ctx.Event.Geo })); + var tasks = new List(); + foreach (var ctx in contextsToProcess) + tasks.Add(_workItemQueue.EnqueueAsync(new SetLocationFromGeoWorkItem { EventId = ctx.Event.Id, Geo = ctx.Event.Geo })); - return Task.WhenAll(tasks); - } + return Task.WhenAll(tasks); + } - private async Task GetGeoLocationFromCacheAsync(IGrouping geoGroup) { - var location = await _cacheClient.GetAsync(geoGroup.Key, null).AnyContext(); - if (location == null) - return; + private async Task GetGeoLocationFromCacheAsync(IGrouping geoGroup) { + var location = await _cacheClient.GetAsync(geoGroup.Key, null).AnyContext(); + if (location == null) + return; - await _cacheClient.SetExpirationAsync(geoGroup.Key, TimeSpan.FromDays(3)).AnyContext(); - geoGroup.ForEach(c => c.Event.Data[Event.KnownDataKeys.Location] = location); - } + await _cacheClient.SetExpirationAsync(geoGroup.Key, TimeSpan.FromDays(3)).AnyContext(); + geoGroup.ForEach(c => c.Event.Data[Event.KnownDataKeys.Location] = location); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/70_SessionPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/70_SessionPlugin.cs index 371256fd7e..92f8908698 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/70_SessionPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/70_SessionPlugin.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Pipeline; using Exceptionless.Core.Repositories; @@ -10,46 +6,118 @@ using Foundatio.Repositories.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor.Default { - [Priority(70)] - public sealed class SessionPlugin : EventProcessorPluginBase { - private static readonly TimeSpan _sessionTimeout = TimeSpan.FromMinutes(15); - private readonly ICacheClient _cache; - private readonly IEventRepository _eventRepository; - private readonly UpdateStatsAction _updateStats; - private readonly AssignToStackAction _assignToStack; - private readonly LocationPlugin _locationPlugin; - - public SessionPlugin(ICacheClient cacheClient, IEventRepository eventRepository, AssignToStackAction assignToStack, UpdateStatsAction updateStats, LocationPlugin locationPlugin, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { - _cache = new ScopedCacheClient(cacheClient, "session"); - _eventRepository = eventRepository; - _assignToStack = assignToStack; - _updateStats = updateStats; - _locationPlugin = locationPlugin; - } +namespace Exceptionless.Core.Plugins.EventProcessor.Default; + +[Priority(70)] +public sealed class SessionPlugin : EventProcessorPluginBase { + private static readonly TimeSpan _sessionTimeout = TimeSpan.FromMinutes(15); + private readonly ICacheClient _cache; + private readonly IEventRepository _eventRepository; + private readonly UpdateStatsAction _updateStats; + private readonly AssignToStackAction _assignToStack; + private readonly LocationPlugin _locationPlugin; + + public SessionPlugin(ICacheClient cacheClient, IEventRepository eventRepository, AssignToStackAction assignToStack, UpdateStatsAction updateStats, LocationPlugin locationPlugin, AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { + _cache = new ScopedCacheClient(cacheClient, "session"); + _eventRepository = eventRepository; + _assignToStack = assignToStack; + _updateStats = updateStats; + _locationPlugin = locationPlugin; + } + + public override Task EventBatchProcessingAsync(ICollection contexts) { + var autoSessionEvents = contexts.Where(c => !String.IsNullOrWhiteSpace(c.Event.GetUserIdentity()?.Identity) && String.IsNullOrEmpty(c.Event.GetSessionId())).ToList(); + var manualSessionsEvents = contexts.Where(c => !String.IsNullOrEmpty(c.Event.GetSessionId())).ToList(); - public override Task EventBatchProcessingAsync(ICollection contexts) { - var autoSessionEvents = contexts.Where(c => !String.IsNullOrWhiteSpace(c.Event.GetUserIdentity()?.Identity) && String.IsNullOrEmpty(c.Event.GetSessionId())).ToList(); - var manualSessionsEvents = contexts.Where(c => !String.IsNullOrEmpty(c.Event.GetSessionId())).ToList(); + return Task.WhenAll( + ProcessAutoSessionsAsync(autoSessionEvents), + ProcessManualSessionsAsync(manualSessionsEvents) + ); + } + + private async Task ProcessManualSessionsAsync(ICollection contexts) { + var sessionIdGroups = contexts + .OrderBy(c => c.Event.Date) + .GroupBy(c => c.Event.GetSessionId()); + + foreach (var session in sessionIdGroups) { + string projectId = session.First().Project.Id; + + var firstSessionEvent = session.First(); + var lastSessionEvent = session.Last(); + + // cancel duplicate start events (1 per session id) + session.Where(ev => ev.Event.IsSessionStart()).Skip(1).ForEach(ev => { + _logger.LogInformation("Discarding duplicate session start events."); + ev.IsCancelled = true; + }); + var sessionStartEvent = session.FirstOrDefault(ev => ev.Event.IsSessionStart()); + + // sync the session start event with the first session event. + if (sessionStartEvent != null) + sessionStartEvent.Event.Date = firstSessionEvent.Event.Date; + + // cancel duplicate end events (1 per session id) + session.Where(ev => ev.Event.IsSessionEnd()).Skip(1).ForEach(ev => { + _logger.LogInformation("Discarding duplicate session end events."); + ev.IsCancelled = true; + }); + var sessionEndEvent = session.FirstOrDefault(ev => ev.Event.IsSessionEnd()); + + // sync the session end event with the last session event. + if (sessionEndEvent != null) + sessionEndEvent.Event.Date = lastSessionEvent.Event.Date; + + // discard the heartbeat events. + session.Where(ev => ev.Event.IsSessionHeartbeat()).ForEach(ctx => { + ctx.IsDiscarded = true; + ctx.IsCancelled = true; + }); + + // try to update an existing session + string sessionStartEventId = await UpdateSessionStartEventAsync(projectId, session.Key, lastSessionEvent.Event.Date.UtcDateTime, sessionEndEvent != null).AnyContext(); + + // do we already have a session start for this session id? + if (!String.IsNullOrEmpty(sessionStartEventId) && sessionStartEvent != null) { + _logger.LogInformation("Discarding duplicate session start event for session: {SessionStartEventId}", sessionStartEventId); + sessionStartEvent.IsCancelled = true; + } + else if (String.IsNullOrEmpty(sessionStartEventId) && sessionStartEvent != null) { + // no existing session, session start is in the batch + sessionStartEvent.Event.UpdateSessionStart(lastSessionEvent.Event.Date.UtcDateTime, sessionEndEvent != null); + sessionStartEvent.SetProperty("SetSessionStartEventId", true); + } + else if (String.IsNullOrEmpty(sessionStartEventId)) { + // no session start event found and none in the batch + + // if session end, without any session events, cancel + if (session.Count(s => !s.IsCancelled) == 1 && firstSessionEvent.Event.IsSessionEnd()) { + _logger.LogInformation("Discarding session end event with no session events."); + firstSessionEvent.IsCancelled = true; + continue; + } - return Task.WhenAll( - ProcessAutoSessionsAsync(autoSessionEvents), - ProcessManualSessionsAsync(manualSessionsEvents) - ); + // create a new session start event + await CreateSessionStartEventAsync(firstSessionEvent, lastSessionEvent.Event.Date.UtcDateTime, sessionEndEvent != null).AnyContext(); + } } + } - private async Task ProcessManualSessionsAsync(ICollection contexts) { - var sessionIdGroups = contexts - .OrderBy(c => c.Event.Date) - .GroupBy(c => c.Event.GetSessionId()); + private async Task ProcessAutoSessionsAsync(ICollection contexts) { + var identityGroups = contexts + .OrderBy(c => c.Event.Date) + .GroupBy(c => c.Event.GetUserIdentity().Identity); - foreach (var session in sessionIdGroups) { - string projectId = session.First().Project.Id; + foreach (var identityGroup in identityGroups) { + string projectId = identityGroup.First().Project.Id; + // group events into sessions (split by session ends) + foreach (var session in CreateSessionGroups(identityGroup)) { + bool isNewSession = false; var firstSessionEvent = session.First(); var lastSessionEvent = session.Last(); - // cancel duplicate start events (1 per session id) + // cancel duplicate start events session.Where(ev => ev.Event.IsSessionStart()).Skip(1).ForEach(ev => { _logger.LogInformation("Discarding duplicate session start events."); ev.IsCancelled = true; @@ -60,213 +128,145 @@ private async Task ProcessManualSessionsAsync(ICollection contexts if (sessionStartEvent != null) sessionStartEvent.Event.Date = firstSessionEvent.Event.Date; - // cancel duplicate end events (1 per session id) - session.Where(ev => ev.Event.IsSessionEnd()).Skip(1).ForEach(ev => { - _logger.LogInformation("Discarding duplicate session end events."); - ev.IsCancelled = true; - }); - var sessionEndEvent = session.FirstOrDefault(ev => ev.Event.IsSessionEnd()); - - // sync the session end event with the last session event. - if (sessionEndEvent != null) - sessionEndEvent.Event.Date = lastSessionEvent.Event.Date; - // discard the heartbeat events. session.Where(ev => ev.Event.IsSessionHeartbeat()).ForEach(ctx => { ctx.IsDiscarded = true; ctx.IsCancelled = true; }); - // try to update an existing session - string sessionStartEventId = await UpdateSessionStartEventAsync(projectId, session.Key, lastSessionEvent.Event.Date.UtcDateTime, sessionEndEvent != null).AnyContext(); - - // do we already have a session start for this session id? - if (!String.IsNullOrEmpty(sessionStartEventId) && sessionStartEvent != null) { - _logger.LogInformation("Discarding duplicate session start event for session: {SessionStartEventId}", sessionStartEventId); - sessionStartEvent.IsCancelled = true; - } else if (String.IsNullOrEmpty(sessionStartEventId) && sessionStartEvent != null) { - // no existing session, session start is in the batch - sessionStartEvent.Event.UpdateSessionStart(lastSessionEvent.Event.Date.UtcDateTime, sessionEndEvent != null); - sessionStartEvent.SetProperty("SetSessionStartEventId", true); - } else if (String.IsNullOrEmpty(sessionStartEventId)) { - // no session start event found and none in the batch - - // if session end, without any session events, cancel - if (session.Count(s => !s.IsCancelled) == 1 && firstSessionEvent.Event.IsSessionEnd()) { - _logger.LogInformation("Discarding session end event with no session events."); - firstSessionEvent.IsCancelled = true; - continue; - } + string sessionId = await GetIdentitySessionIdAsync(projectId, identityGroup.Key).AnyContext(); - // create a new session start event - await CreateSessionStartEventAsync(firstSessionEvent, lastSessionEvent.Event.Date.UtcDateTime, sessionEndEvent != null).AnyContext(); + // if session end, without any session events, cancel + if (String.IsNullOrEmpty(sessionId) && session.Count == 1 && firstSessionEvent.Event.IsSessionEnd()) { + _logger.LogInformation("Discarding session end event with no session events."); + firstSessionEvent.IsCancelled = true; + continue; } - } - } - private async Task ProcessAutoSessionsAsync(ICollection contexts) { - var identityGroups = contexts - .OrderBy(c => c.Event.Date) - .GroupBy(c => c.Event.GetUserIdentity().Identity); - - foreach (var identityGroup in identityGroups) { - string projectId = identityGroup.First().Project.Id; - - // group events into sessions (split by session ends) - foreach (var session in CreateSessionGroups(identityGroup)) { - bool isNewSession = false; - var firstSessionEvent = session.First(); - var lastSessionEvent = session.Last(); - - // cancel duplicate start events - session.Where(ev => ev.Event.IsSessionStart()).Skip(1).ForEach(ev => { - _logger.LogInformation("Discarding duplicate session start events."); - ev.IsCancelled = true; - }); - var sessionStartEvent = session.FirstOrDefault(ev => ev.Event.IsSessionStart()); - - // sync the session start event with the first session event. - if (sessionStartEvent != null) - sessionStartEvent.Event.Date = firstSessionEvent.Event.Date; - - // discard the heartbeat events. - session.Where(ev => ev.Event.IsSessionHeartbeat()).ForEach(ctx => { - ctx.IsDiscarded = true; - ctx.IsCancelled = true; - }); - - string sessionId = await GetIdentitySessionIdAsync(projectId, identityGroup.Key).AnyContext(); - - // if session end, without any session events, cancel - if (String.IsNullOrEmpty(sessionId) && session.Count == 1 && firstSessionEvent.Event.IsSessionEnd()) { - _logger.LogInformation("Discarding session end event with no session events."); - firstSessionEvent.IsCancelled = true; - continue; - } + // no existing session, create a new one + if (String.IsNullOrEmpty(sessionId)) { + sessionId = ObjectId.GenerateNewId(firstSessionEvent.Event.Date.DateTime).ToString(); + isNewSession = true; + } + + session.ForEach(s => s.Event.SetSessionId(sessionId)); - // no existing session, create a new one - if (String.IsNullOrEmpty(sessionId)) { - sessionId = ObjectId.GenerateNewId(firstSessionEvent.Event.Date.DateTime).ToString(); - isNewSession = true; + if (isNewSession) { + if (sessionStartEvent != null) { + sessionStartEvent.Event.UpdateSessionStart(lastSessionEvent.Event.Date.UtcDateTime, lastSessionEvent.Event.IsSessionEnd()); + sessionStartEvent.SetProperty("SetSessionStartEventId", true); + } + else { + await CreateSessionStartEventAsync(firstSessionEvent, lastSessionEvent.Event.Date.UtcDateTime, lastSessionEvent.Event.IsSessionEnd()).AnyContext(); } - session.ForEach(s => s.Event.SetSessionId(sessionId)); - - if (isNewSession) { - if (sessionStartEvent != null) { - sessionStartEvent.Event.UpdateSessionStart(lastSessionEvent.Event.Date.UtcDateTime, lastSessionEvent.Event.IsSessionEnd()); - sessionStartEvent.SetProperty("SetSessionStartEventId", true); - } else { - await CreateSessionStartEventAsync(firstSessionEvent, lastSessionEvent.Event.Date.UtcDateTime, lastSessionEvent.Event.IsSessionEnd()).AnyContext(); - } - - if (!lastSessionEvent.Event.IsSessionEnd()) - await SetIdentitySessionIdAsync(projectId, identityGroup.Key, sessionId).AnyContext(); - } else { - // we already have a session start, cancel this one - if (sessionStartEvent != null) { - _logger.LogInformation("Discarding duplicate session start event."); - sessionStartEvent.IsCancelled = true; - } - - await UpdateSessionStartEventAsync(projectId, sessionId, lastSessionEvent.Event.Date.UtcDateTime, lastSessionEvent.Event.IsSessionEnd()).AnyContext(); + if (!lastSessionEvent.Event.IsSessionEnd()) + await SetIdentitySessionIdAsync(projectId, identityGroup.Key, sessionId).AnyContext(); + } + else { + // we already have a session start, cancel this one + if (sessionStartEvent != null) { + _logger.LogInformation("Discarding duplicate session start event."); + sessionStartEvent.IsCancelled = true; } + + await UpdateSessionStartEventAsync(projectId, sessionId, lastSessionEvent.Event.Date.UtcDateTime, lastSessionEvent.Event.IsSessionEnd()).AnyContext(); } } } + } - public override Task EventProcessedAsync(EventContext context) { - if (context.GetProperty("SetSessionStartEventId") != null) - return SetSessionStartEventIdAsync(context.Project.Id, context.Event.GetSessionId(), context.Event.Id); + public override Task EventProcessedAsync(EventContext context) { + if (context.GetProperty("SetSessionStartEventId") != null) + return SetSessionStartEventIdAsync(context.Project.Id, context.Event.GetSessionId(), context.Event.Id); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private static List> CreateSessionGroups(IGrouping identityGroup) { - var sessions = new List>(); - var currentSession = new List(); - sessions.Add(currentSession); - foreach (var context in identityGroup) { - currentSession.Add(context); - - // start new session, after session end - if (context.Event.IsSessionEnd()) { - currentSession = new List(); - sessions.Add(currentSession); - } + private static List> CreateSessionGroups(IGrouping identityGroup) { + var sessions = new List>(); + var currentSession = new List(); + sessions.Add(currentSession); + foreach (var context in identityGroup) { + currentSession.Add(context); + + // start new session, after session end + if (context.Event.IsSessionEnd()) { + currentSession = new List(); + sessions.Add(currentSession); } - - // remove empty sessions - sessions = sessions.Where(s => s.Count > 0).ToList(); - return sessions; } - private string GetSessionStartEventIdCacheKey(string projectId, string sessionId) { - return String.Concat(projectId, ":start:", sessionId); - } + // remove empty sessions + sessions = sessions.Where(s => s.Count > 0).ToList(); + return sessions; + } - private async Task GetSessionStartEventIdAsync(string projectId, string sessionId) { - string cacheKey = GetSessionStartEventIdCacheKey(projectId, sessionId); - string eventId = await _cache.GetAsync(cacheKey, null).AnyContext(); - if (!String.IsNullOrEmpty(eventId)) - await _cache.SetExpirationAsync(cacheKey, TimeSpan.FromDays(1)).AnyContext(); + private string GetSessionStartEventIdCacheKey(string projectId, string sessionId) { + return String.Concat(projectId, ":start:", sessionId); + } - return eventId; - } + private async Task GetSessionStartEventIdAsync(string projectId, string sessionId) { + string cacheKey = GetSessionStartEventIdCacheKey(projectId, sessionId); + string eventId = await _cache.GetAsync(cacheKey, null).AnyContext(); + if (!String.IsNullOrEmpty(eventId)) + await _cache.SetExpirationAsync(cacheKey, TimeSpan.FromDays(1)).AnyContext(); - private Task SetSessionStartEventIdAsync(string projectId, string sessionId, string eventId) { - return _cache.SetAsync(GetSessionStartEventIdCacheKey(projectId, sessionId), eventId, TimeSpan.FromDays(1)); - } + return eventId; + } - private string GetIdentitySessionIdCacheKey(string projectId, string identity) { - return String.Concat(projectId, ":identity:", identity.ToSHA1()); - } + private Task SetSessionStartEventIdAsync(string projectId, string sessionId, string eventId) { + return _cache.SetAsync(GetSessionStartEventIdCacheKey(projectId, sessionId), eventId, TimeSpan.FromDays(1)); + } - private async Task GetIdentitySessionIdAsync(string projectId, string identity) { - string cacheKey = GetIdentitySessionIdCacheKey(projectId, identity); - string sessionId = await _cache.GetAsync(cacheKey, null).AnyContext(); - if (!String.IsNullOrEmpty(sessionId)) { - await Task.WhenAll( - _cache.SetExpirationAsync(cacheKey, _sessionTimeout), - _cache.SetExpirationAsync(GetSessionStartEventIdCacheKey(projectId, sessionId), TimeSpan.FromDays(1)) - ).AnyContext(); - } + private string GetIdentitySessionIdCacheKey(string projectId, string identity) { + return String.Concat(projectId, ":identity:", identity.ToSHA1()); + } - return sessionId; + private async Task GetIdentitySessionIdAsync(string projectId, string identity) { + string cacheKey = GetIdentitySessionIdCacheKey(projectId, identity); + string sessionId = await _cache.GetAsync(cacheKey, null).AnyContext(); + if (!String.IsNullOrEmpty(sessionId)) { + await Task.WhenAll( + _cache.SetExpirationAsync(cacheKey, _sessionTimeout), + _cache.SetExpirationAsync(GetSessionStartEventIdCacheKey(projectId, sessionId), TimeSpan.FromDays(1)) + ).AnyContext(); } - private Task SetIdentitySessionIdAsync(string projectId, string identity, string sessionId) { - return _cache.SetAsync(GetIdentitySessionIdCacheKey(projectId, identity), sessionId, _sessionTimeout); - } + return sessionId; + } + + private Task SetIdentitySessionIdAsync(string projectId, string identity, string sessionId) { + return _cache.SetAsync(GetIdentitySessionIdCacheKey(projectId, identity), sessionId, _sessionTimeout); + } - private async Task CreateSessionStartEventAsync(EventContext startContext, DateTime? lastActivityUtc, bool? isSessionEnd) { - var startEvent = startContext.Event.ToSessionStartEvent(lastActivityUtc, isSessionEnd, startContext.Organization.HasPremiumFeatures, startContext.IncludePrivateInformation); - var startEventContexts = new List { + private async Task CreateSessionStartEventAsync(EventContext startContext, DateTime? lastActivityUtc, bool? isSessionEnd) { + var startEvent = startContext.Event.ToSessionStartEvent(lastActivityUtc, isSessionEnd, startContext.Organization.HasPremiumFeatures, startContext.IncludePrivateInformation); + var startEventContexts = new List { new EventContext(startEvent, startContext.Organization, startContext.Project) }; - if (_assignToStack.Enabled) - await _assignToStack.ProcessBatchAsync(startEventContexts).AnyContext(); - if (_updateStats.Enabled) - await _updateStats.ProcessBatchAsync(startEventContexts).AnyContext(); - await _eventRepository.AddAsync(startEvent).AnyContext(); - if (_locationPlugin.Enabled) - await _locationPlugin.EventBatchProcessedAsync(startEventContexts).AnyContext(); + if (_assignToStack.Enabled) + await _assignToStack.ProcessBatchAsync(startEventContexts).AnyContext(); + if (_updateStats.Enabled) + await _updateStats.ProcessBatchAsync(startEventContexts).AnyContext(); + await _eventRepository.AddAsync(startEvent).AnyContext(); + if (_locationPlugin.Enabled) + await _locationPlugin.EventBatchProcessedAsync(startEventContexts).AnyContext(); - await SetSessionStartEventIdAsync(startContext.Project.Id, startContext.Event.GetSessionId(), startEvent.Id).AnyContext(); - return startEvent; - } + await SetSessionStartEventIdAsync(startContext.Project.Id, startContext.Event.GetSessionId(), startEvent.Id).AnyContext(); + return startEvent; + } - private async Task UpdateSessionStartEventAsync(string projectId, string sessionId, DateTime lastActivityUtc, bool isSessionEnd = false, bool hasError = false) { - string sessionStartEventId = await GetSessionStartEventIdAsync(projectId, sessionId).AnyContext(); - if (!String.IsNullOrEmpty(sessionStartEventId)) { - await _eventRepository.UpdateSessionStartLastActivityAsync(sessionStartEventId, lastActivityUtc, isSessionEnd, hasError).AnyContext(); + private async Task UpdateSessionStartEventAsync(string projectId, string sessionId, DateTime lastActivityUtc, bool isSessionEnd = false, bool hasError = false) { + string sessionStartEventId = await GetSessionStartEventIdAsync(projectId, sessionId).AnyContext(); + if (!String.IsNullOrEmpty(sessionStartEventId)) { + await _eventRepository.UpdateSessionStartLastActivityAsync(sessionStartEventId, lastActivityUtc, isSessionEnd, hasError).AnyContext(); - if (isSessionEnd) - await _cache.RemoveAsync(GetSessionStartEventIdCacheKey(projectId, sessionId)).AnyContext(); - } - - return sessionStartEventId; + if (isSessionEnd) + await _cache.RemoveAsync(GetSessionStartEventIdCacheKey(projectId, sessionId)).AnyContext(); } + + return sessionStartEventId; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/80_AngularPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/80_AngularPlugin.cs index 73b6fb1acd..89591d87c9 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/80_AngularPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/80_AngularPlugin.cs @@ -1,43 +1,41 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Pipeline; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - [Priority(80)] - public sealed class AngularPlugin : EventProcessorPluginBase { - public AngularPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventProcessor; - public override Task EventProcessingAsync(EventContext context) { - if (!context.Event.IsError()) - return Task.CompletedTask; +[Priority(80)] +public sealed class AngularPlugin : EventProcessorPluginBase { + public AngularPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - var error = context.Event.GetError(); - if (error == null) - return Task.CompletedTask; + public override Task EventProcessingAsync(EventContext context) { + if (!context.Event.IsError()) + return Task.CompletedTask; - string submissionMethod = context.Event.GetSubmissionMethod(); - if (submissionMethod == null || !String.Equals("$exceptionHandler", submissionMethod)) - return Task.CompletedTask; + var error = context.Event.GetError(); + if (error == null) + return Task.CompletedTask; - if (context.StackSignatureData.Count != 1 || !context.StackSignatureData.ContainsKey("NoStackingInformation")) - return Task.CompletedTask; + string submissionMethod = context.Event.GetSubmissionMethod(); + if (submissionMethod == null || !String.Equals("$exceptionHandler", submissionMethod)) + return Task.CompletedTask; - string cause = context.Event.Message; - if (String.IsNullOrEmpty(cause)) - return Task.CompletedTask; + if (context.StackSignatureData.Count != 1 || !context.StackSignatureData.ContainsKey("NoStackingInformation")) + return Task.CompletedTask; - if (cause.StartsWith("Possibly unhandled rejection")) { - context.StackSignatureData.Remove("NoStackingInformation"); - context.StackSignatureData.Add("ExceptionType", error.Type ?? "Error"); - context.StackSignatureData.Add("Source", "unhandledRejection"); + string cause = context.Event.Message; + if (String.IsNullOrEmpty(cause)) + return Task.CompletedTask; - error.Data[Error.KnownDataKeys.TargetInfo] = new SettingsDictionary(context.StackSignatureData); - } + if (cause.StartsWith("Possibly unhandled rejection")) { + context.StackSignatureData.Remove("NoStackingInformation"); + context.StackSignatureData.Add("ExceptionType", error.Type ?? "Error"); + context.StackSignatureData.Add("Source", "unhandledRejection"); - return Task.CompletedTask; + error.Data[Error.KnownDataKeys.TargetInfo] = new SettingsDictionary(context.StackSignatureData); } + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/90_RemovePrivateInformationPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/90_RemovePrivateInformationPlugin.cs index e0bc10c0a4..618b5ee162 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/90_RemovePrivateInformationPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/90_RemovePrivateInformationPlugin.cs @@ -1,30 +1,28 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Exceptionless.Core.Pipeline; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor.Default { - [Priority(90)] - public sealed class RemovePrivateInformationPlugin : EventProcessorPluginBase { - public RemovePrivateInformationPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventProcessor.Default; - public override Task EventProcessingAsync(EventContext context) { - if (context.IncludePrivateInformation) - return Task.CompletedTask; +[Priority(90)] +public sealed class RemovePrivateInformationPlugin : EventProcessorPluginBase { + public RemovePrivateInformationPlugin(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - context.Event.Data.Remove(Event.KnownDataKeys.UserInfo); - var description = context.Event.GetUserDescription(); + public override Task EventProcessingAsync(EventContext context) { + if (context.IncludePrivateInformation) + return Task.CompletedTask; - if (description != null) { - description.EmailAddress = null; - if (!String.IsNullOrEmpty(description.Description)) - context.Event.SetUserDescription(description); - else - context.Event.Data.Remove(Event.KnownDataKeys.UserDescription); - } + context.Event.Data.Remove(Event.KnownDataKeys.UserInfo); + var description = context.Event.GetUserDescription(); - return Task.CompletedTask; + if (description != null) { + description.EmailAddress = null; + if (!String.IsNullOrEmpty(description.Description)) + context.Event.SetUserDescription(description); + else + context.Event.Data.Remove(Event.KnownDataKeys.UserDescription); } + + return Task.CompletedTask; } } diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/EventContext.cs b/src/Exceptionless.Core/Plugins/EventProcessor/EventContext.cs index 548de01c0c..17d3ed0db7 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/EventContext.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/EventContext.cs @@ -1,46 +1,44 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Utility; using Exceptionless.Core.Models; using Exceptionless.Core.Queues.Models; -namespace Exceptionless.Core.Plugins.EventProcessor { - public class EventContext : ExtensibleObject, IPipelineContext { - public EventContext(PersistentEvent ev, Organization organization, Project project, EventPostInfo epi = null) { - Organization = organization; - Project = project; - Event = ev; - Event.OrganizationId = organization.Id; - Event.ProjectId = project.Id; - IncludePrivateInformation = project.Configuration.Settings.GetBoolean(SettingsDictionary.KnownKeys.IncludePrivateInformation, true); - EventPostInfo = epi; - StackSignatureData = new Dictionary(); - } +namespace Exceptionless.Core.Plugins.EventProcessor; - public PersistentEvent Event { get; set; } - public EventPostInfo EventPostInfo { get; set; } - public Stack Stack { get; set; } - public Project Project { get; set; } - public Organization Organization { get; set; } - public bool IsDiscarded { get; set; } - public bool IsNew { get; set; } - public bool IsRegression { get; set; } - public bool IncludePrivateInformation { get; set; } - public string SignatureHash { get; set; } - public IDictionary StackSignatureData { get; private set; } +public class EventContext : ExtensibleObject, IPipelineContext { + public EventContext(PersistentEvent ev, Organization organization, Project project, EventPostInfo epi = null) { + Organization = organization; + Project = project; + Event = ev; + Event.OrganizationId = organization.Id; + Event.ProjectId = project.Id; + IncludePrivateInformation = project.Configuration.Settings.GetBoolean(SettingsDictionary.KnownKeys.IncludePrivateInformation, true); + EventPostInfo = epi; + StackSignatureData = new Dictionary(); + } - public bool IsCancelled { get; set; } - public bool IsProcessed { get; set; } + public PersistentEvent Event { get; set; } + public EventPostInfo EventPostInfo { get; set; } + public Stack Stack { get; set; } + public Project Project { get; set; } + public Organization Organization { get; set; } + public bool IsDiscarded { get; set; } + public bool IsNew { get; set; } + public bool IsRegression { get; set; } + public bool IncludePrivateInformation { get; set; } + public string SignatureHash { get; set; } + public IDictionary StackSignatureData { get; private set; } - public bool HasError => ErrorMessage != null || Exception != null; + public bool IsCancelled { get; set; } + public bool IsProcessed { get; set; } - public void SetError(string message, Exception ex = null) { - ErrorMessage = message; - Exception = ex; - } + public bool HasError => ErrorMessage != null || Exception != null; - public string ErrorMessage { get; private set; } - public Exception Exception { get; private set; } + public void SetError(string message, Exception ex = null) { + ErrorMessage = message; + Exception = ex; } -} \ No newline at end of file + + public string ErrorMessage { get; private set; } + public Exception Exception { get; private set; } +} diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/EventPluginManager.cs b/src/Exceptionless.Core/Plugins/EventProcessor/EventPluginManager.cs index 581652ba83..0de2e48d9d 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/EventPluginManager.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/EventPluginManager.cs @@ -1,69 +1,68 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - public class EventPluginManager : PluginManagerBase { - public EventPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventProcessor; - /// - /// Runs all of the event plugins startup method. - /// - public async Task StartupAsync() { - string metricPrefix = String.Concat(_metricPrefix, nameof(StartupAsync).ToLower(), "."); - foreach (var plugin in Plugins.Values.ToList()) { - try { - string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); - await _metricsClient.TimeAsync(() => plugin.StartupAsync(), metricName).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error calling startup in plugin {PluginName}: {Message}", plugin.Name, ex.Message); - } +public class EventPluginManager : PluginManagerBase { + public EventPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } + + /// + /// Runs all of the event plugins startup method. + /// + public async Task StartupAsync() { + string metricPrefix = String.Concat(_metricPrefix, nameof(StartupAsync).ToLower(), "."); + foreach (var plugin in Plugins.Values.ToList()) { + try { + string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); + await _metricsClient.TimeAsync(() => plugin.StartupAsync(), metricName).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling startup in plugin {PluginName}: {Message}", plugin.Name, ex.Message); } } + } - /// - /// Runs all of the event plugins event processing method. - /// - public async Task EventBatchProcessingAsync(ICollection contexts) { - string metricPrefix = String.Concat(_metricPrefix, nameof(EventBatchProcessingAsync).ToLower(), "."); - foreach (var plugin in Plugins.Values) { - var contextsToProcess = contexts.Where(c => c.IsCancelled == false && !c.HasError).ToList(); - if (contextsToProcess.Count == 0) - break; + /// + /// Runs all of the event plugins event processing method. + /// + public async Task EventBatchProcessingAsync(ICollection contexts) { + string metricPrefix = String.Concat(_metricPrefix, nameof(EventBatchProcessingAsync).ToLower(), "."); + foreach (var plugin in Plugins.Values) { + var contextsToProcess = contexts.Where(c => c.IsCancelled == false && !c.HasError).ToList(); + if (contextsToProcess.Count == 0) + break; - string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); - try { - await _metricsClient.TimeAsync(() => plugin.EventBatchProcessingAsync(contextsToProcess), metricName).AnyContext(); - if (contextsToProcess.All(c => c.IsCancelled || c.HasError)) - break; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling event processing in plugin {PluginName}: {Message}", plugin.Name, ex.Message); - } + string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); + try { + await _metricsClient.TimeAsync(() => plugin.EventBatchProcessingAsync(contextsToProcess), metricName).AnyContext(); + if (contextsToProcess.All(c => c.IsCancelled || c.HasError)) + break; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling event processing in plugin {PluginName}: {Message}", plugin.Name, ex.Message); } } + } - /// - /// Runs all of the event plugins event processed method. - /// - public async Task EventBatchProcessedAsync(ICollection contexts) { - string metricPrefix = String.Concat(_metricPrefix, nameof(EventBatchProcessedAsync).ToLower(), "."); - foreach (var plugin in Plugins.Values) { - var contextsToProcess = contexts.Where(c => c.IsCancelled == false && !c.HasError).ToList(); - if (contextsToProcess.Count == 0) - break; + /// + /// Runs all of the event plugins event processed method. + /// + public async Task EventBatchProcessedAsync(ICollection contexts) { + string metricPrefix = String.Concat(_metricPrefix, nameof(EventBatchProcessedAsync).ToLower(), "."); + foreach (var plugin in Plugins.Values) { + var contextsToProcess = contexts.Where(c => c.IsCancelled == false && !c.HasError).ToList(); + if (contextsToProcess.Count == 0) + break; - string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); - try { - await _metricsClient.TimeAsync(() => plugin.EventBatchProcessedAsync(contextsToProcess), metricName).AnyContext(); - if (contextsToProcess.All(c => c.IsCancelled || c.HasError)) - break; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling event processed in plugin {PluginName}: {Message}", plugin.Name, ex.Message); - } + string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); + try { + await _metricsClient.TimeAsync(() => plugin.EventBatchProcessedAsync(contextsToProcess), metricName).AnyContext(); + if (contextsToProcess.All(c => c.IsCancelled || c.HasError)) + break; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling event processed in plugin {PluginName}: {Message}", plugin.Name, ex.Message); } } } diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/EventProcessorPluginBase.cs b/src/Exceptionless.Core/Plugins/EventProcessor/EventProcessorPluginBase.cs index 0a4c613180..1bc4a90c1c 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/EventProcessorPluginBase.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/EventProcessorPluginBase.cs @@ -1,61 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventProcessor { - public abstract class EventProcessorPluginBase : PluginBase, IEventProcessorPlugin { - public EventProcessorPluginBase(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventProcessor; - protected bool ContinueOnError { get; set; } +public abstract class EventProcessorPluginBase : PluginBase, IEventProcessorPlugin { + public EventProcessorPluginBase(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - public virtual Task StartupAsync() { - return Task.CompletedTask; - } + protected bool ContinueOnError { get; set; } + + public virtual Task StartupAsync() { + return Task.CompletedTask; + } - public virtual async Task EventBatchProcessingAsync(ICollection contexts) { - foreach (var ctx in contexts) { + public virtual async Task EventBatchProcessingAsync(ICollection contexts) { + foreach (var ctx in contexts) { + try { + await EventProcessingAsync(ctx).AnyContext(); + } + catch (Exception ex) { + bool cont = false; try { - await EventProcessingAsync(ctx).AnyContext(); - } catch (Exception ex) { - bool cont = false; - try { - cont = HandleError(ex, ctx); - } catch { } - - if (!cont) - ctx.SetError(ex.Message, ex); + cont = HandleError(ex, ctx); } + catch { } + + if (!cont) + ctx.SetError(ex.Message, ex); } } + } - public virtual Task EventProcessingAsync(EventContext context) { - return Task.CompletedTask; - } + public virtual Task EventProcessingAsync(EventContext context) { + return Task.CompletedTask; + } - public virtual async Task EventBatchProcessedAsync(ICollection contexts) { - foreach (var ctx in contexts) { + public virtual async Task EventBatchProcessedAsync(ICollection contexts) { + foreach (var ctx in contexts) { + try { + await EventProcessedAsync(ctx).AnyContext(); + } + catch (Exception ex) { + bool cont = false; try { - await EventProcessedAsync(ctx).AnyContext(); - } catch (Exception ex) { - bool cont = false; - try { - cont = HandleError(ex, ctx); - } catch { } - - if (!cont) - ctx.SetError(ex.Message, ex); + cont = HandleError(ex, ctx); } + catch { } + + if (!cont) + ctx.SetError(ex.Message, ex); } } + } - public virtual Task EventProcessedAsync(EventContext context) { - return Task.CompletedTask; - } + public virtual Task EventProcessedAsync(EventContext context) { + return Task.CompletedTask; + } - public virtual bool HandleError(Exception exception, EventContext context) { - return ContinueOnError; - } + public virtual bool HandleError(Exception exception, EventContext context) { + return ContinueOnError; } } diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/IEventProcessorPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/IEventProcessorPlugin.cs index 708f6d11ce..c3e617b89c 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/IEventProcessorPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/IEventProcessorPlugin.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +namespace Exceptionless.Core.Plugins.EventProcessor; -namespace Exceptionless.Core.Plugins.EventProcessor { - public interface IEventProcessorPlugin : IPlugin { - Task StartupAsync(); - Task EventBatchProcessingAsync(ICollection contexts); - Task EventBatchProcessedAsync(ICollection contexts); - } +public interface IEventProcessorPlugin : IPlugin { + Task StartupAsync(); + Task EventBatchProcessingAsync(ICollection contexts); + Task EventBatchProcessedAsync(ICollection contexts); } diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/GetVersion.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/GetVersion.cs index f3ca4fab6f..95e02b2837 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/GetVersion.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/GetVersion.cs @@ -1,43 +1,41 @@ -using System; -using System.Linq; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Plugins.EventUpgrader { - [Priority(0)] - public class GetVersion : PluginBase, IEventUpgraderPlugin { - public GetVersion(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - - public void Upgrade(EventUpgraderContext ctx) { - if (ctx.Version != null) - return; - - if (ctx.Documents.Count == 0 || !ctx.Documents.First().HasValues) { - ctx.Version = new Version(); - return; - } - - var doc = ctx.Documents.First(); - if (!(doc["ExceptionlessClientInfo"] is JObject clientInfo) || !clientInfo.HasValues || clientInfo["Version"] == null) { - ctx.Version = new Version(); - return; - } - - if (clientInfo["Version"].ToString().Contains(" ")) { - string version = clientInfo["Version"].ToString().Split(' ').First(); - ctx.Version = new Version(version); - return; - } - - if (clientInfo["Version"].ToString().Contains("-")) { - string version = clientInfo["Version"].ToString().Split('-').First(); - ctx.Version = new Version(version); - return; - } - - // old version format - ctx.Version = new Version(clientInfo["Version"].ToString()); +namespace Exceptionless.Core.Plugins.EventUpgrader; + +[Priority(0)] +public class GetVersion : PluginBase, IEventUpgraderPlugin { + public GetVersion(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } + + public void Upgrade(EventUpgraderContext ctx) { + if (ctx.Version != null) + return; + + if (ctx.Documents.Count == 0 || !ctx.Documents.First().HasValues) { + ctx.Version = new Version(); + return; + } + + var doc = ctx.Documents.First(); + if (!(doc["ExceptionlessClientInfo"] is JObject clientInfo) || !clientInfo.HasValues || clientInfo["Version"] == null) { + ctx.Version = new Version(); + return; } + + if (clientInfo["Version"].ToString().Contains(" ")) { + string version = clientInfo["Version"].ToString().Split(' ').First(); + ctx.Version = new Version(version); + return; + } + + if (clientInfo["Version"].ToString().Contains("-")) { + string version = clientInfo["Version"].ToString().Split('-').First(); + ctx.Version = new Version(version); + return; + } + + // old version format + ctx.Version = new Version(clientInfo["Version"].ToString()); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R500_EventUpgrade.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R500_EventUpgrade.cs index 81e661664f..95d684d1f3 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R500_EventUpgrade.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R500_EventUpgrade.cs @@ -1,32 +1,32 @@ -using System; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Plugins.EventUpgrader { - /// - /// Changed type of InstallDate from DateTime to DateTimeOffset - /// - [Priority(500)] - public class V1R500EventUpgrade : PluginBase, IEventUpgraderPlugin { - public V1R500EventUpgrade(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventUpgrader; - public void Upgrade(EventUpgraderContext ctx) { - if (ctx.Version > new Version(1, 0, 0, 500)) - return; +/// +/// Changed type of InstallDate from DateTime to DateTimeOffset +/// +[Priority(500)] +public class V1R500EventUpgrade : PluginBase, IEventUpgraderPlugin { + public V1R500EventUpgrade(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } + + public void Upgrade(EventUpgraderContext ctx) { + if (ctx.Version > new Version(1, 0, 0, 500)) + return; - foreach (var doc in ctx.Documents) { - if (!(doc["ExceptionlessClientInfo"] is JObject clientInfo) || !clientInfo.HasValues || clientInfo["InstallDate"] == null) - return; + foreach (var doc in ctx.Documents) { + if (!(doc["ExceptionlessClientInfo"] is JObject clientInfo) || !clientInfo.HasValues || clientInfo["InstallDate"] == null) + return; - // This shouldn't hurt using DateTimeOffset to try and parse a date. It insures you won't lose any info. - if (DateTimeOffset.TryParse(clientInfo["InstallDate"].ToString(), out var date)) { - clientInfo.Remove("InstallDate"); - clientInfo.Add("InstallDate", new JValue(date)); - } else { - clientInfo.Remove("InstallDate"); - } + // This shouldn't hurt using DateTimeOffset to try and parse a date. It insures you won't lose any info. + if (DateTimeOffset.TryParse(clientInfo["InstallDate"].ToString(), out var date)) { + clientInfo.Remove("InstallDate"); + clientInfo.Add("InstallDate", new JValue(date)); + } + else { + clientInfo.Remove("InstallDate"); } } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R844_EventUpgrade.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R844_EventUpgrade.cs index 6b837aae52..97651ab4b6 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R844_EventUpgrade.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R844_EventUpgrade.cs @@ -1,37 +1,36 @@ -using System; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Plugins.EventUpgrader { - [Priority(844)] - public class V1R844EventUpgrade : PluginBase, IEventUpgraderPlugin { - public V1R844EventUpgrade(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventUpgrader; - public void Upgrade(EventUpgraderContext ctx) { - if (ctx.Version > new Version(1, 0, 0, 844)) - return; +[Priority(844)] +public class V1R844EventUpgrade : PluginBase, IEventUpgraderPlugin { + public V1R844EventUpgrade(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - foreach (var doc in ctx.Documents) { + public void Upgrade(EventUpgraderContext ctx) { + if (ctx.Version > new Version(1, 0, 0, 844)) + return; - if (!(doc["RequestInfo"] is JObject requestInfo) || !requestInfo.HasValues) - return; + foreach (var doc in ctx.Documents) { - if (requestInfo["Cookies"] != null && requestInfo["Cookies"].HasValues) { - if (requestInfo["Cookies"] is JObject cookies) - cookies.Remove(""); - } + if (!(doc["RequestInfo"] is JObject requestInfo) || !requestInfo.HasValues) + return; - if (requestInfo["Form"] != null && requestInfo["Form"].HasValues) { - if (requestInfo["Form"] is JObject form) - form.Remove(""); - } + if (requestInfo["Cookies"] != null && requestInfo["Cookies"].HasValues) { + if (requestInfo["Cookies"] is JObject cookies) + cookies.Remove(""); + } + + if (requestInfo["Form"] != null && requestInfo["Form"].HasValues) { + if (requestInfo["Form"] is JObject form) + form.Remove(""); + } - if (requestInfo["QueryString"] != null && requestInfo["QueryString"].HasValues) { - if (requestInfo["QueryString"] is JObject queryString) - queryString.Remove(""); - } + if (requestInfo["QueryString"] != null && requestInfo["QueryString"].HasValues) { + if (requestInfo["QueryString"] is JObject queryString) + queryString.Remove(""); } } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R850_EventUpgrade.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R850_EventUpgrade.cs index 47f92ef6fc..cf8a0a1233 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R850_EventUpgrade.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V1R850_EventUpgrade.cs @@ -1,36 +1,34 @@ -using System; -using System.Linq; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Plugins.EventUpgrader { - [Priority(850)] - public class V1R850EventUpgrade : PluginBase, IEventUpgraderPlugin { - public V1R850EventUpgrade(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventUpgrader; - public void Upgrade(EventUpgraderContext ctx) { - if (ctx.Version > new Version(1, 0, 0, 850)) - return; +[Priority(850)] +public class V1R850EventUpgrade : PluginBase, IEventUpgraderPlugin { + public V1R850EventUpgrade(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - foreach (var doc in ctx.Documents.OfType()) { - var current = doc; - while (current != null) { - if (doc["ExtendedData"] is JObject extendedData) { - if (extendedData["ExtraExceptionProperties"] != null) - extendedData.Rename("ExtraExceptionProperties", "__ExceptionInfo"); + public void Upgrade(EventUpgraderContext ctx) { + if (ctx.Version > new Version(1, 0, 0, 850)) + return; - if (extendedData["ExceptionInfo"] != null) - extendedData.Rename("ExceptionInfo", "__ExceptionInfo"); + foreach (var doc in ctx.Documents.OfType()) { + var current = doc; + while (current != null) { + if (doc["ExtendedData"] is JObject extendedData) { + if (extendedData["ExtraExceptionProperties"] != null) + extendedData.Rename("ExtraExceptionProperties", "__ExceptionInfo"); - if (extendedData["TraceInfo"] != null) - extendedData.Rename("TraceInfo", "TraceLog"); - } + if (extendedData["ExceptionInfo"] != null) + extendedData.Rename("ExceptionInfo", "__ExceptionInfo"); - current = current["Inner"] as JObject; + if (extendedData["TraceInfo"] != null) + extendedData.Rename("TraceInfo", "TraceLog"); } + + current = current["Inner"] as JObject; } } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V2_EventUpgrade.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V2_EventUpgrade.cs index a7602d2048..5b235ede9b 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V2_EventUpgrade.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/Default/V2_EventUpgrade.cs @@ -1,145 +1,146 @@ -using System; -using System.Linq; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models.Data; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Plugins.EventUpgrader { - [Priority(2000)] - public class V2EventUpgrade : PluginBase, IEventUpgraderPlugin { - public V2EventUpgrade(AppOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) {} - - public void Upgrade(EventUpgraderContext ctx) { - if (ctx.Version > new Version(2, 0)) - return; - - foreach (var doc in ctx.Documents.OfType()) { - bool isNotFound = doc.GetPropertyStringValue("Code") == "404"; - - if (ctx.IsMigration) { - doc.Rename("ErrorStackId", "StackId"); - } else { - doc.RenameOrRemoveIfNullOrEmpty("Id", "ReferenceId"); - doc.Remove("OrganizationId"); - doc.Remove("ProjectId"); - doc.Remove("ErrorStackId"); - } +namespace Exceptionless.Core.Plugins.EventUpgrader; - doc.RenameOrRemoveIfNullOrEmpty("OccurrenceDate", "Date"); - doc.Remove("ExceptionlessClientInfo"); - if (!doc.RemoveIfNullOrEmpty("Tags")) { - var tags = doc.GetValue("Tags"); - if (tags.Type == JTokenType.Array) { - foreach (var tag in tags.ToList()) { - string t = tag.ToString(); - if (String.IsNullOrEmpty(t) || t.Length > 255) - tag.Remove(); - } - } - } +[Priority(2000)] +public class V2EventUpgrade : PluginBase, IEventUpgraderPlugin { + public V2EventUpgrade(AppOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) { } - doc.RenameOrRemoveIfNullOrEmpty("RequestInfo", "@request"); - bool hasRequestInfo = doc["@request"] != null; + public void Upgrade(EventUpgraderContext ctx) { + if (ctx.Version > new Version(2, 0)) + return; - if (!isNotFound) - doc.RenameOrRemoveIfNullOrEmpty("EnvironmentInfo", "@environment"); - else - doc.Remove("EnvironmentInfo"); + foreach (var doc in ctx.Documents.OfType()) { + bool isNotFound = doc.GetPropertyStringValue("Code") == "404"; - doc.RenameAll("ExtendedData", "Data"); + if (ctx.IsMigration) { + doc.Rename("ErrorStackId", "StackId"); + } + else { + doc.RenameOrRemoveIfNullOrEmpty("Id", "ReferenceId"); + doc.Remove("OrganizationId"); + doc.Remove("ProjectId"); + doc.Remove("ErrorStackId"); + } - var extendedData = doc.Property("Data") != null ? doc.Property("Data").Value as JObject : null; - if (extendedData != null) { - if (!isNotFound) - extendedData.RenameOrRemoveIfNullOrEmpty("TraceLog", "@trace"); - else - extendedData.Remove("TraceLog"); + doc.RenameOrRemoveIfNullOrEmpty("OccurrenceDate", "Date"); + doc.Remove("ExceptionlessClientInfo"); + if (!doc.RemoveIfNullOrEmpty("Tags")) { + var tags = doc.GetValue("Tags"); + if (tags.Type == JTokenType.Array) { + foreach (var tag in tags.ToList()) { + string t = tag.ToString(); + if (String.IsNullOrEmpty(t) || t.Length > 255) + tag.Remove(); + } } + } - if (isNotFound && hasRequestInfo) { - doc.RemoveAll("Code", "Type", "Message", "Inner", "StackTrace", "TargetMethod", "Modules"); - if (extendedData?["__ExceptionInfo"] != null) - extendedData.Remove("__ExceptionInfo"); + doc.RenameOrRemoveIfNullOrEmpty("RequestInfo", "@request"); + bool hasRequestInfo = doc["@request"] != null; - doc.Add("Type", new JValue("404")); - } else { - var error = new JObject(); + if (!isNotFound) + doc.RenameOrRemoveIfNullOrEmpty("EnvironmentInfo", "@environment"); + else + doc.Remove("EnvironmentInfo"); - if (!doc.RemoveIfNullOrEmpty("Message")) - error.Add("Message", doc["Message"].Value()); + doc.RenameAll("ExtendedData", "Data"); - error.MoveOrRemoveIfNullOrEmpty(doc, "Code", "Type", "Inner", "StackTrace", "TargetMethod", "Modules"); + var extendedData = doc.Property("Data") != null ? doc.Property("Data").Value as JObject : null; + if (extendedData != null) { + if (!isNotFound) + extendedData.RenameOrRemoveIfNullOrEmpty("TraceLog", "@trace"); + else + extendedData.Remove("TraceLog"); + } - // Copy the exception info from root extended data to the current errors extended data. - if (extendedData?["__ExceptionInfo"] != null) { - error.Add("Data", new JObject()); - ((JObject)error["Data"]).MoveOrRemoveIfNullOrEmpty(extendedData, "__ExceptionInfo"); - } + if (isNotFound && hasRequestInfo) { + doc.RemoveAll("Code", "Type", "Message", "Inner", "StackTrace", "TargetMethod", "Modules"); + if (extendedData?["__ExceptionInfo"] != null) + extendedData.Remove("__ExceptionInfo"); - string id = doc["Id"]?.Value(); - string projectId = doc["ProjectId"]?.Value(); - RenameAndValidateExtraExceptionProperties(id, error); + doc.Add("Type", new JValue("404")); + } + else { + var error = new JObject(); - var inner = error["Inner"] as JObject; - while (inner != null) { - RenameAndValidateExtraExceptionProperties(id, inner); - inner = inner["Inner"] as JObject; - } + if (!doc.RemoveIfNullOrEmpty("Message")) + error.Add("Message", doc["Message"].Value()); + + error.MoveOrRemoveIfNullOrEmpty(doc, "Code", "Type", "Inner", "StackTrace", "TargetMethod", "Modules"); - doc.Add("Type", new JValue(isNotFound ? "404" : "error")); - doc.Add("@error", error); + // Copy the exception info from root extended data to the current errors extended data. + if (extendedData?["__ExceptionInfo"] != null) { + error.Add("Data", new JObject()); + ((JObject)error["Data"]).MoveOrRemoveIfNullOrEmpty(extendedData, "__ExceptionInfo"); } - string emailAddress = doc.GetPropertyStringValueAndRemove("UserEmail"); - string userDescription = doc.GetPropertyStringValueAndRemove("UserDescription"); - if (!String.IsNullOrWhiteSpace(emailAddress) && !String.IsNullOrWhiteSpace(userDescription)) - doc.Add("@user_description", JObject.FromObject(new UserDescription(emailAddress, userDescription))); + string id = doc["Id"]?.Value(); + string projectId = doc["ProjectId"]?.Value(); + RenameAndValidateExtraExceptionProperties(id, error); - string identity = doc.GetPropertyStringValueAndRemove("UserName"); - if (!String.IsNullOrWhiteSpace(identity)) - doc.Add("@user", JObject.FromObject(new UserInfo(identity))); + var inner = error["Inner"] as JObject; + while (inner != null) { + RenameAndValidateExtraExceptionProperties(id, inner); + inner = inner["Inner"] as JObject; + } - doc.RemoveAllIfNullOrEmpty("Data", "GenericArguments", "Parameters"); + doc.Add("Type", new JValue(isNotFound ? "404" : "error")); + doc.Add("@error", error); } - } - private void RenameAndValidateExtraExceptionProperties(string id, JObject error) { - var extendedData = error?["Data"] as JObject; - if (extendedData?["__ExceptionInfo"] == null) - return; + string emailAddress = doc.GetPropertyStringValueAndRemove("UserEmail"); + string userDescription = doc.GetPropertyStringValueAndRemove("UserDescription"); + if (!String.IsNullOrWhiteSpace(emailAddress) && !String.IsNullOrWhiteSpace(userDescription)) + doc.Add("@user_description", JObject.FromObject(new UserDescription(emailAddress, userDescription))); - string json = extendedData["__ExceptionInfo"].ToString(); - extendedData.Remove("__ExceptionInfo"); + string identity = doc.GetPropertyStringValueAndRemove("UserName"); + if (!String.IsNullOrWhiteSpace(identity)) + doc.Add("@user", JObject.FromObject(new UserInfo(identity))); - if (String.IsNullOrWhiteSpace(json)) - return; + doc.RemoveAllIfNullOrEmpty("Data", "GenericArguments", "Parameters"); + } + } - if (json.Length > 200000) { - _logger.LogError("__ExceptionInfo on {id} is Too Big: {Length}", id, json.Length); - return; - } + private void RenameAndValidateExtraExceptionProperties(string id, JObject error) { + var extendedData = error?["Data"] as JObject; + if (extendedData?["__ExceptionInfo"] == null) + return; - var ext = new JObject(); - try { - var extraProperties = JObject.Parse(json); - foreach (var property in extraProperties.Properties()) { - if (property.IsNullOrEmpty()) - continue; + string json = extendedData["__ExceptionInfo"].ToString(); + extendedData.Remove("__ExceptionInfo"); - string dataKey = property.Name; - if (extendedData[dataKey] != null) - dataKey = "_" + dataKey; + if (String.IsNullOrWhiteSpace(json)) + return; - ext.Add(dataKey, property.Value); - } - } catch (Exception) { } + if (json.Length > 200000) { + _logger.LogError("__ExceptionInfo on {id} is Too Big: {Length}", id, json.Length); + return; + } + + var ext = new JObject(); + try { + var extraProperties = JObject.Parse(json); + foreach (var property in extraProperties.Properties()) { + if (property.IsNullOrEmpty()) + continue; - if (ext.IsNullOrEmpty()) - return; + string dataKey = property.Name; + if (extendedData[dataKey] != null) + dataKey = "_" + dataKey; - extendedData.Add("@ext", ext); + ext.Add(dataKey, property.Value); + } } + catch (Exception) { } + + if (ext.IsNullOrEmpty()) + return; + + extendedData.Add("@ext", ext); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderContext.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderContext.cs index ea1341b00a..fe4f15fa74 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderContext.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderContext.cs @@ -1,41 +1,42 @@ -using System; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Plugins.EventUpgrader { - public class EventUpgraderContext : ExtensibleObject { - public EventUpgraderContext(string json, Version version = null, bool isMigration = false) { - var jsonType = json.GetJsonType(); - if (jsonType == JsonType.Object) { - var doc = JsonConvert.DeserializeObject(json); - Documents = new JArray(doc); - } else if (jsonType == JsonType.Array) { - var docs = JsonConvert.DeserializeObject(json); - Documents = docs; - } else { - throw new ArgumentException("Invalid json data specified.", ""); - } +namespace Exceptionless.Core.Plugins.EventUpgrader; - Version = version; - IsMigration = isMigration; - } - - public EventUpgraderContext(JObject doc, Version version = null, bool isMigration = false) { +public class EventUpgraderContext : ExtensibleObject { + public EventUpgraderContext(string json, Version version = null, bool isMigration = false) { + var jsonType = json.GetJsonType(); + if (jsonType == JsonType.Object) { + var doc = JsonConvert.DeserializeObject(json); Documents = new JArray(doc); - Version = version; - IsMigration = isMigration; } - - public EventUpgraderContext(JArray docs, Version version = null, bool isMigration = false) { + else if (jsonType == JsonType.Array) { + var docs = JsonConvert.DeserializeObject(json); Documents = docs; - Version = version; - IsMigration = isMigration; + } + else { + throw new ArgumentException("Invalid json data specified.", ""); } - public JArray Documents { get; set; } - public Version Version { get; set; } - public bool IsMigration { get; set; } + Version = version; + IsMigration = isMigration; } -} \ No newline at end of file + + public EventUpgraderContext(JObject doc, Version version = null, bool isMigration = false) { + Documents = new JArray(doc); + Version = version; + IsMigration = isMigration; + } + + public EventUpgraderContext(JArray docs, Version version = null, bool isMigration = false) { + Documents = docs; + Version = version; + IsMigration = isMigration; + } + + public JArray Documents { get; set; } + public Version Version { get; set; } + public bool IsMigration { get; set; } +} diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderPluginManager.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderPluginManager.cs index 8098d4448a..0779e0fa12 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderPluginManager.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/EventUpgraderPluginManager.cs @@ -1,28 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Foundatio.Metrics; +using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.EventUpgrader { - public class EventUpgraderPluginManager : PluginManagerBase { - public EventUpgraderPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } +namespace Exceptionless.Core.Plugins.EventUpgrader; - /// - /// Runs all of the event upgrade plugins upgrade method. - /// - public void Upgrade(EventUpgraderContext context) { - string metricPrefix = String.Concat(_metricPrefix, nameof(Upgrade).ToLower(), "."); - foreach (var plugin in Plugins.Values.ToList()) { - string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); - try { - _metricsClient.Time(() => plugin.Upgrade(context), metricName); - } catch (Exception ex) { - using (_logger.BeginScope(new Dictionary { { "Context", context } })) - _logger.LogError(ex, "Error calling upgrade in plugin {PluginName}: {Message}", plugin.Name, ex.Message); +public class EventUpgraderPluginManager : PluginManagerBase { + public EventUpgraderPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } - throw; - } + /// + /// Runs all of the event upgrade plugins upgrade method. + /// + public void Upgrade(EventUpgraderContext context) { + string metricPrefix = String.Concat(_metricPrefix, nameof(Upgrade).ToLower(), "."); + foreach (var plugin in Plugins.Values.ToList()) { + string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); + try { + _metricsClient.Time(() => plugin.Upgrade(context), metricName); + } + catch (Exception ex) { + using (_logger.BeginScope(new Dictionary { { "Context", context } })) + _logger.LogError(ex, "Error calling upgrade in plugin {PluginName}: {Message}", plugin.Name, ex.Message); + + throw; } } } diff --git a/src/Exceptionless.Core/Plugins/EventUpgrader/IEventUpgraderPlugin.cs b/src/Exceptionless.Core/Plugins/EventUpgrader/IEventUpgraderPlugin.cs index 4a7cf7f87d..7228bad065 100644 --- a/src/Exceptionless.Core/Plugins/EventUpgrader/IEventUpgraderPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventUpgrader/IEventUpgraderPlugin.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Core.Plugins.EventUpgrader { - public interface IEventUpgraderPlugin : IPlugin { - void Upgrade(EventUpgraderContext ctx); - } +namespace Exceptionless.Core.Plugins.EventUpgrader; + +public interface IEventUpgraderPlugin : IPlugin { + void Upgrade(EventUpgraderContext ctx); } diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/05_ManualStackingFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/05_ManualStackingFormattingPlugin.cs index 619ba4522b..9a04ab7fe9 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/05_ManualStackingFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/05_ManualStackingFormattingPlugin.cs @@ -1,15 +1,14 @@ -using System; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Exceptionless.Core.Pipeline; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(5)] - public sealed class ManualStackingFormattingPlugin : FormattingPluginBase { - public ManualStackingFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - public override string GetStackTitle(PersistentEvent ev) { - var msi = ev.GetManualStackingInfo(); - return !String.IsNullOrWhiteSpace(msi?.Title) ? msi.Title : null; - } +[Priority(5)] +public sealed class ManualStackingFormattingPlugin : FormattingPluginBase { + public ManualStackingFormattingPlugin(AppOptions options) : base(options) { } + + public override string GetStackTitle(PersistentEvent ev) { + var msi = ev.GetManualStackingInfo(); + return !String.IsNullOrWhiteSpace(msi?.Title) ? msi.Title : null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/10_SimpleErrorFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/10_SimpleErrorFormattingPlugin.cs index 91eac49b27..d06593cd65 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/10_SimpleErrorFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/10_SimpleErrorFormattingPlugin.cs @@ -1,149 +1,146 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(10)] - public sealed class SimpleErrorFormattingPlugin : FormattingPluginBase { - public SimpleErrorFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - private bool ShouldHandle(PersistentEvent ev) { - return ev.IsError() && ev.Data.ContainsKey(Event.KnownDataKeys.SimpleError); - } - - public override SummaryData GetStackSummaryData(Stack stack) { - if (stack.SignatureInfo == null || !stack.SignatureInfo.ContainsKey("StackTrace")) - return null; +[Priority(10)] +public sealed class SimpleErrorFormattingPlugin : FormattingPluginBase { + public SimpleErrorFormattingPlugin(AppOptions options) : base(options) { } - var data = new Dictionary(); - if (stack.SignatureInfo.TryGetValue("ExceptionType", out string value)) { - data.Add("Type", value.TypeName()); - data.Add("TypeFullName", value); - } + private bool ShouldHandle(PersistentEvent ev) { + return ev.IsError() && ev.Data.ContainsKey(Event.KnownDataKeys.SimpleError); + } - if (stack.SignatureInfo.TryGetValue("Path", out value)) - data.Add("Path", value); + public override SummaryData GetStackSummaryData(Stack stack) { + if (stack.SignatureInfo == null || !stack.SignatureInfo.ContainsKey("StackTrace")) + return null; - return new SummaryData { TemplateKey = "stack-simple-summary", Data = data }; + var data = new Dictionary(); + if (stack.SignatureInfo.TryGetValue("ExceptionType", out string value)) { + data.Add("Type", value.TypeName()); + data.Add("TypeFullName", value); } - public override string GetStackTitle(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + if (stack.SignatureInfo.TryGetValue("Path", out value)) + data.Add("Path", value); - var error = ev.GetSimpleError(); - return error?.Message; - } + return new SummaryData { TemplateKey = "stack-simple-summary", Data = data }; + } - public override SummaryData GetEventSummaryData(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + public override string GetStackTitle(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - var error = ev.GetSimpleError(); - if (error == null) - return null; + var error = ev.GetSimpleError(); + return error?.Message; + } - var data = new Dictionary { { "Message", ev.Message } }; - AddUserIdentitySummaryData(data, ev.GetUserIdentity()); + public override SummaryData GetEventSummaryData(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - if (!String.IsNullOrEmpty(error.Type)) { - data.Add("Type", error.Type.TypeName()); - data.Add("TypeFullName", error.Type); - } + var error = ev.GetSimpleError(); + if (error == null) + return null; - var requestInfo = ev.GetRequestInfo(); - if (!String.IsNullOrEmpty(requestInfo?.Path)) - data.Add("Path", requestInfo.Path); + var data = new Dictionary { { "Message", ev.Message } }; + AddUserIdentitySummaryData(data, ev.GetUserIdentity()); - return new SummaryData { TemplateKey = "event-simple-summary", Data = data }; + if (!String.IsNullOrEmpty(error.Type)) { + data.Add("Type", error.Type.TypeName()); + data.Add("TypeFullName", error.Type); } - public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + var requestInfo = ev.GetRequestInfo(); + if (!String.IsNullOrEmpty(requestInfo?.Path)) + data.Add("Path", requestInfo.Path); - var error = ev.GetSimpleError(); - if (error == null) - return null; + return new SummaryData { TemplateKey = "event-simple-summary", Data = data }; + } - string errorTypeName = null; - if (!String.IsNullOrEmpty(error.Type)) - errorTypeName = error.Type.TypeName().Truncate(60); + public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "Error"; - string notificationType = String.Concat(errorType, " occurrence"); - if (isNew) - notificationType = String.Concat(!isCritical ? "New " : "new ", errorType); - else if (isRegression) - notificationType = String.Concat(errorType, " regression"); + var error = ev.GetSimpleError(); + if (error == null) + return null; - if (isCritical) - notificationType = String.Concat("Critical ", notificationType); + string errorTypeName = null; + if (!String.IsNullOrEmpty(error.Type)) + errorTypeName = error.Type.TypeName().Truncate(60); - string subject = String.Concat(notificationType, ": ", error.Message).Truncate(120); - var data = new Dictionary { { "Message", error.Message.Truncate(60) } }; - if (!String.IsNullOrEmpty(errorTypeName)) - data.Add("Type", errorTypeName); + string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "Error"; + string notificationType = String.Concat(errorType, " occurrence"); + if (isNew) + notificationType = String.Concat(!isCritical ? "New " : "new ", errorType); + else if (isRegression) + notificationType = String.Concat(errorType, " regression"); - var requestInfo = ev.GetRequestInfo(); - if (requestInfo != null) - data.Add("Url", requestInfo.GetFullPath(true, true, true)); + if (isCritical) + notificationType = String.Concat("Critical ", notificationType); - return new MailMessageData { Subject = subject, Data = data }; - } + string subject = String.Concat(notificationType, ": ", error.Message).Truncate(120); + var data = new Dictionary { { "Message", error.Message.Truncate(60) } }; + if (!String.IsNullOrEmpty(errorTypeName)) + data.Add("Type", errorTypeName); + + var requestInfo = ev.GetRequestInfo(); + if (requestInfo != null) + data.Add("Url", requestInfo.GetFullPath(true, true, true)); + + return new MailMessageData { Subject = subject, Data = data }; + } - public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - var error = ev.GetSimpleError(); - if (error == null) - return null; + var error = ev.GetSimpleError(); + if (error == null) + return null; - string errorTypeName = null; - if (!String.IsNullOrEmpty(error.Type)) - errorTypeName = error.Type.TypeName().Truncate(60); + string errorTypeName = null; + if (!String.IsNullOrEmpty(error.Type)) + errorTypeName = error.Type.TypeName().Truncate(60); - string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "error"; - string notificationType = String.Concat(errorType, " occurrence"); - if (isNew) - notificationType = String.Concat("new ", errorType); - else if (isRegression) - notificationType = String.Concat(errorType, " regression"); + string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "error"; + string notificationType = String.Concat(errorType, " occurrence"); + if (isNew) + notificationType = String.Concat("new ", errorType); + else if (isRegression) + notificationType = String.Concat(errorType, " regression"); - if (isCritical) - notificationType = String.Concat("critical ", notificationType); + if (isCritical) + notificationType = String.Concat("critical ", notificationType); - var attachment = new SlackMessage.SlackAttachment(ev) { - Color = "#BB423F", - Fields = new List { + var attachment = new SlackMessage.SlackAttachment(ev) { + Color = "#BB423F", + Fields = new List { new SlackMessage.SlackAttachmentFields { Title = "Message", Value = error.Message.Truncate(60) } } - }; - - if (!String.IsNullOrEmpty(errorTypeName)) - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Type", Value = errorTypeName }); + }; - var lines = error.StackTrace.SplitLines().ToList(); - if (lines.Count > 0) { - var frames = lines.Take(3).ToList(); - if (lines.Count > 3) - frames.Add("..."); + if (!String.IsNullOrEmpty(errorTypeName)) + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Type", Value = errorTypeName }); - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Stack Trace", Value = $"```{String.Join("\n", frames)}```" }); - } + var lines = error.StackTrace.SplitLines().ToList(); + if (lines.Count > 0) { + var frames = lines.Take(3).ToList(); + if (lines.Count > 3) + frames.Add("..."); - AddDefaultSlackFields(ev, attachment.Fields); - string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, error.Message.Truncate(120))}*"; - return new SlackMessage(subject) { - Attachments = new List { attachment } - }; + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Stack Trace", Value = $"```{String.Join("\n", frames)}```" }); } + + AddDefaultSlackFields(ev, attachment.Fields); + string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, error.Message.Truncate(120))}*"; + return new SlackMessage(subject) { + Attachments = new List { attachment } + }; } } diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/20_ErrorFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/20_ErrorFormattingPlugin.cs index 44dd976ab3..6d270fa5ab 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/20_ErrorFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/20_ErrorFormattingPlugin.cs @@ -1,162 +1,160 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(20)] - public sealed class ErrorFormattingPlugin : FormattingPluginBase { - public ErrorFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - private bool ShouldHandle(PersistentEvent ev) { - return ev.IsError() && ev.Data.ContainsKey(Event.KnownDataKeys.Error); - } - - public override string GetStackTitle(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; - - var error = ev.GetError(); - return error?.Message; - } +[Priority(20)] +public sealed class ErrorFormattingPlugin : FormattingPluginBase { + public ErrorFormattingPlugin(AppOptions options) : base(options) { } - public override SummaryData GetStackSummaryData(Stack stack) { - if (stack.SignatureInfo == null || !stack.SignatureInfo.ContainsKey("ExceptionType")) - return null; + private bool ShouldHandle(PersistentEvent ev) { + return ev.IsError() && ev.Data.ContainsKey(Event.KnownDataKeys.Error); + } - var data = new Dictionary(); - if (stack.SignatureInfo.TryGetValue("ExceptionType", out string value) && !String.IsNullOrEmpty(value)) { - data.Add("Type", value.TypeName()); - data.Add("TypeFullName", value); - } + public override string GetStackTitle(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - if (stack.SignatureInfo.TryGetValue("Method", out value) && !String.IsNullOrEmpty(value)) { - string method = value.TypeName(); - int index = method.IndexOf('('); - data.Add("Method", index > 0 ? method.Substring(0, index) : method); - data.Add("MethodFullName", value); - } + var error = ev.GetError(); + return error?.Message; + } - if (stack.SignatureInfo.TryGetValue("Message", out value) && !String.IsNullOrEmpty(value)) - data.Add("Message", value); + public override SummaryData GetStackSummaryData(Stack stack) { + if (stack.SignatureInfo == null || !stack.SignatureInfo.ContainsKey("ExceptionType")) + return null; - if (stack.SignatureInfo.TryGetValue("Path", out value) && !String.IsNullOrEmpty(value)) - data.Add("Path", value); + var data = new Dictionary(); + if (stack.SignatureInfo.TryGetValue("ExceptionType", out string value) && !String.IsNullOrEmpty(value)) { + data.Add("Type", value.TypeName()); + data.Add("TypeFullName", value); + } - return new SummaryData { TemplateKey = "stack-error-summary", Data = data }; + if (stack.SignatureInfo.TryGetValue("Method", out value) && !String.IsNullOrEmpty(value)) { + string method = value.TypeName(); + int index = method.IndexOf('('); + data.Add("Method", index > 0 ? method.Substring(0, index) : method); + data.Add("MethodFullName", value); } - public override SummaryData GetEventSummaryData(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + if (stack.SignatureInfo.TryGetValue("Message", out value) && !String.IsNullOrEmpty(value)) + data.Add("Message", value); + + if (stack.SignatureInfo.TryGetValue("Path", out value) && !String.IsNullOrEmpty(value)) + data.Add("Path", value); - var stackingTarget = ev.GetStackingTarget(); - if (stackingTarget?.Error == null) - return null; + return new SummaryData { TemplateKey = "stack-error-summary", Data = data }; + } - var data = new Dictionary { { "Id", ev.Id }, { "Message", ev.Message } }; - AddUserIdentitySummaryData(data, ev.GetUserIdentity()); + public override SummaryData GetEventSummaryData(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - if (!String.IsNullOrEmpty(stackingTarget.Error.Type)) { - data.Add("Type", stackingTarget.Error.Type.TypeName()); - data.Add("TypeFullName", stackingTarget.Error.Type); - } + var stackingTarget = ev.GetStackingTarget(); + if (stackingTarget?.Error == null) + return null; - if (stackingTarget.Method != null) { - data.Add("Method", stackingTarget.Method.Name); - data.Add("MethodFullName", stackingTarget.Method.GetFullName()); - } + var data = new Dictionary { { "Id", ev.Id }, { "Message", ev.Message } }; + AddUserIdentitySummaryData(data, ev.GetUserIdentity()); - var requestInfo = ev.GetRequestInfo(); - if (!String.IsNullOrEmpty(requestInfo?.Path)) - data.Add("Path", requestInfo.Path); + if (!String.IsNullOrEmpty(stackingTarget.Error.Type)) { + data.Add("Type", stackingTarget.Error.Type.TypeName()); + data.Add("TypeFullName", stackingTarget.Error.Type); + } - return new SummaryData { TemplateKey = "event-error-summary", Data = data }; + if (stackingTarget.Method != null) { + data.Add("Method", stackingTarget.Method.Name); + data.Add("MethodFullName", stackingTarget.Method.GetFullName()); } - public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + var requestInfo = ev.GetRequestInfo(); + if (!String.IsNullOrEmpty(requestInfo?.Path)) + data.Add("Path", requestInfo.Path); - var error = ev.GetError(); - var stackingTarget = error?.GetStackingTarget(); - if (stackingTarget?.Error == null) - return null; + return new SummaryData { TemplateKey = "event-error-summary", Data = data }; + } - string errorTypeName = null; - if (!String.IsNullOrEmpty(stackingTarget.Error.Type)) - errorTypeName = stackingTarget.Error.Type.TypeName().Truncate(60); + public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "Error"; - string notificationType = String.Concat(errorType, " occurrence"); - if (isNew) - notificationType = String.Concat(!isCritical ? "New " : "new ", errorType); - else if (isRegression) - notificationType = String.Concat(errorType, " regression"); + var error = ev.GetError(); + var stackingTarget = error?.GetStackingTarget(); + if (stackingTarget?.Error == null) + return null; - if (isCritical) - notificationType = String.Concat("Critical ", notificationType); + string errorTypeName = null; + if (!String.IsNullOrEmpty(stackingTarget.Error.Type)) + errorTypeName = stackingTarget.Error.Type.TypeName().Truncate(60); - string subject = String.Concat(notificationType, ": ", stackingTarget.Error.Message).Truncate(120); - var data = new Dictionary { { "Message", stackingTarget.Error.Message.Truncate(60) } }; - if (!String.IsNullOrEmpty(errorTypeName)) - data.Add("Type", errorTypeName); + string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "Error"; + string notificationType = String.Concat(errorType, " occurrence"); + if (isNew) + notificationType = String.Concat(!isCritical ? "New " : "new ", errorType); + else if (isRegression) + notificationType = String.Concat(errorType, " regression"); - if (stackingTarget.Method != null) - data.Add("Method", stackingTarget.Method.Name.Truncate(60)); + if (isCritical) + notificationType = String.Concat("Critical ", notificationType); - var requestInfo = ev.GetRequestInfo(); - if (requestInfo != null) - data.Add("Url", requestInfo.GetFullPath(true, true, true)); + string subject = String.Concat(notificationType, ": ", stackingTarget.Error.Message).Truncate(120); + var data = new Dictionary { { "Message", stackingTarget.Error.Message.Truncate(60) } }; + if (!String.IsNullOrEmpty(errorTypeName)) + data.Add("Type", errorTypeName); - return new MailMessageData { Subject = subject, Data = data }; - } + if (stackingTarget.Method != null) + data.Add("Method", stackingTarget.Method.Name.Truncate(60)); - public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + var requestInfo = ev.GetRequestInfo(); + if (requestInfo != null) + data.Add("Url", requestInfo.GetFullPath(true, true, true)); - var error = ev.GetError(); - var stackingTarget = error?.GetStackingTarget(); - if (stackingTarget?.Error == null) - return null; + return new MailMessageData { Subject = subject, Data = data }; + } + + public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string errorTypeName = null; - if (!String.IsNullOrEmpty(stackingTarget.Error.Type)) - errorTypeName = stackingTarget.Error.Type.TypeName().Truncate(60); + var error = ev.GetError(); + var stackingTarget = error?.GetStackingTarget(); + if (stackingTarget?.Error == null) + return null; - string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "error"; - string notificationType = String.Concat(errorType, " occurrence"); - if (isNew) - notificationType = String.Concat("new ", errorType); - else if (isRegression) - notificationType = String.Concat(errorType, " regression"); + string errorTypeName = null; + if (!String.IsNullOrEmpty(stackingTarget.Error.Type)) + errorTypeName = stackingTarget.Error.Type.TypeName().Truncate(60); - if (isCritical) - notificationType = String.Concat("critical ", notificationType); + string errorType = !String.IsNullOrEmpty(errorTypeName) ? errorTypeName : "error"; + string notificationType = String.Concat(errorType, " occurrence"); + if (isNew) + notificationType = String.Concat("new ", errorType); + else if (isRegression) + notificationType = String.Concat(errorType, " regression"); - var attachment = new SlackMessage.SlackAttachment(ev) { - Color = "#BB423F", - Fields = new List { + if (isCritical) + notificationType = String.Concat("critical ", notificationType); + + var attachment = new SlackMessage.SlackAttachment(ev) { + Color = "#BB423F", + Fields = new List { new SlackMessage.SlackAttachmentFields { Title = "Message", Value = stackingTarget.Error.Message.Truncate(60) } } - }; + }; - if (!String.IsNullOrEmpty(errorTypeName)) - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Type", Value = errorTypeName }); + if (!String.IsNullOrEmpty(errorTypeName)) + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Type", Value = errorTypeName }); - if (stackingTarget.Method != null) - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Method", Value = stackingTarget.Method.Name.Truncate(60) }); + if (stackingTarget.Method != null) + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Method", Value = stackingTarget.Method.Name.Truncate(60) }); - AddDefaultSlackFields(ev, attachment.Fields); - string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, stackingTarget.Error.Message.Truncate(120))}*"; - return new SlackMessage(subject) { - Attachments = new List { attachment } - }; - } + AddDefaultSlackFields(ev, attachment.Fields); + string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, stackingTarget.Error.Message.Truncate(120))}*"; + return new SlackMessage(subject) { + Attachments = new List { attachment } + }; } } diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/30_NotFoundFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/30_NotFoundFormattingPlugin.cs index 9e3ba1423c..8df29eff10 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/30_NotFoundFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/30_NotFoundFormattingPlugin.cs @@ -1,98 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(30)] - public sealed class NotFoundFormattingPlugin : FormattingPluginBase { - public NotFoundFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - private bool ShouldHandle(PersistentEvent ev) { - return ev.IsNotFound(); - } +[Priority(30)] +public sealed class NotFoundFormattingPlugin : FormattingPluginBase { + public NotFoundFormattingPlugin(AppOptions options) : base(options) { } - public override SummaryData GetStackSummaryData(Stack stack) { - if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.NotFound)) - return null; + private bool ShouldHandle(PersistentEvent ev) { + return ev.IsNotFound(); + } + + public override SummaryData GetStackSummaryData(Stack stack) { + if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.NotFound)) + return null; - return new SummaryData { TemplateKey = "stack-notfound-summary", Data = new Dictionary() }; - } + return new SummaryData { TemplateKey = "stack-notfound-summary", Data = new Dictionary() }; + } - public override string GetStackTitle(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + public override string GetStackTitle(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - return !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Unknown)"; - } + return !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Unknown)"; + } - public override SummaryData GetEventSummaryData(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + public override SummaryData GetEventSummaryData(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - var data = new Dictionary { { "Source", ev.Source } }; - AddUserIdentitySummaryData(data, ev.GetUserIdentity()); + var data = new Dictionary { { "Source", ev.Source } }; + AddUserIdentitySummaryData(data, ev.GetUserIdentity()); - var ips = ev.GetIpAddresses().ToList(); - if (ips.Count > 0) - data.Add("IpAddress", ips); + var ips = ev.GetIpAddresses().ToList(); + if (ips.Count > 0) + data.Add("IpAddress", ips); - return new SummaryData { TemplateKey = "event-notfound-summary", Data = data }; - } + return new SummaryData { TemplateKey = "event-notfound-summary", Data = data }; + } - public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string notificationType = "Occurrence 404"; - if (isNew) - notificationType = "New 404"; - else if (isRegression) - notificationType = "Regression 404"; + string notificationType = "Occurrence 404"; + if (isNew) + notificationType = "New 404"; + else if (isRegression) + notificationType = "Regression 404"; - if (isCritical) - notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); + if (isCritical) + notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); - string subject = String.Concat(notificationType, ": ", ev.Source).Truncate(120); - var requestInfo = ev.GetRequestInfo(); - var data = new Dictionary { + string subject = String.Concat(notificationType, ": ", ev.Source).Truncate(120); + var requestInfo = ev.GetRequestInfo(); + var data = new Dictionary { { "Url", requestInfo?.GetFullPath(true, true, true) ?? ev.Source.Truncate(60) } }; - return new MailMessageData { Subject = subject, Data = data }; - } + return new MailMessageData { Subject = subject, Data = data }; + } - public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string notificationType = "occurrence 404"; - if (isNew) - notificationType = "new 404"; - else if (isRegression) - notificationType = "regression 404"; + string notificationType = "occurrence 404"; + if (isNew) + notificationType = "new 404"; + else if (isRegression) + notificationType = "regression 404"; - if (isCritical) - notificationType = String.Concat("critical ", notificationType); + if (isCritical) + notificationType = String.Concat("critical ", notificationType); - var requestInfo = ev.GetRequestInfo(); - var attachment = new SlackMessage.SlackAttachment(ev) { - Color = "#BB423F", - Fields = new List { + var requestInfo = ev.GetRequestInfo(); + var attachment = new SlackMessage.SlackAttachment(ev) { + Color = "#BB423F", + Fields = new List { new SlackMessage.SlackAttachmentFields { Title = "Url", Value = requestInfo != null ? requestInfo.GetFullPath(true, true, true) : ev.Source.Truncate(60) } } - }; + }; - AddDefaultSlackFields(ev, attachment.Fields, false); - string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, ev.Source.Truncate(120))}*"; - return new SlackMessage(subject) { - Attachments = new List { attachment } - }; - } + AddDefaultSlackFields(ev, attachment.Fields, false); + string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, ev.Source.Truncate(120))}*"; + return new SlackMessage(subject) { + Attachments = new List { attachment } + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/40_UsageFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/40_UsageFormattingPlugin.cs index a21065cd44..cb43804a68 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/40_UsageFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/40_UsageFormattingPlugin.cs @@ -1,72 +1,70 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(40)] - public sealed class UsageFormattingPlugin : FormattingPluginBase { - public UsageFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - private bool ShouldHandle(PersistentEvent ev) { - return ev.IsFeatureUsage(); - } +[Priority(40)] +public sealed class UsageFormattingPlugin : FormattingPluginBase { + public UsageFormattingPlugin(AppOptions options) : base(options) { } - public override SummaryData GetStackSummaryData(Stack stack) { - if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.FeatureUsage)) - return null; + private bool ShouldHandle(PersistentEvent ev) { + return ev.IsFeatureUsage(); + } + + public override SummaryData GetStackSummaryData(Stack stack) { + if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.FeatureUsage)) + return null; - return new SummaryData { TemplateKey = "stack-feature-summary", Data = new Dictionary() }; - } + return new SummaryData { TemplateKey = "stack-feature-summary", Data = new Dictionary() }; + } - public override string GetStackTitle(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + public override string GetStackTitle(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - return !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Unknown)"; - } + return !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Unknown)"; + } - public override SummaryData GetEventSummaryData(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + public override SummaryData GetEventSummaryData(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - var data = new Dictionary { { "Source", ev.Source } }; - AddUserIdentitySummaryData(data, ev.GetUserIdentity()); + var data = new Dictionary { { "Source", ev.Source } }; + AddUserIdentitySummaryData(data, ev.GetUserIdentity()); - return new SummaryData { TemplateKey = "event-feature-summary", Data = data }; - } + return new SummaryData { TemplateKey = "event-feature-summary", Data = data }; + } - public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string subject = String.Concat("Feature: ", ev.Source).Truncate(120); - var data = new Dictionary { + string subject = String.Concat("Feature: ", ev.Source).Truncate(120); + var data = new Dictionary { { "Source", ev.Source.Truncate(60) } }; - return new MailMessageData { Subject = subject, Data = data }; - } + return new MailMessageData { Subject = subject, Data = data }; + } - public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - var attachment = new SlackMessage.SlackAttachment(ev) { - Fields = new List { + var attachment = new SlackMessage.SlackAttachment(ev) { + Fields = new List { new SlackMessage.SlackAttachmentFields { Title = "Source", Value = ev.Source.Truncate(60) } } - }; + }; - AddDefaultSlackFields(ev, attachment.Fields, false); - string subject = $"[{project.Name}] Feature: *{GetSlackEventUrl(ev.Id, ev.Source).Truncate(120)}*"; - return new SlackMessage(subject) { - Attachments = new List { attachment } - }; - } + AddDefaultSlackFields(ev, attachment.Fields, false); + string subject = $"[{project.Name}] Feature: *{GetSlackEventUrl(ev.Id, ev.Source).Truncate(120)}*"; + return new SlackMessage(subject) { + Attachments = new List { attachment } + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/50_SessionFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/50_SessionFormattingPlugin.cs index c29d7c7e6c..3e18b2eac8 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/50_SessionFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/50_SessionFormattingPlugin.cs @@ -1,50 +1,49 @@ -using System.Collections.Generic; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(50)] - public sealed class SessionFormattingPlugin : FormattingPluginBase { - public SessionFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - private bool ShouldHandle(PersistentEvent ev) { - return ev.IsSessionStart() || ev.IsSessionEnd() || ev.IsSessionHeartbeat(); - } +[Priority(50)] +public sealed class SessionFormattingPlugin : FormattingPluginBase { + public SessionFormattingPlugin(AppOptions options) : base(options) { } - public override SummaryData GetStackSummaryData(Stack stack) { - if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.Session, Event.KnownTypes.SessionEnd, Event.KnownTypes.SessionHeartbeat)) - return null; + private bool ShouldHandle(PersistentEvent ev) { + return ev.IsSessionStart() || ev.IsSessionEnd() || ev.IsSessionHeartbeat(); + } - return new SummaryData { TemplateKey = "stack-session-summary", Data = new Dictionary() }; - } + public override SummaryData GetStackSummaryData(Stack stack) { + if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.Session, Event.KnownTypes.SessionEnd, Event.KnownTypes.SessionHeartbeat)) + return null; - public override string GetStackTitle(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + return new SummaryData { TemplateKey = "stack-session-summary", Data = new Dictionary() }; + } - if (ev.IsSessionHeartbeat()) - return "Session Heartbeat"; + public override string GetStackTitle(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - return ev.IsSessionStart() ? "Session Start" : "Session End"; - } + if (ev.IsSessionHeartbeat()) + return "Session Heartbeat"; - public override SummaryData GetEventSummaryData(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + return ev.IsSessionStart() ? "Session Start" : "Session End"; + } - var data = new Dictionary { { "SessionId", ev.GetSessionId() }, { "Type", ev.Type } }; - AddUserIdentitySummaryData(data, ev.GetUserIdentity()); + public override SummaryData GetEventSummaryData(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - if (ev.IsSessionStart()) { - data.Add("Value", ev.Value.GetValueOrDefault()); + var data = new Dictionary { { "SessionId", ev.GetSessionId() }, { "Type", ev.Type } }; + AddUserIdentitySummaryData(data, ev.GetUserIdentity()); - var endTime = ev.GetSessionEndTime(); - if (endTime.HasValue) - data.Add("SessionEnd", endTime); - } + if (ev.IsSessionStart()) { + data.Add("Value", ev.Value.GetValueOrDefault()); - return new SummaryData { TemplateKey = "event-session-summary", Data = data }; + var endTime = ev.GetSessionEndTime(); + if (endTime.HasValue) + data.Add("SessionEnd", endTime); } + + return new SummaryData { TemplateKey = "event-session-summary", Data = data }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/60_LogFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/60_LogFormattingPlugin.cs index e0cd7fd412..9ba8119441 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/60_LogFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/60_LogFormattingPlugin.cs @@ -1,152 +1,149 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(60)] - public sealed class LogFormattingPlugin : FormattingPluginBase { - public LogFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - private bool ShouldHandle(PersistentEvent ev) { - return ev.IsLog(); - } +[Priority(60)] +public sealed class LogFormattingPlugin : FormattingPluginBase { + public LogFormattingPlugin(AppOptions options) : base(options) { } - public override SummaryData GetStackSummaryData(Stack stack) { - if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.Log)) - return null; - - var data = new Dictionary(); - string source = stack.SignatureInfo?.GetString("Source"); - if (!String.IsNullOrWhiteSpace(source) && String.Equals(source, stack.Title)) { - string[] parts = source.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length > 1 && !String.Equals(source, parts.Last()) && parts.All(p => p.IsValidIdentifier())) { - data.Add("Source", source); - data.Add("SourceShortName", parts.Last()); - } - } + private bool ShouldHandle(PersistentEvent ev) { + return ev.IsLog(); + } - return new SummaryData { TemplateKey = "stack-log-summary", Data = data }; + public override SummaryData GetStackSummaryData(Stack stack) { + if (!stack.SignatureInfo.ContainsKeyWithValue("Type", Event.KnownTypes.Log)) + return null; + + var data = new Dictionary(); + string source = stack.SignatureInfo?.GetString("Source"); + if (!String.IsNullOrWhiteSpace(source) && String.Equals(source, stack.Title)) { + string[] parts = source.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length > 1 && !String.Equals(source, parts.Last()) && parts.All(p => p.IsValidIdentifier())) { + data.Add("Source", source); + data.Add("SourceShortName", parts.Last()); + } } - public override string GetStackTitle(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; - - return !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Global)"; - } + return new SummaryData { TemplateKey = "stack-log-summary", Data = data }; + } - public override SummaryData GetEventSummaryData(PersistentEvent ev) { - if (!ShouldHandle(ev)) - return null; + public override string GetStackTitle(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - var data = new Dictionary { { "Message", ev.Message } }; - AddUserIdentitySummaryData(data, ev.GetUserIdentity()); + return !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Global)"; + } - if (!String.IsNullOrWhiteSpace(ev.Source)) { - data.Add("Source", ev.Source); + public override SummaryData GetEventSummaryData(PersistentEvent ev) { + if (!ShouldHandle(ev)) + return null; - string[] parts = ev.Source.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length > 1 && !String.Equals(ev.Source, parts.Last()) && parts.All(p => p.IsValidIdentifier())) - data.Add("SourceShortName", parts.Last()); - } + var data = new Dictionary { { "Message", ev.Message } }; + AddUserIdentitySummaryData(data, ev.GetUserIdentity()); - string level = ev.Data.TryGetValue(Event.KnownDataKeys.Level, out object temp) ? temp as string : null; - if (!String.IsNullOrWhiteSpace(level)) - data.Add("Level", level.Trim()); + if (!String.IsNullOrWhiteSpace(ev.Source)) { + data.Add("Source", ev.Source); - return new SummaryData { TemplateKey = "event-log-summary", Data = data }; + string[] parts = ev.Source.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length > 1 && !String.Equals(ev.Source, parts.Last()) && parts.All(p => p.IsValidIdentifier())) + data.Add("SourceShortName", parts.Last()); } - public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + string level = ev.Data.TryGetValue(Event.KnownDataKeys.Level, out object temp) ? temp as string : null; + if (!String.IsNullOrWhiteSpace(level)) + data.Add("Level", level.Trim()); - string notificationType = "Log message"; - if (isNew) - notificationType = "New log source"; - else if (isRegression) - notificationType = "Log regression"; + return new SummaryData { TemplateKey = "event-log-summary", Data = data }; + } - if (isCritical) - notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); + public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string source = !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Global)"; - string subject = String.Concat(notificationType, ": ", source).Truncate(120); - var data = new Dictionary { { "Source", source.Truncate(60) } }; - if (!String.IsNullOrEmpty(ev.Message)) - data.Add("Message", ev.Message.Truncate(60)); + string notificationType = "Log message"; + if (isNew) + notificationType = "New log source"; + else if (isRegression) + notificationType = "Log regression"; - string level = ev.GetLevel(); - if (!String.IsNullOrEmpty(level)) - data.Add("Level", level.Truncate(60)); + if (isCritical) + notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); - var requestInfo = ev.GetRequestInfo(); - if (requestInfo != null) - data.Add("Url", requestInfo.GetFullPath(true, true, true)); + string source = !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Global)"; + string subject = String.Concat(notificationType, ": ", source).Truncate(120); + var data = new Dictionary { { "Source", source.Truncate(60) } }; + if (!String.IsNullOrEmpty(ev.Message)) + data.Add("Message", ev.Message.Truncate(60)); - return new MailMessageData { Subject = subject, Data = data }; - } + string level = ev.GetLevel(); + if (!String.IsNullOrEmpty(level)) + data.Add("Level", level.Truncate(60)); + + var requestInfo = ev.GetRequestInfo(); + if (requestInfo != null) + data.Add("Url", requestInfo.GetFullPath(true, true, true)); + + return new MailMessageData { Subject = subject, Data = data }; + } - public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - if (!ShouldHandle(ev)) - return null; + public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + if (!ShouldHandle(ev)) + return null; - string notificationType = "log message"; - if (isNew) - notificationType = "new log source"; - else if (isRegression) - notificationType = "log regression"; + string notificationType = "log message"; + if (isNew) + notificationType = "new log source"; + else if (isRegression) + notificationType = "log regression"; - if (isCritical) - notificationType = String.Concat("critical ", notificationType); + if (isCritical) + notificationType = String.Concat("critical ", notificationType); - string source = !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Global)"; - var attachment = new SlackMessage.SlackAttachment(ev) { - Fields = new List { + string source = !String.IsNullOrEmpty(ev.Source) ? ev.Source : "(Global)"; + var attachment = new SlackMessage.SlackAttachment(ev) { + Fields = new List { new SlackMessage.SlackAttachmentFields { Title = "Source", Value = source.Truncate(60) } } - }; - - if (!String.IsNullOrEmpty(ev.Message)) - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Message", Value = ev.Message.Truncate(60) }); - - string level = ev.GetLevel(); - if (!String.IsNullOrEmpty(level)) { - switch (level.ToLower()) { - case "trace": - case "debug": - attachment.Color = "#5cb85c"; - break; - case "info": - attachment.Color = "#5bc0de"; - break; - case "warn": - attachment.Color = "#f0ad4e"; - break; - case "error": - case "fatal": - attachment.Color = "#d9534f"; - break; - } - - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Level", Value = level.Truncate(60) }); + }; + + if (!String.IsNullOrEmpty(ev.Message)) + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Message", Value = ev.Message.Truncate(60) }); + + string level = ev.GetLevel(); + if (!String.IsNullOrEmpty(level)) { + switch (level.ToLower()) { + case "trace": + case "debug": + attachment.Color = "#5cb85c"; + break; + case "info": + attachment.Color = "#5bc0de"; + break; + case "warn": + attachment.Color = "#f0ad4e"; + break; + case "error": + case "fatal": + attachment.Color = "#d9534f"; + break; } - var requestInfo = ev.GetRequestInfo(); - if (requestInfo != null) - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Url", Value = requestInfo.GetFullPath(true, true, true) }); - - AddDefaultSlackFields(ev, attachment.Fields); - string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, source.Truncate(120))}*"; - return new SlackMessage(subject) { - Attachments = new List { attachment } - }; + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Level", Value = level.Truncate(60) }); } + + var requestInfo = ev.GetRequestInfo(); + if (requestInfo != null) + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Url", Value = requestInfo.GetFullPath(true, true, true) }); + + AddDefaultSlackFields(ev, attachment.Fields); + string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, source.Truncate(120))}*"; + return new SlackMessage(subject) { + Attachments = new List { attachment } + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/Default/99_DefaultFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/Default/99_DefaultFormattingPlugin.cs index 49c8270643..1421605401 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/Default/99_DefaultFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/Default/99_DefaultFormattingPlugin.cs @@ -1,97 +1,95 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - [Priority(99)] - public sealed class DefaultFormattingPlugin : FormattingPluginBase { - public DefaultFormattingPlugin(AppOptions options) : base(options) { } +namespace Exceptionless.Core.Plugins.Formatting; - public override string GetStackTitle(PersistentEvent ev) { - if (String.IsNullOrWhiteSpace(ev.Message) && ev.IsError()) - return "Unknown Error"; +[Priority(99)] +public sealed class DefaultFormattingPlugin : FormattingPluginBase { + public DefaultFormattingPlugin(AppOptions options) : base(options) { } - return ev.Message ?? ev.Source ?? $"{ev.Type} Event".TrimStart(); - } + public override string GetStackTitle(PersistentEvent ev) { + if (String.IsNullOrWhiteSpace(ev.Message) && ev.IsError()) + return "Unknown Error"; - public override SummaryData GetStackSummaryData(Stack stack) { - var data = new Dictionary { { "Type", stack.Type } }; + return ev.Message ?? ev.Source ?? $"{ev.Type} Event".TrimStart(); + } + + public override SummaryData GetStackSummaryData(Stack stack) { + var data = new Dictionary { { "Type", stack.Type } }; - if (stack.SignatureInfo.TryGetValue("Source", out string value)) - data.Add("Source", value); + if (stack.SignatureInfo.TryGetValue("Source", out string value)) + data.Add("Source", value); - return new SummaryData { TemplateKey = "stack-summary", Data = data }; - } + return new SummaryData { TemplateKey = "stack-summary", Data = data }; + } - public override SummaryData GetEventSummaryData(PersistentEvent ev) { - var data = new Dictionary { + public override SummaryData GetEventSummaryData(PersistentEvent ev) { + var data = new Dictionary { { "Message", GetStackTitle(ev) }, { "Source", ev.Source }, { "Type", ev.Type } }; - AddUserIdentitySummaryData(data, ev.GetUserIdentity()); + AddUserIdentitySummaryData(data, ev.GetUserIdentity()); - return new SummaryData { TemplateKey = "event-summary", Data = data }; - } + return new SummaryData { TemplateKey = "event-summary", Data = data }; + } - public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - string messageOrSource = !String.IsNullOrEmpty(ev.Message) ? ev.Message : ev.Source; - if (String.IsNullOrEmpty(messageOrSource)) - return null; + public override MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + string messageOrSource = !String.IsNullOrEmpty(ev.Message) ? ev.Message : ev.Source; + if (String.IsNullOrEmpty(messageOrSource)) + return null; - string notificationType = "Occurrence event"; - if (isNew) - notificationType = "New event"; - else if (isRegression) - notificationType = "Regression event"; + string notificationType = "Occurrence event"; + if (isNew) + notificationType = "New event"; + else if (isRegression) + notificationType = "Regression event"; - if (isCritical) - notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); + if (isCritical) + notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); - string subject = String.Concat(notificationType, ": ", messageOrSource).Truncate(120); - var data = new Dictionary(); - if (!String.IsNullOrEmpty(ev.Message)) - data.Add("Message", ev.Message.Truncate(60)); + string subject = String.Concat(notificationType, ": ", messageOrSource).Truncate(120); + var data = new Dictionary(); + if (!String.IsNullOrEmpty(ev.Message)) + data.Add("Message", ev.Message.Truncate(60)); - if (!String.IsNullOrEmpty(ev.Source)) - data.Add("Source", ev.Source.Truncate(60)); + if (!String.IsNullOrEmpty(ev.Source)) + data.Add("Source", ev.Source.Truncate(60)); - var requestInfo = ev.GetRequestInfo(); - if (requestInfo != null) - data.Add("Url", requestInfo.GetFullPath(true, true, true)); + var requestInfo = ev.GetRequestInfo(); + if (requestInfo != null) + data.Add("Url", requestInfo.GetFullPath(true, true, true)); - return new MailMessageData { Subject = subject, Data = data }; - } + return new MailMessageData { Subject = subject, Data = data }; + } - public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - string messageOrSource = !String.IsNullOrEmpty(ev.Message) ? ev.Message : ev.Source; - if (String.IsNullOrEmpty(messageOrSource)) - return null; + public override SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + string messageOrSource = !String.IsNullOrEmpty(ev.Message) ? ev.Message : ev.Source; + if (String.IsNullOrEmpty(messageOrSource)) + return null; - string notificationType = "Occurrence event"; - if (isNew) - notificationType = "New event"; - else if (isRegression) - notificationType = "Regression event"; + string notificationType = "Occurrence event"; + if (isNew) + notificationType = "New event"; + else if (isRegression) + notificationType = "Regression event"; - if (isCritical) - notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); + if (isCritical) + notificationType = String.Concat("Critical ", notificationType.ToLowerInvariant()); - var attachment = new SlackMessage.SlackAttachment(ev); - if (!String.IsNullOrEmpty(ev.Message)) - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Message", Value = ev.Message.Truncate(60) }); + var attachment = new SlackMessage.SlackAttachment(ev); + if (!String.IsNullOrEmpty(ev.Message)) + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Message", Value = ev.Message.Truncate(60) }); - if (!String.IsNullOrEmpty(ev.Source)) - attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Source", Value = ev.Source.Truncate(60) }); + if (!String.IsNullOrEmpty(ev.Source)) + attachment.Fields.Add(new SlackMessage.SlackAttachmentFields { Title = "Source", Value = ev.Source.Truncate(60) }); - AddDefaultSlackFields(ev, attachment.Fields); - string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, messageOrSource.Truncate(120))}*"; - return new SlackMessage(subject) { - Attachments = new List { attachment } - }; - } + AddDefaultSlackFields(ev, attachment.Fields); + string subject = $"[{project.Name}] A {notificationType}: *{GetSlackEventUrl(ev.Id, messageOrSource.Truncate(120))}*"; + return new SlackMessage(subject) { + Attachments = new List { attachment } + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginBase.cs b/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginBase.cs index 17d3b15c2e..1079e8a7ec 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginBase.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginBase.cs @@ -1,75 +1,73 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Plugins.Formatting { - public abstract class FormattingPluginBase : PluginBase, IFormattingPlugin { - public FormattingPluginBase(AppOptions options) : base(options) {} +namespace Exceptionless.Core.Plugins.Formatting; - public virtual SummaryData GetStackSummaryData(Stack stack) { - return null; - } +public abstract class FormattingPluginBase : PluginBase, IFormattingPlugin { + public FormattingPluginBase(AppOptions options) : base(options) { } - public virtual SummaryData GetEventSummaryData(PersistentEvent ev) { - return null; - } + public virtual SummaryData GetStackSummaryData(Stack stack) { + return null; + } + + public virtual SummaryData GetEventSummaryData(PersistentEvent ev) { + return null; + } - public virtual string GetStackTitle(PersistentEvent ev) { - return null; - } + public virtual string GetStackTitle(PersistentEvent ev) { + return null; + } - public virtual MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - return null; - } + public virtual MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + return null; + } - public virtual SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - return null; - } + public virtual SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + return null; + } - protected void AddDefaultSlackFields(PersistentEvent ev, List attachmentFields, bool includeUrl = true) { - var requestInfo = ev.GetRequestInfo(); - if (requestInfo != null && includeUrl) - attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Url", Value = requestInfo.GetFullPath(true, true, true) }); + protected void AddDefaultSlackFields(PersistentEvent ev, List attachmentFields, bool includeUrl = true) { + var requestInfo = ev.GetRequestInfo(); + if (requestInfo != null && includeUrl) + attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Url", Value = requestInfo.GetFullPath(true, true, true) }); - if (ev.Tags.Count > 0) - attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Tags", Value = String.Join(", ", ev.Tags), Short = true }); + if (ev.Tags.Count > 0) + attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Tags", Value = String.Join(", ", ev.Tags), Short = true }); - if (ev.Value.GetValueOrDefault() != 0) - attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Value", Value = ev.Value.ToString(), Short = true }); + if (ev.Value.GetValueOrDefault() != 0) + attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Value", Value = ev.Value.ToString(), Short = true }); - string version = ev.GetVersion(); - if (!String.IsNullOrEmpty(version)) - attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Version", Value = version, Short = true }); + string version = ev.GetVersion(); + if (!String.IsNullOrEmpty(version)) + attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Version", Value = version, Short = true }); - string baseUrl = _options.BaseURL; - var actions = new List { $"• {GetSlackEventUrl(ev.Id, "View Event")}" }; - actions.Add($"• <{baseUrl}/stack/{ev.StackId}/mark-fixed|Mark event as fixed>"); - actions.Add($"• <{baseUrl}/stack/{ev.StackId}/ignored|Stop sending notifications for this event>"); - actions.Add($"• <{baseUrl}/stack/{ev.StackId}/discarded|Discard future event occurrences>"); - actions.Add($"• <{baseUrl}/project/{ev.ProjectId}/manage?tab=integrations|Change your notification settings for this project>"); + string baseUrl = _options.BaseURL; + var actions = new List { $"• {GetSlackEventUrl(ev.Id, "View Event")}" }; + actions.Add($"• <{baseUrl}/stack/{ev.StackId}/mark-fixed|Mark event as fixed>"); + actions.Add($"• <{baseUrl}/stack/{ev.StackId}/ignored|Stop sending notifications for this event>"); + actions.Add($"• <{baseUrl}/stack/{ev.StackId}/discarded|Discard future event occurrences>"); + actions.Add($"• <{baseUrl}/project/{ev.ProjectId}/manage?tab=integrations|Change your notification settings for this project>"); - attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Other Actions", Value = String.Join("\n", actions) }); - } + attachmentFields.Add(new SlackMessage.SlackAttachmentFields { Title = "Other Actions", Value = String.Join("\n", actions) }); + } - protected string GetSlackEventUrl(string eventId, string message = null) { - var parts = new List { $"{_options.BaseURL}/event/{eventId}" }; - if (!String.IsNullOrEmpty(message)) - parts.Add($"|{message.Replace("&", "&").Replace("<", "<").Replace(">", ">")}"); + protected string GetSlackEventUrl(string eventId, string message = null) { + var parts = new List { $"{_options.BaseURL}/event/{eventId}" }; + if (!String.IsNullOrEmpty(message)) + parts.Add($"|{message.Replace("&", "&").Replace("<", "<").Replace(">", ">")}"); - return $"<{String.Join(String.Empty, parts)}>"; - } + return $"<{String.Join(String.Empty, parts)}>"; + } - protected void AddUserIdentitySummaryData(Dictionary data, UserInfo identity) { - if (identity == null) - return; + protected void AddUserIdentitySummaryData(Dictionary data, UserInfo identity) { + if (identity == null) + return; - if (!String.IsNullOrEmpty(identity.Identity)) - data.Add("Identity", identity.Identity); + if (!String.IsNullOrEmpty(identity.Identity)) + data.Add("Identity", identity.Identity); - if (!String.IsNullOrEmpty(identity.Name)) - data.Add("Name", identity.Name); - } + if (!String.IsNullOrEmpty(identity.Name)) + data.Add("Name", identity.Name); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginManager.cs b/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginManager.cs index 0c680f7493..89ee20c6eb 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginManager.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/FormattingPluginManager.cs @@ -1,96 +1,99 @@ -using System; -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.Formatting { - public class FormattingPluginManager : PluginManagerBase { - public FormattingPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } +namespace Exceptionless.Core.Plugins.Formatting; - /// - /// Runs through the formatting plugins to calculate an html summary for the stack based on the event data. - /// - public SummaryData GetStackSummaryData(Stack stack) { - foreach (var plugin in Plugins.Values.ToList()) { - try { - var result = plugin.GetStackSummaryData(stack); - if (result != null) - return result; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling GetStackSummaryHtml for stack {stack} in plugin {PluginName}: {Message}", stack.Id, plugin.Name, ex.Message); - } - } +public class FormattingPluginManager : PluginManagerBase { + public FormattingPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } - return null; + /// + /// Runs through the formatting plugins to calculate an html summary for the stack based on the event data. + /// + public SummaryData GetStackSummaryData(Stack stack) { + foreach (var plugin in Plugins.Values.ToList()) { + try { + var result = plugin.GetStackSummaryData(stack); + if (result != null) + return result; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling GetStackSummaryHtml for stack {stack} in plugin {PluginName}: {Message}", stack.Id, plugin.Name, ex.Message); + } } - /// - /// Runs through the formatting plugins to calculate an html summary for the event. - /// - public SummaryData GetEventSummaryData(PersistentEvent ev) { - foreach (var plugin in Plugins.Values.ToList()) { - try { - var result = plugin.GetEventSummaryData(ev); - if (result != null) - return result; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling GetEventSummaryHtml for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); - } - } + return null; + } - return null; + /// + /// Runs through the formatting plugins to calculate an html summary for the event. + /// + public SummaryData GetEventSummaryData(PersistentEvent ev) { + foreach (var plugin in Plugins.Values.ToList()) { + try { + var result = plugin.GetEventSummaryData(ev); + if (result != null) + return result; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling GetEventSummaryHtml for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); + } } - /// - /// Runs through the formatting plugins to calculate a stack title based on an event. - /// - public string GetStackTitle(PersistentEvent ev) { - foreach (var plugin in Plugins.Values.ToList()) { - try { - string result = plugin.GetStackTitle(ev); - if (!String.IsNullOrEmpty(result)) - return result; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling GetStackTitle for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); - } - } + return null; + } - return null; + /// + /// Runs through the formatting plugins to calculate a stack title based on an event. + /// + public string GetStackTitle(PersistentEvent ev) { + foreach (var plugin in Plugins.Values.ToList()) { + try { + string result = plugin.GetStackTitle(ev); + if (!String.IsNullOrEmpty(result)) + return result; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling GetStackTitle for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); + } } - /// - /// Runs through the formatting plugins to get notification mail content for an event. - /// - public MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { - foreach (var plugin in Plugins.Values.ToList()) { - try { - var result = plugin.GetEventNotificationMailMessageData(ev, isCritical, isNew, isRegression); - if (result != null) - return result; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling GetEventNotificationMailMessage for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); - } - } + return null; + } - return null; + /// + /// Runs through the formatting plugins to get notification mail content for an event. + /// + public MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression) { + foreach (var plugin in Plugins.Values.ToList()) { + try { + var result = plugin.GetEventNotificationMailMessageData(ev, isCritical, isNew, isRegression); + if (result != null) + return result; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling GetEventNotificationMailMessage for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); + } } - /// - /// Runs through the formatting plugins to get notification mail content for an event. - /// - public SlackMessage GetSlackEventNotificationMessage(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { - foreach (var plugin in Plugins.Values.ToList()) { - try { - var message = plugin.GetSlackEventNotification(ev, project, isCritical, isNew, isRegression); - if (message != null) - return message; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling GetSlackEventNotificationMessage for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); - } - } + return null; + } - return null; + /// + /// Runs through the formatting plugins to get notification mail content for an event. + /// + public SlackMessage GetSlackEventNotificationMessage(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression) { + foreach (var plugin in Plugins.Values.ToList()) { + try { + var message = plugin.GetSlackEventNotification(ev, project, isCritical, isNew, isRegression); + if (message != null) + return message; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling GetSlackEventNotificationMessage for Event {id} in plugin {PluginName}: {Message}", ev.Id, plugin.Name, ex.Message); + } } + + return null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/Formatting/IFormattingPlugin.cs b/src/Exceptionless.Core/Plugins/Formatting/IFormattingPlugin.cs index 235933c7fa..b0858a881f 100644 --- a/src/Exceptionless.Core/Plugins/Formatting/IFormattingPlugin.cs +++ b/src/Exceptionless.Core/Plugins/Formatting/IFormattingPlugin.cs @@ -1,11 +1,11 @@ using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.Formatting { - public interface IFormattingPlugin : IPlugin { - string GetStackTitle(PersistentEvent ev); - SummaryData GetStackSummaryData(Stack stack); - SummaryData GetEventSummaryData(PersistentEvent ev); - MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression); - SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression); - } -} \ No newline at end of file +namespace Exceptionless.Core.Plugins.Formatting; + +public interface IFormattingPlugin : IPlugin { + string GetStackTitle(PersistentEvent ev); + SummaryData GetStackSummaryData(Stack stack); + SummaryData GetEventSummaryData(PersistentEvent ev); + MailMessageData GetEventNotificationMailMessageData(PersistentEvent ev, bool isCritical, bool isNew, bool isRegression); + SlackMessage GetSlackEventNotification(PersistentEvent ev, Project project, bool isCritical, bool isNew, bool isRegression); +} diff --git a/src/Exceptionless.Core/Plugins/IPlugin.cs b/src/Exceptionless.Core/Plugins/IPlugin.cs index d5d6c90f22..051b443f84 100644 --- a/src/Exceptionless.Core/Plugins/IPlugin.cs +++ b/src/Exceptionless.Core/Plugins/IPlugin.cs @@ -1,26 +1,24 @@ -using System; -using System.Linq; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins { - public interface IPlugin { - string Name { get; } - bool Enabled { get; } - } +namespace Exceptionless.Core.Plugins; - public abstract class PluginBase : IPlugin { - protected readonly ILogger _logger; - protected readonly AppOptions _options; +public interface IPlugin { + string Name { get; } + bool Enabled { get; } +} - public PluginBase(AppOptions options, ILoggerFactory loggerFactory = null) { - _options = options; - var type = GetType(); - Name = type.Name; - Enabled = !_options.DisabledPlugins.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase); - _logger = loggerFactory?.CreateLogger(type); - } +public abstract class PluginBase : IPlugin { + protected readonly ILogger _logger; + protected readonly AppOptions _options; - public string Name { get; } - public bool Enabled { get; } + public PluginBase(AppOptions options, ILoggerFactory loggerFactory = null) { + _options = options; + var type = GetType(); + Name = type.Name; + Enabled = !_options.DisabledPlugins.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase); + _logger = loggerFactory?.CreateLogger(type); } -} \ No newline at end of file + + public string Name { get; } + public bool Enabled { get; } +} diff --git a/src/Exceptionless.Core/Plugins/PluginManagerBase.cs b/src/Exceptionless.Core/Plugins/PluginManagerBase.cs index ff808b6a14..998ac340f9 100644 --- a/src/Exceptionless.Core/Plugins/PluginManagerBase.cs +++ b/src/Exceptionless.Core/Plugins/PluginManagerBase.cs @@ -1,57 +1,55 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Helpers; using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins { - public abstract class PluginManagerBase where TPlugin : class, IPlugin { - protected readonly IServiceProvider _serviceProvider; - private readonly AppOptions _options; - protected readonly string _metricPrefix; - protected readonly IMetricsClient _metricsClient; - protected readonly ILogger _logger; - - public PluginManagerBase(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) { - var type = GetType(); - _metricPrefix = String.Concat(type.Name.ToLower(), "."); - _metricsClient = metricsClient ?? new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = loggerFactory }); - _logger = loggerFactory?.CreateLogger(type); - _serviceProvider = serviceProvider; - _options = options; - - Plugins = new SortedList(); - LoadDefaultPlugins(); - } +namespace Exceptionless.Core.Plugins; + +public abstract class PluginManagerBase where TPlugin : class, IPlugin { + protected readonly IServiceProvider _serviceProvider; + private readonly AppOptions _options; + protected readonly string _metricPrefix; + protected readonly IMetricsClient _metricsClient; + protected readonly ILogger _logger; + + public PluginManagerBase(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) { + var type = GetType(); + _metricPrefix = String.Concat(type.Name.ToLower(), "."); + _metricsClient = metricsClient ?? new InMemoryMetricsClient(new InMemoryMetricsClientOptions { LoggerFactory = loggerFactory }); + _logger = loggerFactory?.CreateLogger(type); + _serviceProvider = serviceProvider; + _options = options; + + Plugins = new SortedList(); + LoadDefaultPlugins(); + } - public SortedList Plugins { get; private set; } + public SortedList Plugins { get; private set; } - public void AddPlugin(Type pluginType) { - var attr = pluginType.GetCustomAttributes(typeof(PriorityAttribute), true).FirstOrDefault() as PriorityAttribute; - int priority = attr?.Priority ?? 0; + public void AddPlugin(Type pluginType) { + var attr = pluginType.GetCustomAttributes(typeof(PriorityAttribute), true).FirstOrDefault() as PriorityAttribute; + int priority = attr?.Priority ?? 0; - var plugin = (TPlugin)_serviceProvider.GetService(pluginType); - Plugins.Add(priority, plugin); - } + var plugin = (TPlugin)_serviceProvider.GetService(pluginType); + Plugins.Add(priority, plugin); + } - private void LoadDefaultPlugins() { - var pluginTypes = TypeHelper.GetDerivedTypes(); - - foreach (var type in pluginTypes) { - if (_options.DisabledPlugins.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase)) { - _logger.LogWarning("Plugin {TypeName} is currently disabled and won't be executed.", type.Name); - continue; - } - - try { - AddPlugin(type); - } catch (Exception ex) { - _logger.LogError(ex, "Unable to instantiate plugin of type {TypeFullName}: {Message}", type.FullName, ex.Message); - throw; - } + private void LoadDefaultPlugins() { + var pluginTypes = TypeHelper.GetDerivedTypes(); + + foreach (var type in pluginTypes) { + if (_options.DisabledPlugins.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase)) { + _logger.LogWarning("Plugin {TypeName} is currently disabled and won't be executed.", type.Name); + continue; + } + + try { + AddPlugin(type); + } + catch (Exception ex) { + _logger.LogError(ex, "Unable to instantiate plugin of type {TypeFullName}: {Message}", type.FullName, ex.Message); + throw; } } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/WebHook/Default/LoadDefaults.cs b/src/Exceptionless.Core/Plugins/WebHook/Default/LoadDefaults.cs index 7cbcb2aab4..61dc37ca79 100644 --- a/src/Exceptionless.Core/Plugins/WebHook/Default/LoadDefaults.cs +++ b/src/Exceptionless.Core/Plugins/WebHook/Default/LoadDefaults.cs @@ -1,65 +1,63 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Pipeline; using Exceptionless.Core.Repositories; using Foundatio.Repositories; -namespace Exceptionless.Core.Plugins.WebHook { - [Priority(0)] - public sealed class LoadDefaults : WebHookDataPluginBase { - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IStackRepository _stackRepository; +namespace Exceptionless.Core.Plugins.WebHook; - public LoadDefaults(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IStackRepository stackRepository, AppOptions options) : base(options) { - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _stackRepository = stackRepository; - } +[Priority(0)] +public sealed class LoadDefaults : WebHookDataPluginBase { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IStackRepository _stackRepository; - public override async Task CreateFromEventAsync(WebHookDataContext ctx) { - if (ctx.Event == null) - throw new ArgumentException("Event cannot be null."); + public LoadDefaults(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IStackRepository stackRepository, AppOptions options) : base(options) { + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _stackRepository = stackRepository; + } + + public override async Task CreateFromEventAsync(WebHookDataContext ctx) { + if (ctx.Event == null) + throw new ArgumentException("Event cannot be null."); - if (ctx.Project == null) - ctx.Project = await _projectRepository.GetByIdAsync(ctx.Event.ProjectId, o => o.Cache()).AnyContext(); + if (ctx.Project == null) + ctx.Project = await _projectRepository.GetByIdAsync(ctx.Event.ProjectId, o => o.Cache()).AnyContext(); - if (ctx.Project == null) - throw new ArgumentException("Project not found."); + if (ctx.Project == null) + throw new ArgumentException("Project not found."); - if (ctx.Organization == null) - ctx.Organization = await _organizationRepository.GetByIdAsync(ctx.Event.OrganizationId, o => o.Cache()).AnyContext(); + if (ctx.Organization == null) + ctx.Organization = await _organizationRepository.GetByIdAsync(ctx.Event.OrganizationId, o => o.Cache()).AnyContext(); - if (ctx.Organization == null) - throw new ArgumentException("Organization not found."); + if (ctx.Organization == null) + throw new ArgumentException("Organization not found."); - if (ctx.Stack == null) - ctx.Stack = await _stackRepository.GetByIdAsync(ctx.Event.StackId).AnyContext(); + if (ctx.Stack == null) + ctx.Stack = await _stackRepository.GetByIdAsync(ctx.Event.StackId).AnyContext(); - if (ctx.Stack == null) - throw new ArgumentException("Stack not found."); + if (ctx.Stack == null) + throw new ArgumentException("Stack not found."); - return null; - } + return null; + } - public override async Task CreateFromStackAsync(WebHookDataContext ctx) { - if (ctx.Stack == null) - throw new ArgumentException("Stack cannot be null."); + public override async Task CreateFromStackAsync(WebHookDataContext ctx) { + if (ctx.Stack == null) + throw new ArgumentException("Stack cannot be null."); - if (ctx.Project == null) - ctx.Project = await _projectRepository.GetByIdAsync(ctx.Stack.ProjectId, o => o.Cache()).AnyContext(); + if (ctx.Project == null) + ctx.Project = await _projectRepository.GetByIdAsync(ctx.Stack.ProjectId, o => o.Cache()).AnyContext(); - if (ctx.Project == null) - throw new ArgumentException("Project not found."); + if (ctx.Project == null) + throw new ArgumentException("Project not found."); - if (ctx.Organization == null) - ctx.Organization = await _organizationRepository.GetByIdAsync(ctx.Stack.OrganizationId, o => o.Cache()).AnyContext(); + if (ctx.Organization == null) + ctx.Organization = await _organizationRepository.GetByIdAsync(ctx.Stack.OrganizationId, o => o.Cache()).AnyContext(); - if (ctx.Organization == null) - throw new ArgumentException("Organization not found."); + if (ctx.Organization == null) + throw new ArgumentException("Organization not found."); - return null; - } + return null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/WebHook/Default/V1_WebHookDataPlugin.cs b/src/Exceptionless.Core/Plugins/WebHook/Default/V1_WebHookDataPlugin.cs index 1f08d85cb8..d8e419d9a8 100644 --- a/src/Exceptionless.Core/Plugins/WebHook/Default/V1_WebHookDataPlugin.cs +++ b/src/Exceptionless.Core/Plugins/WebHook/Default/V1_WebHookDataPlugin.cs @@ -1,146 +1,144 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Pipeline; -namespace Exceptionless.Core.Plugins.WebHook { - [Priority(10)] - public sealed class VersionOne : WebHookDataPluginBase { - public VersionOne(AppOptions options) : base(options) {} +namespace Exceptionless.Core.Plugins.WebHook; - public override Task CreateFromEventAsync(WebHookDataContext ctx) { - if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version1)) - return Task.FromResult(null); +[Priority(10)] +public sealed class VersionOne : WebHookDataPluginBase { + public VersionOne(AppOptions options) : base(options) { } - var error = ctx.Event.GetError(); - if (error == null) - return Task.FromResult(null); + public override Task CreateFromEventAsync(WebHookDataContext ctx) { + if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version1)) + return Task.FromResult(null); - var requestInfo = ctx.Event.GetRequestInfo(); - var environmentInfo = ctx.Event.GetEnvironmentInfo(); + var error = ctx.Event.GetError(); + if (error == null) + return Task.FromResult(null); - return Task.FromResult(new VersionOneWebHookEvent(_options.BaseURL) { - Id = ctx.Event.Id, - OccurrenceDate = ctx.Event.Date, - Tags = ctx.Event.Tags, - MachineName = environmentInfo?.MachineName, - RequestPath = requestInfo?.GetFullPath(), - IpAddress = requestInfo != null ? requestInfo.ClientIpAddress : environmentInfo?.IpAddress, - Message = error.Message, - Type = error.Type, - Code = error.Code, - TargetMethod = error.TargetMethod?.GetFullName(), - ProjectId = ctx.Event.ProjectId, - ProjectName = ctx.Project.Name, - OrganizationId = ctx.Event.OrganizationId, - OrganizationName = ctx.Organization.Name, - ErrorStackId = ctx.Event.StackId, - ErrorStackStatus = ctx.Stack.Status, - ErrorStackTitle = ctx.Stack.Title, - ErrorStackDescription = ctx.Stack.Description, - ErrorStackTags = ctx.Stack.Tags, - TotalOccurrences = ctx.Stack.TotalOccurrences, - FirstOccurrence = ctx.Stack.FirstOccurrence, - LastOccurrence = ctx.Stack.LastOccurrence, - DateFixed = ctx.Stack.DateFixed, - IsRegression = ctx.IsRegression, - IsNew = ctx.IsNew - }); - } + var requestInfo = ctx.Event.GetRequestInfo(); + var environmentInfo = ctx.Event.GetEnvironmentInfo(); - public override Task CreateFromStackAsync(WebHookDataContext ctx) { - if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version1)) - return Task.FromResult(null); + return Task.FromResult(new VersionOneWebHookEvent(_options.BaseURL) { + Id = ctx.Event.Id, + OccurrenceDate = ctx.Event.Date, + Tags = ctx.Event.Tags, + MachineName = environmentInfo?.MachineName, + RequestPath = requestInfo?.GetFullPath(), + IpAddress = requestInfo != null ? requestInfo.ClientIpAddress : environmentInfo?.IpAddress, + Message = error.Message, + Type = error.Type, + Code = error.Code, + TargetMethod = error.TargetMethod?.GetFullName(), + ProjectId = ctx.Event.ProjectId, + ProjectName = ctx.Project.Name, + OrganizationId = ctx.Event.OrganizationId, + OrganizationName = ctx.Organization.Name, + ErrorStackId = ctx.Event.StackId, + ErrorStackStatus = ctx.Stack.Status, + ErrorStackTitle = ctx.Stack.Title, + ErrorStackDescription = ctx.Stack.Description, + ErrorStackTags = ctx.Stack.Tags, + TotalOccurrences = ctx.Stack.TotalOccurrences, + FirstOccurrence = ctx.Stack.FirstOccurrence, + LastOccurrence = ctx.Stack.LastOccurrence, + DateFixed = ctx.Stack.DateFixed, + IsRegression = ctx.IsRegression, + IsNew = ctx.IsNew + }); + } - return Task.FromResult(new VersionOneWebHookStack(_options.BaseURL) { - Id = ctx.Stack.Id, - Status = ctx.Stack.Status, - Title = ctx.Stack.Title, - Description = ctx.Stack.Description, - Tags = ctx.Stack.Tags, - RequestPath = ctx.Stack.SignatureInfo.ContainsKey("Path") ? ctx.Stack.SignatureInfo["Path"] : null, - Type = ctx.Stack.SignatureInfo.ContainsKey("ExceptionType") ? ctx.Stack.SignatureInfo["ExceptionType"] : null, - TargetMethod = ctx.Stack.SignatureInfo.ContainsKey("Method") ? ctx.Stack.SignatureInfo["Method"] : null, - ProjectId = ctx.Stack.ProjectId, - ProjectName = ctx.Project.Name, - OrganizationId = ctx.Stack.OrganizationId, - OrganizationName = ctx.Organization.Name, - TotalOccurrences = ctx.Stack.TotalOccurrences, - FirstOccurrence = ctx.Stack.FirstOccurrence, - LastOccurrence = ctx.Stack.LastOccurrence, - DateFixed = ctx.Stack.DateFixed, - IsRegression = ctx.Stack.Status == StackStatus.Regressed, - IsCritical = ctx.Stack.OccurrencesAreCritical || ctx.Stack.Tags != null && ctx.Stack.Tags.Contains("Critical"), - FixedInVersion = ctx.Stack.FixedInVersion - }); - } + public override Task CreateFromStackAsync(WebHookDataContext ctx) { + if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version1)) + return Task.FromResult(null); - public class VersionOneWebHookEvent { - private readonly string _baseUrl; + return Task.FromResult(new VersionOneWebHookStack(_options.BaseURL) { + Id = ctx.Stack.Id, + Status = ctx.Stack.Status, + Title = ctx.Stack.Title, + Description = ctx.Stack.Description, + Tags = ctx.Stack.Tags, + RequestPath = ctx.Stack.SignatureInfo.ContainsKey("Path") ? ctx.Stack.SignatureInfo["Path"] : null, + Type = ctx.Stack.SignatureInfo.ContainsKey("ExceptionType") ? ctx.Stack.SignatureInfo["ExceptionType"] : null, + TargetMethod = ctx.Stack.SignatureInfo.ContainsKey("Method") ? ctx.Stack.SignatureInfo["Method"] : null, + ProjectId = ctx.Stack.ProjectId, + ProjectName = ctx.Project.Name, + OrganizationId = ctx.Stack.OrganizationId, + OrganizationName = ctx.Organization.Name, + TotalOccurrences = ctx.Stack.TotalOccurrences, + FirstOccurrence = ctx.Stack.FirstOccurrence, + LastOccurrence = ctx.Stack.LastOccurrence, + DateFixed = ctx.Stack.DateFixed, + IsRegression = ctx.Stack.Status == StackStatus.Regressed, + IsCritical = ctx.Stack.OccurrencesAreCritical || ctx.Stack.Tags != null && ctx.Stack.Tags.Contains("Critical"), + FixedInVersion = ctx.Stack.FixedInVersion + }); + } - public VersionOneWebHookEvent(string baseUrl) { - _baseUrl = baseUrl; - } + public class VersionOneWebHookEvent { + private readonly string _baseUrl; - public string Id { get; set; } - public string Url => String.Concat(_baseUrl, "/event/", Id); - public DateTimeOffset OccurrenceDate { get; set; } - public TagSet Tags { get; set; } - public string MachineName { get; set; } - public string RequestPath { get; set; } - public string IpAddress { get; set; } - public string Message { get; set; } - public string Type { get; set; } - public string Code { get; set; } - public string TargetMethod { get; set; } - public string ProjectId { get; set; } - public string ProjectName { get; set; } - public string OrganizationId { get; set; } - public string OrganizationName { get; set; } - public string ErrorStackId { get; set; } - public StackStatus ErrorStackStatus { get; set; } - public string ErrorStackUrl => String.Concat(_baseUrl, "/stack/", ErrorStackId); - public string ErrorStackTitle { get; set; } - public string ErrorStackDescription { get; set; } - public TagSet ErrorStackTags { get; set; } - public int TotalOccurrences { get; set; } - public DateTime FirstOccurrence { get; set; } - public DateTime LastOccurrence { get; set; } - public DateTime? DateFixed { get; set; } - public bool IsNew { get; set; } - public bool IsRegression { get; set; } - public bool IsCritical => Tags != null && Tags.Contains("Critical"); + public VersionOneWebHookEvent(string baseUrl) { + _baseUrl = baseUrl; } - public class VersionOneWebHookStack { - private readonly string _baseUrl; - - public VersionOneWebHookStack(string baseUrl) { - _baseUrl = baseUrl; - } + public string Id { get; set; } + public string Url => String.Concat(_baseUrl, "/event/", Id); + public DateTimeOffset OccurrenceDate { get; set; } + public TagSet Tags { get; set; } + public string MachineName { get; set; } + public string RequestPath { get; set; } + public string IpAddress { get; set; } + public string Message { get; set; } + public string Type { get; set; } + public string Code { get; set; } + public string TargetMethod { get; set; } + public string ProjectId { get; set; } + public string ProjectName { get; set; } + public string OrganizationId { get; set; } + public string OrganizationName { get; set; } + public string ErrorStackId { get; set; } + public StackStatus ErrorStackStatus { get; set; } + public string ErrorStackUrl => String.Concat(_baseUrl, "/stack/", ErrorStackId); + public string ErrorStackTitle { get; set; } + public string ErrorStackDescription { get; set; } + public TagSet ErrorStackTags { get; set; } + public int TotalOccurrences { get; set; } + public DateTime FirstOccurrence { get; set; } + public DateTime LastOccurrence { get; set; } + public DateTime? DateFixed { get; set; } + public bool IsNew { get; set; } + public bool IsRegression { get; set; } + public bool IsCritical => Tags != null && Tags.Contains("Critical"); + } - public string Id { get; set; } - public StackStatus Status { get; set; } - public string Url => String.Concat(_baseUrl, "/stack/", Id); - public string Title { get; set; } - public string Description { get; set; } + public class VersionOneWebHookStack { + private readonly string _baseUrl; - public TagSet Tags { get; set; } - public string RequestPath { get; set; } - public string Type { get; set; } - public string TargetMethod { get; set; } - public string ProjectId { get; set; } - public string ProjectName { get; set; } - public string OrganizationId { get; set; } - public string OrganizationName { get; set; } - public int TotalOccurrences { get; set; } - public DateTime FirstOccurrence { get; set; } - public DateTime LastOccurrence { get; set; } - public DateTime? DateFixed { get; set; } - public string FixedInVersion { get; set; } - public bool IsRegression { get; set; } - public bool IsCritical { get; set; } + public VersionOneWebHookStack(string baseUrl) { + _baseUrl = baseUrl; } + + public string Id { get; set; } + public StackStatus Status { get; set; } + public string Url => String.Concat(_baseUrl, "/stack/", Id); + public string Title { get; set; } + public string Description { get; set; } + + public TagSet Tags { get; set; } + public string RequestPath { get; set; } + public string Type { get; set; } + public string TargetMethod { get; set; } + public string ProjectId { get; set; } + public string ProjectName { get; set; } + public string OrganizationId { get; set; } + public string OrganizationName { get; set; } + public int TotalOccurrences { get; set; } + public DateTime FirstOccurrence { get; set; } + public DateTime LastOccurrence { get; set; } + public DateTime? DateFixed { get; set; } + public string FixedInVersion { get; set; } + public bool IsRegression { get; set; } + public bool IsCritical { get; set; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/WebHook/Default/V2_WebHookDataPlugin.cs b/src/Exceptionless.Core/Plugins/WebHook/Default/V2_WebHookDataPlugin.cs index c772c70123..58864048f2 100644 --- a/src/Exceptionless.Core/Plugins/WebHook/Default/V2_WebHookDataPlugin.cs +++ b/src/Exceptionless.Core/Plugins/WebHook/Default/V2_WebHookDataPlugin.cs @@ -1,66 +1,64 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Pipeline; +using Exceptionless.Core.Pipeline; using Exceptionless.Core.Models; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.WebHook { - [Priority(20)] - public sealed class VersionTwo : WebHookDataPluginBase { - public VersionTwo(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.WebHook; - public override Task CreateFromEventAsync(WebHookDataContext ctx) { - if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version2)) - return Task.FromResult(null); +[Priority(20)] +public sealed class VersionTwo : WebHookDataPluginBase { + public VersionTwo(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - return Task.FromResult(new WebHookEvent(_options.BaseURL) { - Id = ctx.Event.Id, - OccurrenceDate = ctx.Event.Date, - Tags = ctx.Event.Tags, - Message = ctx.Event.Message, - Type = ctx.Event.Type, - Source = ctx.Event.Source, - ProjectId = ctx.Event.ProjectId, - ProjectName = ctx.Project.Name, - OrganizationId = ctx.Event.OrganizationId, - OrganizationName = ctx.Organization.Name, - StackId = ctx.Event.StackId, - StackTitle = ctx.Stack.Title, - StackDescription = ctx.Stack.Description, - StackTags = ctx.Stack.Tags, - TotalOccurrences = ctx.Stack.TotalOccurrences, - FirstOccurrence = ctx.Stack.FirstOccurrence, - LastOccurrence = ctx.Stack.LastOccurrence, - DateFixed = ctx.Stack.DateFixed, - IsRegression = ctx.IsRegression, - IsNew = ctx.IsNew - }); - } + public override Task CreateFromEventAsync(WebHookDataContext ctx) { + if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version2)) + return Task.FromResult(null); - public override Task CreateFromStackAsync(WebHookDataContext ctx) { - if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version2)) - return Task.FromResult(null); + return Task.FromResult(new WebHookEvent(_options.BaseURL) { + Id = ctx.Event.Id, + OccurrenceDate = ctx.Event.Date, + Tags = ctx.Event.Tags, + Message = ctx.Event.Message, + Type = ctx.Event.Type, + Source = ctx.Event.Source, + ProjectId = ctx.Event.ProjectId, + ProjectName = ctx.Project.Name, + OrganizationId = ctx.Event.OrganizationId, + OrganizationName = ctx.Organization.Name, + StackId = ctx.Event.StackId, + StackTitle = ctx.Stack.Title, + StackDescription = ctx.Stack.Description, + StackTags = ctx.Stack.Tags, + TotalOccurrences = ctx.Stack.TotalOccurrences, + FirstOccurrence = ctx.Stack.FirstOccurrence, + LastOccurrence = ctx.Stack.LastOccurrence, + DateFixed = ctx.Stack.DateFixed, + IsRegression = ctx.IsRegression, + IsNew = ctx.IsNew + }); + } + + public override Task CreateFromStackAsync(WebHookDataContext ctx) { + if (!String.Equals(ctx.Version, Models.WebHook.KnownVersions.Version2)) + return Task.FromResult(null); - return Task.FromResult(new WebHookStack(_options.BaseURL) { - Id = ctx.Stack.Id, - Title = ctx.Stack.Title, - Description = ctx.Stack.Description, - Tags = ctx.Stack.Tags, - RequestPath = ctx.Stack.SignatureInfo.ContainsKey("Path") ? ctx.Stack.SignatureInfo["Path"] : null, - Type = ctx.Stack.SignatureInfo.ContainsKey("ExceptionType") ? ctx.Stack.SignatureInfo["ExceptionType"] : null, - TargetMethod = ctx.Stack.SignatureInfo.ContainsKey("Method") ? ctx.Stack.SignatureInfo["Method"] : null, - ProjectId = ctx.Stack.ProjectId, - ProjectName = ctx.Project.Name, - OrganizationId = ctx.Stack.OrganizationId, - OrganizationName = ctx.Organization.Name, - TotalOccurrences = ctx.Stack.TotalOccurrences, - FirstOccurrence = ctx.Stack.FirstOccurrence, - LastOccurrence = ctx.Stack.LastOccurrence, - DateFixed = ctx.Stack.DateFixed, - IsRegression = ctx.Stack.Status == StackStatus.Regressed, - IsCritical = ctx.Stack.OccurrencesAreCritical || ctx.Stack.Tags != null && ctx.Stack.Tags.Contains("Critical"), - FixedInVersion = ctx.Stack.FixedInVersion - }); - } + return Task.FromResult(new WebHookStack(_options.BaseURL) { + Id = ctx.Stack.Id, + Title = ctx.Stack.Title, + Description = ctx.Stack.Description, + Tags = ctx.Stack.Tags, + RequestPath = ctx.Stack.SignatureInfo.ContainsKey("Path") ? ctx.Stack.SignatureInfo["Path"] : null, + Type = ctx.Stack.SignatureInfo.ContainsKey("ExceptionType") ? ctx.Stack.SignatureInfo["ExceptionType"] : null, + TargetMethod = ctx.Stack.SignatureInfo.ContainsKey("Method") ? ctx.Stack.SignatureInfo["Method"] : null, + ProjectId = ctx.Stack.ProjectId, + ProjectName = ctx.Project.Name, + OrganizationId = ctx.Stack.OrganizationId, + OrganizationName = ctx.Organization.Name, + TotalOccurrences = ctx.Stack.TotalOccurrences, + FirstOccurrence = ctx.Stack.FirstOccurrence, + LastOccurrence = ctx.Stack.LastOccurrence, + DateFixed = ctx.Stack.DateFixed, + IsRegression = ctx.Stack.Status == StackStatus.Regressed, + IsCritical = ctx.Stack.OccurrencesAreCritical || ctx.Stack.Tags != null && ctx.Stack.Tags.Contains("Critical"), + FixedInVersion = ctx.Stack.FixedInVersion + }); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Plugins/WebHook/IWebHookDataPlugin.cs b/src/Exceptionless.Core/Plugins/WebHook/IWebHookDataPlugin.cs index c4c2963027..e0837bfb38 100644 --- a/src/Exceptionless.Core/Plugins/WebHook/IWebHookDataPlugin.cs +++ b/src/Exceptionless.Core/Plugins/WebHook/IWebHookDataPlugin.cs @@ -1,8 +1,6 @@ -using System.Threading.Tasks; +namespace Exceptionless.Core.Plugins.WebHook; -namespace Exceptionless.Core.Plugins.WebHook { - public interface IWebHookDataPlugin : IPlugin { - Task CreateFromEventAsync(WebHookDataContext ctx); - Task CreateFromStackAsync(WebHookDataContext ctx); - } +public interface IWebHookDataPlugin : IPlugin { + Task CreateFromEventAsync(WebHookDataContext ctx); + Task CreateFromStackAsync(WebHookDataContext ctx); } diff --git a/src/Exceptionless.Core/Plugins/WebHook/WebHookDataContext.cs b/src/Exceptionless.Core/Plugins/WebHook/WebHookDataContext.cs index 18c3444435..2e042dc7e1 100644 --- a/src/Exceptionless.Core/Plugins/WebHook/WebHookDataContext.cs +++ b/src/Exceptionless.Core/Plugins/WebHook/WebHookDataContext.cs @@ -1,35 +1,34 @@ -using System; -using Exceptionless.Core.Utility; +using Exceptionless.Core.Utility; using Exceptionless.Core.Models; -namespace Exceptionless.Core.Plugins.WebHook { - public class WebHookDataContext : ExtensibleObject { - public WebHookDataContext(string version, PersistentEvent ev, Organization organization = null, Project project = null, Stack stack = null, bool isNew = false, bool isRegression = false) { - Version = version ?? throw new ArgumentException("Version cannot be null.", nameof(version)); - Organization = organization; - Project = project; - Stack = stack; - Event = ev ?? throw new ArgumentException("Event cannot be null.", nameof(ev)); - IsNew = isNew; - IsRegression = isRegression; - } +namespace Exceptionless.Core.Plugins.WebHook; - public WebHookDataContext(string version, Stack stack, Organization organization = null, Project project = null, bool isNew = false, bool isRegression = false) { - Version = version ?? throw new ArgumentException("Version cannot be null.", nameof(version)); - Organization = organization; - Project = project; - Stack = stack ?? throw new ArgumentException("Stack cannot be null.", nameof(stack)); - IsNew = isNew; - IsRegression = isRegression; - } - - public PersistentEvent Event { get; set; } - public Stack Stack { get; set; } - public Organization Organization { get; set; } - public Project Project { get; set; } +public class WebHookDataContext : ExtensibleObject { + public WebHookDataContext(string version, PersistentEvent ev, Organization organization = null, Project project = null, Stack stack = null, bool isNew = false, bool isRegression = false) { + Version = version ?? throw new ArgumentException("Version cannot be null.", nameof(version)); + Organization = organization; + Project = project; + Stack = stack; + Event = ev ?? throw new ArgumentException("Event cannot be null.", nameof(ev)); + IsNew = isNew; + IsRegression = isRegression; + } - public string Version { get; set; } - public bool IsNew { get; set; } - public bool IsRegression { get; set; } + public WebHookDataContext(string version, Stack stack, Organization organization = null, Project project = null, bool isNew = false, bool isRegression = false) { + Version = version ?? throw new ArgumentException("Version cannot be null.", nameof(version)); + Organization = organization; + Project = project; + Stack = stack ?? throw new ArgumentException("Stack cannot be null.", nameof(stack)); + IsNew = isNew; + IsRegression = isRegression; } -} \ No newline at end of file + + public PersistentEvent Event { get; set; } + public Stack Stack { get; set; } + public Organization Organization { get; set; } + public Project Project { get; set; } + + public string Version { get; set; } + public bool IsNew { get; set; } + public bool IsRegression { get; set; } +} diff --git a/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginBase.cs b/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginBase.cs index e3ac52f2fb..5f2b041a3d 100644 --- a/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginBase.cs +++ b/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginBase.cs @@ -1,13 +1,12 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.WebHook { - public abstract class WebHookDataPluginBase : PluginBase, IWebHookDataPlugin { - protected WebHookDataPluginBase(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } +namespace Exceptionless.Core.Plugins.WebHook; - public abstract Task CreateFromEventAsync(WebHookDataContext ctx); +public abstract class WebHookDataPluginBase : PluginBase, IWebHookDataPlugin { + protected WebHookDataPluginBase(AppOptions options, ILoggerFactory loggerFactory = null) : base(options, loggerFactory) { } - public abstract Task CreateFromStackAsync(WebHookDataContext ctx); + public abstract Task CreateFromEventAsync(WebHookDataContext ctx); + + public abstract Task CreateFromStackAsync(WebHookDataContext ctx); - } } diff --git a/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginManager.cs b/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginManager.cs index 48ac11724b..0fa3f475d8 100644 --- a/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginManager.cs +++ b/src/Exceptionless.Core/Plugins/WebHook/WebHookDataPluginManager.cs @@ -1,55 +1,55 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Metrics; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Plugins.WebHook { - public class WebHookDataPluginManager : PluginManagerBase { - public WebHookDataPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) {} - - /// - /// Runs all of the event plugins create method. - /// - public async Task CreateFromEventAsync(WebHookDataContext context) { - string metricPrefix = String.Concat(_metricPrefix, nameof(CreateFromEventAsync).ToLower(), "."); - foreach (var plugin in Plugins.Values) { - string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); - try { - object data = null; - await _metricsClient.TimeAsync(async () => data = await plugin.CreateFromEventAsync(context).AnyContext(), metricName).AnyContext(); - if (data == null) - continue; - - return data; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling create from event {id} in plugin {PluginName}: {Message}", context.Event.Id, plugin.Name, ex.Message); - } - } +namespace Exceptionless.Core.Plugins.WebHook; - return null; - } +public class WebHookDataPluginManager : PluginManagerBase { + public WebHookDataPluginManager(IServiceProvider serviceProvider, AppOptions options, IMetricsClient metricsClient = null, ILoggerFactory loggerFactory = null) : base(serviceProvider, options, metricsClient, loggerFactory) { } + + /// + /// Runs all of the event plugins create method. + /// + public async Task CreateFromEventAsync(WebHookDataContext context) { + string metricPrefix = String.Concat(_metricPrefix, nameof(CreateFromEventAsync).ToLower(), "."); + foreach (var plugin in Plugins.Values) { + string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); + try { + object data = null; + await _metricsClient.TimeAsync(async () => data = await plugin.CreateFromEventAsync(context).AnyContext(), metricName).AnyContext(); + if (data == null) + continue; - /// - /// Runs all of the event plugins create method. - /// - public async Task CreateFromStackAsync(WebHookDataContext context) { - string metricPrefix = String.Concat(_metricPrefix, nameof(CreateFromStackAsync).ToLower(), "."); - foreach (var plugin in Plugins.Values) { - string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); - try { - object data = null; - await _metricsClient.TimeAsync(async () => data = await plugin.CreateFromStackAsync(context).AnyContext(), metricName).AnyContext(); - if (data == null) - continue; - - return data; - } catch (Exception ex) { - _logger.LogError(ex, "Error calling create from stack {stack} in plugin {PluginName}: {Message}", context.Stack.Id, plugin.Name, ex.Message); - } + return data; } + catch (Exception ex) { + _logger.LogError(ex, "Error calling create from event {id} in plugin {PluginName}: {Message}", context.Event.Id, plugin.Name, ex.Message); + } + } - return null; + return null; + } + + /// + /// Runs all of the event plugins create method. + /// + public async Task CreateFromStackAsync(WebHookDataContext context) { + string metricPrefix = String.Concat(_metricPrefix, nameof(CreateFromStackAsync).ToLower(), "."); + foreach (var plugin in Plugins.Values) { + string metricName = String.Concat(metricPrefix, plugin.Name.ToLower()); + try { + object data = null; + await _metricsClient.TimeAsync(async () => data = await plugin.CreateFromStackAsync(context).AnyContext(), metricName).AnyContext(); + if (data == null) + continue; + + return data; + } + catch (Exception ex) { + _logger.LogError(ex, "Error calling create from stack {stack} in plugin {PluginName}: {Message}", context.Stack.Id, plugin.Name, ex.Message); + } } + + return null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Base/RepositoryBase.cs b/src/Exceptionless.Core/Repositories/Base/RepositoryBase.cs index 79042c470c..e123e97035 100644 --- a/src/Exceptionless.Core/Repositories/Base/RepositoryBase.cs +++ b/src/Exceptionless.Core/Repositories/Base/RepositoryBase.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Messaging.Models; +using Exceptionless.Core.Messaging.Models; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Options; using FluentValidation; @@ -13,103 +9,103 @@ using Foundatio.Repositories.Options; using Foundatio.Repositories.Queries; -namespace Exceptionless.Core.Repositories { - public abstract class RepositoryBase : ElasticRepositoryBase where T : class, IIdentity, new() { - protected readonly IValidator _validator; - protected readonly AppOptions _options; +namespace Exceptionless.Core.Repositories; - public RepositoryBase(IIndex index, IValidator validator, AppOptions options) : base(index) { - _validator = validator; - _options = options; - NotificationsEnabled = options.EnableRepositoryNotifications; - } +public abstract class RepositoryBase : ElasticRepositoryBase where T : class, IIdentity, new() { + protected readonly IValidator _validator; + protected readonly AppOptions _options; - protected override Task ValidateAndThrowAsync(T document) { - return _validator.ValidateAndThrowAsync(document); - } + public RepositoryBase(IIndex index, IValidator validator, AppOptions options) : base(index) { + _validator = validator; + _options = options; + NotificationsEnabled = options.EnableRepositoryNotifications; + } - protected override Task PublishChangeTypeMessageAsync(ChangeType changeType, T document, IDictionary data = null, TimeSpan? delay = null) { - if (!NotificationsEnabled) - return Task.CompletedTask; + protected override Task ValidateAndThrowAsync(T document) { + return _validator.ValidateAndThrowAsync(document); + } - string organizationId = (document as IOwnedByOrganization)?.OrganizationId; - string projectId = (document as IOwnedByProject)?.ProjectId; - string stackId = (document as IOwnedByStack)?.StackId; - return PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId, stackId, document?.Id, data), delay); - } + protected override Task PublishChangeTypeMessageAsync(ChangeType changeType, T document, IDictionary data = null, TimeSpan? delay = null) { + if (!NotificationsEnabled) + return Task.CompletedTask; - protected override Task SendQueryNotificationsAsync(ChangeType changeType, IRepositoryQuery query, ICommandOptions options) { - if (!NotificationsEnabled || !options.ShouldNotify()) - return Task.CompletedTask; + string organizationId = (document as IOwnedByOrganization)?.OrganizationId; + string projectId = (document as IOwnedByProject)?.ProjectId; + string stackId = (document as IOwnedByStack)?.StackId; + return PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId, stackId, document?.Id, data), delay); + } - var delay = TimeSpan.FromSeconds(1.5); - var organizations = query.GetOrganizations(); - var projects = query.GetProjects(); - var stacks = query.GetStacks(); - var ids = query.GetIds(); - var tasks = new List(); + protected override Task SendQueryNotificationsAsync(ChangeType changeType, IRepositoryQuery query, ICommandOptions options) { + if (!NotificationsEnabled || !options.ShouldNotify()) + return Task.CompletedTask; - string organizationId = organizations.Count == 1 ? organizations.Single() : null; - if (ids.Count > 0) { - string projectId = projects.Count == 1 ? projects.Single() : null; - string stackId = stacks.Count == 1 ? stacks.Single() : null; + var delay = TimeSpan.FromSeconds(1.5); + var organizations = query.GetOrganizations(); + var projects = query.GetProjects(); + var stacks = query.GetStacks(); + var ids = query.GetIds(); + var tasks = new List(); - foreach (string id in ids) - tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId, stackId, id), delay)); + string organizationId = organizations.Count == 1 ? organizations.Single() : null; + if (ids.Count > 0) { + string projectId = projects.Count == 1 ? projects.Single() : null; + string stackId = stacks.Count == 1 ? stacks.Single() : null; - return Task.WhenAll(tasks); - } + foreach (string id in ids) + tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId, stackId, id), delay)); - if (stacks.Count > 0) { - string projectId = projects.Count == 1 ? projects.Single() : null; - foreach (string stackId in stacks) - tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId, stackId), delay)); + return Task.WhenAll(tasks); + } - return Task.WhenAll(tasks); - } + if (stacks.Count > 0) { + string projectId = projects.Count == 1 ? projects.Single() : null; + foreach (string stackId in stacks) + tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId, stackId), delay)); - if (projects.Count > 0) { - foreach (string projectId in projects) - tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId), delay)); + return Task.WhenAll(tasks); + } - return Task.WhenAll(tasks); - } + if (projects.Count > 0) { + foreach (string projectId in projects) + tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organizationId, projectId), delay)); - if (organizations.Count > 0) { - foreach (string organization in organizations) - tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organization), delay)); + return Task.WhenAll(tasks); + } - return Task.WhenAll(tasks); - } + if (organizations.Count > 0) { + foreach (string organization in organizations) + tasks.Add(PublishMessageAsync(CreateEntityChanged(changeType, organization), delay)); - return PublishMessageAsync(new EntityChanged { - ChangeType = changeType, - Type = EntityTypeName - }, delay); + return Task.WhenAll(tasks); } - protected EntityChanged CreateEntityChanged(ChangeType changeType, string organizationId = null, string projectId = null, string stackId = null, string id = null, IDictionary data = null) { - var model = new EntityChanged { - ChangeType = changeType, - Type = EntityTypeName, - Id = id - }; + return PublishMessageAsync(new EntityChanged { + ChangeType = changeType, + Type = EntityTypeName + }, delay); + } + + protected EntityChanged CreateEntityChanged(ChangeType changeType, string organizationId = null, string projectId = null, string stackId = null, string id = null, IDictionary data = null) { + var model = new EntityChanged { + ChangeType = changeType, + Type = EntityTypeName, + Id = id + }; - if (data != null) { - foreach (var kvp in data) - model.Data[kvp.Key] = kvp.Value; - } + if (data != null) { + foreach (var kvp in data) + model.Data[kvp.Key] = kvp.Value; + } - if (organizationId != null) - model.Data[ExtendedEntityChanged.KnownKeys.OrganizationId] = organizationId; + if (organizationId != null) + model.Data[ExtendedEntityChanged.KnownKeys.OrganizationId] = organizationId; - if (projectId != null) - model.Data[ExtendedEntityChanged.KnownKeys.ProjectId] = projectId; + if (projectId != null) + model.Data[ExtendedEntityChanged.KnownKeys.ProjectId] = projectId; - if (stackId != null) - model.Data[ExtendedEntityChanged.KnownKeys.StackId] = stackId; + if (stackId != null) + model.Data[ExtendedEntityChanged.KnownKeys.StackId] = stackId; - return model; - } + return model; } } diff --git a/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganization.cs b/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganization.cs index 7386ff8ee6..58cf2347b6 100644 --- a/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganization.cs +++ b/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganization.cs @@ -1,34 +1,32 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using FluentValidation; using Foundatio.Repositories; using Foundatio.Repositories.Elasticsearch.Configuration; using Foundatio.Repositories.Models; using Foundatio.Repositories.Options; -namespace Exceptionless.Core.Repositories { - public abstract class RepositoryOwnedByOrganization : RepositoryBase, IRepositoryOwnedByOrganization where T : class, IOwnedByOrganization, IIdentity, new() { - public RepositoryOwnedByOrganization(IIndex index, IValidator validator, AppOptions options) : base(index, validator, options) { - AddPropertyRequiredForRemove(o => o.OrganizationId); - } +namespace Exceptionless.Core.Repositories; - public virtual Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); +public abstract class RepositoryOwnedByOrganization : RepositoryBase, IRepositoryOwnedByOrganization where T : class, IOwnedByOrganization, IIdentity, new() { + public RepositoryOwnedByOrganization(IIndex index, IValidator validator, AppOptions options) : base(index, validator, options) { + AddPropertyRequiredForRemove(o => o.OrganizationId); + } + + public virtual Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); - var commandOptions = options.Configure(); - if (commandOptions.ShouldUseCache()) - throw new Exception("Caching of paged queries is not allowed"); + var commandOptions = options.Configure(); + if (commandOptions.ShouldUseCache()) + throw new Exception("Caching of paged queries is not allowed"); - return FindAsync(q => q.Organization(organizationId), o => commandOptions); - } + return FindAsync(q => q.Organization(organizationId), o => commandOptions); + } - public virtual Task RemoveAllByOrganizationIdAsync(string organizationId) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); + public virtual Task RemoveAllByOrganizationIdAsync(string organizationId) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); - return RemoveAllAsync(q => q.Organization(organizationId)); - } + return RemoveAllAsync(q => q.Organization(organizationId)); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganizationAndProject.cs b/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganizationAndProject.cs index 79354cbc90..9a74b3d937 100644 --- a/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganizationAndProject.cs +++ b/src/Exceptionless.Core/Repositories/Base/RepositoryOwnedByOrganizationAndProject.cs @@ -1,32 +1,30 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using FluentValidation; using Foundatio.Repositories; using Foundatio.Repositories.Elasticsearch.Configuration; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public abstract class RepositoryOwnedByOrganizationAndProject : RepositoryOwnedByOrganization, IRepositoryOwnedByProject where T : class, IOwnedByProject, IIdentity, IOwnedByOrganization, new() { - public RepositoryOwnedByOrganizationAndProject(IIndex index, IValidator validator, AppOptions options) : base(index, validator, options) { - AddPropertyRequiredForRemove(o => o.ProjectId); - } +namespace Exceptionless.Core.Repositories; - public virtual Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null) { - if (String.IsNullOrEmpty(projectId)) - throw new ArgumentNullException(nameof(projectId)); - - return FindAsync(q => q.Project(projectId), options); - } +public abstract class RepositoryOwnedByOrganizationAndProject : RepositoryOwnedByOrganization, IRepositoryOwnedByProject where T : class, IOwnedByProject, IIdentity, IOwnedByOrganization, new() { + public RepositoryOwnedByOrganizationAndProject(IIndex index, IValidator validator, AppOptions options) : base(index, validator, options) { + AddPropertyRequiredForRemove(o => o.ProjectId); + } + + public virtual Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null) { + if (String.IsNullOrEmpty(projectId)) + throw new ArgumentNullException(nameof(projectId)); + + return FindAsync(q => q.Project(projectId), options); + } + + public virtual Task RemoveAllByProjectIdAsync(string organizationId, string projectId) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); - public virtual Task RemoveAllByProjectIdAsync(string organizationId, string projectId) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); + if (String.IsNullOrEmpty(projectId)) + throw new ArgumentNullException(nameof(projectId)); - if (String.IsNullOrEmpty(projectId)) - throw new ArgumentNullException(nameof(projectId)); - - return RemoveAllAsync(q => q.Organization(organizationId).Project(projectId)); - } + return RemoveAllAsync(q => q.Organization(organizationId).Project(projectId)); } } diff --git a/src/Exceptionless.Core/Repositories/Configuration/ExceptionlessElasticConfiguration.cs b/src/Exceptionless.Core/Repositories/Configuration/ExceptionlessElasticConfiguration.cs index 5f98e41d06..0b82f5345b 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/ExceptionlessElasticConfiguration.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/ExceptionlessElasticConfiguration.cs @@ -1,7 +1,3 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Elasticsearch.Net; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; @@ -19,85 +15,85 @@ using Nest; using Newtonsoft.Json; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class ExceptionlessElasticConfiguration : ElasticConfiguration, IStartupAction { - private readonly AppOptions _appOptions; - private readonly JsonSerializerSettings _serializerSettings; +namespace Exceptionless.Core.Repositories.Configuration; - public ExceptionlessElasticConfiguration( - AppOptions appOptions, - IQueue workItemQueue, - JsonSerializerSettings serializerSettings, - ICacheClient cacheClient, - IMessageBus messageBus, - IServiceProvider serviceProvider, - ILoggerFactory loggerFactory - ) : base(workItemQueue, cacheClient, messageBus, loggerFactory) { - _appOptions = appOptions; - _serializerSettings = serializerSettings; +public sealed class ExceptionlessElasticConfiguration : ElasticConfiguration, IStartupAction { + private readonly AppOptions _appOptions; + private readonly JsonSerializerSettings _serializerSettings; - _logger.LogInformation("All new indexes will be created with {ElasticsearchNumberOfShards} Shards and {ElasticsearchNumberOfReplicas} Replicas", _appOptions.ElasticsearchOptions.NumberOfShards, _appOptions.ElasticsearchOptions.NumberOfReplicas); - AddIndex(Stacks = new StackIndex(this)); - AddIndex(Events = new EventIndex(this, serviceProvider, appOptions)); - AddIndex(Migrations = new MigrationIndex(this, _appOptions.ElasticsearchOptions.ScopePrefix + "migrations", appOptions.ElasticsearchOptions.NumberOfReplicas)); - AddIndex(Organizations = new OrganizationIndex(this)); - AddIndex(Projects = new ProjectIndex(this)); - AddIndex(Tokens = new TokenIndex(this)); - AddIndex(Users = new UserIndex(this)); - AddIndex(WebHooks = new WebHookIndex(this)); - } + public ExceptionlessElasticConfiguration( + AppOptions appOptions, + IQueue workItemQueue, + JsonSerializerSettings serializerSettings, + ICacheClient cacheClient, + IMessageBus messageBus, + IServiceProvider serviceProvider, + ILoggerFactory loggerFactory + ) : base(workItemQueue, cacheClient, messageBus, loggerFactory) { + _appOptions = appOptions; + _serializerSettings = serializerSettings; - public Task RunAsync(CancellationToken shutdownToken = default) { - if (_appOptions.ElasticsearchOptions.DisableIndexConfiguration) - return Task.CompletedTask; + _logger.LogInformation("All new indexes will be created with {ElasticsearchNumberOfShards} Shards and {ElasticsearchNumberOfReplicas} Replicas", _appOptions.ElasticsearchOptions.NumberOfShards, _appOptions.ElasticsearchOptions.NumberOfReplicas); + AddIndex(Stacks = new StackIndex(this)); + AddIndex(Events = new EventIndex(this, serviceProvider, appOptions)); + AddIndex(Migrations = new MigrationIndex(this, _appOptions.ElasticsearchOptions.ScopePrefix + "migrations", appOptions.ElasticsearchOptions.NumberOfReplicas)); + AddIndex(Organizations = new OrganizationIndex(this)); + AddIndex(Projects = new ProjectIndex(this)); + AddIndex(Tokens = new TokenIndex(this)); + AddIndex(Users = new UserIndex(this)); + AddIndex(WebHooks = new WebHookIndex(this)); + } + + public Task RunAsync(CancellationToken shutdownToken = default) { + if (_appOptions.ElasticsearchOptions.DisableIndexConfiguration) + return Task.CompletedTask; - return ConfigureIndexesAsync(); - } + return ConfigureIndexesAsync(); + } + + public override void ConfigureGlobalQueryBuilders(ElasticQueryBuilder builder) { + builder.Register(new AppFilterQueryBuilder(_appOptions)); + builder.Register(new OrganizationQueryBuilder()); + builder.Register(new ProjectQueryBuilder()); + builder.Register(new StackQueryBuilder()); + } - public override void ConfigureGlobalQueryBuilders(ElasticQueryBuilder builder) { - builder.Register(new AppFilterQueryBuilder(_appOptions)); - builder.Register(new OrganizationQueryBuilder()); - builder.Register(new ProjectQueryBuilder()); - builder.Register(new StackQueryBuilder()); - } + public ElasticsearchOptions Options => _appOptions.ElasticsearchOptions; + public StackIndex Stacks { get; } + public EventIndex Events { get; } + public MigrationIndex Migrations { get; } + public OrganizationIndex Organizations { get; } + public ProjectIndex Projects { get; } + public TokenIndex Tokens { get; } + public UserIndex Users { get; } + public WebHookIndex WebHooks { get; } - public ElasticsearchOptions Options => _appOptions.ElasticsearchOptions; - public StackIndex Stacks { get; } - public EventIndex Events { get; } - public MigrationIndex Migrations { get; } - public OrganizationIndex Organizations { get; } - public ProjectIndex Projects { get; } - public TokenIndex Tokens { get; } - public UserIndex Users { get; } - public WebHookIndex WebHooks { get; } + protected override IElasticClient CreateElasticClient() { + var connectionPool = CreateConnectionPool(); + var settings = new ConnectionSettings(connectionPool, (serializer, values) => new ElasticJsonNetSerializer(serializer, values, _serializerSettings)); - protected override IElasticClient CreateElasticClient() { - var connectionPool = CreateConnectionPool(); - var settings = new ConnectionSettings(connectionPool, (serializer, values) => new ElasticJsonNetSerializer(serializer, values, _serializerSettings)); + ConfigureSettings(settings); + foreach (var index in Indexes) + index.ConfigureSettings(settings); - ConfigureSettings(settings); - foreach (var index in Indexes) - index.ConfigureSettings(settings); - - if (!String.IsNullOrEmpty(_appOptions.ElasticsearchOptions.UserName) && !String.IsNullOrEmpty(_appOptions.ElasticsearchOptions.Password)) - settings.BasicAuthentication(_appOptions.ElasticsearchOptions.UserName, _appOptions.ElasticsearchOptions.Password); - - var client = new ElasticClient(settings); - return client; - } + if (!String.IsNullOrEmpty(_appOptions.ElasticsearchOptions.UserName) && !String.IsNullOrEmpty(_appOptions.ElasticsearchOptions.Password)) + settings.BasicAuthentication(_appOptions.ElasticsearchOptions.UserName, _appOptions.ElasticsearchOptions.Password); + + var client = new ElasticClient(settings); + return client; + } + + protected override IConnectionPool CreateConnectionPool() { + var serverUris = Options?.ServerUrl.Split(',').Select(url => new Uri(url)); + return new StaticConnectionPool(serverUris); + } - protected override IConnectionPool CreateConnectionPool() { - var serverUris = Options?.ServerUrl.Split(',').Select(url => new Uri(url)); - return new StaticConnectionPool(serverUris); - } + protected override void ConfigureSettings(ConnectionSettings settings) { + if (_appOptions.AppMode == AppMode.Development) + settings.EnableDebugMode(); - protected override void ConfigureSettings(ConnectionSettings settings) { - if (_appOptions.AppMode == AppMode.Development) - settings.EnableDebugMode(); - - settings.EnableTcpKeepAlive(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(2)) - .DefaultFieldNameInferrer(p => p.ToLowerUnderscoredWords()) - .MaximumRetries(5); - } + settings.EnableTcpKeepAlive(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(2)) + .DefaultFieldNameInferrer(p => p.ToLowerUnderscoredWords()) + .MaximumRetries(5); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs index c6af0d927d..88c30e12d6 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Exceptionless.Core.Configuration; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; @@ -15,117 +12,118 @@ using Microsoft.Extensions.Logging; using Nest; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class EventIndex : DailyIndex { - private readonly ExceptionlessElasticConfiguration _configuration; - private readonly IServiceProvider _serviceProvider; +namespace Exceptionless.Core.Repositories.Configuration; - public EventIndex(ExceptionlessElasticConfiguration configuration, IServiceProvider serviceProvider, AppOptions appOptions) : base(configuration, configuration.Options.ScopePrefix + "events", 1, doc => ((PersistentEvent)doc).Date.UtcDateTime) { - _configuration = configuration; - _serviceProvider = serviceProvider; +public sealed class EventIndex : DailyIndex { + private readonly ExceptionlessElasticConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; - if (appOptions.MaximumRetentionDays > 0) - MaxIndexAge = TimeSpan.FromDays(appOptions.MaximumRetentionDays); + public EventIndex(ExceptionlessElasticConfiguration configuration, IServiceProvider serviceProvider, AppOptions appOptions) : base(configuration, configuration.Options.ScopePrefix + "events", 1, doc => ((PersistentEvent)doc).Date.UtcDateTime) { + _configuration = configuration; + _serviceProvider = serviceProvider; - AddAlias($"{Name}-today", TimeSpan.FromDays(1)); - AddAlias($"{Name}-last3days", TimeSpan.FromDays(7)); - AddAlias($"{Name}-last7days", TimeSpan.FromDays(7)); - AddAlias($"{Name}-last30days", TimeSpan.FromDays(30)); - AddAlias($"{Name}-last90days", TimeSpan.FromDays(90)); - } + if (appOptions.MaximumRetentionDays > 0) + MaxIndexAge = TimeSpan.FromDays(appOptions.MaximumRetentionDays); - protected override void ConfigureQueryBuilder(ElasticQueryBuilder builder) { - var stacksRepository = _serviceProvider.GetRequiredService(); - var cacheClient = _serviceProvider.GetRequiredService(); - base.ConfigureQueryBuilder(builder); - builder.RegisterBefore(new EventStackFilterQueryBuilder(stacksRepository, cacheClient, _configuration.LoggerFactory)); - } + AddAlias($"{Name}-today", TimeSpan.FromDays(1)); + AddAlias($"{Name}-last3days", TimeSpan.FromDays(7)); + AddAlias($"{Name}-last7days", TimeSpan.FromDays(7)); + AddAlias($"{Name}-last30days", TimeSpan.FromDays(30)); + AddAlias($"{Name}-last90days", TimeSpan.FromDays(90)); + } - public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { - var mapping = map - .Dynamic(false) - .DynamicTemplates(dt => dt - .DynamicTemplate("idx_bool", t => t.Match("*-b").Mapping(m => m.Boolean(s => s))) - .DynamicTemplate("idx_date", t => t.Match("*-d").Mapping(m => m.Date(s => s))) - .DynamicTemplate("idx_number", t => t.Match("*-n").Mapping(m => m.Number(s => s.Type(NumberType.Double)))) - .DynamicTemplate("idx_reference", t => t.Match("*-r").Mapping(m => m.Keyword(s => s.IgnoreAbove(256)))) - .DynamicTemplate("idx_string", t => t.Match("*-s").Mapping(m => m.Keyword(s => s.IgnoreAbove(1024))))) - .Properties(p => p - .SetupDefaults() - .Keyword(f => f.Name(e => e.Id)) - .Keyword(f => f.Name(e => e.OrganizationId)) - .FieldAlias(a => a.Name(Alias.OrganizationId).Path(f => f.OrganizationId)) - .Keyword(f => f.Name(e => e.ProjectId)) - .FieldAlias(a => a.Name(Alias.ProjectId).Path(f => f.ProjectId)) - .Keyword(f => f.Name(e => e.StackId)) - .FieldAlias(a => a.Name(Alias.StackId).Path(f => f.StackId)) - .Keyword(f => f.Name(e => e.ReferenceId)) - .FieldAlias(a => a.Name(Alias.ReferenceId).Path(f => f.ReferenceId)) - .Text(f => f.Name(e => e.Type).Analyzer(LOWER_KEYWORD_ANALYZER).AddKeywordField()) - .Text(f => f.Name(e => e.Source).Analyzer(STANDARDPLUS_ANALYZER).SearchAnalyzer(WHITESPACE_LOWERCASE_ANALYZER).Boost(1.2).AddKeywordField()) - .Date(f => f.Name(e => e.Date)) - .Text(f => f.Name(e => e.Message)) - .Text(f => f.Name(e => e.Tags).Analyzer(LOWER_KEYWORD_ANALYZER).Boost(1.2).AddKeywordField()) - .FieldAlias(a => a.Name(Alias.Tags).Path(f => f.Tags)) - .GeoPoint(f => f.Name(e => e.Geo)) - .Scalar(f => f.Value) - .Scalar(f => f.Count) - .Boolean(f => f.Name(e => e.IsFirstOccurrence)) - .FieldAlias(a => a.Name(Alias.IsFirstOccurrence).Path(f => f.IsFirstOccurrence)) - .Object(f => f.Name(e => e.Idx).Dynamic()) - .Object(f => f.Name(e => e.Data).Properties(p2 => p2 - .AddVersionMapping() - .AddLevelMapping() - .AddSubmissionMethodMapping() - .AddSubmissionClientMapping() - .AddLocationMapping() - .AddRequestInfoMapping() - .AddErrorMapping() - .AddSimpleErrorMapping() - .AddEnvironmentInfoMapping() - .AddUserDescriptionMapping() - .AddUserInfoMapping())) - .AddCopyToMappings() - .AddDataDictionaryAliases() - ); - - if (Options != null && Options.EnableMapperSizePlugin) - return mapping.SizeField(s => s.Enabled()); - - return mapping; - } + protected override void ConfigureQueryBuilder(ElasticQueryBuilder builder) { + var stacksRepository = _serviceProvider.GetRequiredService(); + var cacheClient = _serviceProvider.GetRequiredService(); + base.ConfigureQueryBuilder(builder); + builder.RegisterBefore(new EventStackFilterQueryBuilder(stacksRepository, cacheClient, _configuration.LoggerFactory)); + } - public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { - return base.ConfigureIndex(idx.Settings(s => s - .Analysis(BuildAnalysis) - .NumberOfShards(_configuration.Options.NumberOfShards) - .NumberOfReplicas(_configuration.Options.NumberOfReplicas) - .Setting("index.mapping.total_fields.limit", _configuration.Options.FieldsLimit) - .Setting("index.mapping.ignore_malformed", true) - .Priority(1))); - } + public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { + var mapping = map + .Dynamic(false) + .DynamicTemplates(dt => dt + .DynamicTemplate("idx_bool", t => t.Match("*-b").Mapping(m => m.Boolean(s => s))) + .DynamicTemplate("idx_date", t => t.Match("*-d").Mapping(m => m.Date(s => s))) + .DynamicTemplate("idx_number", t => t.Match("*-n").Mapping(m => m.Number(s => s.Type(NumberType.Double)))) + .DynamicTemplate("idx_reference", t => t.Match("*-r").Mapping(m => m.Keyword(s => s.IgnoreAbove(256)))) + .DynamicTemplate("idx_string", t => t.Match("*-s").Mapping(m => m.Keyword(s => s.IgnoreAbove(1024))))) + .Properties(p => p + .SetupDefaults() + .Keyword(f => f.Name(e => e.Id)) + .Keyword(f => f.Name(e => e.OrganizationId)) + .FieldAlias(a => a.Name(Alias.OrganizationId).Path(f => f.OrganizationId)) + .Keyword(f => f.Name(e => e.ProjectId)) + .FieldAlias(a => a.Name(Alias.ProjectId).Path(f => f.ProjectId)) + .Keyword(f => f.Name(e => e.StackId)) + .FieldAlias(a => a.Name(Alias.StackId).Path(f => f.StackId)) + .Keyword(f => f.Name(e => e.ReferenceId)) + .FieldAlias(a => a.Name(Alias.ReferenceId).Path(f => f.ReferenceId)) + .Text(f => f.Name(e => e.Type).Analyzer(LOWER_KEYWORD_ANALYZER).AddKeywordField()) + .Text(f => f.Name(e => e.Source).Analyzer(STANDARDPLUS_ANALYZER).SearchAnalyzer(WHITESPACE_LOWERCASE_ANALYZER).Boost(1.2).AddKeywordField()) + .Date(f => f.Name(e => e.Date)) + .Text(f => f.Name(e => e.Message)) + .Text(f => f.Name(e => e.Tags).Analyzer(LOWER_KEYWORD_ANALYZER).Boost(1.2).AddKeywordField()) + .FieldAlias(a => a.Name(Alias.Tags).Path(f => f.Tags)) + .GeoPoint(f => f.Name(e => e.Geo)) + .Scalar(f => f.Value) + .Scalar(f => f.Count) + .Boolean(f => f.Name(e => e.IsFirstOccurrence)) + .FieldAlias(a => a.Name(Alias.IsFirstOccurrence).Path(f => f.IsFirstOccurrence)) + .Object(f => f.Name(e => e.Idx).Dynamic()) + .Object(f => f.Name(e => e.Data).Properties(p2 => p2 + .AddVersionMapping() + .AddLevelMapping() + .AddSubmissionMethodMapping() + .AddSubmissionClientMapping() + .AddLocationMapping() + .AddRequestInfoMapping() + .AddErrorMapping() + .AddSimpleErrorMapping() + .AddEnvironmentInfoMapping() + .AddUserDescriptionMapping() + .AddUserInfoMapping())) + .AddCopyToMappings() + .AddDataDictionaryAliases() + ); + + if (Options != null && Options.EnableMapperSizePlugin) + return mapping.SizeField(s => s.Enabled()); + + return mapping; + } - public override async Task ConfigureAsync() { - const string pipeline = "events-pipeline"; - var response = await Configuration.Client.Ingest.PutPipelineAsync(pipeline, d => d.Processors(p => p - .Script(s => new ScriptProcessor { - Source = FLATTEN_ERRORS_SCRIPT.Replace("\r", String.Empty).Replace("\n", String.Empty).Replace(" ", " ") - }))); + public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { + return base.ConfigureIndex(idx.Settings(s => s + .Analysis(BuildAnalysis) + .NumberOfShards(_configuration.Options.NumberOfShards) + .NumberOfReplicas(_configuration.Options.NumberOfReplicas) + .Setting("index.mapping.total_fields.limit", _configuration.Options.FieldsLimit) + .Setting("index.mapping.ignore_malformed", true) + .Priority(1))); + } - var logger = Configuration.LoggerFactory.CreateLogger(); - logger.LogRequest(response); + public override async Task ConfigureAsync() { + const string pipeline = "events-pipeline"; + var response = await Configuration.Client.Ingest.PutPipelineAsync(pipeline, d => d.Processors(p => p + .Script(s => new ScriptProcessor { + Source = FLATTEN_ERRORS_SCRIPT.Replace("\r", String.Empty).Replace("\n", String.Empty).Replace(" ", " ") + }))); - if (!response.IsValid) { - logger.LogError(response.OriginalException, "Error creating the pipeline {Pipeline}: {Message}", pipeline, response.GetErrorMessage()); - throw new ApplicationException($"Error creating the pipeline {pipeline}: {response.GetErrorMessage()}", response.OriginalException); - } + var logger = Configuration.LoggerFactory.CreateLogger(); + logger.LogRequest(response); - await base.ConfigureAsync(); + if (!response.IsValid) { + logger.LogError(response.OriginalException, "Error creating the pipeline {Pipeline}: {Message}", pipeline, response.GetErrorMessage()); + throw new ApplicationException($"Error creating the pipeline {pipeline}: {response.GetErrorMessage()}", response.OriginalException); } - protected override void ConfigureQueryParser(ElasticQueryParserConfiguration config) { - config - .SetDefaultFields(new[] { + await base.ConfigureAsync(); + } + + protected override void ConfigureQueryParser(ElasticQueryParserConfiguration config) { + config + .SetDefaultFields(new[] { "id", "source", "message", @@ -139,9 +137,9 @@ protected override void ConfigureQueryParser(ElasticQueryParserConfiguration con $"data.{Event.KnownDataKeys.UserDescription}.email_address", $"data.{Event.KnownDataKeys.UserInfo}.identity", $"data.{Event.KnownDataKeys.UserInfo}.name" - }) - .AddQueryVisitor(new EventFieldsQueryVisitor()) - .UseFieldMap(new Dictionary { + }) + .AddQueryVisitor(new EventFieldsQueryVisitor()) + .UseFieldMap(new Dictionary { { Alias.BrowserVersion, $"data.{Event.KnownDataKeys.RequestInfo}.data.{RequestInfo.KnownDataKeys.BrowserVersion}" }, { Alias.BrowserMajorVersion, $"data.{Event.KnownDataKeys.RequestInfo}.data.{RequestInfo.KnownDataKeys.BrowserMajorVersion}" }, { Alias.User, $"data.{Event.KnownDataKeys.UserInfo}.identity" }, @@ -150,75 +148,75 @@ protected override void ConfigureQueryParser(ElasticQueryParserConfiguration con { Alias.UserDescription, $"data.{Event.KnownDataKeys.UserDescription}.description" }, { Alias.OperatingSystemVersion, $"data.{Event.KnownDataKeys.RequestInfo}.data.{RequestInfo.KnownDataKeys.OSVersion}" }, { Alias.OperatingSystemMajorVersion, $"data.{Event.KnownDataKeys.RequestInfo}.data.{RequestInfo.KnownDataKeys.OSMajorVersion}" } - }); - } + }); + } - public ElasticsearchOptions Options => (Configuration as ExceptionlessElasticConfiguration)?.Options; - - private AnalysisDescriptor BuildAnalysis(AnalysisDescriptor ad) { - return ad.Analyzers(a => a - .Pattern(COMMA_WHITESPACE_ANALYZER, p => p.Pattern(@"[,\s]+")) - .Custom(EMAIL_ANALYZER, c => c.Filters(EMAIL_TOKEN_FILTER, "lowercase", TLD_STOPWORDS_TOKEN_FILTER, EDGE_NGRAM_TOKEN_FILTER, "unique").Tokenizer("keyword")) - .Custom(VERSION_INDEX_ANALYZER, c => c.Filters(VERSION_PAD1_TOKEN_FILTER, VERSION_PAD2_TOKEN_FILTER, VERSION_PAD3_TOKEN_FILTER, VERSION_PAD4_TOKEN_FILTER, VERSION_TOKEN_FILTER, "lowercase", "unique").Tokenizer("whitespace")) - .Custom(VERSION_SEARCH_ANALYZER, c => c.Filters(VERSION_PAD1_TOKEN_FILTER, VERSION_PAD2_TOKEN_FILTER, VERSION_PAD3_TOKEN_FILTER, VERSION_PAD4_TOKEN_FILTER, "lowercase").Tokenizer("whitespace")) - .Custom(WHITESPACE_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer(COMMA_WHITESPACE_TOKENIZER)) - .Custom(TYPENAME_ANALYZER, c => c.Filters(TYPENAME_TOKEN_FILTER, "lowercase", "unique").Tokenizer(TYPENAME_HIERARCHY_TOKENIZER)) - .Custom(STANDARDPLUS_ANALYZER, c => c.Filters(STANDARDPLUS_TOKEN_FILTER, "lowercase", "unique").Tokenizer(COMMA_WHITESPACE_TOKENIZER)) - .Custom(LOWER_KEYWORD_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")) - .Custom(HOST_ANALYZER, c => c.Filters("lowercase").Tokenizer(HOST_TOKENIZER)) - .Custom(URL_PATH_ANALYZER, c => c.Filters("lowercase").Tokenizer(URL_PATH_TOKENIZER))) - .TokenFilters(f => f - .EdgeNGram(EDGE_NGRAM_TOKEN_FILTER, p => p.MaxGram(50).MinGram(2).Side(EdgeNGramSide.Front)) - .PatternCapture(EMAIL_TOKEN_FILTER, p => p.PreserveOriginal().Patterns("(\\w+)","(\\p{L}+)","(\\d+)","@(.+)","@(.+)\\.","(.+)@")) - .PatternCapture(STANDARDPLUS_TOKEN_FILTER, p => p.PreserveOriginal().Patterns( - @"([^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+)", - @"([^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+[\.\/\\][^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+)", - @"([^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+[\.\/\\][^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+[\.\/\\][^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+)" - )) - .PatternCapture(TYPENAME_TOKEN_FILTER, p => p.PreserveOriginal().Patterns(@" ^ (\w+)", @"\.(\w+)", @"([^\(\)]+)")) - .PatternCapture(VERSION_TOKEN_FILTER, p => p.Patterns(@"^(\d+)\.", @"^(\d+\.\d+)", @"^(\d+\.\d+\.\d+)")) - .PatternReplace(VERSION_PAD1_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{1})(?=\.|-|$)").Replacement("$10000$2")) - .PatternReplace(VERSION_PAD2_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{2})(?=\.|-|$)").Replacement("$1000$2")) - .PatternReplace(VERSION_PAD3_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{3})(?=\.|-|$)").Replacement("$100$2")) - .PatternReplace(VERSION_PAD4_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{4})(?=\.|-|$)").Replacement("$10$2")) - .Stop(TLD_STOPWORDS_TOKEN_FILTER, p => p.StopWords("com", "net", "org", "info", "me", "edu", "mil", "gov", "biz", "co", "io", "dev")) - .WordDelimiter(ALL_WORDS_DELIMITER_TOKEN_FILTER, p => p.CatenateNumbers().PreserveOriginal().CatenateAll().CatenateWords())) - .Tokenizers(t => t - .CharGroup(COMMA_WHITESPACE_TOKENIZER, p => p.TokenizeOnCharacters(",", "whitespace")) - .CharGroup(URL_PATH_TOKENIZER, p => p.TokenizeOnCharacters("/", "-", ".")) - .CharGroup(HOST_TOKENIZER, p => p.TokenizeOnCharacters(".")) - .PathHierarchy(TYPENAME_HIERARCHY_TOKENIZER, p => p.Delimiter('.'))); - } + public ElasticsearchOptions Options => (Configuration as ExceptionlessElasticConfiguration)?.Options; + + private AnalysisDescriptor BuildAnalysis(AnalysisDescriptor ad) { + return ad.Analyzers(a => a + .Pattern(COMMA_WHITESPACE_ANALYZER, p => p.Pattern(@"[,\s]+")) + .Custom(EMAIL_ANALYZER, c => c.Filters(EMAIL_TOKEN_FILTER, "lowercase", TLD_STOPWORDS_TOKEN_FILTER, EDGE_NGRAM_TOKEN_FILTER, "unique").Tokenizer("keyword")) + .Custom(VERSION_INDEX_ANALYZER, c => c.Filters(VERSION_PAD1_TOKEN_FILTER, VERSION_PAD2_TOKEN_FILTER, VERSION_PAD3_TOKEN_FILTER, VERSION_PAD4_TOKEN_FILTER, VERSION_TOKEN_FILTER, "lowercase", "unique").Tokenizer("whitespace")) + .Custom(VERSION_SEARCH_ANALYZER, c => c.Filters(VERSION_PAD1_TOKEN_FILTER, VERSION_PAD2_TOKEN_FILTER, VERSION_PAD3_TOKEN_FILTER, VERSION_PAD4_TOKEN_FILTER, "lowercase").Tokenizer("whitespace")) + .Custom(WHITESPACE_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer(COMMA_WHITESPACE_TOKENIZER)) + .Custom(TYPENAME_ANALYZER, c => c.Filters(TYPENAME_TOKEN_FILTER, "lowercase", "unique").Tokenizer(TYPENAME_HIERARCHY_TOKENIZER)) + .Custom(STANDARDPLUS_ANALYZER, c => c.Filters(STANDARDPLUS_TOKEN_FILTER, "lowercase", "unique").Tokenizer(COMMA_WHITESPACE_TOKENIZER)) + .Custom(LOWER_KEYWORD_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")) + .Custom(HOST_ANALYZER, c => c.Filters("lowercase").Tokenizer(HOST_TOKENIZER)) + .Custom(URL_PATH_ANALYZER, c => c.Filters("lowercase").Tokenizer(URL_PATH_TOKENIZER))) + .TokenFilters(f => f + .EdgeNGram(EDGE_NGRAM_TOKEN_FILTER, p => p.MaxGram(50).MinGram(2).Side(EdgeNGramSide.Front)) + .PatternCapture(EMAIL_TOKEN_FILTER, p => p.PreserveOriginal().Patterns("(\\w+)", "(\\p{L}+)", "(\\d+)", "@(.+)", "@(.+)\\.", "(.+)@")) + .PatternCapture(STANDARDPLUS_TOKEN_FILTER, p => p.PreserveOriginal().Patterns( + @"([^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+)", + @"([^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+[\.\/\\][^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+)", + @"([^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+[\.\/\\][^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+[\.\/\\][^\.\(\)\[\]\/\\\{\}\?=&;:\<\>]+)" + )) + .PatternCapture(TYPENAME_TOKEN_FILTER, p => p.PreserveOriginal().Patterns(@" ^ (\w+)", @"\.(\w+)", @"([^\(\)]+)")) + .PatternCapture(VERSION_TOKEN_FILTER, p => p.Patterns(@"^(\d+)\.", @"^(\d+\.\d+)", @"^(\d+\.\d+\.\d+)")) + .PatternReplace(VERSION_PAD1_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{1})(?=\.|-|$)").Replacement("$10000$2")) + .PatternReplace(VERSION_PAD2_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{2})(?=\.|-|$)").Replacement("$1000$2")) + .PatternReplace(VERSION_PAD3_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{3})(?=\.|-|$)").Replacement("$100$2")) + .PatternReplace(VERSION_PAD4_TOKEN_FILTER, p => p.Pattern(@"(\.|^)(\d{4})(?=\.|-|$)").Replacement("$10$2")) + .Stop(TLD_STOPWORDS_TOKEN_FILTER, p => p.StopWords("com", "net", "org", "info", "me", "edu", "mil", "gov", "biz", "co", "io", "dev")) + .WordDelimiter(ALL_WORDS_DELIMITER_TOKEN_FILTER, p => p.CatenateNumbers().PreserveOriginal().CatenateAll().CatenateWords())) + .Tokenizers(t => t + .CharGroup(COMMA_WHITESPACE_TOKENIZER, p => p.TokenizeOnCharacters(",", "whitespace")) + .CharGroup(URL_PATH_TOKENIZER, p => p.TokenizeOnCharacters("/", "-", ".")) + .CharGroup(HOST_TOKENIZER, p => p.TokenizeOnCharacters(".")) + .PathHierarchy(TYPENAME_HIERARCHY_TOKENIZER, p => p.Delimiter('.'))); + } - private const string ALL_WORDS_DELIMITER_TOKEN_FILTER = "all_word_delimiter"; - private const string EDGE_NGRAM_TOKEN_FILTER = "edge_ngram"; - private const string EMAIL_TOKEN_FILTER = "email"; - private const string TYPENAME_TOKEN_FILTER = "typename"; - private const string VERSION_TOKEN_FILTER = "version"; - private const string STANDARDPLUS_TOKEN_FILTER = "standardplus"; - private const string VERSION_PAD1_TOKEN_FILTER = "version_pad1"; - private const string VERSION_PAD2_TOKEN_FILTER = "version_pad2"; - private const string VERSION_PAD3_TOKEN_FILTER = "version_pad3"; - private const string VERSION_PAD4_TOKEN_FILTER = "version_pad4"; - private const string TLD_STOPWORDS_TOKEN_FILTER = "tld_stopwords"; - - internal const string COMMA_WHITESPACE_ANALYZER = "comma_whitespace"; - internal const string EMAIL_ANALYZER = "email"; - internal const string VERSION_INDEX_ANALYZER = "version_index"; - internal const string VERSION_SEARCH_ANALYZER = "version_search"; - internal const string WHITESPACE_LOWERCASE_ANALYZER = "whitespace_lower"; - internal const string TYPENAME_ANALYZER = "typename"; - internal const string STANDARDPLUS_ANALYZER = "standardplus"; - internal const string LOWER_KEYWORD_ANALYZER = "lowerkeyword"; - internal const string URL_PATH_ANALYZER = "urlpath"; - internal const string HOST_ANALYZER = "hostname"; - - private const string COMMA_WHITESPACE_TOKENIZER = "comma_whitespace"; - private const string URL_PATH_TOKENIZER = "urlpath"; - private const string HOST_TOKENIZER = "hostname"; - private const string TYPENAME_HIERARCHY_TOKENIZER = "typename_hierarchy"; - - private const string FLATTEN_ERRORS_SCRIPT = @" + private const string ALL_WORDS_DELIMITER_TOKEN_FILTER = "all_word_delimiter"; + private const string EDGE_NGRAM_TOKEN_FILTER = "edge_ngram"; + private const string EMAIL_TOKEN_FILTER = "email"; + private const string TYPENAME_TOKEN_FILTER = "typename"; + private const string VERSION_TOKEN_FILTER = "version"; + private const string STANDARDPLUS_TOKEN_FILTER = "standardplus"; + private const string VERSION_PAD1_TOKEN_FILTER = "version_pad1"; + private const string VERSION_PAD2_TOKEN_FILTER = "version_pad2"; + private const string VERSION_PAD3_TOKEN_FILTER = "version_pad3"; + private const string VERSION_PAD4_TOKEN_FILTER = "version_pad4"; + private const string TLD_STOPWORDS_TOKEN_FILTER = "tld_stopwords"; + + internal const string COMMA_WHITESPACE_ANALYZER = "comma_whitespace"; + internal const string EMAIL_ANALYZER = "email"; + internal const string VERSION_INDEX_ANALYZER = "version_index"; + internal const string VERSION_SEARCH_ANALYZER = "version_search"; + internal const string WHITESPACE_LOWERCASE_ANALYZER = "whitespace_lower"; + internal const string TYPENAME_ANALYZER = "typename"; + internal const string STANDARDPLUS_ANALYZER = "standardplus"; + internal const string LOWER_KEYWORD_ANALYZER = "lowerkeyword"; + internal const string URL_PATH_ANALYZER = "urlpath"; + internal const string HOST_ANALYZER = "hostname"; + + private const string COMMA_WHITESPACE_TOKENIZER = "comma_whitespace"; + private const string URL_PATH_TOKENIZER = "urlpath"; + private const string HOST_TOKENIZER = "hostname"; + private const string TYPENAME_HIERARCHY_TOKENIZER = "typename_hierarchy"; + + private const string FLATTEN_ERRORS_SCRIPT = @" if (!ctx.containsKey('data') || !(ctx.data.containsKey('@error') || ctx.data.containsKey('@simple_error'))) return; @@ -244,182 +242,181 @@ private AnalysisDescriptor BuildAnalysis(AnalysisDescriptor ad) { ctx.error.message = messages; ctx.error.code = codes;"; - public sealed class Alias { - public const string OrganizationId = "organization"; - public const string ProjectId = "project"; - public const string StackId = "stack"; - public const string Id = "id"; - public const string ReferenceId = "reference"; - public const string Date = "date"; - public const string Type = "type"; - public const string Source = "source"; - public const string Message = "message"; - public const string Tags = "tag"; - public const string Geo = "geo"; - public const string Value = "value"; - public const string Count = "count"; - public const string IsFirstOccurrence = "first"; - public const string IDX = "idx"; - - public const string Version = "version"; - public const string Level = "level"; - public const string SubmissionMethod = "submission"; - - public const string IpAddress = "ip"; - - public const string RequestUserAgent = "useragent"; - public const string RequestPath = "path"; - - public const string Browser = "browser"; - public const string BrowserVersion = "browser.version"; - public const string BrowserMajorVersion = "browser.major"; - public const string RequestIsBot = "bot"; - - public const string ClientVersion = "client.version"; - public const string ClientUserAgent = "client.useragent"; - - public const string Device = "device"; - - public const string OperatingSystem = "os"; - public const string OperatingSystemVersion = "os.version"; - public const string OperatingSystemMajorVersion = "os.major"; - - public const string CommandLine = "cmd"; - public const string MachineName = "machine"; - public const string MachineArchitecture = "architecture"; - - public const string User = "user"; - public const string UserName = "user.name"; - public const string UserEmail = "user.email"; - public const string UserDescription = "user.description"; - - public const string LocationCountry = "country"; - public const string LocationLevel1 = "level1"; - public const string LocationLevel2 = "level2"; - public const string LocationLocality = "locality"; - - public const string ErrorCode = "error.code"; - public const string ErrorType = "error.type"; - public const string ErrorMessage = "error.message"; - public const string ErrorTargetType = "error.targettype"; - public const string ErrorTargetMethod = "error.targetmethod"; - } + public sealed class Alias { + public const string OrganizationId = "organization"; + public const string ProjectId = "project"; + public const string StackId = "stack"; + public const string Id = "id"; + public const string ReferenceId = "reference"; + public const string Date = "date"; + public const string Type = "type"; + public const string Source = "source"; + public const string Message = "message"; + public const string Tags = "tag"; + public const string Geo = "geo"; + public const string Value = "value"; + public const string Count = "count"; + public const string IsFirstOccurrence = "first"; + public const string IDX = "idx"; + + public const string Version = "version"; + public const string Level = "level"; + public const string SubmissionMethod = "submission"; + + public const string IpAddress = "ip"; + + public const string RequestUserAgent = "useragent"; + public const string RequestPath = "path"; + + public const string Browser = "browser"; + public const string BrowserVersion = "browser.version"; + public const string BrowserMajorVersion = "browser.major"; + public const string RequestIsBot = "bot"; + + public const string ClientVersion = "client.version"; + public const string ClientUserAgent = "client.useragent"; + + public const string Device = "device"; + + public const string OperatingSystem = "os"; + public const string OperatingSystemVersion = "os.version"; + public const string OperatingSystemMajorVersion = "os.major"; + + public const string CommandLine = "cmd"; + public const string MachineName = "machine"; + public const string MachineArchitecture = "architecture"; + + public const string User = "user"; + public const string UserName = "user.name"; + public const string UserEmail = "user.email"; + public const string UserDescription = "user.description"; + + public const string LocationCountry = "country"; + public const string LocationLevel1 = "level1"; + public const string LocationLevel2 = "level2"; + public const string LocationLocality = "locality"; + + public const string ErrorCode = "error.code"; + public const string ErrorType = "error.type"; + public const string ErrorMessage = "error.message"; + public const string ErrorTargetType = "error.targettype"; + public const string ErrorTargetMethod = "error.targetmethod"; } +} - internal static class EventIndexExtensions { - public static PropertiesDescriptor AddCopyToMappings(this PropertiesDescriptor descriptor) { - return descriptor - .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER)) - .Text(f => f.Name(EventIndex.Alias.OperatingSystem).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) - .Object(f => f.Name("error").Properties(p1 => p1 - .Keyword(f3 => f3.Name("code").IgnoreAbove(1024).Boost(1.1)) - .Text(f3 => f3.Name("message").AddKeywordField()) - .Text(f3 => f3.Name("type").Analyzer(EventIndex.TYPENAME_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.1).AddKeywordField()) - .Text(f6 => f6.Name("targettype").Analyzer(EventIndex.TYPENAME_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.2).AddKeywordField()) - .Text(f6 => f6.Name("targetmethod").Analyzer(EventIndex.TYPENAME_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.2).AddKeywordField()))); - } +internal static class EventIndexExtensions { + public static PropertiesDescriptor AddCopyToMappings(this PropertiesDescriptor descriptor) { + return descriptor + .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER)) + .Text(f => f.Name(EventIndex.Alias.OperatingSystem).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) + .Object(f => f.Name("error").Properties(p1 => p1 + .Keyword(f3 => f3.Name("code").IgnoreAbove(1024).Boost(1.1)) + .Text(f3 => f3.Name("message").AddKeywordField()) + .Text(f3 => f3.Name("type").Analyzer(EventIndex.TYPENAME_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.1).AddKeywordField()) + .Text(f6 => f6.Name("targettype").Analyzer(EventIndex.TYPENAME_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.2).AddKeywordField()) + .Text(f6 => f6.Name("targetmethod").Analyzer(EventIndex.TYPENAME_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.2).AddKeywordField()))); + } - public static PropertiesDescriptor AddDataDictionaryAliases(this PropertiesDescriptor descriptor) { - return descriptor - .FieldAlias(a => a.Name(EventIndex.Alias.Version).Path(f => (string)f.Data[Event.KnownDataKeys.Version])) - .FieldAlias(a => a.Name(EventIndex.Alias.Level).Path(f => (string)f.Data[Event.KnownDataKeys.Level])) - .FieldAlias(a => a.Name(EventIndex.Alias.SubmissionMethod).Path(f => (string)f.Data[Event.KnownDataKeys.SubmissionMethod])) - .FieldAlias(a => a.Name(EventIndex.Alias.ClientUserAgent).Path(f => ((SubmissionClient)f.Data[Event.KnownDataKeys.SubmissionClient]).UserAgent)) - .FieldAlias(a => a.Name(EventIndex.Alias.ClientVersion).Path(f => ((SubmissionClient)f.Data[Event.KnownDataKeys.SubmissionClient]).Version)) - .FieldAlias(a => a.Name(EventIndex.Alias.LocationCountry).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Country)) - .FieldAlias(a => a.Name(EventIndex.Alias.LocationLevel1).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Level1)) - .FieldAlias(a => a.Name(EventIndex.Alias.LocationLevel2).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Level2)) - .FieldAlias(a => a.Name(EventIndex.Alias.LocationLocality).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Locality)) - .FieldAlias(a => a.Name(EventIndex.Alias.Browser).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Data[RequestInfo.KnownDataKeys.Browser])) - .FieldAlias(a => a.Name(EventIndex.Alias.Device).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Data[RequestInfo.KnownDataKeys.Device])) - .FieldAlias(a => a.Name(EventIndex.Alias.RequestIsBot).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Data[RequestInfo.KnownDataKeys.IsBot])) - .FieldAlias(a => a.Name(EventIndex.Alias.RequestPath).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Path)) - .FieldAlias(a => a.Name(EventIndex.Alias.RequestUserAgent).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).UserAgent)) - .FieldAlias(a => a.Name(EventIndex.Alias.CommandLine).Path(f => ((EnvironmentInfo)f.Data[Event.KnownDataKeys.EnvironmentInfo]).CommandLine)) - .FieldAlias(a => a.Name(EventIndex.Alias.MachineArchitecture).Path(f => ((EnvironmentInfo)f.Data[Event.KnownDataKeys.EnvironmentInfo]).Architecture)) - .FieldAlias(a => a.Name(EventIndex.Alias.MachineName).Path(f => ((EnvironmentInfo)f.Data[Event.KnownDataKeys.EnvironmentInfo]).MachineName)); - } + public static PropertiesDescriptor AddDataDictionaryAliases(this PropertiesDescriptor descriptor) { + return descriptor + .FieldAlias(a => a.Name(EventIndex.Alias.Version).Path(f => (string)f.Data[Event.KnownDataKeys.Version])) + .FieldAlias(a => a.Name(EventIndex.Alias.Level).Path(f => (string)f.Data[Event.KnownDataKeys.Level])) + .FieldAlias(a => a.Name(EventIndex.Alias.SubmissionMethod).Path(f => (string)f.Data[Event.KnownDataKeys.SubmissionMethod])) + .FieldAlias(a => a.Name(EventIndex.Alias.ClientUserAgent).Path(f => ((SubmissionClient)f.Data[Event.KnownDataKeys.SubmissionClient]).UserAgent)) + .FieldAlias(a => a.Name(EventIndex.Alias.ClientVersion).Path(f => ((SubmissionClient)f.Data[Event.KnownDataKeys.SubmissionClient]).Version)) + .FieldAlias(a => a.Name(EventIndex.Alias.LocationCountry).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Country)) + .FieldAlias(a => a.Name(EventIndex.Alias.LocationLevel1).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Level1)) + .FieldAlias(a => a.Name(EventIndex.Alias.LocationLevel2).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Level2)) + .FieldAlias(a => a.Name(EventIndex.Alias.LocationLocality).Path(f => ((Location)f.Data[Event.KnownDataKeys.Location]).Locality)) + .FieldAlias(a => a.Name(EventIndex.Alias.Browser).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Data[RequestInfo.KnownDataKeys.Browser])) + .FieldAlias(a => a.Name(EventIndex.Alias.Device).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Data[RequestInfo.KnownDataKeys.Device])) + .FieldAlias(a => a.Name(EventIndex.Alias.RequestIsBot).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Data[RequestInfo.KnownDataKeys.IsBot])) + .FieldAlias(a => a.Name(EventIndex.Alias.RequestPath).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).Path)) + .FieldAlias(a => a.Name(EventIndex.Alias.RequestUserAgent).Path(f => ((RequestInfo)f.Data[Event.KnownDataKeys.RequestInfo]).UserAgent)) + .FieldAlias(a => a.Name(EventIndex.Alias.CommandLine).Path(f => ((EnvironmentInfo)f.Data[Event.KnownDataKeys.EnvironmentInfo]).CommandLine)) + .FieldAlias(a => a.Name(EventIndex.Alias.MachineArchitecture).Path(f => ((EnvironmentInfo)f.Data[Event.KnownDataKeys.EnvironmentInfo]).Architecture)) + .FieldAlias(a => a.Name(EventIndex.Alias.MachineName).Path(f => ((EnvironmentInfo)f.Data[Event.KnownDataKeys.EnvironmentInfo]).MachineName)); + } - public static PropertiesDescriptor AddVersionMapping(this PropertiesDescriptor descriptor) { - return descriptor.Text(f2 => f2.Name(Event.KnownDataKeys.Version).Analyzer(EventIndex.VERSION_INDEX_ANALYZER).SearchAnalyzer(EventIndex.VERSION_SEARCH_ANALYZER).AddKeywordField()); - } + public static PropertiesDescriptor AddVersionMapping(this PropertiesDescriptor descriptor) { + return descriptor.Text(f2 => f2.Name(Event.KnownDataKeys.Version).Analyzer(EventIndex.VERSION_INDEX_ANALYZER).SearchAnalyzer(EventIndex.VERSION_SEARCH_ANALYZER).AddKeywordField()); + } - public static PropertiesDescriptor AddLevelMapping(this PropertiesDescriptor descriptor) { - return descriptor.Text(f2 => f2.Name(Event.KnownDataKeys.Level).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()); - } + public static PropertiesDescriptor AddLevelMapping(this PropertiesDescriptor descriptor) { + return descriptor.Text(f2 => f2.Name(Event.KnownDataKeys.Level).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()); + } - public static PropertiesDescriptor AddSubmissionMethodMapping(this PropertiesDescriptor descriptor) { - return descriptor.Keyword(f2 => f2.Name(Event.KnownDataKeys.SubmissionMethod).IgnoreAbove(1024)); - } + public static PropertiesDescriptor AddSubmissionMethodMapping(this PropertiesDescriptor descriptor) { + return descriptor.Keyword(f2 => f2.Name(Event.KnownDataKeys.SubmissionMethod).IgnoreAbove(1024)); + } - public static PropertiesDescriptor AddSubmissionClientMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.SubmissionClient).Properties(p3 => p3 - .Text(f3 => f3.Name(r => r.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).CopyTo(fd => fd.Field(EventIndex.Alias.IpAddress))) - .Text(f3 => f3.Name(r => r.UserAgent).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()) - .Keyword(f3 => f3.Name(r => r.Version).IgnoreAbove(1024)))); - } + public static PropertiesDescriptor AddSubmissionClientMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.SubmissionClient).Properties(p3 => p3 + .Text(f3 => f3.Name(r => r.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).CopyTo(fd => fd.Field(EventIndex.Alias.IpAddress))) + .Text(f3 => f3.Name(r => r.UserAgent).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()) + .Keyword(f3 => f3.Name(r => r.Version).IgnoreAbove(1024)))); + } - public static PropertiesDescriptor AddLocationMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.Location).Properties(p3 => p3 - .Text(f3 => f3.Name(r => r.Country).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()) - .Keyword(f3 => f3.Name(r => r.Level1).IgnoreAbove(1024)) - .Keyword(f3 => f3.Name(r => r.Level2).IgnoreAbove(1024)) - .Keyword(f3 => f3.Name(r => r.Locality).IgnoreAbove(1024)))); - } + public static PropertiesDescriptor AddLocationMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.Location).Properties(p3 => p3 + .Text(f3 => f3.Name(r => r.Country).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()) + .Keyword(f3 => f3.Name(r => r.Level1).IgnoreAbove(1024)) + .Keyword(f3 => f3.Name(r => r.Level2).IgnoreAbove(1024)) + .Keyword(f3 => f3.Name(r => r.Locality).IgnoreAbove(1024)))); + } - public static PropertiesDescriptor AddRequestInfoMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.RequestInfo).Properties(p3 => p3 - .Text(f3 => f3.Name(r => r.ClientIpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).CopyTo(fd => fd.Field(EventIndex.Alias.IpAddress))) - .Text(f3 => f3.Name(r => r.UserAgent).AddKeywordField()) - .Text(f3 => f3.Name(r => r.Path).Analyzer(EventIndex.URL_PATH_ANALYZER).AddKeywordField()) - .Text(f3 => f3.Name(r => r.Host).Analyzer(EventIndex.HOST_ANALYZER).AddKeywordField()) - .Scalar(r => r.Port) - .Keyword(f3 => f3.Name(r => r.HttpMethod)) - .Object(f3 => f3.Name(e => e.Data).Properties(p4 => p4 - .Text(f4 => f4.Name(RequestInfo.KnownDataKeys.Browser).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) - .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.BrowserVersion).IgnoreAbove(1024)) - .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.BrowserMajorVersion).IgnoreAbove(1024)) - .Text(f4 => f4.Name(RequestInfo.KnownDataKeys.Device).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) - .Text(f4 => f4.Name(RequestInfo.KnownDataKeys.OS).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField().CopyTo(fd => fd.Field(EventIndex.Alias.OperatingSystem)).Index(false)) - .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.OSVersion).IgnoreAbove(1024)) - .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.OSMajorVersion).IgnoreAbove(1024)) - .Boolean(f4 => f4.Name(RequestInfo.KnownDataKeys.IsBot)))))); - } + public static PropertiesDescriptor AddRequestInfoMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.RequestInfo).Properties(p3 => p3 + .Text(f3 => f3.Name(r => r.ClientIpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).CopyTo(fd => fd.Field(EventIndex.Alias.IpAddress))) + .Text(f3 => f3.Name(r => r.UserAgent).AddKeywordField()) + .Text(f3 => f3.Name(r => r.Path).Analyzer(EventIndex.URL_PATH_ANALYZER).AddKeywordField()) + .Text(f3 => f3.Name(r => r.Host).Analyzer(EventIndex.HOST_ANALYZER).AddKeywordField()) + .Scalar(r => r.Port) + .Keyword(f3 => f3.Name(r => r.HttpMethod)) + .Object(f3 => f3.Name(e => e.Data).Properties(p4 => p4 + .Text(f4 => f4.Name(RequestInfo.KnownDataKeys.Browser).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) + .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.BrowserVersion).IgnoreAbove(1024)) + .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.BrowserMajorVersion).IgnoreAbove(1024)) + .Text(f4 => f4.Name(RequestInfo.KnownDataKeys.Device).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) + .Text(f4 => f4.Name(RequestInfo.KnownDataKeys.OS).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField().CopyTo(fd => fd.Field(EventIndex.Alias.OperatingSystem)).Index(false)) + .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.OSVersion).IgnoreAbove(1024)) + .Keyword(f4 => f4.Name(RequestInfo.KnownDataKeys.OSMajorVersion).IgnoreAbove(1024)) + .Boolean(f4 => f4.Name(RequestInfo.KnownDataKeys.IsBot)))))); + } - public static PropertiesDescriptor AddErrorMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.Error).Properties(p3 => p3 - .Object(f4 => f4.Name(e => e.Data).Properties(p4 => p4 - .Object(f5 => f5.Name(Error.KnownDataKeys.TargetInfo).Properties(p5 => p5 - .Keyword(f6 => f6.Name("ExceptionType").IgnoreAbove(1024).CopyTo(fd => fd.Field(EventIndex.Alias.ErrorTargetType))) - .Keyword(f6 => f6.Name("Method").IgnoreAbove(1024).CopyTo(fd => fd.Field(EventIndex.Alias.ErrorTargetMethod))))))))); - } + public static PropertiesDescriptor AddErrorMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.Error).Properties(p3 => p3 + .Object(f4 => f4.Name(e => e.Data).Properties(p4 => p4 + .Object(f5 => f5.Name(Error.KnownDataKeys.TargetInfo).Properties(p5 => p5 + .Keyword(f6 => f6.Name("ExceptionType").IgnoreAbove(1024).CopyTo(fd => fd.Field(EventIndex.Alias.ErrorTargetType))) + .Keyword(f6 => f6.Name("Method").IgnoreAbove(1024).CopyTo(fd => fd.Field(EventIndex.Alias.ErrorTargetMethod))))))))); + } - public static PropertiesDescriptor AddSimpleErrorMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.SimpleError).Properties(p3 => p3 - .Object(f4 => f4.Name(e => e.Data).Properties(p4 => p4 - .Object(f5 => f5.Name(Error.KnownDataKeys.TargetInfo).Properties(p5 => p5 - .Keyword(f6 => f6.Name("ExceptionType").IgnoreAbove(1024).CopyTo(fd => fd.Field(EventIndex.Alias.ErrorTargetType))))))))); - } + public static PropertiesDescriptor AddSimpleErrorMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.SimpleError).Properties(p3 => p3 + .Object(f4 => f4.Name(e => e.Data).Properties(p4 => p4 + .Object(f5 => f5.Name(Error.KnownDataKeys.TargetInfo).Properties(p5 => p5 + .Keyword(f6 => f6.Name("ExceptionType").IgnoreAbove(1024).CopyTo(fd => fd.Field(EventIndex.Alias.ErrorTargetType))))))))); + } - public static PropertiesDescriptor AddEnvironmentInfoMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.EnvironmentInfo).Properties(p3 => p3 - .Text(f3 => f3.Name(r => r.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).CopyTo(fd => fd.Field(EventIndex.Alias.IpAddress))) - .Text(f3 => f3.Name(r => r.MachineName).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField().Boost(1.1)) - .Text(f3 => f3.Name(r => r.OSName).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField().CopyTo(fd => fd.Field(EventIndex.Alias.OperatingSystem))) - .Keyword(f3 => f3.Name(r => r.CommandLine).IgnoreAbove(1024)) - .Keyword(f3 => f3.Name(r => r.Architecture).IgnoreAbove(1024)))); - } + public static PropertiesDescriptor AddEnvironmentInfoMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.EnvironmentInfo).Properties(p3 => p3 + .Text(f3 => f3.Name(r => r.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).CopyTo(fd => fd.Field(EventIndex.Alias.IpAddress))) + .Text(f3 => f3.Name(r => r.MachineName).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField().Boost(1.1)) + .Text(f3 => f3.Name(r => r.OSName).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField().CopyTo(fd => fd.Field(EventIndex.Alias.OperatingSystem))) + .Keyword(f3 => f3.Name(r => r.CommandLine).IgnoreAbove(1024)) + .Keyword(f3 => f3.Name(r => r.Architecture).IgnoreAbove(1024)))); + } - public static PropertiesDescriptor AddUserDescriptionMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.UserDescription).Properties(p3 => p3 - .Text(f3 => f3.Name(r => r.Description)) - .Text(f3 => f3.Name(r => r.EmailAddress).Analyzer(EventIndex.EMAIL_ANALYZER).SearchAnalyzer("simple").Boost(1.1).AddKeywordField().CopyTo(f4 => f4.Field($"data.{Event.KnownDataKeys.UserInfo}.identity"))))); - } + public static PropertiesDescriptor AddUserDescriptionMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.UserDescription).Properties(p3 => p3 + .Text(f3 => f3.Name(r => r.Description)) + .Text(f3 => f3.Name(r => r.EmailAddress).Analyzer(EventIndex.EMAIL_ANALYZER).SearchAnalyzer("simple").Boost(1.1).AddKeywordField().CopyTo(f4 => f4.Field($"data.{Event.KnownDataKeys.UserInfo}.identity"))))); + } - public static PropertiesDescriptor AddUserInfoMapping(this PropertiesDescriptor descriptor) { - return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.UserInfo).Properties(p3 => p3 - .Text(f3 => f3.Name(r => r.Identity).Analyzer(EventIndex.EMAIL_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.1).AddKeywordField()) - .Text(f3 => f3.Name(r => r.Name).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()))); - } + public static PropertiesDescriptor AddUserInfoMapping(this PropertiesDescriptor descriptor) { + return descriptor.Object(f2 => f2.Name(Event.KnownDataKeys.UserInfo).Properties(p3 => p3 + .Text(f3 => f3.Name(r => r.Identity).Analyzer(EventIndex.EMAIL_ANALYZER).SearchAnalyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).Boost(1.1).AddKeywordField()) + .Text(f3 => f3.Name(r => r.Name).Analyzer(EventIndex.LOWER_KEYWORD_ANALYZER).AddKeywordField()))); } } diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/OrganizationIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/OrganizationIndex.cs index b40e2f5321..e0033e3a19 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/OrganizationIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/OrganizationIndex.cs @@ -1,64 +1,63 @@ -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories.Elasticsearch.Configuration; using Foundatio.Repositories.Elasticsearch.Extensions; using Nest; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class OrganizationIndex : VersionedIndex { - private const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; - private readonly ExceptionlessElasticConfiguration _configuration; +namespace Exceptionless.Core.Repositories.Configuration; - public OrganizationIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "organizations", 1) { - _configuration = configuration; - } +public sealed class OrganizationIndex : VersionedIndex { + private const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; + private readonly ExceptionlessElasticConfiguration _configuration; - public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { - return map - .Dynamic(false) - .Properties(p => p - .SetupDefaults() - .Text(f => f.Name(e => e.Name).AddKeywordField()) - .Keyword(f => f.Name(u => u.StripeCustomerId)) - .Boolean(f => f.Name(u => u.HasPremiumFeatures)) - .Keyword(f => f.Name(u => u.PlanId)) - .Keyword(f => f.Name(u => u.PlanName).IgnoreAbove(256)) - .Date(f => f.Name(u => u.SubscribeDate)) - .Number(f => f.Name(u => u.BillingStatus)) - .Scalar(f => f.BillingPrice, f => f) - .Boolean(f => f.Name(u => u.IsSuspended)) - .Scalar(f => f.RetentionDays, f => f) - .Object(f => f.Name(o => o.Invites.First()).Properties(ip => ip - .Keyword(fu => fu.Name(i => i.Token)) - .Text(fu => fu.Name(i => i.EmailAddress).Analyzer(KEYWORD_LOWERCASE_ANALYZER)))) - .Date(f => f.Name(s => s.LastEventDateUtc)) - .AddUsageMappings()); - } + public OrganizationIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "organizations", 1) { + _configuration = configuration; + } + + public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { + return map + .Dynamic(false) + .Properties(p => p + .SetupDefaults() + .Text(f => f.Name(e => e.Name).AddKeywordField()) + .Keyword(f => f.Name(u => u.StripeCustomerId)) + .Boolean(f => f.Name(u => u.HasPremiumFeatures)) + .Keyword(f => f.Name(u => u.PlanId)) + .Keyword(f => f.Name(u => u.PlanName).IgnoreAbove(256)) + .Date(f => f.Name(u => u.SubscribeDate)) + .Number(f => f.Name(u => u.BillingStatus)) + .Scalar(f => f.BillingPrice, f => f) + .Boolean(f => f.Name(u => u.IsSuspended)) + .Scalar(f => f.RetentionDays, f => f) + .Object(f => f.Name(o => o.Invites.First()).Properties(ip => ip + .Keyword(fu => fu.Name(i => i.Token)) + .Text(fu => fu.Name(i => i.EmailAddress).Analyzer(KEYWORD_LOWERCASE_ANALYZER)))) + .Date(f => f.Name(s => s.LastEventDateUtc)) + .AddUsageMappings()); + } - public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { - return base.ConfigureIndex(idx.Settings(s => s - .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) - .NumberOfShards(_configuration.Options.NumberOfShards) - .NumberOfReplicas(_configuration.Options.NumberOfReplicas) - .Priority(10))); - } + public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { + return base.ConfigureIndex(idx.Settings(s => s + .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) + .NumberOfShards(_configuration.Options.NumberOfShards) + .NumberOfReplicas(_configuration.Options.NumberOfReplicas) + .Priority(10))); } +} - internal static class OrganizationIndexExtensions { - public static PropertiesDescriptor AddUsageMappings(this PropertiesDescriptor descriptor) { - return descriptor - .Object(ui => ui.Name(o => o.Usage.First()).Properties(p => p - .Date(fu => fu.Name(i => i.Date)) - .Number(fu => fu.Name(i => i.Total)) - .Number(fu => fu.Name(i => i.Blocked)) - .Number(fu => fu.Name(i => i.Limit)) - .Number(fu => fu.Name(i => i.TooBig)))) - .Object(ui => ui.Name(o => o.OverageHours.First()).Properties(p => p - .Date(fu => fu.Name(i => i.Date)) - .Number(fu => fu.Name(i => i.Total)) - .Number(fu => fu.Name(i => i.Blocked)) - .Number(fu => fu.Name(i => i.Limit)) - .Number(fu => fu.Name(i => i.TooBig)))); - } +internal static class OrganizationIndexExtensions { + public static PropertiesDescriptor AddUsageMappings(this PropertiesDescriptor descriptor) { + return descriptor + .Object(ui => ui.Name(o => o.Usage.First()).Properties(p => p + .Date(fu => fu.Name(i => i.Date)) + .Number(fu => fu.Name(i => i.Total)) + .Number(fu => fu.Name(i => i.Blocked)) + .Number(fu => fu.Name(i => i.Limit)) + .Number(fu => fu.Name(i => i.TooBig)))) + .Object(ui => ui.Name(o => o.OverageHours.First()).Properties(p => p + .Date(fu => fu.Name(i => i.Date)) + .Number(fu => fu.Name(i => i.Total)) + .Number(fu => fu.Name(i => i.Blocked)) + .Number(fu => fu.Name(i => i.Limit)) + .Number(fu => fu.Name(i => i.TooBig)))); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/ProjectIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/ProjectIndex.cs index 38aa6609b9..3e21989224 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/ProjectIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/ProjectIndex.cs @@ -1,56 +1,55 @@ -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories.Elasticsearch.Configuration; using Foundatio.Repositories.Elasticsearch.Extensions; using Nest; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class ProjectIndex : VersionedIndex { - internal const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; - private readonly ExceptionlessElasticConfiguration _configuration; +namespace Exceptionless.Core.Repositories.Configuration; - public ProjectIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "projects", 1) { - _configuration = configuration; - } +public sealed class ProjectIndex : VersionedIndex { + internal const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; + private readonly ExceptionlessElasticConfiguration _configuration; + + public ProjectIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "projects", 1) { + _configuration = configuration; + } - public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { - return map - .Dynamic(false) - .Properties(p => p - .SetupDefaults() - .Keyword(f => f.Name(e => e.OrganizationId)) - .Text(f => f.Name(e => e.Name).AddKeywordField()) - .Scalar(f => f.NextSummaryEndOfDayTicks, f => f) - .Date(f => f.Name(s => s.LastEventDateUtc)) - .AddUsageMappings() - ); - } + public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { + return map + .Dynamic(false) + .Properties(p => p + .SetupDefaults() + .Keyword(f => f.Name(e => e.OrganizationId)) + .Text(f => f.Name(e => e.Name).AddKeywordField()) + .Scalar(f => f.NextSummaryEndOfDayTicks, f => f) + .Date(f => f.Name(s => s.LastEventDateUtc)) + .AddUsageMappings() + ); + } - public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { - return base.ConfigureIndex(idx.Settings(s => s - .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) - .NumberOfShards(_configuration.Options.NumberOfShards) - .NumberOfReplicas(_configuration.Options.NumberOfReplicas) - .Priority(10))); - } + public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { + return base.ConfigureIndex(idx.Settings(s => s + .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) + .NumberOfShards(_configuration.Options.NumberOfShards) + .NumberOfReplicas(_configuration.Options.NumberOfReplicas) + .Priority(10))); } +} - internal static class ProjectIndexExtensions { - public static PropertiesDescriptor AddUsageMappings(this PropertiesDescriptor descriptor) { - return descriptor - .Object(ui => ui.Name(o => o.Usage.First()).Properties(p => p - .Date(fu => fu.Name(i => i.Date)) - .Number(fu => fu.Name(i => i.Total)) - .Number(fu => fu.Name(i => i.Blocked)) - .Number(fu => fu.Name(i => i.Limit)) - .Number(fu => fu.Name(i => i.TooBig)))) - .Object(ui => ui.Name(o => o.OverageHours.First()).Properties(p => p - .Date(fu => fu.Name(i => i.Date)) - .Number(fu => fu.Name(i => i.Total)) - .Number(fu => fu.Name(i => i.Blocked)) - .Number(fu => fu.Name(i => i.Limit)) - .Number(fu => fu.Name(i => i.TooBig)))); - } +internal static class ProjectIndexExtensions { + public static PropertiesDescriptor AddUsageMappings(this PropertiesDescriptor descriptor) { + return descriptor + .Object(ui => ui.Name(o => o.Usage.First()).Properties(p => p + .Date(fu => fu.Name(i => i.Date)) + .Number(fu => fu.Name(i => i.Total)) + .Number(fu => fu.Name(i => i.Blocked)) + .Number(fu => fu.Name(i => i.Limit)) + .Number(fu => fu.Name(i => i.TooBig)))) + .Object(ui => ui.Name(o => o.OverageHours.First()).Properties(p => p + .Date(fu => fu.Name(i => i.Date)) + .Number(fu => fu.Name(i => i.Total)) + .Number(fu => fu.Name(i => i.Blocked)) + .Number(fu => fu.Name(i => i.Limit)) + .Number(fu => fu.Name(i => i.TooBig)))); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs index eadb22c6a6..75c8c1b393 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Queries; using Foundatio.Parsers.ElasticQueries; @@ -6,103 +5,103 @@ using Foundatio.Repositories.Elasticsearch.Extensions; using Nest; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class StackIndex : VersionedIndex { - private const string COMMA_WHITESPACE_ANALYZER = "comma_whitespace"; - private const string STANDARDPLUS_ANALYZER = "standardplus"; - private const string WHITESPACE_LOWERCASE_ANALYZER = "whitespace_lower"; - private const string COMMA_WHITESPACE_TOKENIZER = "comma_whitespace"; - - private readonly ExceptionlessElasticConfiguration _configuration; +namespace Exceptionless.Core.Repositories.Configuration; - public StackIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "stacks", 1) { - _configuration = configuration; - } - - public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { - return base.ConfigureIndex(idx.Settings(s => s - .Analysis(BuildAnalysis) - .NumberOfShards(_configuration.Options.NumberOfShards) - .NumberOfReplicas(_configuration.Options.NumberOfReplicas) - .Priority(5))); - } - - public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { - return map - .Dynamic(false) - .Properties(p => p - .SetupDefaults() - .Keyword(f => f.Name(s => s.OrganizationId).IgnoreAbove(1024)) - .FieldAlias(a => a.Name(Alias.OrganizationId).Path(f => f.OrganizationId)) - .Keyword(f => f.Name(s => s.ProjectId).IgnoreAbove(1024)) - .FieldAlias(a => a.Name(Alias.ProjectId).Path(f => f.ProjectId)) - .Keyword(f => f.Name(s => s.Status)) - .Date(f => f.Name(s => s.SnoozeUntilUtc)) - .Keyword(f => f.Name(s => s.SignatureHash).IgnoreAbove(1024)) - .FieldAlias(a => a.Name(Alias.SignatureHash).Path(f => f.SignatureHash)) - .Keyword(f => f.Name(s => s.DuplicateSignature)) - .Keyword(f => f.Name(e => e.Type).IgnoreAbove(1024)) - .Date(f => f.Name(s => s.FirstOccurrence)) - .FieldAlias(a => a.Name(Alias.FirstOccurrence).Path(f => f.FirstOccurrence)) - .Date(f => f.Name(s => s.LastOccurrence)) - .FieldAlias(a => a.Name(Alias.LastOccurrence).Path(f => f.LastOccurrence)) - .Text(f => f.Name(s => s.Title).Boost(1.1)) - .Text(f => f.Name(s => s.Description)) - .Keyword(f => f.Name(s => s.Tags).IgnoreAbove(1024).Boost(1.2)) - .FieldAlias(a => a.Name(Alias.Tags).Path(f => f.Tags)) - .Keyword(f => f.Name(s => s.References).IgnoreAbove(1024)) - .FieldAlias(a => a.Name(Alias.References).Path(f => f.References)) - .Date(f => f.Name(s => s.DateFixed)) - .FieldAlias(a => a.Name(Alias.DateFixed).Path(f => f.DateFixed)) - .Boolean(f => f.Name(Alias.IsFixed)) - .Keyword(f => f.Name(s => s.FixedInVersion).IgnoreAbove(1024)) - .FieldAlias(a => a.Name(Alias.FixedInVersion).Path(f => f.FixedInVersion)) - .Boolean(f => f.Name(s => s.OccurrencesAreCritical)) - .FieldAlias(a => a.Name(Alias.OccurrencesAreCritical).Path(f => f.OccurrencesAreCritical)) - .Scalar(f => f.TotalOccurrences) - .FieldAlias(a => a.Name(Alias.TotalOccurrences).Path(f => f.TotalOccurrences)) - ); - } +public sealed class StackIndex : VersionedIndex { + private const string COMMA_WHITESPACE_ANALYZER = "comma_whitespace"; + private const string STANDARDPLUS_ANALYZER = "standardplus"; + private const string WHITESPACE_LOWERCASE_ANALYZER = "whitespace_lower"; + private const string COMMA_WHITESPACE_TOKENIZER = "comma_whitespace"; - protected override void ConfigureQueryParser(ElasticQueryParserConfiguration config) { - string dateFixedFieldName = InferPropertyName(f => f.DateFixed); - config - .SetDefaultFields(new[] { "id", Alias.Title, Alias.Description, Alias.Tags, Alias.References }) - .AddVisitor(new StackDateFixedQueryVisitor(dateFixedFieldName)) - .UseFieldMap(new Dictionary { + private readonly ExceptionlessElasticConfiguration _configuration; + + public StackIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "stacks", 1) { + _configuration = configuration; + } + + public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { + return base.ConfigureIndex(idx.Settings(s => s + .Analysis(BuildAnalysis) + .NumberOfShards(_configuration.Options.NumberOfShards) + .NumberOfReplicas(_configuration.Options.NumberOfReplicas) + .Priority(5))); + } + + public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { + return map + .Dynamic(false) + .Properties(p => p + .SetupDefaults() + .Keyword(f => f.Name(s => s.OrganizationId).IgnoreAbove(1024)) + .FieldAlias(a => a.Name(Alias.OrganizationId).Path(f => f.OrganizationId)) + .Keyword(f => f.Name(s => s.ProjectId).IgnoreAbove(1024)) + .FieldAlias(a => a.Name(Alias.ProjectId).Path(f => f.ProjectId)) + .Keyword(f => f.Name(s => s.Status)) + .Date(f => f.Name(s => s.SnoozeUntilUtc)) + .Keyword(f => f.Name(s => s.SignatureHash).IgnoreAbove(1024)) + .FieldAlias(a => a.Name(Alias.SignatureHash).Path(f => f.SignatureHash)) + .Keyword(f => f.Name(s => s.DuplicateSignature)) + .Keyword(f => f.Name(e => e.Type).IgnoreAbove(1024)) + .Date(f => f.Name(s => s.FirstOccurrence)) + .FieldAlias(a => a.Name(Alias.FirstOccurrence).Path(f => f.FirstOccurrence)) + .Date(f => f.Name(s => s.LastOccurrence)) + .FieldAlias(a => a.Name(Alias.LastOccurrence).Path(f => f.LastOccurrence)) + .Text(f => f.Name(s => s.Title).Boost(1.1)) + .Text(f => f.Name(s => s.Description)) + .Keyword(f => f.Name(s => s.Tags).IgnoreAbove(1024).Boost(1.2)) + .FieldAlias(a => a.Name(Alias.Tags).Path(f => f.Tags)) + .Keyword(f => f.Name(s => s.References).IgnoreAbove(1024)) + .FieldAlias(a => a.Name(Alias.References).Path(f => f.References)) + .Date(f => f.Name(s => s.DateFixed)) + .FieldAlias(a => a.Name(Alias.DateFixed).Path(f => f.DateFixed)) + .Boolean(f => f.Name(Alias.IsFixed)) + .Keyword(f => f.Name(s => s.FixedInVersion).IgnoreAbove(1024)) + .FieldAlias(a => a.Name(Alias.FixedInVersion).Path(f => f.FixedInVersion)) + .Boolean(f => f.Name(s => s.OccurrencesAreCritical)) + .FieldAlias(a => a.Name(Alias.OccurrencesAreCritical).Path(f => f.OccurrencesAreCritical)) + .Scalar(f => f.TotalOccurrences) + .FieldAlias(a => a.Name(Alias.TotalOccurrences).Path(f => f.TotalOccurrences)) + ); + } + + protected override void ConfigureQueryParser(ElasticQueryParserConfiguration config) { + string dateFixedFieldName = InferPropertyName(f => f.DateFixed); + config + .SetDefaultFields(new[] { "id", Alias.Title, Alias.Description, Alias.Tags, Alias.References }) + .AddVisitor(new StackDateFixedQueryVisitor(dateFixedFieldName)) + .UseFieldMap(new Dictionary { { Alias.Stack, "id" } - }); - } - - private AnalysisDescriptor BuildAnalysis(AnalysisDescriptor ad) { - return ad.Analyzers(a => a - .Pattern(COMMA_WHITESPACE_ANALYZER, p => p.Pattern(@"[,\s]+")) - .Custom(STANDARDPLUS_ANALYZER, c => c.Filters("lowercase", "stop", "unique").Tokenizer(COMMA_WHITESPACE_TOKENIZER)) - .Custom(WHITESPACE_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("whitespace"))) - .Tokenizers(t => t - .Pattern(COMMA_WHITESPACE_TOKENIZER, p => p.Pattern(@"[,\s]+"))); - } + }); + } + + private AnalysisDescriptor BuildAnalysis(AnalysisDescriptor ad) { + return ad.Analyzers(a => a + .Pattern(COMMA_WHITESPACE_ANALYZER, p => p.Pattern(@"[,\s]+")) + .Custom(STANDARDPLUS_ANALYZER, c => c.Filters("lowercase", "stop", "unique").Tokenizer(COMMA_WHITESPACE_TOKENIZER)) + .Custom(WHITESPACE_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("whitespace"))) + .Tokenizers(t => t + .Pattern(COMMA_WHITESPACE_TOKENIZER, p => p.Pattern(@"[,\s]+"))); + } - public class Alias { - public const string Stack = "stack"; - public const string Status = "status"; - public const string OrganizationId = "organization"; - public const string ProjectId = "project"; - public const string SignatureHash = "signature"; - public const string Type = "type"; - public const string FirstOccurrence = "first"; - public const string LastOccurrence = "last"; - public const string Title = "title"; - public const string Description = "description"; - public const string Tags = "tag"; - public const string References = "links"; - public const string DateFixed = "fixedon"; - public const string IsFixed = "fixed"; - public const string FixedInVersion = "version_fixed"; - public const string IsHidden = "hidden"; - public const string IsRegressed = "regressed"; - public const string OccurrencesAreCritical = "critical"; - public const string TotalOccurrences = "occurrences"; - } + public class Alias { + public const string Stack = "stack"; + public const string Status = "status"; + public const string OrganizationId = "organization"; + public const string ProjectId = "project"; + public const string SignatureHash = "signature"; + public const string Type = "type"; + public const string FirstOccurrence = "first"; + public const string LastOccurrence = "last"; + public const string Title = "title"; + public const string Description = "description"; + public const string Tags = "tag"; + public const string References = "links"; + public const string DateFixed = "fixedon"; + public const string IsFixed = "fixed"; + public const string FixedInVersion = "version_fixed"; + public const string IsHidden = "hidden"; + public const string IsRegressed = "regressed"; + public const string OccurrencesAreCritical = "critical"; + public const string TotalOccurrences = "occurrences"; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/TokenIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/TokenIndex.cs index ff55b53df8..5fd5f79a95 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/TokenIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/TokenIndex.cs @@ -2,38 +2,38 @@ using Foundatio.Repositories.Elasticsearch.Extensions; using Nest; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class TokenIndex : VersionedIndex { - internal const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; - private readonly ExceptionlessElasticConfiguration _configuration; +namespace Exceptionless.Core.Repositories.Configuration; - public TokenIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "tokens", 1) { - _configuration = configuration; - } +public sealed class TokenIndex : VersionedIndex { + internal const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; + private readonly ExceptionlessElasticConfiguration _configuration; - public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { - return map - .Dynamic(false) - .Properties(p => p - .SetupDefaults() - .Date(f => f.Name(e => e.ExpiresUtc)) - .Keyword(f => f.Name(e => e.OrganizationId)) - .Keyword(f => f.Name(e => e.ProjectId)) - .Keyword(f => f.Name(e => e.DefaultProjectId)) - .Keyword(f => f.Name(e => e.UserId)) - .Keyword(f => f.Name(u => u.CreatedBy)) - .Keyword(f => f.Name(e => e.Refresh)) - .Keyword(f => f.Name(e => e.Scopes)) - .Boolean(f => f.Name(e => e.IsDisabled)) - .Number(f => f.Name(e => e.Type).Type(NumberType.Byte))); - } + public TokenIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "tokens", 1) { + _configuration = configuration; + } + + public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { + return map + .Dynamic(false) + .Properties(p => p + .SetupDefaults() + .Date(f => f.Name(e => e.ExpiresUtc)) + .Keyword(f => f.Name(e => e.OrganizationId)) + .Keyword(f => f.Name(e => e.ProjectId)) + .Keyword(f => f.Name(e => e.DefaultProjectId)) + .Keyword(f => f.Name(e => e.UserId)) + .Keyword(f => f.Name(u => u.CreatedBy)) + .Keyword(f => f.Name(e => e.Refresh)) + .Keyword(f => f.Name(e => e.Scopes)) + .Boolean(f => f.Name(e => e.IsDisabled)) + .Number(f => f.Name(e => e.Type).Type(NumberType.Byte))); + } - public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { - return base.ConfigureIndex(idx.Settings(s => s - .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) - .NumberOfShards(_configuration.Options.NumberOfShards) - .NumberOfReplicas(_configuration.Options.NumberOfReplicas) - .Priority(10))); - } + public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { + return base.ConfigureIndex(idx.Settings(s => s + .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) + .NumberOfShards(_configuration.Options.NumberOfShards) + .NumberOfReplicas(_configuration.Options.NumberOfReplicas) + .Priority(10))); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/UserIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/UserIndex.cs index 2e8a3ee1ad..50a1a82b99 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/UserIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/UserIndex.cs @@ -1,45 +1,44 @@ -using System.Linq; using Exceptionless.Core.Models; using Foundatio.Repositories.Elasticsearch.Configuration; using Foundatio.Repositories.Elasticsearch.Extensions; using Nest; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class UserIndex : VersionedIndex { - private const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; - private readonly ExceptionlessElasticConfiguration _configuration; +namespace Exceptionless.Core.Repositories.Configuration; - public UserIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "users", 1) { - _configuration = configuration; - } +public sealed class UserIndex : VersionedIndex { + private const string KEYWORD_LOWERCASE_ANALYZER = "keyword_lowercase"; + private readonly ExceptionlessElasticConfiguration _configuration; - public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { - return map - .Dynamic(false) - .Properties(p => p - .SetupDefaults() - .Keyword(f => f.Name(e => e.OrganizationIds)) - .Text(f => f.Name(u => u.FullName).AddKeywordField()) - .Text(f => f.Name(u => u.EmailAddress).Analyzer(KEYWORD_LOWERCASE_ANALYZER).AddKeywordField()) - .Boolean(f => f.Name(u => u.IsEmailAddressVerified)) - .Keyword(f => f.Name(u => u.VerifyEmailAddressToken)) - .Date(f => f.Name(u => u.VerifyEmailAddressTokenExpiration)) - .Keyword(f => f.Name(u => u.PasswordResetToken)) - .Date(f => f.Name(u => u.PasswordResetTokenExpiration)) - .Keyword(f => f.Name(u => u.Roles)) - .Object(f => f.Name(o => o.OAuthAccounts.First()).Properties(mp => mp - .Keyword(fu => fu.Name(m => m.Provider)) - .Keyword(fu => fu.Name(m => m.ProviderUserId)) - .Keyword(fu => fu.Name(m => m.Username)))) - ); - } + public UserIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "users", 1) { + _configuration = configuration; + } + + public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { + return map + .Dynamic(false) + .Properties(p => p + .SetupDefaults() + .Keyword(f => f.Name(e => e.OrganizationIds)) + .Text(f => f.Name(u => u.FullName).AddKeywordField()) + .Text(f => f.Name(u => u.EmailAddress).Analyzer(KEYWORD_LOWERCASE_ANALYZER).AddKeywordField()) + .Boolean(f => f.Name(u => u.IsEmailAddressVerified)) + .Keyword(f => f.Name(u => u.VerifyEmailAddressToken)) + .Date(f => f.Name(u => u.VerifyEmailAddressTokenExpiration)) + .Keyword(f => f.Name(u => u.PasswordResetToken)) + .Date(f => f.Name(u => u.PasswordResetTokenExpiration)) + .Keyword(f => f.Name(u => u.Roles)) + .Object(f => f.Name(o => o.OAuthAccounts.First()).Properties(mp => mp + .Keyword(fu => fu.Name(m => m.Provider)) + .Keyword(fu => fu.Name(m => m.ProviderUserId)) + .Keyword(fu => fu.Name(m => m.Username)))) + ); + } - public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { - return base.ConfigureIndex(idx.Settings(s => s - .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) - .NumberOfShards(_configuration.Options.NumberOfShards) - .NumberOfReplicas(_configuration.Options.NumberOfReplicas) - .Priority(5))); - } + public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { + return base.ConfigureIndex(idx.Settings(s => s + .Analysis(d => d.Analyzers(b => b.Custom(KEYWORD_LOWERCASE_ANALYZER, c => c.Filters("lowercase").Tokenizer("keyword")))) + .NumberOfShards(_configuration.Options.NumberOfShards) + .NumberOfReplicas(_configuration.Options.NumberOfReplicas) + .Priority(5))); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/WebHookIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/WebHookIndex.cs index efc881197b..48ae85ad69 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/WebHookIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/WebHookIndex.cs @@ -3,32 +3,32 @@ using Foundatio.Repositories.Elasticsearch.Extensions; using Nest; -namespace Exceptionless.Core.Repositories.Configuration { - public sealed class WebHookIndex : VersionedIndex { - private readonly ExceptionlessElasticConfiguration _configuration; +namespace Exceptionless.Core.Repositories.Configuration; - public WebHookIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "webhooks", 1) { - _configuration = configuration; - } +public sealed class WebHookIndex : VersionedIndex { + private readonly ExceptionlessElasticConfiguration _configuration; - public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { - return map - .Dynamic(false) - .Properties(p => p - .SetupDefaults() - .Keyword(f => f.Name(e => e.OrganizationId)) - .Keyword(f => f.Name(e => e.ProjectId)) - .Keyword(f => f.Name(e => e.Url)) - .Keyword(f => f.Name(e => e.EventTypes)) - .Boolean(f => f.Name(e => e.IsEnabled)) - ); - } + public WebHookIndex(ExceptionlessElasticConfiguration configuration) : base(configuration, configuration.Options.ScopePrefix + "webhooks", 1) { + _configuration = configuration; + } + + public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) { + return map + .Dynamic(false) + .Properties(p => p + .SetupDefaults() + .Keyword(f => f.Name(e => e.OrganizationId)) + .Keyword(f => f.Name(e => e.ProjectId)) + .Keyword(f => f.Name(e => e.Url)) + .Keyword(f => f.Name(e => e.EventTypes)) + .Boolean(f => f.Name(e => e.IsEnabled)) + ); + } - public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { - return base.ConfigureIndex(idx.Settings(s => s - .NumberOfShards(_configuration.Options.NumberOfShards) - .NumberOfReplicas(_configuration.Options.NumberOfReplicas) - .Priority(5))); - } + public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) { + return base.ConfigureIndex(idx.Settings(s => s + .NumberOfShards(_configuration.Options.NumberOfShards) + .NumberOfReplicas(_configuration.Options.NumberOfReplicas) + .Priority(5))); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/EventRepository.cs b/src/Exceptionless.Core/Repositories/EventRepository.cs index 6dac462b3f..f241e3df0e 100644 --- a/src/Exceptionless.Core/Repositories/EventRepository.cs +++ b/src/Exceptionless.Core/Repositories/EventRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using Exceptionless.Core.Repositories.Queries; @@ -12,175 +9,175 @@ using Foundatio.Utility; using Nest; -namespace Exceptionless.Core.Repositories { - public class EventRepository : RepositoryOwnedByOrganizationAndProject, IEventRepository { - public EventRepository(ExceptionlessElasticConfiguration configuration, AppOptions options, IValidator validator) - : base(configuration.Events, validator, options) { - DisableCache(); // NOTE: If cache is ever enabled, then fast paths for patching/deleting with scripts will be super slow! - BatchNotifications = true; - DefaultPipeline = "events-pipeline"; - - AddDefaultExclude(e => e.Idx); - // copy to fields - AddDefaultExclude(EventIndex.Alias.IpAddress); - AddDefaultExclude(EventIndex.Alias.OperatingSystem); - AddDefaultExclude("error"); - - AddPropertyRequiredForRemove(e => e.Date); - } - - public Task> GetOpenSessionsAsync(DateTime createdBeforeUtc, CommandOptionsDescriptor options = null) { - var filter = Query.Term(e => e.Type, Event.KnownTypes.Session) && !Query.Exists(f => f.Field(e => e.Idx[Event.KnownDataKeys.SessionEnd + "-d"])); - if (createdBeforeUtc.Ticks > 0) - filter &= Query.DateRange(r => r.Field(e => e.Date).LessThanOrEquals(createdBeforeUtc)); - - return FindAsync(q => q.ElasticFilter(filter).SortDescending(e => e.Date), options); - } - - public async Task UpdateSessionStartLastActivityAsync(string id, DateTime lastActivityUtc, bool isSessionEnd = false, bool hasError = false, bool sendNotifications = true) { - var ev = await GetByIdAsync(id).AnyContext(); - if (!ev.UpdateSessionStart(lastActivityUtc, isSessionEnd)) - return false; - - await SaveAsync(ev, o => o.Notifications(sendNotifications)).AnyContext(); - return true; - } - - public Task RemoveAllAsync(string organizationId, string clientIpAddress, DateTime? utcStart, DateTime? utcEnd, CommandOptionsDescriptor options = null) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); - - var query = new RepositoryQuery().Organization(organizationId); - if (utcStart.HasValue && utcEnd.HasValue) - query = query.DateRange(utcStart, utcEnd, InferField(e => e.Date)).Index(utcStart, utcEnd); - else if (utcEnd.HasValue) - query = query.ElasticFilter(Query.DateRange(r => r.Field(e => e.Date).LessThan(utcEnd))); - else if (utcStart.HasValue) - query = query.ElasticFilter(Query.DateRange(r => r.Field(e => e.Date).GreaterThan(utcStart))); - - if (!String.IsNullOrEmpty(clientIpAddress)) - query = query.FieldEquals(EventIndex.Alias.IpAddress, clientIpAddress); - - return RemoveAllAsync(q => query, options); - } - - public Task> GetByReferenceIdAsync(string projectId, string referenceId) { - var filter = Query.Term(e => e.ReferenceId, referenceId); - return FindAsync(q => q.Project(projectId).ElasticFilter(filter).SortDescending(e => e.Date), o => o.PageLimit(10)); - } - - public async Task GetPreviousAndNextEventIdsAsync(PersistentEvent ev, AppFilter systemFilter, DateTime? utcStart, DateTime? utcEnd) { - var previous = GetPreviousEventIdAsync(ev, systemFilter, utcStart, utcEnd); - var next = GetNextEventIdAsync(ev, systemFilter, utcStart, utcEnd); - await Task.WhenAll(previous, next).AnyContext(); - - return new PreviousAndNextEventIdResult { - Previous = previous.Result, - Next = next.Result - }; - } - - private async Task GetPreviousEventIdAsync(PersistentEvent ev, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { - if (ev == null) - return null; - - var retentionDate = _options.MaximumRetentionDays > 0 ? SystemClock.UtcNow.Date.SubtractDays(_options.MaximumRetentionDays) : DateTime.MinValue; - if (!utcStart.HasValue || utcStart.Value.IsBefore(retentionDate)) - utcStart = retentionDate; - - if (!utcEnd.HasValue || utcEnd.Value.IsAfter(ev.Date.UtcDateTime)) - utcEnd = ev.Date.UtcDateTime; - - var utcEventDate = ev.Date.UtcDateTime; - // utcEnd is before the current event date. - if (utcStart > utcEventDate || utcEnd < utcEventDate) - return null; - - var results = await FindAsync(q => q - .DateRange(utcStart, utcEventDate, (PersistentEvent e) => e.Date) - .Index(utcStart, utcEventDate) - .SortDescending(e => e.Date) - .Include(e => e.Id, e => e.Date) - .AppFilter(systemFilter) - .ElasticFilter(!Query.Ids(ids => ids.Values(ev.Id))) - .FilterExpression(String.Concat(EventIndex.Alias.StackId, ":", ev.StackId)) - .EnforceEventStackFilter(false), o => o.PageLimit(10)).AnyContext(); - - if (results.Total == 0) - return null; - - // make sure we don't have records with the exact same occurrence date - if (results.Documents.All(t => t.Date != ev.Date)) - return results.Documents.OrderByDescending(t => t.Date).ThenByDescending(t => t.Id).First().Id; - - // we have records with the exact same occurrence date, we need to figure out the order of those - // put our target error into the mix, sort it and return the result before the target - var unionResults = results.Documents.Union(new[] { ev }) - .OrderBy(t => t.Date.UtcTicks).ThenBy(t => t.Id) - .ToList(); - - int index = unionResults.FindIndex(t => t.Id == ev.Id); - return index == 0 ? null : unionResults[index - 1].Id; - } - - private async Task GetNextEventIdAsync(PersistentEvent ev, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { - if (ev == null) - return null; - - if (!utcStart.HasValue || utcStart.Value.IsBefore(ev.Date.UtcDateTime)) - utcStart = ev.Date.UtcDateTime; - - if (!utcEnd.HasValue || utcEnd.Value.IsAfter(SystemClock.UtcNow)) - utcEnd = SystemClock.UtcNow; - - var utcEventDate = ev.Date.UtcDateTime; - // utcEnd is before the current event date. - if (utcStart > utcEventDate || utcEnd < utcEventDate) - return null; - - var results = await FindAsync(q => q - .DateRange(utcEventDate, utcEnd, (PersistentEvent e) => e.Date) - .Index(utcEventDate, utcEnd) - .SortAscending(e => e.Date) - .Include(e => e.Id, e => e.Date) - .AppFilter(systemFilter) - .ElasticFilter(!Query.Ids(ids => ids.Values(ev.Id))) - .FilterExpression(String.Concat(EventIndex.Alias.StackId, ":", ev.StackId)) - .EnforceEventStackFilter(false), o => o.PageLimit(10)).AnyContext(); - - if (results.Total == 0) - return null; - - // make sure we don't have records with the exact same occurrence date - if (results.Documents.All(t => t.Date != ev.Date)) - return results.Documents.OrderBy(t => t.Date).ThenBy(t => t.Id).First().Id; - - // we have records with the exact same occurrence date, we need to figure out the order of those - // put our target error into the mix, sort it and return the result after the target - var unionResults = results.Documents.Union(new[] { ev }) - .OrderBy(t => t.Date.Ticks).ThenBy(t => t.Id) - .ToList(); - - int index = unionResults.FindIndex(t => t.Id == ev.Id); - return index == unionResults.Count - 1 ? null : unionResults[index + 1].Id; - } - - public override Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); - - return FindAsync(q => q.Organization(organizationId).SortDescending(e => e.Date).SortDescending(e => e.Id), options); - } - - public override Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null) { - return FindAsync(q => q.Project(projectId).SortDescending(e => e.Date).SortDescending(e => e.Id), options); - } - - public Task RemoveAllByStackIdsAsync(string[] stackIds) { - if (stackIds is null || stackIds.Length == 0) - throw new ArgumentNullException(nameof(stackIds)); - - return RemoveAllAsync(q => q.Stack(stackIds)); - } +namespace Exceptionless.Core.Repositories; + +public class EventRepository : RepositoryOwnedByOrganizationAndProject, IEventRepository { + public EventRepository(ExceptionlessElasticConfiguration configuration, AppOptions options, IValidator validator) + : base(configuration.Events, validator, options) { + DisableCache(); // NOTE: If cache is ever enabled, then fast paths for patching/deleting with scripts will be super slow! + BatchNotifications = true; + DefaultPipeline = "events-pipeline"; + + AddDefaultExclude(e => e.Idx); + // copy to fields + AddDefaultExclude(EventIndex.Alias.IpAddress); + AddDefaultExclude(EventIndex.Alias.OperatingSystem); + AddDefaultExclude("error"); + + AddPropertyRequiredForRemove(e => e.Date); + } + + public Task> GetOpenSessionsAsync(DateTime createdBeforeUtc, CommandOptionsDescriptor options = null) { + var filter = Query.Term(e => e.Type, Event.KnownTypes.Session) && !Query.Exists(f => f.Field(e => e.Idx[Event.KnownDataKeys.SessionEnd + "-d"])); + if (createdBeforeUtc.Ticks > 0) + filter &= Query.DateRange(r => r.Field(e => e.Date).LessThanOrEquals(createdBeforeUtc)); + + return FindAsync(q => q.ElasticFilter(filter).SortDescending(e => e.Date), options); + } + + public async Task UpdateSessionStartLastActivityAsync(string id, DateTime lastActivityUtc, bool isSessionEnd = false, bool hasError = false, bool sendNotifications = true) { + var ev = await GetByIdAsync(id).AnyContext(); + if (!ev.UpdateSessionStart(lastActivityUtc, isSessionEnd)) + return false; + + await SaveAsync(ev, o => o.Notifications(sendNotifications)).AnyContext(); + return true; + } + + public Task RemoveAllAsync(string organizationId, string clientIpAddress, DateTime? utcStart, DateTime? utcEnd, CommandOptionsDescriptor options = null) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); + + var query = new RepositoryQuery().Organization(organizationId); + if (utcStart.HasValue && utcEnd.HasValue) + query = query.DateRange(utcStart, utcEnd, InferField(e => e.Date)).Index(utcStart, utcEnd); + else if (utcEnd.HasValue) + query = query.ElasticFilter(Query.DateRange(r => r.Field(e => e.Date).LessThan(utcEnd))); + else if (utcStart.HasValue) + query = query.ElasticFilter(Query.DateRange(r => r.Field(e => e.Date).GreaterThan(utcStart))); + + if (!String.IsNullOrEmpty(clientIpAddress)) + query = query.FieldEquals(EventIndex.Alias.IpAddress, clientIpAddress); + + return RemoveAllAsync(q => query, options); + } + + public Task> GetByReferenceIdAsync(string projectId, string referenceId) { + var filter = Query.Term(e => e.ReferenceId, referenceId); + return FindAsync(q => q.Project(projectId).ElasticFilter(filter).SortDescending(e => e.Date), o => o.PageLimit(10)); + } + + public async Task GetPreviousAndNextEventIdsAsync(PersistentEvent ev, AppFilter systemFilter, DateTime? utcStart, DateTime? utcEnd) { + var previous = GetPreviousEventIdAsync(ev, systemFilter, utcStart, utcEnd); + var next = GetNextEventIdAsync(ev, systemFilter, utcStart, utcEnd); + await Task.WhenAll(previous, next).AnyContext(); + + return new PreviousAndNextEventIdResult { + Previous = previous.Result, + Next = next.Result + }; + } + + private async Task GetPreviousEventIdAsync(PersistentEvent ev, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { + if (ev == null) + return null; + + var retentionDate = _options.MaximumRetentionDays > 0 ? SystemClock.UtcNow.Date.SubtractDays(_options.MaximumRetentionDays) : DateTime.MinValue; + if (!utcStart.HasValue || utcStart.Value.IsBefore(retentionDate)) + utcStart = retentionDate; + + if (!utcEnd.HasValue || utcEnd.Value.IsAfter(ev.Date.UtcDateTime)) + utcEnd = ev.Date.UtcDateTime; + + var utcEventDate = ev.Date.UtcDateTime; + // utcEnd is before the current event date. + if (utcStart > utcEventDate || utcEnd < utcEventDate) + return null; + + var results = await FindAsync(q => q + .DateRange(utcStart, utcEventDate, (PersistentEvent e) => e.Date) + .Index(utcStart, utcEventDate) + .SortDescending(e => e.Date) + .Include(e => e.Id, e => e.Date) + .AppFilter(systemFilter) + .ElasticFilter(!Query.Ids(ids => ids.Values(ev.Id))) + .FilterExpression(String.Concat(EventIndex.Alias.StackId, ":", ev.StackId)) + .EnforceEventStackFilter(false), o => o.PageLimit(10)).AnyContext(); + + if (results.Total == 0) + return null; + + // make sure we don't have records with the exact same occurrence date + if (results.Documents.All(t => t.Date != ev.Date)) + return results.Documents.OrderByDescending(t => t.Date).ThenByDescending(t => t.Id).First().Id; + + // we have records with the exact same occurrence date, we need to figure out the order of those + // put our target error into the mix, sort it and return the result before the target + var unionResults = results.Documents.Union(new[] { ev }) + .OrderBy(t => t.Date.UtcTicks).ThenBy(t => t.Id) + .ToList(); + + int index = unionResults.FindIndex(t => t.Id == ev.Id); + return index == 0 ? null : unionResults[index - 1].Id; + } + + private async Task GetNextEventIdAsync(PersistentEvent ev, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { + if (ev == null) + return null; + + if (!utcStart.HasValue || utcStart.Value.IsBefore(ev.Date.UtcDateTime)) + utcStart = ev.Date.UtcDateTime; + + if (!utcEnd.HasValue || utcEnd.Value.IsAfter(SystemClock.UtcNow)) + utcEnd = SystemClock.UtcNow; + + var utcEventDate = ev.Date.UtcDateTime; + // utcEnd is before the current event date. + if (utcStart > utcEventDate || utcEnd < utcEventDate) + return null; + + var results = await FindAsync(q => q + .DateRange(utcEventDate, utcEnd, (PersistentEvent e) => e.Date) + .Index(utcEventDate, utcEnd) + .SortAscending(e => e.Date) + .Include(e => e.Id, e => e.Date) + .AppFilter(systemFilter) + .ElasticFilter(!Query.Ids(ids => ids.Values(ev.Id))) + .FilterExpression(String.Concat(EventIndex.Alias.StackId, ":", ev.StackId)) + .EnforceEventStackFilter(false), o => o.PageLimit(10)).AnyContext(); + + if (results.Total == 0) + return null; + + // make sure we don't have records with the exact same occurrence date + if (results.Documents.All(t => t.Date != ev.Date)) + return results.Documents.OrderBy(t => t.Date).ThenBy(t => t.Id).First().Id; + + // we have records with the exact same occurrence date, we need to figure out the order of those + // put our target error into the mix, sort it and return the result after the target + var unionResults = results.Documents.Union(new[] { ev }) + .OrderBy(t => t.Date.Ticks).ThenBy(t => t.Id) + .ToList(); + + int index = unionResults.FindIndex(t => t.Id == ev.Id); + return index == unionResults.Count - 1 ? null : unionResults[index + 1].Id; + } + + public override Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); + + return FindAsync(q => q.Organization(organizationId).SortDescending(e => e.Date).SortDescending(e => e.Id), options); + } + + public override Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null) { + return FindAsync(q => q.Project(projectId).SortDescending(e => e.Date).SortDescending(e => e.Id), options); + } + + public Task RemoveAllByStackIdsAsync(string[] stackIds) { + if (stackIds is null || stackIds.Length == 0) + throw new ArgumentNullException(nameof(stackIds)); + + return RemoveAllAsync(q => q.Stack(stackIds)); } } diff --git a/src/Exceptionless.Core/Repositories/Exceptions/DocumentLimitExceededException.cs b/src/Exceptionless.Core/Repositories/Exceptions/DocumentLimitExceededException.cs index 7b116c4398..02be9cb742 100644 --- a/src/Exceptionless.Core/Repositories/Exceptions/DocumentLimitExceededException.cs +++ b/src/Exceptionless.Core/Repositories/Exceptions/DocumentLimitExceededException.cs @@ -1,9 +1,6 @@ -using System; +namespace Exceptionless.Core.Repositories.Base; -namespace Exceptionless.Core.Repositories.Base -{ - public class DocumentLimitExceededException : ApplicationException { - public DocumentLimitExceededException(string message = null) : base(message) { - } +public class DocumentLimitExceededException : ApplicationException { + public DocumentLimitExceededException(string message = null) : base(message) { } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs b/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs index 6eacab95d3..687619ce3a 100644 --- a/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs +++ b/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs @@ -1,21 +1,19 @@ -using System; +namespace Exceptionless.Core.Repositories.Base; -namespace Exceptionless.Core.Repositories.Base { - public class DocumentNotFoundException : ApplicationException { - public DocumentNotFoundException(string id, string message = null) : base(message) { - Id = id; - } +public class DocumentNotFoundException : ApplicationException { + public DocumentNotFoundException(string id, string message = null) : base(message) { + Id = id; + } - public string Id { get; private set; } + public string Id { get; private set; } - public override string ToString() { - if (!String.IsNullOrEmpty(Message)) - return Message; + public override string ToString() { + if (!String.IsNullOrEmpty(Message)) + return Message; - if (!String.IsNullOrEmpty(Id)) - return $"Document \"{Id}\" could not be found"; + if (!String.IsNullOrEmpty(Id)) + return $"Document \"{Id}\" could not be found"; - return base.ToString(); - } + return base.ToString(); } } diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs index 1f36b14534..676ca0b9c7 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs @@ -1,25 +1,23 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Queries; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IEventRepository : IRepositoryOwnedByOrganizationAndProject { - Task> GetByReferenceIdAsync(string projectId, string referenceId); - Task GetPreviousAndNextEventIdsAsync(PersistentEvent ev, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null); - Task> GetOpenSessionsAsync(DateTime createdBeforeUtc, CommandOptionsDescriptor options = null); - Task UpdateSessionStartLastActivityAsync(string id, DateTime lastActivityUtc, bool isSessionEnd = false, bool hasError = false, bool sendNotifications = true); - Task RemoveAllAsync(string organizationId, string clientIpAddress, DateTime? utcStart, DateTime? utcEnd, CommandOptionsDescriptor options = null); - Task RemoveAllByStackIdsAsync(string[] stackIds); - } +namespace Exceptionless.Core.Repositories; + +public interface IEventRepository : IRepositoryOwnedByOrganizationAndProject { + Task> GetByReferenceIdAsync(string projectId, string referenceId); + Task GetPreviousAndNextEventIdsAsync(PersistentEvent ev, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null); + Task> GetOpenSessionsAsync(DateTime createdBeforeUtc, CommandOptionsDescriptor options = null); + Task UpdateSessionStartLastActivityAsync(string id, DateTime lastActivityUtc, bool isSessionEnd = false, bool hasError = false, bool sendNotifications = true); + Task RemoveAllAsync(string organizationId, string clientIpAddress, DateTime? utcStart, DateTime? utcEnd, CommandOptionsDescriptor options = null); + Task RemoveAllByStackIdsAsync(string[] stackIds); +} - public static class EventRepositoryExtensions { - public static async Task GetPreviousAndNextEventIdsAsync(this IEventRepository repository, string id, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { - var ev = await repository.GetByIdAsync(id, o => o.Cache()).AnyContext(); - return await repository.GetPreviousAndNextEventIdsAsync(ev, systemFilter, utcStart, utcEnd).AnyContext(); - } +public static class EventRepositoryExtensions { + public static async Task GetPreviousAndNextEventIdsAsync(this IEventRepository repository, string id, AppFilter systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { + var ev = await repository.GetByIdAsync(id, o => o.Cache()).AnyContext(); + return await repository.GetPreviousAndNextEventIdsAsync(ev, systemFilter, utcStart, utcEnd).AnyContext(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IOrganizationRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IOrganizationRepository.cs index 35be770991..65a52e4a3a 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IOrganizationRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IOrganizationRepository.cs @@ -1,14 +1,13 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Models.Billing; +using Exceptionless.Core.Models.Billing; using Exceptionless.Core.Models; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IOrganizationRepository : ISearchableRepository { - Task GetByInviteTokenAsync(string token); - Task GetByStripeCustomerIdAsync(string customerId); - Task> GetByCriteriaAsync(string criteria, CommandOptionsDescriptor options, OrganizationSortBy sortBy, bool? paid = null, bool? suspended = null); - Task GetBillingPlanStatsAsync(); - } +namespace Exceptionless.Core.Repositories; + +public interface IOrganizationRepository : ISearchableRepository { + Task GetByInviteTokenAsync(string token); + Task GetByStripeCustomerIdAsync(string customerId); + Task> GetByCriteriaAsync(string criteria, CommandOptionsDescriptor options, OrganizationSortBy sortBy, bool? paid = null, bool? suspended = null); + Task GetBillingPlanStatsAsync(); } diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IProjectRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IProjectRepository.cs index 2a18a086fd..302a2556b1 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IProjectRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IProjectRepository.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Queries; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IProjectRepository : IRepositoryOwnedByOrganization { - Task> GetByNextSummaryNotificationOffsetAsync(byte hourToSendNotificationsAfterUtcMidnight, int limit = 50); - Task IncrementNextSummaryEndOfDayTicksAsync(IReadOnlyCollection projects); - Task GetCountByOrganizationIdAsync(string organizationId); - Task> GetByOrganizationIdsAsync(ICollection organizationIds, CommandOptionsDescriptor options = null); - Task> GetByFilterAsync(AppFilter systemFilter, string userFilter, string sort, CommandOptionsDescriptor options = null); - } -} \ No newline at end of file +namespace Exceptionless.Core.Repositories; + +public interface IProjectRepository : IRepositoryOwnedByOrganization { + Task> GetByNextSummaryNotificationOffsetAsync(byte hourToSendNotificationsAfterUtcMidnight, int limit = 50); + Task IncrementNextSummaryEndOfDayTicksAsync(IReadOnlyCollection projects); + Task GetCountByOrganizationIdAsync(string organizationId); + Task> GetByOrganizationIdsAsync(ICollection organizationIds, CommandOptionsDescriptor options = null); + Task> GetByFilterAsync(AppFilter systemFilter, string userFilter, string sort, CommandOptionsDescriptor options = null); +} diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganization.cs b/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganization.cs index ba812dd9e9..fc3379e6f6 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganization.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganization.cs @@ -1,11 +1,10 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IRepositoryOwnedByOrganization : ISearchableRepository where T : class, IOwnedByOrganization, IIdentity, new() { - Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null); - Task RemoveAllByOrganizationIdAsync(string organizationId); - } +namespace Exceptionless.Core.Repositories; + +public interface IRepositoryOwnedByOrganization : ISearchableRepository where T : class, IOwnedByOrganization, IIdentity, new() { + Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null); + Task RemoveAllByOrganizationIdAsync(string organizationId); } diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganizationAndProject.cs b/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganizationAndProject.cs index 77b6d3d756..294f75ddc9 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganizationAndProject.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByOrganizationAndProject.cs @@ -1,5 +1,5 @@ using Exceptionless.Core.Models; -namespace Exceptionless.Core.Repositories { - public interface IRepositoryOwnedByOrganizationAndProject : IRepositoryOwnedByOrganization, IRepositoryOwnedByProject where T : class, IOwnedByOrganizationAndProjectWithIdentity, new() {} -} \ No newline at end of file +namespace Exceptionless.Core.Repositories; + +public interface IRepositoryOwnedByOrganizationAndProject : IRepositoryOwnedByOrganization, IRepositoryOwnedByProject where T : class, IOwnedByOrganizationAndProjectWithIdentity, new() { } diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByProject.cs b/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByProject.cs index 7089c7ff88..8f87fb944d 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByProject.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IRepositoryOwnedByProject.cs @@ -1,11 +1,10 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IRepositoryOwnedByProject : ISearchableRepository where T : class, IOwnedByProject, IIdentity, new() { - Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null); - Task RemoveAllByProjectIdAsync(string organizationId, string projectId); - } +namespace Exceptionless.Core.Repositories; + +public interface IRepositoryOwnedByProject : ISearchableRepository where T : class, IOwnedByProject, IIdentity, new() { + Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null); + Task RemoveAllByProjectIdAsync(string organizationId, string projectId); } diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs index 86bf5fd060..f440c01e1d 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs @@ -1,18 +1,16 @@ -using System; -using System.Threading.Tasks; using Exceptionless.Core.Models; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IStackRepository : IRepositoryOwnedByOrganizationAndProject { - Task GetStackBySignatureHashAsync(string projectId, string signatureHash); - Task> GetIdsByQueryAsync(RepositoryQueryDescriptor query, CommandOptionsDescriptor options = null); - Task> GetExpiredSnoozedStatuses(DateTime utcNow, CommandOptionsDescriptor options = null); - Task MarkAsRegressedAsync(string stackId); - Task IncrementEventCounterAsync(string organizationId, string projectId, string stackId, DateTime minOccurrenceDateUtc, DateTime maxOccurrenceDateUtc, int count, bool sendNotifications = true); - Task> GetStacksForCleanupAsync(string organizationId, DateTime cutoff); - Task> GetSoftDeleted(); - Task SoftDeleteByProjectIdAsync(string organizationId, string projectId); - } -} \ No newline at end of file +namespace Exceptionless.Core.Repositories; + +public interface IStackRepository : IRepositoryOwnedByOrganizationAndProject { + Task GetStackBySignatureHashAsync(string projectId, string signatureHash); + Task> GetIdsByQueryAsync(RepositoryQueryDescriptor query, CommandOptionsDescriptor options = null); + Task> GetExpiredSnoozedStatuses(DateTime utcNow, CommandOptionsDescriptor options = null); + Task MarkAsRegressedAsync(string stackId); + Task IncrementEventCounterAsync(string organizationId, string projectId, string stackId, DateTime minOccurrenceDateUtc, DateTime maxOccurrenceDateUtc, int count, bool sendNotifications = true); + Task> GetStacksForCleanupAsync(string organizationId, DateTime cutoff); + Task> GetSoftDeleted(); + Task SoftDeleteByProjectIdAsync(string organizationId, string projectId); +} diff --git a/src/Exceptionless.Core/Repositories/Interfaces/ITokenRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/ITokenRepository.cs index b824349be4..5850b590c1 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/ITokenRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/ITokenRepository.cs @@ -1,13 +1,12 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface ITokenRepository : IRepositoryOwnedByOrganizationAndProject { - Task> GetByTypeAndUserIdAsync(TokenType type, string userId, CommandOptionsDescriptor options = null); - Task> GetByTypeAndOrganizationIdAsync(TokenType type, string organizationId, CommandOptionsDescriptor options = null); - Task> GetByTypeAndProjectIdAsync(TokenType type, string projectId, CommandOptionsDescriptor options = null); - Task RemoveAllByUserIdAsync(string userId, CommandOptionsDescriptor options = null); - } -} \ No newline at end of file +namespace Exceptionless.Core.Repositories; + +public interface ITokenRepository : IRepositoryOwnedByOrganizationAndProject { + Task> GetByTypeAndUserIdAsync(TokenType type, string userId, CommandOptionsDescriptor options = null); + Task> GetByTypeAndOrganizationIdAsync(TokenType type, string organizationId, CommandOptionsDescriptor options = null); + Task> GetByTypeAndProjectIdAsync(TokenType type, string projectId, CommandOptionsDescriptor options = null); + Task RemoveAllByUserIdAsync(string userId, CommandOptionsDescriptor options = null); +} diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IUserRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IUserRepository.cs index e899c7c30a..8a1eda4892 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IUserRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IUserRepository.cs @@ -1,14 +1,13 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IUserRepository : ISearchableRepository { - Task GetByEmailAddressAsync(string emailAddress); - Task GetByPasswordResetTokenAsync(string token); - Task GetUserByOAuthProviderAsync(string provider, string providerUserId); - Task GetByVerifyEmailAddressTokenAsync(string token); - Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null); - } -} \ No newline at end of file +namespace Exceptionless.Core.Repositories; + +public interface IUserRepository : ISearchableRepository { + Task GetByEmailAddressAsync(string emailAddress); + Task GetByPasswordResetTokenAsync(string token); + Task GetUserByOAuthProviderAsync(string provider, string providerUserId); + Task GetByVerifyEmailAddressTokenAsync(string token); + Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null); +} diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IWebHookRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IWebHookRepository.cs index 186b79c201..45959d0645 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IWebHookRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IWebHookRepository.cs @@ -1,11 +1,10 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories.Models; -namespace Exceptionless.Core.Repositories { - public interface IWebHookRepository : IRepositoryOwnedByOrganizationAndProject { - Task> GetByUrlAsync(string targetUrl); - Task> GetByOrganizationIdOrProjectIdAsync(string organizationId, string projectId); - Task MarkDisabledAsync(string id); - } -} \ No newline at end of file +namespace Exceptionless.Core.Repositories; + +public interface IWebHookRepository : IRepositoryOwnedByOrganizationAndProject { + Task> GetByUrlAsync(string targetUrl); + Task> GetByOrganizationIdOrProjectIdAsync(string organizationId, string projectId); + Task MarkDisabledAsync(string id); +} diff --git a/src/Exceptionless.Core/Repositories/OrganizationRepository.cs b/src/Exceptionless.Core/Repositories/OrganizationRepository.cs index a63f006186..9a3a87fa64 100644 --- a/src/Exceptionless.Core/Repositories/OrganizationRepository.cs +++ b/src/Exceptionless.Core/Repositories/OrganizationRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Billing; @@ -11,119 +8,119 @@ using Foundatio.Repositories.Models; using Nest; -namespace Exceptionless.Core.Repositories { - public class OrganizationRepository : RepositoryBase, IOrganizationRepository { - private readonly BillingPlans _plans; +namespace Exceptionless.Core.Repositories; - public OrganizationRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, BillingPlans plans, AppOptions options) - : base(configuration.Organizations, validator, options) { - _plans = plans; - } +public class OrganizationRepository : RepositoryBase, IOrganizationRepository { + private readonly BillingPlans _plans; - public async Task GetByInviteTokenAsync(string token) { - if (String.IsNullOrEmpty(token)) - throw new ArgumentNullException(nameof(token)); + public OrganizationRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, BillingPlans plans, AppOptions options) + : base(configuration.Organizations, validator, options) { + _plans = plans; + } - var filter = Query.Term(f => f.Field(o => o.Invites.First().Token).Value(token)); - var hit = await FindOneAsync(q => q.ElasticFilter(filter)).AnyContext(); - return hit?.Document; - } + public async Task GetByInviteTokenAsync(string token) { + if (String.IsNullOrEmpty(token)) + throw new ArgumentNullException(nameof(token)); - public async Task GetByStripeCustomerIdAsync(string customerId) { - if (String.IsNullOrEmpty(customerId)) - throw new ArgumentNullException(nameof(customerId)); + var filter = Query.Term(f => f.Field(o => o.Invites.First().Token).Value(token)); + var hit = await FindOneAsync(q => q.ElasticFilter(filter)).AnyContext(); + return hit?.Document; + } + + public async Task GetByStripeCustomerIdAsync(string customerId) { + if (String.IsNullOrEmpty(customerId)) + throw new ArgumentNullException(nameof(customerId)); + + var filter = Query.Term(f => f.Field(o => o.StripeCustomerId).Value(customerId)); + var hit = await FindOneAsync(q => q.ElasticFilter(filter)).AnyContext(); + return hit?.Document; + } - var filter = Query.Term(f => f.Field(o => o.StripeCustomerId).Value(customerId)); - var hit = await FindOneAsync(q => q.ElasticFilter(filter)).AnyContext(); - return hit?.Document; + public Task> GetByCriteriaAsync(string criteria, CommandOptionsDescriptor options, OrganizationSortBy sortBy, bool? paid = null, bool? suspended = null) { + var filter = Query.MatchAll(); + if (!String.IsNullOrWhiteSpace(criteria)) + filter &= Query.Term(o => o.Name, criteria); + + if (paid.HasValue) { + if (paid.Value) + filter &= !Query.Term(o => o.PlanId, _plans.FreePlan.Id); + else + filter &= Query.Term(o => o.PlanId, _plans.FreePlan.Id); } - public Task> GetByCriteriaAsync(string criteria, CommandOptionsDescriptor options, OrganizationSortBy sortBy, bool? paid = null, bool? suspended = null) { - var filter = Query.MatchAll(); - if (!String.IsNullOrWhiteSpace(criteria)) - filter &= Query.Term(o => o.Name, criteria); - - if (paid.HasValue) { - if (paid.Value) - filter &= !Query.Term(o => o.PlanId, _plans.FreePlan.Id); - else - filter &= Query.Term(o => o.PlanId, _plans.FreePlan.Id); - } - - if (suspended.HasValue) { - if (suspended.Value) - filter &= (!Query.Term(o => o.BillingStatus, BillingStatus.Active) && - !Query.Term(o => o.BillingStatus, BillingStatus.Trialing) && - !Query.Term(o => o.BillingStatus, BillingStatus.Canceled) - ) || Query.Term(o => o.IsSuspended, true); - else - filter &= ( - Query.Term(o => o.BillingStatus, BillingStatus.Active) && - Query.Term(o => o.BillingStatus, BillingStatus.Trialing) && - Query.Term(o => o.BillingStatus, BillingStatus.Canceled) - ) || Query.Term(o => o.IsSuspended, false); - } - - var query = new RepositoryQuery().ElasticFilter(filter); - switch (sortBy) { - case OrganizationSortBy.Newest: - query.SortDescending((Organization o) => o.Id); - break; - case OrganizationSortBy.Subscribed: - query.SortDescending((Organization o) => o.SubscribeDate); - break; - // case OrganizationSortBy.MostActive: - // query.WithSortDescending((Organization o) => o.TotalEventCount); - // break; - default: - query.SortAscending(o => o.Name.Suffix("keyword")); - break; - } - - return FindAsync(q => query, options); + if (suspended.HasValue) { + if (suspended.Value) + filter &= (!Query.Term(o => o.BillingStatus, BillingStatus.Active) && + !Query.Term(o => o.BillingStatus, BillingStatus.Trialing) && + !Query.Term(o => o.BillingStatus, BillingStatus.Canceled) + ) || Query.Term(o => o.IsSuspended, true); + else + filter &= ( + Query.Term(o => o.BillingStatus, BillingStatus.Active) && + Query.Term(o => o.BillingStatus, BillingStatus.Trialing) && + Query.Term(o => o.BillingStatus, BillingStatus.Canceled) + ) || Query.Term(o => o.IsSuspended, false); } - public async Task GetBillingPlanStatsAsync() { - var results = (await FindAsync(q => q - .Include(o => o.PlanId, o => o.IsSuspended, o => o.BillingPrice, o => o.BillingStatus) - .SortDescending(o => o.PlanId)).AnyContext()).Documents; - var smallOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.SmallPlan.Id) && o.BillingPrice > 0).ToList(); - var mediumOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.MediumPlan.Id) && o.BillingPrice > 0).ToList(); - var largeOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.LargePlan.Id) && o.BillingPrice > 0).ToList(); - decimal monthlyTotalPaid = smallOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) - + mediumOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) - + largeOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice); - - var smallYearlyOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.SmallYearlyPlan.Id) && o.BillingPrice > 0).ToList(); - var mediumYearlyOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.MediumYearlyPlan.Id) && o.BillingPrice > 0).ToList(); - var largeYearlyOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.LargeYearlyPlan.Id) && o.BillingPrice > 0).ToList(); - decimal yearlyTotalPaid = smallYearlyOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) - + mediumYearlyOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) - + largeYearlyOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice); - - return new BillingPlanStats { - SmallTotal = smallOrganizations.Count, - SmallYearlyTotal = smallYearlyOrganizations.Count, - MediumTotal = mediumOrganizations.Count, - MediumYearlyTotal = mediumYearlyOrganizations.Count, - LargeTotal = largeOrganizations.Count, - LargeYearlyTotal = largeYearlyOrganizations.Count, - MonthlyTotal = monthlyTotalPaid + (yearlyTotalPaid / 12), - YearlyTotal = (monthlyTotalPaid * 12) + yearlyTotalPaid, - MonthlyTotalAccounts = smallOrganizations.Count + mediumOrganizations.Count + largeOrganizations.Count, - YearlyTotalAccounts = smallYearlyOrganizations.Count + mediumYearlyOrganizations.Count + largeYearlyOrganizations.Count, - FreeAccounts = results.Count(o => String.Equals(o.PlanId, _plans.FreePlan.Id)), - PaidAccounts = results.Count(o => !String.Equals(o.PlanId, _plans.FreePlan.Id) && o.BillingPrice > 0), - FreeloaderAccounts = results.Count(o => !String.Equals(o.PlanId, _plans.FreePlan.Id) && o.BillingPrice <= 0), - SuspendedAccounts = results.Count(o => o.IsSuspended), - }; + var query = new RepositoryQuery().ElasticFilter(filter); + switch (sortBy) { + case OrganizationSortBy.Newest: + query.SortDescending((Organization o) => o.Id); + break; + case OrganizationSortBy.Subscribed: + query.SortDescending((Organization o) => o.SubscribeDate); + break; + // case OrganizationSortBy.MostActive: + // query.WithSortDescending((Organization o) => o.TotalEventCount); + // break; + default: + query.SortAscending(o => o.Name.Suffix("keyword")); + break; } + + return FindAsync(q => query, options); } - public enum OrganizationSortBy { - Newest = 0, - Subscribed = 1, - MostActive = 2, - Alphabetical = 3, + public async Task GetBillingPlanStatsAsync() { + var results = (await FindAsync(q => q + .Include(o => o.PlanId, o => o.IsSuspended, o => o.BillingPrice, o => o.BillingStatus) + .SortDescending(o => o.PlanId)).AnyContext()).Documents; + var smallOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.SmallPlan.Id) && o.BillingPrice > 0).ToList(); + var mediumOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.MediumPlan.Id) && o.BillingPrice > 0).ToList(); + var largeOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.LargePlan.Id) && o.BillingPrice > 0).ToList(); + decimal monthlyTotalPaid = smallOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) + + mediumOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) + + largeOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice); + + var smallYearlyOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.SmallYearlyPlan.Id) && o.BillingPrice > 0).ToList(); + var mediumYearlyOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.MediumYearlyPlan.Id) && o.BillingPrice > 0).ToList(); + var largeYearlyOrganizations = results.Where(o => String.Equals(o.PlanId, _plans.LargeYearlyPlan.Id) && o.BillingPrice > 0).ToList(); + decimal yearlyTotalPaid = smallYearlyOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) + + mediumYearlyOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice) + + largeYearlyOrganizations.Where(o => !o.IsSuspended && o.BillingStatus == BillingStatus.Active).Sum(o => o.BillingPrice); + + return new BillingPlanStats { + SmallTotal = smallOrganizations.Count, + SmallYearlyTotal = smallYearlyOrganizations.Count, + MediumTotal = mediumOrganizations.Count, + MediumYearlyTotal = mediumYearlyOrganizations.Count, + LargeTotal = largeOrganizations.Count, + LargeYearlyTotal = largeYearlyOrganizations.Count, + MonthlyTotal = monthlyTotalPaid + (yearlyTotalPaid / 12), + YearlyTotal = (monthlyTotalPaid * 12) + yearlyTotalPaid, + MonthlyTotalAccounts = smallOrganizations.Count + mediumOrganizations.Count + largeOrganizations.Count, + YearlyTotalAccounts = smallYearlyOrganizations.Count + mediumYearlyOrganizations.Count + largeYearlyOrganizations.Count, + FreeAccounts = results.Count(o => String.Equals(o.PlanId, _plans.FreePlan.Id)), + PaidAccounts = results.Count(o => !String.Equals(o.PlanId, _plans.FreePlan.Id) && o.BillingPrice > 0), + FreeloaderAccounts = results.Count(o => !String.Equals(o.PlanId, _plans.FreePlan.Id) && o.BillingPrice <= 0), + SuspendedAccounts = results.Count(o => o.IsSuspended), + }; } } + +public enum OrganizationSortBy { + Newest = 0, + Subscribed = 1, + MostActive = 2, + Alphabetical = 3, +} diff --git a/src/Exceptionless.Core/Repositories/ProjectRepository.cs b/src/Exceptionless.Core/Repositories/ProjectRepository.cs index 0683800b32..f57d0644d7 100644 --- a/src/Exceptionless.Core/Repositories/ProjectRepository.cs +++ b/src/Exceptionless.Core/Repositories/ProjectRepository.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using Exceptionless.Core.Repositories.Queries; @@ -12,58 +8,58 @@ using Foundatio.Utility; using Nest; -namespace Exceptionless.Core.Repositories { - public class ProjectRepository : RepositoryOwnedByOrganization, IProjectRepository { - public ProjectRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) - : base(configuration.Projects, validator, options) { - } +namespace Exceptionless.Core.Repositories; - public Task GetCountByOrganizationIdAsync(string organizationId) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); +public class ProjectRepository : RepositoryOwnedByOrganization, IProjectRepository { + public ProjectRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) + : base(configuration.Projects, validator, options) { + } - return CountAsync(q => q.Organization(organizationId), o => o.Cache(String.Concat("Organization:", organizationId))); - } + public Task GetCountByOrganizationIdAsync(string organizationId) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); - public Task> GetByOrganizationIdsAsync(ICollection organizationIds, CommandOptionsDescriptor options = null) { - if (organizationIds == null) - throw new ArgumentNullException(nameof(organizationIds)); + return CountAsync(q => q.Organization(organizationId), o => o.Cache(String.Concat("Organization:", organizationId))); + } - if (organizationIds.Count == 0) - return Task.FromResult(new FindResults()); - - return FindAsync(q => q.Organization(organizationIds).SortAscending(p => p.Name.Suffix("keyword")), options); - } - - public Task> GetByFilterAsync(AppFilter systemFilter, string userFilter, string sort, CommandOptionsDescriptor options = null) { - IRepositoryQuery query = new RepositoryQuery() - .AppFilter(systemFilter) - .FilterExpression(userFilter); + public Task> GetByOrganizationIdsAsync(ICollection organizationIds, CommandOptionsDescriptor options = null) { + if (organizationIds == null) + throw new ArgumentNullException(nameof(organizationIds)); - query = !String.IsNullOrEmpty(sort) ? query.SortExpression(sort) : query.SortAscending(p => p.Name.Suffix("keyword")); - return FindAsync(q => query, options); - } + if (organizationIds.Count == 0) + return Task.FromResult(new FindResults()); - public Task> GetByNextSummaryNotificationOffsetAsync(byte hourToSendNotificationsAfterUtcMidnight, int limit = 50) { - var filter = Query.Range(r => r.Field(o => o.NextSummaryEndOfDayTicks).LessThan(SystemClock.UtcNow.Ticks - (TimeSpan.TicksPerHour * hourToSendNotificationsAfterUtcMidnight))); - return FindAsync(q => q.ElasticFilter(filter).SortAscending(p => p.OrganizationId), o => o.SearchAfterPaging().PageLimit(limit)); - } + return FindAsync(q => q.Organization(organizationIds).SortAscending(p => p.Name.Suffix("keyword")), options); + } - public async Task IncrementNextSummaryEndOfDayTicksAsync(IReadOnlyCollection projects) { - if (projects == null) - throw new ArgumentNullException(nameof(projects)); + public Task> GetByFilterAsync(AppFilter systemFilter, string userFilter, string sort, CommandOptionsDescriptor options = null) { + IRepositoryQuery query = new RepositoryQuery() + .AppFilter(systemFilter) + .FilterExpression(userFilter); - if (projects.Count == 0) - return; + query = !String.IsNullOrEmpty(sort) ? query.SortExpression(sort) : query.SortAscending(p => p.Name.Suffix("keyword")); + return FindAsync(q => query, options); + } - string script = $"ctx._source.next_summary_end_of_day_ticks += {TimeSpan.TicksPerDay}L;"; - await PatchAsync(projects.Select(p => p.Id).ToArray(), new ScriptPatch(script), o => o.Notifications(false)).AnyContext(); - await InvalidateCacheAsync(projects).AnyContext(); - } + public Task> GetByNextSummaryNotificationOffsetAsync(byte hourToSendNotificationsAfterUtcMidnight, int limit = 50) { + var filter = Query.Range(r => r.Field(o => o.NextSummaryEndOfDayTicks).LessThan(SystemClock.UtcNow.Ticks - (TimeSpan.TicksPerHour * hourToSendNotificationsAfterUtcMidnight))); + return FindAsync(q => q.ElasticFilter(filter).SortAscending(p => p.OrganizationId), o => o.SearchAfterPaging().PageLimit(limit)); + } + + public async Task IncrementNextSummaryEndOfDayTicksAsync(IReadOnlyCollection projects) { + if (projects == null) + throw new ArgumentNullException(nameof(projects)); + + if (projects.Count == 0) + return; + + string script = $"ctx._source.next_summary_end_of_day_ticks += {TimeSpan.TicksPerDay}L;"; + await PatchAsync(projects.Select(p => p.Id).ToArray(), new ScriptPatch(script), o => o.Notifications(false)).AnyContext(); + await InvalidateCacheAsync(projects).AnyContext(); + } - protected override Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { - var organizations = documents.Select(d => d.Value.OrganizationId).Distinct().Where(id => !String.IsNullOrEmpty(id)); - return Task.WhenAll(Cache.RemoveAllAsync(organizations.Select(id => $"count:Organization:{id}")), base.InvalidateCacheAsync(documents, changeType)); - } + protected override Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { + var organizations = documents.Select(d => d.Value.OrganizationId).Distinct().Where(id => !String.IsNullOrEmpty(id)); + return Task.WhenAll(Cache.RemoveAllAsync(organizations.Select(id => $"count:Organization:{id}")), base.InvalidateCacheAsync(documents, changeType)); } } diff --git a/src/Exceptionless.Core/Repositories/Queries/AppFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/AppFilterQuery.cs index 1c7682d4c5..396b02d270 100644 --- a/src/Exceptionless.Core/Repositories/Queries/AppFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/AppFilterQuery.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using Exceptionless.Core.Repositories.Options; diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index fbe0a2a7c4..50af0dcdca 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Base; diff --git a/src/Exceptionless.Core/Repositories/Queries/OrganizationQuery.cs b/src/Exceptionless.Core/Repositories/Queries/OrganizationQuery.cs index e34a4c9119..e51a0a8f00 100644 --- a/src/Exceptionless.Core/Repositories/Queries/OrganizationQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/OrganizationQuery.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Options; using Foundatio.Repositories; diff --git a/src/Exceptionless.Core/Repositories/Queries/ProjectQuery.cs b/src/Exceptionless.Core/Repositories/Queries/ProjectQuery.cs index e15fbdc332..eaa2c620c2 100644 --- a/src/Exceptionless.Core/Repositories/Queries/ProjectQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/ProjectQuery.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Options; using Foundatio.Repositories; diff --git a/src/Exceptionless.Core/Repositories/Queries/QueryExtensions.cs b/src/Exceptionless.Core/Repositories/Queries/QueryExtensions.cs index a7a7aaca96..355215c780 100644 --- a/src/Exceptionless.Core/Repositories/Queries/QueryExtensions.cs +++ b/src/Exceptionless.Core/Repositories/Queries/QueryExtensions.cs @@ -1,15 +1,14 @@ -using System; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories; -namespace Exceptionless.Core.Repositories { - public static class QueryExtensions { - public static IRepositoryQuery DateRange(this IRepositoryQuery query, DateTime? utcStart, DateTime? utcEnd) { - return query.DateRange(utcStart, utcEnd, (Event e) => e.Date); - } - - public static IRepositoryQuery DateRange(this IRepositoryQuery query, DateTime? utcStart, DateTime? utcEnd) { - return query.DateRange(utcStart, utcEnd, (Stack e) => e.CreatedUtc); - } +namespace Exceptionless.Core.Repositories; + +public static class QueryExtensions { + public static IRepositoryQuery DateRange(this IRepositoryQuery query, DateTime? utcStart, DateTime? utcEnd) { + return query.DateRange(utcStart, utcEnd, (Event e) => e.Date); + } + + public static IRepositoryQuery DateRange(this IRepositoryQuery query, DateTime? utcStart, DateTime? utcEnd) { + return query.DateRange(utcStart, utcEnd, (Stack e) => e.CreatedUtc); } } diff --git a/src/Exceptionless.Core/Repositories/Queries/StackQuery.cs b/src/Exceptionless.Core/Repositories/Queries/StackQuery.cs index 4a2a06cc80..36df324cf6 100644 --- a/src/Exceptionless.Core/Repositories/Queries/StackQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/StackQuery.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Options; using Foundatio.Repositories; diff --git a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs index e483fa76ce..4487325456 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs @@ -1,84 +1,84 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Parsers.ElasticQueries.Visitors; using Foundatio.Parsers.LuceneQueries; using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Queries.Validation { - public interface IAppQueryValidator { - Task ValidateQueryAsync(string query); +namespace Exceptionless.Core.Queries.Validation; - Task ValidateQueryAsync(IQueryNode query); +public interface IAppQueryValidator { + Task ValidateQueryAsync(string query); - Task ValidateAggregationsAsync(string aggs); + Task ValidateQueryAsync(IQueryNode query); - Task ValidateAggregationsAsync(IQueryNode query); - } + Task ValidateAggregationsAsync(string aggs); - public class AppQueryValidator : IAppQueryValidator { - private readonly IQueryParser _parser; - private readonly ILogger _logger; + Task ValidateAggregationsAsync(IQueryNode query); +} - public AppQueryValidator(IQueryParser parser, ILoggerFactory loggerFactory) { - _parser = parser; - _logger = loggerFactory?.CreateLogger(GetType()); - } +public class AppQueryValidator : IAppQueryValidator { + private readonly IQueryParser _parser; + private readonly ILogger _logger; - public async Task ValidateQueryAsync(string query) { - if (String.IsNullOrWhiteSpace(query)) - return new QueryProcessResult { IsValid = true }; + public AppQueryValidator(IQueryParser parser, ILoggerFactory loggerFactory) { + _parser = parser; + _logger = loggerFactory?.CreateLogger(GetType()); + } - IQueryNode parsedResult; - try { - parsedResult = await _parser.ParseAsync(query, new ElasticQueryVisitorContext { QueryType = QueryType.Query }).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error parsing query: {Query}", query); - return new QueryProcessResult { Message = ex.Message }; - } + public async Task ValidateQueryAsync(string query) { + if (String.IsNullOrWhiteSpace(query)) + return new QueryProcessResult { IsValid = true }; - return await ValidateQueryAsync(parsedResult).AnyContext(); + IQueryNode parsedResult; + try { + parsedResult = await _parser.ParseAsync(query, new ElasticQueryVisitorContext { QueryType = QueryType.Query }).AnyContext(); } - - public async Task ValidateQueryAsync(IQueryNode query) { - var info = await ValidationVisitor.RunAsync(query).AnyContext(); - return ApplyQueryRules(info); + catch (Exception ex) { + _logger.LogError(ex, "Error parsing query: {Query}", query); + return new QueryProcessResult { Message = ex.Message }; } - protected virtual QueryProcessResult ApplyQueryRules(QueryValidationInfo info) { - return new QueryProcessResult { IsValid = info.IsValid }; - } + return await ValidateQueryAsync(parsedResult).AnyContext(); + } - public async Task ValidateAggregationsAsync(string aggs) { - if (String.IsNullOrWhiteSpace(aggs)) - return new QueryProcessResult { IsValid = true }; + public async Task ValidateQueryAsync(IQueryNode query) { + var info = await ValidationVisitor.RunAsync(query).AnyContext(); + return ApplyQueryRules(info); + } - IQueryNode parsedResult; - try { - parsedResult = await _parser.ParseAsync(aggs, new ElasticQueryVisitorContext { QueryType = QueryType.Aggregation }).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error parsing aggregation: {Aggregation}", aggs); - return new QueryProcessResult { Message = ex.Message }; - } + protected virtual QueryProcessResult ApplyQueryRules(QueryValidationInfo info) { + return new QueryProcessResult { IsValid = info.IsValid }; + } - return await ValidateAggregationsAsync(parsedResult).AnyContext(); - } + public async Task ValidateAggregationsAsync(string aggs) { + if (String.IsNullOrWhiteSpace(aggs)) + return new QueryProcessResult { IsValid = true }; - public async Task ValidateAggregationsAsync(IQueryNode query) { - var info = await ValidationVisitor.RunAsync(query).AnyContext(); - return ApplyAggregationRules(info); + IQueryNode parsedResult; + try { + parsedResult = await _parser.ParseAsync(aggs, new ElasticQueryVisitorContext { QueryType = QueryType.Aggregation }).AnyContext(); } - - protected virtual QueryProcessResult ApplyAggregationRules(QueryValidationInfo info) { - return new QueryProcessResult { IsValid = info.IsValid }; + catch (Exception ex) { + _logger.LogError(ex, "Error parsing aggregation: {Aggregation}", aggs); + return new QueryProcessResult { Message = ex.Message }; } - public class QueryProcessResult { - public bool IsValid { get; set; } - public string Message { get; set; } - public bool UsesPremiumFeatures { get; set; } - } + return await ValidateAggregationsAsync(parsedResult).AnyContext(); + } + + public async Task ValidateAggregationsAsync(IQueryNode query) { + var info = await ValidationVisitor.RunAsync(query).AnyContext(); + return ApplyAggregationRules(info); + } + + protected virtual QueryProcessResult ApplyAggregationRules(QueryValidationInfo info) { + return new QueryProcessResult { IsValid = info.IsValid }; + } + + public class QueryProcessResult { + public bool IsValid { get; set; } + public string Message { get; set; } + public bool UsesPremiumFeatures { get; set; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Queries/Validation/PersistentEventQueryValidator.cs b/src/Exceptionless.Core/Repositories/Queries/Validation/PersistentEventQueryValidator.cs index c0fd35c7c5..8d84c97f1a 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Validation/PersistentEventQueryValidator.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Validation/PersistentEventQueryValidator.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Foundatio.Parsers.LuceneQueries.Visitors; +using Foundatio.Parsers.LuceneQueries.Visitors; using Exceptionless.Core.Repositories.Configuration; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Queries.Validation { - public sealed class PersistentEventQueryValidator : AppQueryValidator { - private readonly HashSet _freeQueryFields = new HashSet(StringComparer.OrdinalIgnoreCase) { +namespace Exceptionless.Core.Queries.Validation; + +public sealed class PersistentEventQueryValidator : AppQueryValidator { + private readonly HashSet _freeQueryFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "date", "type", EventIndex.Alias.ReferenceId, @@ -21,7 +19,7 @@ public sealed class PersistentEventQueryValidator : AppQueryValidator { "status" }; - private static readonly HashSet _freeAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { + private static readonly HashSet _freeAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "date", "type", "value", @@ -35,7 +33,7 @@ public sealed class PersistentEventQueryValidator : AppQueryValidator { "status" }; - private static readonly HashSet _allowedAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { + private static readonly HashSet _allowedAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "date", "source", "tags", @@ -86,42 +84,41 @@ public sealed class PersistentEventQueryValidator : AppQueryValidator { "data.@level" }; - public PersistentEventQueryValidator(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(configuration.Events.QueryParser, loggerFactory) {} + public PersistentEventQueryValidator(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(configuration.Events.QueryParser, loggerFactory) { } - protected override QueryProcessResult ApplyQueryRules(QueryValidationInfo info) { - return new QueryProcessResult { - IsValid = info.IsValid, - UsesPremiumFeatures = !info.ReferencedFields.All(_freeQueryFields.Contains) - }; - } + protected override QueryProcessResult ApplyQueryRules(QueryValidationInfo info) { + return new QueryProcessResult { + IsValid = info.IsValid, + UsesPremiumFeatures = !info.ReferencedFields.All(_freeQueryFields.Contains) + }; + } - protected override QueryProcessResult ApplyAggregationRules(QueryValidationInfo info) { - if (!info.IsValid) - return new QueryProcessResult { Message = "Invalid aggregation" }; + protected override QueryProcessResult ApplyAggregationRules(QueryValidationInfo info) { + if (!info.IsValid) + return new QueryProcessResult { Message = "Invalid aggregation" }; - if (info.MaxNodeDepth > 6) - return new QueryProcessResult { Message = "Aggregation max depth exceeded" }; + if (info.MaxNodeDepth > 6) + return new QueryProcessResult { Message = "Aggregation max depth exceeded" }; - if (info.Operations.Values.Sum(o => o.Count) > 10) - return new QueryProcessResult { Message = "Aggregation count exceeded" }; + if (info.Operations.Values.Sum(o => o.Count) > 10) + return new QueryProcessResult { Message = "Aggregation count exceeded" }; - // Only allow fields that are numeric or have high commonality. - if (!info.ReferencedFields.All(_allowedAggregationFields.Contains)) - return new QueryProcessResult { Message = "One or more aggregation fields are not allowed" }; + // Only allow fields that are numeric or have high commonality. + if (!info.ReferencedFields.All(_allowedAggregationFields.Contains)) + return new QueryProcessResult { Message = "One or more aggregation fields are not allowed" }; - // Distinct queries are expensive. - if (info.Operations.TryGetValue(AggregationType.Cardinality, out var values) && values.Count > 3) - return new QueryProcessResult { Message = "Cardinality aggregation count exceeded" }; + // Distinct queries are expensive. + if (info.Operations.TryGetValue(AggregationType.Cardinality, out var values) && values.Count > 3) + return new QueryProcessResult { Message = "Cardinality aggregation count exceeded" }; - // Term queries are expensive. - if (info.Operations.TryGetValue(AggregationType.Terms, out values) && (values.Count > 3)) - return new QueryProcessResult { Message = "Terms aggregation count exceeded" }; + // Term queries are expensive. + if (info.Operations.TryGetValue(AggregationType.Terms, out values) && (values.Count > 3)) + return new QueryProcessResult { Message = "Terms aggregation count exceeded" }; - bool usesPremiumFeatures = !info.ReferencedFields.All(_freeAggregationFields.Contains); - return new QueryProcessResult { - IsValid = info.IsValid, - UsesPremiumFeatures = usesPremiumFeatures - }; - } + bool usesPremiumFeatures = !info.ReferencedFields.All(_freeAggregationFields.Contains); + return new QueryProcessResult { + IsValid = info.IsValid, + UsesPremiumFeatures = usesPremiumFeatures + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Queries/Validation/StackQueryValidator.cs b/src/Exceptionless.Core/Repositories/Queries/Validation/StackQueryValidator.cs index d419b50670..e0601f5ea7 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Validation/StackQueryValidator.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Validation/StackQueryValidator.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Parsers.LuceneQueries.Visitors; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Queries.Validation { - public sealed class StackQueryValidator : AppQueryValidator { - private readonly HashSet _freeQueryFields = new HashSet(StringComparer.OrdinalIgnoreCase) { +namespace Exceptionless.Core.Queries.Validation; + +public sealed class StackQueryValidator : AppQueryValidator { + private readonly HashSet _freeQueryFields = new HashSet(StringComparer.OrdinalIgnoreCase) { StackIndex.Alias.FirstOccurrence, "first_occurrence", StackIndex.Alias.LastOccurrence, @@ -22,7 +20,7 @@ public sealed class StackQueryValidator : AppQueryValidator { "project_id" }; - private static readonly HashSet _freeAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { + private static readonly HashSet _freeAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { StackIndex.Alias.FirstOccurrence, "first_occurrence", StackIndex.Alias.LastOccurrence, @@ -33,7 +31,7 @@ public sealed class StackQueryValidator : AppQueryValidator { StackIndex.Alias.Type }; - private static readonly HashSet _allowedAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { + private static readonly HashSet _allowedAggregationFields = new HashSet(StringComparer.OrdinalIgnoreCase) { StackIndex.Alias.FirstOccurrence, "first_occurrence", StackIndex.Alias.LastOccurrence, @@ -52,43 +50,42 @@ public sealed class StackQueryValidator : AppQueryValidator { StackIndex.Alias.ProjectId, "project_id" }; - - public StackQueryValidator(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(configuration.Stacks.QueryParser, loggerFactory) { } - protected override QueryProcessResult ApplyQueryRules(QueryValidationInfo info) { - return new QueryProcessResult { - IsValid = info.IsValid, - UsesPremiumFeatures = !info.ReferencedFields.All(_freeQueryFields.Contains) - }; - } + public StackQueryValidator(ExceptionlessElasticConfiguration configuration, ILoggerFactory loggerFactory) : base(configuration.Stacks.QueryParser, loggerFactory) { } - protected override QueryProcessResult ApplyAggregationRules(QueryValidationInfo info) { - if (!info.IsValid) - return new QueryProcessResult { Message = "Invalid aggregation" }; + protected override QueryProcessResult ApplyQueryRules(QueryValidationInfo info) { + return new QueryProcessResult { + IsValid = info.IsValid, + UsesPremiumFeatures = !info.ReferencedFields.All(_freeQueryFields.Contains) + }; + } - if (info.MaxNodeDepth > 6) - return new QueryProcessResult { Message = "Aggregation max depth exceeded" }; + protected override QueryProcessResult ApplyAggregationRules(QueryValidationInfo info) { + if (!info.IsValid) + return new QueryProcessResult { Message = "Invalid aggregation" }; - if (info.Operations.Values.Sum(o => o.Count) > 10) - return new QueryProcessResult { Message = "Aggregation count exceeded" }; + if (info.MaxNodeDepth > 6) + return new QueryProcessResult { Message = "Aggregation max depth exceeded" }; - // Only allow fields that are numeric or have high commonality. - if (!info.ReferencedFields.All(_allowedAggregationFields.Contains)) - return new QueryProcessResult { Message = "One or more aggregation fields are not allowed" }; + if (info.Operations.Values.Sum(o => o.Count) > 10) + return new QueryProcessResult { Message = "Aggregation count exceeded" }; - // Distinct queries are expensive. - if (info.Operations.TryGetValue(AggregationType.Cardinality, out var values) && values.Count > 3) - return new QueryProcessResult { Message = "Cardinality aggregation count exceeded" }; + // Only allow fields that are numeric or have high commonality. + if (!info.ReferencedFields.All(_allowedAggregationFields.Contains)) + return new QueryProcessResult { Message = "One or more aggregation fields are not allowed" }; - // Term queries are expensive. - if (info.Operations.TryGetValue(AggregationType.Terms, out values) && values.Count > 3) - return new QueryProcessResult { Message = "Terms aggregation count exceeded" }; + // Distinct queries are expensive. + if (info.Operations.TryGetValue(AggregationType.Cardinality, out var values) && values.Count > 3) + return new QueryProcessResult { Message = "Cardinality aggregation count exceeded" }; - bool usesPremiumFeatures = !info.ReferencedFields.All(_freeAggregationFields.Contains); - return new QueryProcessResult { - IsValid = info.IsValid, - UsesPremiumFeatures = usesPremiumFeatures - }; - } + // Term queries are expensive. + if (info.Operations.TryGetValue(AggregationType.Terms, out values) && values.Count > 3) + return new QueryProcessResult { Message = "Terms aggregation count exceeded" }; + + bool usesPremiumFeatures = !info.ReferencedFields.All(_freeAggregationFields.Contains); + return new QueryProcessResult { + IsValid = info.IsValid, + UsesPremiumFeatures = usesPremiumFeatures + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs index 53bb835a5f..2086d2ddbc 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs @@ -1,118 +1,115 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; -namespace Exceptionless.Core.Repositories.Queries { - public class EventFieldsQueryVisitor : ChainableQueryVisitor { - public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context) { - var childTerms = new List(); - if (node.Left is TermNode leftTermNode && leftTermNode.Field == null) - childTerms.Add(leftTermNode.Term); - - if (node.Left is TermRangeNode leftTermRangeNode && leftTermRangeNode.Field == null) { - childTerms.Add(leftTermRangeNode.Min); - childTerms.Add(leftTermRangeNode.Max); - } - - if (node.Right is TermNode rightTermNode && rightTermNode.Field == null) - childTerms.Add(rightTermNode.Term); - - if (node.Right is TermRangeNode rightTermRangeNode && rightTermRangeNode.Field == null) { - childTerms.Add(rightTermRangeNode.Min); - childTerms.Add(rightTermRangeNode.Max); - } - - node.Field = GetCustomFieldName(node.Field, childTerms.ToArray()) ?? node.Field; - foreach (var child in node.Children) - await child.AcceptAsync(this, context).AnyContext(); - } +namespace Exceptionless.Core.Repositories.Queries; - public override Task VisitAsync(TermNode node, IQueryVisitorContext context) { - // using all fields search - if (String.IsNullOrEmpty(node.Field)) { - return Task.CompletedTask; - } +public class EventFieldsQueryVisitor : ChainableQueryVisitor { + public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context) { + var childTerms = new List(); + if (node.Left is TermNode leftTermNode && leftTermNode.Field == null) + childTerms.Add(leftTermNode.Term); - node.Field = GetCustomFieldName(node.Field, new [] { node.Term }); - return Task.CompletedTask; + if (node.Left is TermRangeNode leftTermRangeNode && leftTermRangeNode.Field == null) { + childTerms.Add(leftTermRangeNode.Min); + childTerms.Add(leftTermRangeNode.Max); } - public override Task VisitAsync(TermRangeNode node, IQueryVisitorContext context) { - node.Field = GetCustomFieldName(node.Field, new [] { node.Min, node.Max }); - return Task.CompletedTask; - } + if (node.Right is TermNode rightTermNode && rightTermNode.Field == null) + childTerms.Add(rightTermNode.Term); - public override Task VisitAsync(ExistsNode node, IQueryVisitorContext context) { - node.Field = GetCustomFieldName(node.Field, Array.Empty()); - return Task.CompletedTask; + if (node.Right is TermRangeNode rightTermRangeNode && rightTermRangeNode.Field == null) { + childTerms.Add(rightTermRangeNode.Min); + childTerms.Add(rightTermRangeNode.Max); } - public override Task VisitAsync(MissingNode node, IQueryVisitorContext context) { - node.Field = GetCustomFieldName(node.Field, Array.Empty()); + node.Field = GetCustomFieldName(node.Field, childTerms.ToArray()) ?? node.Field; + foreach (var child in node.Children) + await child.AcceptAsync(this, context).AnyContext(); + } + + public override Task VisitAsync(TermNode node, IQueryVisitorContext context) { + // using all fields search + if (String.IsNullOrEmpty(node.Field)) { return Task.CompletedTask; } - private string GetCustomFieldName(string field, string[] terms) { - if (String.IsNullOrEmpty(field)) - return null; + node.Field = GetCustomFieldName(node.Field, new[] { node.Term }); + return Task.CompletedTask; + } + + public override Task VisitAsync(TermRangeNode node, IQueryVisitorContext context) { + node.Field = GetCustomFieldName(node.Field, new[] { node.Min, node.Max }); + return Task.CompletedTask; + } - string[] parts = field.Split('.'); - if (parts.Length != 2 || (parts.Length == 2 && parts[1].StartsWith("@"))) - return field; + public override Task VisitAsync(ExistsNode node, IQueryVisitorContext context) { + node.Field = GetCustomFieldName(node.Field, Array.Empty()); + return Task.CompletedTask; + } - if (String.Equals(parts[0], "data", StringComparison.OrdinalIgnoreCase)) { - string termType; - if (String.Equals(parts[1], Event.KnownDataKeys.SessionEnd, StringComparison.OrdinalIgnoreCase)) - termType = "d"; - else if (String.Equals(parts[1], Event.KnownDataKeys.SessionHasError, StringComparison.OrdinalIgnoreCase)) - termType = "b"; - else - termType = GetTermType(terms); + public override Task VisitAsync(MissingNode node, IQueryVisitorContext context) { + node.Field = GetCustomFieldName(node.Field, Array.Empty()); + return Task.CompletedTask; + } - field = $"idx.{parts[1].ToLowerInvariant()}-{termType}"; - } else if (String.Equals(parts[0], "ref", StringComparison.OrdinalIgnoreCase)) { - field = $"idx.{parts[1].ToLowerInvariant()}-r"; - } + private string GetCustomFieldName(string field, string[] terms) { + if (String.IsNullOrEmpty(field)) + return null; + string[] parts = field.Split('.'); + if (parts.Length != 2 || (parts.Length == 2 && parts[1].StartsWith("@"))) return field; - } - private static string GetTermType(string[] terms) { - string termType = "s"; + if (String.Equals(parts[0], "data", StringComparison.OrdinalIgnoreCase)) { + string termType; + if (String.Equals(parts[1], Event.KnownDataKeys.SessionEnd, StringComparison.OrdinalIgnoreCase)) + termType = "d"; + else if (String.Equals(parts[1], Event.KnownDataKeys.SessionHasError, StringComparison.OrdinalIgnoreCase)) + termType = "b"; + else + termType = GetTermType(terms); + + field = $"idx.{parts[1].ToLowerInvariant()}-{termType}"; + } + else if (String.Equals(parts[0], "ref", StringComparison.OrdinalIgnoreCase)) { + field = $"idx.{parts[1].ToLowerInvariant()}-r"; + } - var trimmedTerms = terms.Where(t => t != null).Distinct().ToList(); - foreach (string term in trimmedTerms) { - if (term.StartsWith("*")) - continue; + return field; + } - if (Boolean.TryParse(term, out bool boolResult)) - termType = "b"; - else if (term.IsNumeric()) - termType = "n"; - else if (DateTime.TryParse(term, out var dateResult)) - termType = "d"; + private static string GetTermType(string[] terms) { + string termType = "s"; - break; - } + var trimmedTerms = terms.Where(t => t != null).Distinct().ToList(); + foreach (string term in trimmedTerms) { + if (term.StartsWith("*")) + continue; - // Some terms can be a string date range: [now TO now/d+1d} - if (String.Equals(termType, "s") && trimmedTerms.Count > 0 && trimmedTerms.All(t => String.Equals(t, "now", StringComparison.OrdinalIgnoreCase) || t.StartsWith("now/", StringComparison.OrdinalIgnoreCase))) + if (Boolean.TryParse(term, out bool boolResult)) + termType = "b"; + else if (term.IsNumeric()) + termType = "n"; + else if (DateTime.TryParse(term, out var dateResult)) termType = "d"; - return termType; + break; } - public static Task RunAsync(IQueryNode node, IQueryVisitorContext context = null) { - return new EventFieldsQueryVisitor().AcceptAsync(node, context); - } + // Some terms can be a string date range: [now TO now/d+1d} + if (String.Equals(termType, "s") && trimmedTerms.Count > 0 && trimmedTerms.All(t => String.Equals(t, "now", StringComparison.OrdinalIgnoreCase) || t.StartsWith("now/", StringComparison.OrdinalIgnoreCase))) + termType = "d"; - public static IQueryNode Run(IQueryNode node, IQueryVisitorContext context = null) { - return RunAsync(node, context).GetAwaiter().GetResult(); - } + return termType; + } + + public static Task RunAsync(IQueryNode node, IQueryVisitorContext context = null) { + return new EventFieldsQueryVisitor().AcceptAsync(node, context); + } + + public static IQueryNode Run(IQueryNode node, IQueryVisitorContext context = null) { + return RunAsync(node, context).GetAwaiter().GetResult(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs index 1ce610177f..75931f4a20 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Parsers.ElasticQueries.Visitors; @@ -10,16 +6,17 @@ using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; -namespace Exceptionless.Core.Repositories.Queries { - public class EventStackFilter { - private readonly ISet _stackNonInvertedFields = new HashSet(StringComparer.OrdinalIgnoreCase) { +namespace Exceptionless.Core.Repositories.Queries; + +public class EventStackFilter { + private readonly ISet _stackNonInvertedFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "organization_id", StackIndex.Alias.OrganizationId, "project_id", StackIndex.Alias.ProjectId, EventIndex.Alias.StackId, "stack_id", StackIndex.Alias.Type, }; - private readonly ISet _stackAndEventFields = new HashSet(StringComparer.OrdinalIgnoreCase) { + private readonly ISet _stackAndEventFields = new HashSet(StringComparer.OrdinalIgnoreCase) { "organization_id", StackIndex.Alias.OrganizationId, "project_id", StackIndex.Alias.ProjectId, EventIndex.Alias.StackId, "stack_id", @@ -27,7 +24,7 @@ public class EventStackFilter { StackIndex.Alias.Tags, "tags" }; - private readonly ISet _stackOnlyFields = new HashSet(StringComparer.OrdinalIgnoreCase) { + private readonly ISet _stackOnlyFields = new HashSet(StringComparer.OrdinalIgnoreCase) { StackIndex.Alias.LastOccurrence, "last_occurrence", StackIndex.Alias.References, "references", "status", @@ -42,158 +39,158 @@ public class EventStackFilter { StackIndex.Alias.TotalOccurrences, "total_occurrences" }; - private readonly ISet _stackOnlySpecialFields = new HashSet(StringComparer.OrdinalIgnoreCase) { + private readonly ISet _stackOnlySpecialFields = new HashSet(StringComparer.OrdinalIgnoreCase) { StackIndex.Alias.IsFixed, "is_fixed", StackIndex.Alias.IsRegressed, "is_regressed", StackIndex.Alias.IsHidden, "is_hidden" }; - private readonly LuceneQueryParser _parser; - private readonly ChainedQueryVisitor _eventQueryVisitor; - private readonly ChainedQueryVisitor _stackQueryVisitor; - private readonly ChainedQueryVisitor _invertedStackQueryVisitor; - - public EventStackFilter() { - var stackOnlyFields = _stackOnlyFields.Union(_stackOnlySpecialFields); - var stackFields = stackOnlyFields.Union(_stackAndEventFields); - - _parser = new LuceneQueryParser(); - _eventQueryVisitor = new ChainedQueryVisitor(); - _eventQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(stackOnlyFields)); - _eventQueryVisitor.AddVisitor(new CleanupQueryVisitor()); - - _stackQueryVisitor = new ChainedQueryVisitor(); - // remove everything not in the stack fields list - _stackQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(f => !stackFields.Contains(f))); - _stackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); - // handles stack special fields and changing event field names to their stack equivalent - _stackQueryVisitor.AddVisitor(new StackFilterQueryVisitor()); - _stackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); - - _invertedStackQueryVisitor = new ChainedQueryVisitor(); - // remove everything not in the stack fields list - _invertedStackQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(f => !stackFields.Contains(f))); - _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); - // handles stack special fields and changing event field names to their stack equivalent - _invertedStackQueryVisitor.AddVisitor(new StackFilterQueryVisitor()); - _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); - // inverts the filter - _invertedStackQueryVisitor.AddVisitor(new InvertQueryVisitor(_stackNonInvertedFields)); - _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); - } + private readonly LuceneQueryParser _parser; + private readonly ChainedQueryVisitor _eventQueryVisitor; + private readonly ChainedQueryVisitor _stackQueryVisitor; + private readonly ChainedQueryVisitor _invertedStackQueryVisitor; + + public EventStackFilter() { + var stackOnlyFields = _stackOnlyFields.Union(_stackOnlySpecialFields); + var stackFields = stackOnlyFields.Union(_stackAndEventFields); + + _parser = new LuceneQueryParser(); + _eventQueryVisitor = new ChainedQueryVisitor(); + _eventQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(stackOnlyFields)); + _eventQueryVisitor.AddVisitor(new CleanupQueryVisitor()); + + _stackQueryVisitor = new ChainedQueryVisitor(); + // remove everything not in the stack fields list + _stackQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(f => !stackFields.Contains(f))); + _stackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); + // handles stack special fields and changing event field names to their stack equivalent + _stackQueryVisitor.AddVisitor(new StackFilterQueryVisitor()); + _stackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); + + _invertedStackQueryVisitor = new ChainedQueryVisitor(); + // remove everything not in the stack fields list + _invertedStackQueryVisitor.AddVisitor(new RemoveFieldsQueryVisitor(f => !stackFields.Contains(f))); + _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); + // handles stack special fields and changing event field names to their stack equivalent + _invertedStackQueryVisitor.AddVisitor(new StackFilterQueryVisitor()); + _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); + // inverts the filter + _invertedStackQueryVisitor.AddVisitor(new InvertQueryVisitor(_stackNonInvertedFields)); + _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); + } - public async Task GetEventFilterAsync(string query, IQueryVisitorContext context = null) { - context ??= new ElasticQueryVisitorContext(); - var result = await _parser.ParseAsync(query, context); - await _eventQueryVisitor.AcceptAsync(result, context); - return result.ToString(); - } + public async Task GetEventFilterAsync(string query, IQueryVisitorContext context = null) { + context ??= new ElasticQueryVisitorContext(); + var result = await _parser.ParseAsync(query, context); + await _eventQueryVisitor.AcceptAsync(result, context); + return result.ToString(); + } - public async Task GetStackFilterAsync(string query, IQueryVisitorContext context = null) { - context ??= new ElasticQueryVisitorContext(); - var result = await _parser.ParseAsync(query, context); - var invertedResult = result.Clone(); - - result = await _stackQueryVisitor.AcceptAsync(result, context); - invertedResult = await _invertedStackQueryVisitor.AcceptAsync(invertedResult, context); - - return new StackFilter { - Filter = result.ToString(), - InvertedFilter = invertedResult.ToString(), - HasStatus = context.GetBoolean(nameof(StackFilter.HasStatus)), - HasStackIds = context.GetBoolean(nameof(StackFilter.HasStackIds)), - HasStatusOpen = context.GetBoolean(nameof(StackFilter.HasStatusOpen)) - }; - } + public async Task GetStackFilterAsync(string query, IQueryVisitorContext context = null) { + context ??= new ElasticQueryVisitorContext(); + var result = await _parser.ParseAsync(query, context); + var invertedResult = result.Clone(); + + result = await _stackQueryVisitor.AcceptAsync(result, context); + invertedResult = await _invertedStackQueryVisitor.AcceptAsync(invertedResult, context); + + return new StackFilter { + Filter = result.ToString(), + InvertedFilter = invertedResult.ToString(), + HasStatus = context.GetBoolean(nameof(StackFilter.HasStatus)), + HasStackIds = context.GetBoolean(nameof(StackFilter.HasStackIds)), + HasStatusOpen = context.GetBoolean(nameof(StackFilter.HasStatusOpen)) + }; } +} - public class StackFilterQueryVisitor : ChainableQueryVisitor { - public override Task VisitAsync(TermNode node, IQueryVisitorContext context) { - IQueryNode result = node; +public class StackFilterQueryVisitor : ChainableQueryVisitor { + public override Task VisitAsync(TermNode node, IQueryVisitorContext context) { + IQueryNode result = node; - // don't include terms without fields - if (node.Field == null) { - node.RemoveSelf(); - return Task.FromResult(null); - } + // don't include terms without fields + if (node.Field == null) { + node.RemoveSelf(); + return Task.FromResult(null); + } + + // process special stack fields + switch (node.Field?.ToLowerInvariant()) { + case EventIndex.Alias.StackId: + case "stack_id": + node.Field = "id"; + break; + case "is_fixed": + case StackIndex.Alias.IsFixed: + bool isFixed = Boolean.TryParse(node.Term, out bool temp) && temp; + node.Field = "status"; + node.Term = "fixed"; + node.IsNegated = !isFixed; + break; + case "is_regressed": + case StackIndex.Alias.IsRegressed: + bool isRegressed = Boolean.TryParse(node.Term, out bool regressed) && regressed; + node.Field = "status"; + node.Term = "regressed"; + node.IsNegated = !isRegressed; + break; + case "is_hidden": + case StackIndex.Alias.IsHidden: + bool isHidden = Boolean.TryParse(node.Term, out bool hidden) && hidden; + if (isHidden) { + var isHiddenNode = new GroupNode { + HasParens = true, + IsNegated = true, + Operator = GroupOperator.Or, + Left = new TermNode { Field = "status", Term = "open" }, + Right = new TermNode { Field = "status", Term = "regressed" } + }; + + result = node.ReplaceSelf(isHiddenNode); - // process special stack fields - switch (node.Field?.ToLowerInvariant()) { - case EventIndex.Alias.StackId: - case "stack_id": - node.Field = "id"; - break; - case "is_fixed": - case StackIndex.Alias.IsFixed: - bool isFixed = Boolean.TryParse(node.Term, out bool temp) && temp; - node.Field = "status"; - node.Term = "fixed"; - node.IsNegated = !isFixed; - break; - case "is_regressed": - case StackIndex.Alias.IsRegressed: - bool isRegressed = Boolean.TryParse(node.Term, out bool regressed) && regressed; - node.Field = "status"; - node.Term = "regressed"; - node.IsNegated = !isRegressed; break; - case "is_hidden": - case StackIndex.Alias.IsHidden: - bool isHidden = Boolean.TryParse(node.Term, out bool hidden) && hidden; - if (isHidden) { - var isHiddenNode = new GroupNode { - HasParens = true, - IsNegated = true, - Operator = GroupOperator.Or, - Left = new TermNode { Field = "status", Term = "open" }, - Right = new TermNode { Field = "status", Term = "regressed" } - }; - - result = node.ReplaceSelf(isHiddenNode); - - break; - } else { - var notHiddenNode = new GroupNode { - HasParens = true, - Operator = GroupOperator.Or, - Left = new TermNode { Field = "status", Term = "open" }, - Right = new TermNode { Field = "status", Term = "regressed" } - }; - - result = node.ReplaceSelf(notHiddenNode); - - break; - } - } + } + else { + var notHiddenNode = new GroupNode { + HasParens = true, + Operator = GroupOperator.Or, + Left = new TermNode { Field = "status", Term = "open" }, + Right = new TermNode { Field = "status", Term = "regressed" } + }; - if (result is TermNode termNode) { - if (String.Equals(termNode.Field, "status", StringComparison.OrdinalIgnoreCase)) { - context.SetValue(nameof(StackFilter.HasStatus), true); + result = node.ReplaceSelf(notHiddenNode); - if (!termNode.IsNegated.GetValueOrDefault() && String.Equals(termNode.Term, "open", StringComparison.OrdinalIgnoreCase)) - context.SetValue(nameof(StackFilter.HasStatusOpen), true); + break; } + } - if ((String.Equals(termNode.Field, EventIndex.Alias.StackId, StringComparison.OrdinalIgnoreCase) - || String.Equals(termNode.Field, "stack_id", StringComparison.OrdinalIgnoreCase)) - && !String.IsNullOrEmpty(termNode.Term)) { - context.SetValue(nameof(StackFilter.HasStackIds), true); - } + if (result is TermNode termNode) { + if (String.Equals(termNode.Field, "status", StringComparison.OrdinalIgnoreCase)) { + context.SetValue(nameof(StackFilter.HasStatus), true); + + if (!termNode.IsNegated.GetValueOrDefault() && String.Equals(termNode.Term, "open", StringComparison.OrdinalIgnoreCase)) + context.SetValue(nameof(StackFilter.HasStatusOpen), true); } - return Task.FromResult(result); + if ((String.Equals(termNode.Field, EventIndex.Alias.StackId, StringComparison.OrdinalIgnoreCase) + || String.Equals(termNode.Field, "stack_id", StringComparison.OrdinalIgnoreCase)) + && !String.IsNullOrEmpty(termNode.Term)) { + context.SetValue(nameof(StackFilter.HasStackIds), true); + } } - public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) { - return node.AcceptAsync(this, context); - } + return Task.FromResult(result); } - public class StackFilter { - public string Filter { get; set; } - public string InvertedFilter { get; set; } - public bool HasStatus { get; set; } - public bool HasStatusOpen { get; set; } - public bool HasStackIds { get; set; } + public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) { + return node.AcceptAsync(this, context); } -} \ No newline at end of file +} + +public class StackFilter { + public string Filter { get; set; } + public string InvertedFilter { get; set; } + public bool HasStatus { get; set; } + public bool HasStatusOpen { get; set; } + public bool HasStackIds { get; set; } +} diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/StackDateFixedQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/StackDateFixedQueryVisitor.cs index 4e876e746a..492d071480 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/StackDateFixedQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/StackDateFixedQueryVisitor.cs @@ -1,31 +1,29 @@ -using System; -using System.Threading.Tasks; -using Foundatio.Parsers.ElasticQueries.Extensions; +using Foundatio.Parsers.ElasticQueries.Extensions; using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; using Nest; -namespace Exceptionless.Core.Repositories.Queries { - public class StackDateFixedQueryVisitor : ChainableQueryVisitor { - private readonly string _dateFixedFieldName; - public StackDateFixedQueryVisitor(string dateFixedFieldName) { - if (String.IsNullOrEmpty(dateFixedFieldName)) - throw new ArgumentNullException(nameof(dateFixedFieldName)); +namespace Exceptionless.Core.Repositories.Queries; - _dateFixedFieldName = dateFixedFieldName; - } +public class StackDateFixedQueryVisitor : ChainableQueryVisitor { + private readonly string _dateFixedFieldName; + public StackDateFixedQueryVisitor(string dateFixedFieldName) { + if (String.IsNullOrEmpty(dateFixedFieldName)) + throw new ArgumentNullException(nameof(dateFixedFieldName)); - public override Task VisitAsync(TermNode node, IQueryVisitorContext context) { - if (!String.Equals(node.Field, "fixed", StringComparison.OrdinalIgnoreCase)) - return Task.FromResult(node); - - if (!Boolean.TryParse(node.Term, out bool isFixed)) - return Task.FromResult(node); + _dateFixedFieldName = dateFixedFieldName; + } - var query = new ExistsQuery { Field = _dateFixedFieldName }; - node.SetQuery(isFixed ? query : !query); + public override Task VisitAsync(TermNode node, IQueryVisitorContext context) { + if (!String.Equals(node.Field, "fixed", StringComparison.OrdinalIgnoreCase)) + return Task.FromResult(node); + if (!Boolean.TryParse(node.Term, out bool isFixed)) return Task.FromResult(node); - } + + var query = new ExistsQuery { Field = _dateFixedFieldName }; + node.SetQuery(isFixed ? query : !query); + + return Task.FromResult(node); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/StackRepository.cs b/src/Exceptionless.Core/Repositories/StackRepository.cs index c1c0c96fce..6eff38d353 100644 --- a/src/Exceptionless.Core/Repositories/StackRepository.cs +++ b/src/Exceptionless.Core/Repositories/StackRepository.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; @@ -13,40 +9,41 @@ using Microsoft.Extensions.Logging; using Nest; -namespace Exceptionless.Core.Repositories { - public class StackRepository : RepositoryOwnedByOrganizationAndProject, IStackRepository { - private const string STACKING_VERSION = "v2"; +namespace Exceptionless.Core.Repositories; - public StackRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) - : base(configuration.Stacks, validator, options) { - AddPropertyRequiredForRemove(s => s.SignatureHash); - } +public class StackRepository : RepositoryOwnedByOrganizationAndProject, IStackRepository { + private const string STACKING_VERSION = "v2"; - public Task> GetExpiredSnoozedStatuses(DateTime utcNow, CommandOptionsDescriptor options = null) { - return FindAsync(q => q.ElasticFilter(Query.DateRange(d => d.Field(f => f.SnoozeUntilUtc).LessThanOrEquals(utcNow))), options); - } - - public Task> GetStacksForCleanupAsync(string organizationId, DateTime cutoff) { - return FindAsync(q => q - .Organization(organizationId) - .ElasticFilter(Query.DateRange(d => d.Field(f => f.LastOccurrence).LessThanOrEquals(cutoff))) - .FieldEquals(f => f.Status, StackStatus.Open) - .FieldEmpty(f => f.References) - .Include(f => f.Id, f => f.OrganizationId, f => f.ProjectId, f => f.SignatureHash) - , o => o.SearchAfterPaging().PageLimit(500)); - } + public StackRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) + : base(configuration.Stacks, validator, options) { + AddPropertyRequiredForRemove(s => s.SignatureHash); + } - public Task> GetSoftDeleted() { - return FindAsync( - q => q.Include(f => f.Id, f => f.OrganizationId, f => f.ProjectId, f => f.SignatureHash), - o => o.SoftDeleteMode(SoftDeleteQueryMode.DeletedOnly).SearchAfterPaging().PageLimit(500) - ); - } + public Task> GetExpiredSnoozedStatuses(DateTime utcNow, CommandOptionsDescriptor options = null) { + return FindAsync(q => q.ElasticFilter(Query.DateRange(d => d.Field(f => f.SnoozeUntilUtc).LessThanOrEquals(utcNow))), options); + } - public async Task IncrementEventCounterAsync(string organizationId, string projectId, string stackId, DateTime minOccurrenceDateUtc, DateTime maxOccurrenceDateUtc, int count, bool sendNotifications = true) { - // If total occurrences are zero (stack data was reset), then set first occurrence date - // Only update the LastOccurrence if the new date is greater then the existing date. - const string script = @" + public Task> GetStacksForCleanupAsync(string organizationId, DateTime cutoff) { + return FindAsync(q => q + .Organization(organizationId) + .ElasticFilter(Query.DateRange(d => d.Field(f => f.LastOccurrence).LessThanOrEquals(cutoff))) + .FieldEquals(f => f.Status, StackStatus.Open) + .FieldEmpty(f => f.References) + .Include(f => f.Id, f => f.OrganizationId, f => f.ProjectId, f => f.SignatureHash) + , o => o.SearchAfterPaging().PageLimit(500)); + } + + public Task> GetSoftDeleted() { + return FindAsync( + q => q.Include(f => f.Id, f => f.OrganizationId, f => f.ProjectId, f => f.SignatureHash), + o => o.SoftDeleteMode(SoftDeleteQueryMode.DeletedOnly).SearchAfterPaging().PageLimit(500) + ); + } + + public async Task IncrementEventCounterAsync(string organizationId, string projectId, string stackId, DateTime minOccurrenceDateUtc, DateTime maxOccurrenceDateUtc, int count, bool sendNotifications = true) { + // If total occurrences are zero (stack data was reset), then set first occurrence date + // Only update the LastOccurrence if the new date is greater then the existing date. + const string script = @" Instant parseDate(def dt) { if (dt != null) { try { @@ -67,77 +64,76 @@ Instant parseDate(def dt) { } ctx._source.total_occurrences += params.count;"; - var request = new UpdateRequest(ElasticIndex.GetIndex(stackId), stackId) { - Script = new InlineScript(script.TrimScript()) { - Params = new Dictionary(3) { + var request = new UpdateRequest(ElasticIndex.GetIndex(stackId), stackId) { + Script = new InlineScript(script.TrimScript()) { + Params = new Dictionary(3) { { "minOccurrenceDateUtc", minOccurrenceDateUtc }, { "maxOccurrenceDateUtc", maxOccurrenceDateUtc }, { "count", count }, { "updatedUtc", SystemClock.UtcNow } } - } - }; - - var result = await _client.UpdateAsync(request).AnyContext(); - if (!result.IsValid) { - _logger.LogError(result.OriginalException, "Error occurred incrementing total event occurrences on stack {stack}. Error: {Message}", stackId, result.ServerError?.Error); - return result.ServerError?.Status == 404; } + }; - await Cache.RemoveAsync(stackId).AnyContext(); - if (sendNotifications) - await PublishMessageAsync(CreateEntityChanged(ChangeType.Saved, organizationId, projectId, null, stackId), TimeSpan.FromSeconds(1.5)).AnyContext(); - - return true; + var result = await _client.UpdateAsync(request).AnyContext(); + if (!result.IsValid) { + _logger.LogError(result.OriginalException, "Error occurred incrementing total event occurrences on stack {stack}. Error: {Message}", stackId, result.ServerError?.Error); + return result.ServerError?.Status == 404; } - public async Task GetStackBySignatureHashAsync(string projectId, string signatureHash) { - string key = GetStackSignatureCacheKey(projectId, signatureHash); - var hit = await FindOneAsync(q => q.Project(projectId).ElasticFilter(Query.Term(s => s.SignatureHash, signatureHash)), o => o.Cache(key)).AnyContext(); - return hit?.Document; - } + await Cache.RemoveAsync(stackId).AnyContext(); + if (sendNotifications) + await PublishMessageAsync(CreateEntityChanged(ChangeType.Saved, organizationId, projectId, null, stackId), TimeSpan.FromSeconds(1.5)).AnyContext(); - public Task> GetIdsByQueryAsync(RepositoryQueryDescriptor query, CommandOptionsDescriptor options = null) { - return FindAsync(q => query.Configure().OnlyIds(), options); - } + return true; + } - public async Task MarkAsRegressedAsync(string stackId) { - var stack = await GetByIdAsync(stackId).AnyContext(); - stack.Status = StackStatus.Regressed; - await SaveAsync(stack, o => o.Cache()).AnyContext(); - } + public async Task GetStackBySignatureHashAsync(string projectId, string signatureHash) { + string key = GetStackSignatureCacheKey(projectId, signatureHash); + var hit = await FindOneAsync(q => q.Project(projectId).ElasticFilter(Query.Term(s => s.SignatureHash, signatureHash)), o => o.Cache(key)).AnyContext(); + return hit?.Document; + } - public Task SoftDeleteByProjectIdAsync(string organizationId, string projectId) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); + public Task> GetIdsByQueryAsync(RepositoryQueryDescriptor query, CommandOptionsDescriptor options = null) { + return FindAsync(q => query.Configure().OnlyIds(), options); + } - if (String.IsNullOrEmpty(projectId)) - throw new ArgumentNullException(nameof(projectId)); + public async Task MarkAsRegressedAsync(string stackId) { + var stack = await GetByIdAsync(stackId).AnyContext(); + stack.Status = StackStatus.Regressed; + await SaveAsync(stack, o => o.Cache()).AnyContext(); + } - return PatchAllAsync( - q => q.Organization(organizationId).Project(projectId), - new PartialPatch(new { is_deleted = true, updated_utc = SystemClock.UtcNow }) - ); - } + public Task SoftDeleteByProjectIdAsync(string organizationId, string projectId) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); - protected override async Task AddDocumentsToCacheAsync(ICollection> findHits, ICommandOptions options, bool isDirtyRead) { - await base.AddDocumentsToCacheAsync(findHits, options, isDirtyRead).AnyContext(); + if (String.IsNullOrEmpty(projectId)) + throw new ArgumentNullException(nameof(projectId)); - var cacheEntries = new Dictionary>(); - foreach (var hit in findHits) - cacheEntries.Add(GetStackSignatureCacheKey(hit.Document), hit); + return PatchAllAsync( + q => q.Organization(organizationId).Project(projectId), + new PartialPatch(new { is_deleted = true, updated_utc = SystemClock.UtcNow }) + ); + } - if (cacheEntries.Count > 0) - await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()); - } + protected override async Task AddDocumentsToCacheAsync(ICollection> findHits, ICommandOptions options, bool isDirtyRead) { + await base.AddDocumentsToCacheAsync(findHits, options, isDirtyRead).AnyContext(); - protected override async Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { - var keys = documents.UnionOriginalAndModified().Select(GetStackSignatureCacheKey).Distinct(); - await Cache.RemoveAllAsync(keys).AnyContext(); - await base.InvalidateCacheAsync(documents, changeType).AnyContext(); - } + var cacheEntries = new Dictionary>(); + foreach (var hit in findHits) + cacheEntries.Add(GetStackSignatureCacheKey(hit.Document), hit); - private string GetStackSignatureCacheKey(Stack stack) => GetStackSignatureCacheKey(stack.ProjectId, stack.SignatureHash); - private string GetStackSignatureCacheKey(string projectId, string signatureHash) => String.Concat(projectId, ":", signatureHash, ":", STACKING_VERSION); + if (cacheEntries.Count > 0) + await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()); } + + protected override async Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { + var keys = documents.UnionOriginalAndModified().Select(GetStackSignatureCacheKey).Distinct(); + await Cache.RemoveAllAsync(keys).AnyContext(); + await base.InvalidateCacheAsync(documents, changeType).AnyContext(); + } + + private string GetStackSignatureCacheKey(Stack stack) => GetStackSignatureCacheKey(stack.ProjectId, stack.SignatureHash); + private string GetStackSignatureCacheKey(string projectId, string signatureHash) => String.Concat(projectId, ":", signatureHash, ":", STACKING_VERSION); } diff --git a/src/Exceptionless.Core/Repositories/TokenRepository.cs b/src/Exceptionless.Core/Repositories/TokenRepository.cs index 3bf9bbd38a..f2848ff286 100644 --- a/src/Exceptionless.Core/Repositories/TokenRepository.cs +++ b/src/Exceptionless.Core/Repositories/TokenRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Messaging.Models; +using Exceptionless.Core.Messaging.Models; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using FluentValidation; @@ -10,44 +7,44 @@ using Nest; using Token = Exceptionless.Core.Models.Token; -namespace Exceptionless.Core.Repositories { - public class TokenRepository : RepositoryOwnedByOrganizationAndProject, ITokenRepository { - public TokenRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) - : base(configuration.Tokens, validator, options) { - } +namespace Exceptionless.Core.Repositories; - public Task> GetByTypeAndUserIdAsync(TokenType type, string userId, CommandOptionsDescriptor options = null) { - var filter = Query.Term(e => e.UserId, userId) && Query.Term(t => t.Type, type); - return FindAsync(q => q.ElasticFilter(filter), options); - } +public class TokenRepository : RepositoryOwnedByOrganizationAndProject, ITokenRepository { + public TokenRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) + : base(configuration.Tokens, validator, options) { + } - public Task> GetByTypeAndOrganizationIdAsync(TokenType type, string organizationId, CommandOptionsDescriptor options = null) { - return FindAsync(q => q.Organization(organizationId).ElasticFilter(Query.Term(t => t.Type, type)), options); - } + public Task> GetByTypeAndUserIdAsync(TokenType type, string userId, CommandOptionsDescriptor options = null) { + var filter = Query.Term(e => e.UserId, userId) && Query.Term(t => t.Type, type); + return FindAsync(q => q.ElasticFilter(filter), options); + } - public Task> GetByTypeAndProjectIdAsync(TokenType type, string projectId, CommandOptionsDescriptor options = null) { - var filter = ( - Query.Term(t => t.ProjectId, projectId) || Query.Term(t => t.DefaultProjectId, projectId) - ) && Query.Term(t => t.Type, type); + public Task> GetByTypeAndOrganizationIdAsync(TokenType type, string organizationId, CommandOptionsDescriptor options = null) { + return FindAsync(q => q.Organization(organizationId).ElasticFilter(Query.Term(t => t.Type, type)), options); + } - return FindAsync(q => q.ElasticFilter(filter), options); - } + public Task> GetByTypeAndProjectIdAsync(TokenType type, string projectId, CommandOptionsDescriptor options = null) { + var filter = ( + Query.Term(t => t.ProjectId, projectId) || Query.Term(t => t.DefaultProjectId, projectId) + ) && Query.Term(t => t.Type, type); - public override Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null) { - var filter = (Query.Term(t => t.ProjectId, projectId) || Query.Term(t => t.DefaultProjectId, projectId)); - return FindAsync(q => q.ElasticFilter(filter), options); - } + return FindAsync(q => q.ElasticFilter(filter), options); + } - public Task RemoveAllByUserIdAsync(string userId, CommandOptionsDescriptor options = null) { - return RemoveAllAsync(q => q.ElasticFilter(Query.Term(t => t.UserId, userId)), options); - } + public override Task> GetByProjectIdAsync(string projectId, CommandOptionsDescriptor options = null) { + var filter = (Query.Term(t => t.ProjectId, projectId) || Query.Term(t => t.DefaultProjectId, projectId)); + return FindAsync(q => q.ElasticFilter(filter), options); + } + + public Task RemoveAllByUserIdAsync(string userId, CommandOptionsDescriptor options = null) { + return RemoveAllAsync(q => q.ElasticFilter(Query.Term(t => t.UserId, userId)), options); + } - protected override Task PublishChangeTypeMessageAsync(ChangeType changeType, Token document, IDictionary data = null, TimeSpan? delay = null) { - var items = new Foundatio.Utility.DataDictionary(data ?? new Dictionary()) { + protected override Task PublishChangeTypeMessageAsync(ChangeType changeType, Token document, IDictionary data = null, TimeSpan? delay = null) { + var items = new Foundatio.Utility.DataDictionary(data ?? new Dictionary()) { { ExtendedEntityChanged.KnownKeys.IsAuthenticationToken, TokenType.Authentication == document?.Type }, { ExtendedEntityChanged.KnownKeys.UserId, document?.UserId } }; - return PublishMessageAsync(CreateEntityChanged(changeType, document?.OrganizationId, document?.ProjectId ?? document?.DefaultProjectId, null, document?.Id, items), delay); - } + return PublishMessageAsync(CreateEntityChanged(changeType, document?.OrganizationId, document?.ProjectId ?? document?.DefaultProjectId, null, document?.Id, items), delay); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Repositories/UserRepository.cs b/src/Exceptionless.Core/Repositories/UserRepository.cs index a2ca12d5d9..64071ee9ac 100644 --- a/src/Exceptionless.Core/Repositories/UserRepository.cs +++ b/src/Exceptionless.Core/Repositories/UserRepository.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories.Configuration; using FluentValidation; using Foundatio.Repositories; @@ -11,77 +7,77 @@ using Nest; using User = Exceptionless.Core.Models.User; -namespace Exceptionless.Core.Repositories { - public class UserRepository : RepositoryBase, IUserRepository { - public UserRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) - : base(configuration.Users, validator, options) { - AddPropertyRequiredForRemove(u => u.EmailAddress, u => u.OrganizationIds); - } - - public async Task GetByEmailAddressAsync(string emailAddress) { - if (String.IsNullOrWhiteSpace(emailAddress)) - return null; - - emailAddress = emailAddress.Trim().ToLowerInvariant(); - var hit = await FindOneAsync(q => q.ElasticFilter(Query.Term(u => u.EmailAddress.Suffix("keyword"), emailAddress)), o => o.Cache(EmailCacheKey(emailAddress))).AnyContext(); - return hit?.Document; - } - - public async Task GetByPasswordResetTokenAsync(string token) { - if (String.IsNullOrEmpty(token)) - return null; - - var hit = await FindOneAsync(q => q.ElasticFilter(Query.Term(u => u.PasswordResetToken, token))).AnyContext(); - return hit?.Document; - } - - public async Task GetUserByOAuthProviderAsync(string provider, string providerUserId) { - if (String.IsNullOrEmpty(provider) || String.IsNullOrEmpty(providerUserId)) - return null; - - provider = provider.ToLowerInvariant(); - var filter = Query.Term(u => u.OAuthAccounts.First().ProviderUserId, providerUserId); - var results = (await FindAsync(q => q.ElasticFilter(filter)).AnyContext()).Documents; - return results.FirstOrDefault(u => u.OAuthAccounts.Any(o => o.Provider == provider)); - } - - public async Task GetByVerifyEmailAddressTokenAsync(string token) { - if (String.IsNullOrEmpty(token)) - return null; - - var filter = Query.Term(u => u.VerifyEmailAddressToken, token); - var hit = await FindOneAsync(q => q.ElasticFilter(filter)).AnyContext(); - return hit?.Document; - } - - public Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null) { - if (String.IsNullOrEmpty(organizationId)) - return Task.FromResult(new FindResults()); - - var commandOptions = options.Configure(); - if (commandOptions.ShouldUseCache()) - throw new Exception("Caching of paged queries is not allowed"); - - var filter = Query.Term(u => u.OrganizationIds, organizationId); - return FindAsync(q => q.ElasticFilter(filter).SortAscending(u => u.EmailAddress.Suffix("keyword")), o => commandOptions); - } - - protected override async Task AddDocumentsToCacheAsync(ICollection> findHits, ICommandOptions options, bool isDirtyRead) { - await base.AddDocumentsToCacheAsync(findHits, options, isDirtyRead).AnyContext(); - - var cacheEntries = new Dictionary>(); - foreach (var hit in findHits.Where(d => !String.IsNullOrEmpty(d.Document?.EmailAddress))) - cacheEntries.Add(EmailCacheKey(hit.Document.EmailAddress), hit); - - if (cacheEntries.Count > 0) - await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()).AnyContext(); - } - - protected override Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { - var keysToRemove = documents.UnionOriginalAndModified().Select(u => EmailCacheKey(u.EmailAddress)).Distinct(); - return Task.WhenAll(Cache.RemoveAllAsync(keysToRemove), base.InvalidateCacheAsync(documents, changeType)); - } - - private string EmailCacheKey(string emailAddress) => String.Concat("Email:", emailAddress.Trim().ToLowerInvariant()); +namespace Exceptionless.Core.Repositories; + +public class UserRepository : RepositoryBase, IUserRepository { + public UserRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) + : base(configuration.Users, validator, options) { + AddPropertyRequiredForRemove(u => u.EmailAddress, u => u.OrganizationIds); + } + + public async Task GetByEmailAddressAsync(string emailAddress) { + if (String.IsNullOrWhiteSpace(emailAddress)) + return null; + + emailAddress = emailAddress.Trim().ToLowerInvariant(); + var hit = await FindOneAsync(q => q.ElasticFilter(Query.Term(u => u.EmailAddress.Suffix("keyword"), emailAddress)), o => o.Cache(EmailCacheKey(emailAddress))).AnyContext(); + return hit?.Document; + } + + public async Task GetByPasswordResetTokenAsync(string token) { + if (String.IsNullOrEmpty(token)) + return null; + + var hit = await FindOneAsync(q => q.ElasticFilter(Query.Term(u => u.PasswordResetToken, token))).AnyContext(); + return hit?.Document; + } + + public async Task GetUserByOAuthProviderAsync(string provider, string providerUserId) { + if (String.IsNullOrEmpty(provider) || String.IsNullOrEmpty(providerUserId)) + return null; + + provider = provider.ToLowerInvariant(); + var filter = Query.Term(u => u.OAuthAccounts.First().ProviderUserId, providerUserId); + var results = (await FindAsync(q => q.ElasticFilter(filter)).AnyContext()).Documents; + return results.FirstOrDefault(u => u.OAuthAccounts.Any(o => o.Provider == provider)); + } + + public async Task GetByVerifyEmailAddressTokenAsync(string token) { + if (String.IsNullOrEmpty(token)) + return null; + + var filter = Query.Term(u => u.VerifyEmailAddressToken, token); + var hit = await FindOneAsync(q => q.ElasticFilter(filter)).AnyContext(); + return hit?.Document; + } + + public Task> GetByOrganizationIdAsync(string organizationId, CommandOptionsDescriptor options = null) { + if (String.IsNullOrEmpty(organizationId)) + return Task.FromResult(new FindResults()); + + var commandOptions = options.Configure(); + if (commandOptions.ShouldUseCache()) + throw new Exception("Caching of paged queries is not allowed"); + + var filter = Query.Term(u => u.OrganizationIds, organizationId); + return FindAsync(q => q.ElasticFilter(filter).SortAscending(u => u.EmailAddress.Suffix("keyword")), o => commandOptions); + } + + protected override async Task AddDocumentsToCacheAsync(ICollection> findHits, ICommandOptions options, bool isDirtyRead) { + await base.AddDocumentsToCacheAsync(findHits, options, isDirtyRead).AnyContext(); + + var cacheEntries = new Dictionary>(); + foreach (var hit in findHits.Where(d => !String.IsNullOrEmpty(d.Document?.EmailAddress))) + cacheEntries.Add(EmailCacheKey(hit.Document.EmailAddress), hit); + + if (cacheEntries.Count > 0) + await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()).AnyContext(); } -} \ No newline at end of file + + protected override Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { + var keysToRemove = documents.UnionOriginalAndModified().Select(u => EmailCacheKey(u.EmailAddress)).Distinct(); + return Task.WhenAll(Cache.RemoveAllAsync(keysToRemove), base.InvalidateCacheAsync(documents, changeType)); + } + + private string EmailCacheKey(string emailAddress) => String.Concat("Email:", emailAddress.Trim().ToLowerInvariant()); +} diff --git a/src/Exceptionless.Core/Repositories/WebHookRepository.cs b/src/Exceptionless.Core/Repositories/WebHookRepository.cs index 6c48011d9b..2cd3b0bdee 100644 --- a/src/Exceptionless.Core/Repositories/WebHookRepository.cs +++ b/src/Exceptionless.Core/Repositories/WebHookRepository.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Configuration; using FluentValidation; @@ -10,53 +6,53 @@ using Foundatio.Repositories.Models; using Nest; -namespace Exceptionless.Core.Repositories { - public sealed class WebHookRepository : RepositoryOwnedByOrganizationAndProject, IWebHookRepository { - public WebHookRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) - : base(configuration.WebHooks, validator, options) {} - - public Task> GetByUrlAsync(string targetUrl) { - return FindAsync(q => q.FieldEquals(w => w.Url, targetUrl)); - } - - public Task> GetByOrganizationIdOrProjectIdAsync(string organizationId, string projectId) { - var filter = (Query.Term(e => e.OrganizationId, organizationId) && !Query.Exists(e => e.Field(f => f.ProjectId))) || Query.Term(e => e.ProjectId, projectId); - - // TODO: This cache key may not always be cleared out if the web hook doesn't have both a org and project id. - return FindAsync(q => q.ElasticFilter(filter), o => o.Cache(PagedCacheKey(organizationId, projectId))); - } - - public async Task MarkDisabledAsync(string id) { - var webHook = await GetByIdAsync(id).AnyContext(); - if (!webHook.IsEnabled) - return; - - webHook.IsEnabled = false; - await SaveAsync(webHook, o => o.Cache()).AnyContext(); - } - - public static class EventTypes { - // TODO: Add support for these new web hook types. - public const string NewError = "NewError"; - public const string CriticalError = "CriticalError"; - public const string NewEvent = "NewEvent"; - public const string CriticalEvent = "CriticalEvent"; - public const string StackRegression = "StackRegression"; - public const string StackPromoted = "StackPromoted"; - } - - protected override async Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { - var keysToRemove = documents.Select(d => d.Value).Select(CacheKey).Distinct(); - await Cache.RemoveAllAsync(keysToRemove).AnyContext(); - - var pagedKeysToRemove = documents.Select(d => PagedCacheKey(d.Value.OrganizationId, d.Value.ProjectId)).Distinct(); - foreach (string key in pagedKeysToRemove) - await Cache.RemoveByPrefixAsync(key).AnyContext(); - - await base.InvalidateCacheAsync(documents, changeType).AnyContext(); - } - - private string CacheKey(WebHook webHook) => String.Concat("Organization:", webHook.OrganizationId, ":Project:", webHook.ProjectId); - private string PagedCacheKey(string organizationId, string projectId) => String.Concat("paged:Organization:", organizationId, ":Project:", projectId); +namespace Exceptionless.Core.Repositories; + +public sealed class WebHookRepository : RepositoryOwnedByOrganizationAndProject, IWebHookRepository { + public WebHookRepository(ExceptionlessElasticConfiguration configuration, IValidator validator, AppOptions options) + : base(configuration.WebHooks, validator, options) { } + + public Task> GetByUrlAsync(string targetUrl) { + return FindAsync(q => q.FieldEquals(w => w.Url, targetUrl)); + } + + public Task> GetByOrganizationIdOrProjectIdAsync(string organizationId, string projectId) { + var filter = (Query.Term(e => e.OrganizationId, organizationId) && !Query.Exists(e => e.Field(f => f.ProjectId))) || Query.Term(e => e.ProjectId, projectId); + + // TODO: This cache key may not always be cleared out if the web hook doesn't have both a org and project id. + return FindAsync(q => q.ElasticFilter(filter), o => o.Cache(PagedCacheKey(organizationId, projectId))); + } + + public async Task MarkDisabledAsync(string id) { + var webHook = await GetByIdAsync(id).AnyContext(); + if (!webHook.IsEnabled) + return; + + webHook.IsEnabled = false; + await SaveAsync(webHook, o => o.Cache()).AnyContext(); } -} \ No newline at end of file + + public static class EventTypes { + // TODO: Add support for these new web hook types. + public const string NewError = "NewError"; + public const string CriticalError = "CriticalError"; + public const string NewEvent = "NewEvent"; + public const string CriticalEvent = "CriticalEvent"; + public const string StackRegression = "StackRegression"; + public const string StackPromoted = "StackPromoted"; + } + + protected override async Task InvalidateCacheAsync(IReadOnlyCollection> documents, ChangeType? changeType = null) { + var keysToRemove = documents.Select(d => d.Value).Select(CacheKey).Distinct(); + await Cache.RemoveAllAsync(keysToRemove).AnyContext(); + + var pagedKeysToRemove = documents.Select(d => PagedCacheKey(d.Value.OrganizationId, d.Value.ProjectId)).Distinct(); + foreach (string key in pagedKeysToRemove) + await Cache.RemoveByPrefixAsync(key).AnyContext(); + + await base.InvalidateCacheAsync(documents, changeType).AnyContext(); + } + + private string CacheKey(WebHook webHook) => String.Concat("Organization:", webHook.OrganizationId, ":Project:", webHook.ProjectId); + private string PagedCacheKey(string organizationId, string projectId) => String.Concat("paged:Organization:", organizationId, ":Project:", projectId); +} diff --git a/src/Exceptionless.Core/Serialization/DataObjectConverter.cs b/src/Exceptionless.Core/Serialization/DataObjectConverter.cs index d3644a969c..66005823b5 100644 --- a/src/Exceptionless.Core/Serialization/DataObjectConverter.cs +++ b/src/Exceptionless.Core/Serialization/DataObjectConverter.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; @@ -13,153 +9,161 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; -namespace Exceptionless.Serializer { - public class DataObjectConverter : CustomCreationConverter where T : IData, new() { - private static readonly Type _type = typeof(T); - private static readonly ConcurrentDictionary _propertyAccessors = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _dataTypeRegistry = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly ILogger _logger; - private readonly char[] _filteredChars = { '.', '-', '_' }; +namespace Exceptionless.Serializer; - public DataObjectConverter(ILogger logger, IEnumerable> knownDataTypes = null) { - _logger = logger; +public class DataObjectConverter : CustomCreationConverter where T : IData, new() { + private static readonly Type _type = typeof(T); + private static readonly ConcurrentDictionary _propertyAccessors = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _dataTypeRegistry = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private readonly ILogger _logger; + private readonly char[] _filteredChars = { '.', '-', '_' }; - if (knownDataTypes != null) - _dataTypeRegistry.AddRange(knownDataTypes); + public DataObjectConverter(ILogger logger, IEnumerable> knownDataTypes = null) { + _logger = logger; - if (_propertyAccessors.Count != 0) - return; + if (knownDataTypes != null) + _dataTypeRegistry.AddRange(knownDataTypes); - foreach (var prop in _type.GetProperties(BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public).Where(p => p.CanWrite)) - _propertyAccessors.TryAdd(prop.Name, LateBinder.GetPropertyAccessor(prop)); - } + if (_propertyAccessors.Count != 0) + return; - public void AddKnownDataType(string name, Type dataType) { - _dataTypeRegistry.TryAdd(name, dataType); - } + foreach (var prop in _type.GetProperties(BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public).Where(p => p.CanWrite)) + _propertyAccessors.TryAdd(prop.Name, LateBinder.GetPropertyAccessor(prop)); + } + + public void AddKnownDataType(string name, Type dataType) { + _dataTypeRegistry.TryAdd(name, dataType); + } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - var target = Create(objectType); - var json = JObject.Load(reader); + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { + var target = Create(objectType); + var json = JObject.Load(reader); - foreach (var p in json.Properties()) { - string propertyName = p.Name.ToLowerFiltered(_filteredChars); + foreach (var p in json.Properties()) { + string propertyName = p.Name.ToLowerFiltered(_filteredChars); - if (propertyName == "data" && p.Value is JObject) { - foreach (var dataProp in ((JObject)p.Value).Properties()) - AddDataEntry(serializer, dataProp, target); + if (propertyName == "data" && p.Value is JObject) { + foreach (var dataProp in ((JObject)p.Value).Properties()) + AddDataEntry(serializer, dataProp, target); + continue; + } + + var accessor = _propertyAccessors.TryGetValue(propertyName, out var value) ? value : null; + if (accessor != null) { + if (p.Value.Type == JTokenType.None || p.Value.Type == JTokenType.Undefined) continue; - } - var accessor = _propertyAccessors.TryGetValue(propertyName, out var value) ? value : null; - if (accessor != null) { - if (p.Value.Type == JTokenType.None || p.Value.Type == JTokenType.Undefined) - continue; + if (p.Value.Type == JTokenType.Null) { + accessor.SetValue(target, null); + continue; + } - if (p.Value.Type == JTokenType.Null) { - accessor.SetValue(target, null); + if (accessor.MemberType == typeof(DateTime)) { + if (p.Value.Type == JTokenType.Date || p.Value.Type == JTokenType.String && p.Value.Value().Contains("+")) { + accessor.SetValue(target, p.Value.ToObject(serializer).DateTime); continue; } - - if (accessor.MemberType == typeof(DateTime)) { - if (p.Value.Type == JTokenType.Date || p.Value.Type == JTokenType.String && p.Value.Value().Contains("+")) { - accessor.SetValue(target, p.Value.ToObject(serializer).DateTime); - continue; - } - } else if (accessor.MemberType == typeof(DateTime?)) { - if (p.Value.Type == JTokenType.Date || p.Value.Type == JTokenType.String && p.Value.Value().Contains("+")) { - var offset = p.Value.ToObject(serializer); - accessor.SetValue(target, offset?.DateTime); - continue; - } + } + else if (accessor.MemberType == typeof(DateTime?)) { + if (p.Value.Type == JTokenType.Date || p.Value.Type == JTokenType.String && p.Value.Value().Contains("+")) { + var offset = p.Value.ToObject(serializer); + accessor.SetValue(target, offset?.DateTime); + continue; } - - accessor.SetValue(target, p.Value.ToObject(accessor.MemberType, serializer)); - continue; } - AddDataEntry(serializer, p, target); + accessor.SetValue(target, p.Value.ToObject(accessor.MemberType, serializer)); + continue; } - return target; + AddDataEntry(serializer, p, target); } - private void AddDataEntry(JsonSerializer serializer, JProperty p, T target) { - if (target.Data == null) - target.Data = new DataDictionary(); - - string dataKey = GetDataKey(target.Data, p.Name); - string unknownTypeDataKey = GetDataKey(target.Data, p.Name, true); - - // when adding items to data, see if they are a known type and deserialize to the registered type - if (_dataTypeRegistry.TryGetValue(p.Name, out var dataType)) { - try { - if (p.Value is JValue && p.Value.Type == JTokenType.String) { - string value = p.Value.ToString(); - if (value.IsJson()) - target.Data[dataKey] = serializer.Deserialize(new StringReader(value), dataType); - else - target.Data[dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; - } else { - target.Data[dataKey] = p.Value.ToObject(dataType, serializer); - } + return target; + } + + private void AddDataEntry(JsonSerializer serializer, JProperty p, T target) { + if (target.Data == null) + target.Data = new DataDictionary(); - return; - } catch (Exception) { - _logger.LogInformation("Error deserializing known data type {Name}: {Value}", p.Name, p.Value.ToString()); + string dataKey = GetDataKey(target.Data, p.Name); + string unknownTypeDataKey = GetDataKey(target.Data, p.Name, true); + + // when adding items to data, see if they are a known type and deserialize to the registered type + if (_dataTypeRegistry.TryGetValue(p.Name, out var dataType)) { + try { + if (p.Value is JValue && p.Value.Type == JTokenType.String) { + string value = p.Value.ToString(); + if (value.IsJson()) + target.Data[dataKey] = serializer.Deserialize(new StringReader(value), dataType); + else + target.Data[dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; } + else { + target.Data[dataKey] = p.Value.ToObject(dataType, serializer); + } + + return; } + catch (Exception) { + _logger.LogInformation("Error deserializing known data type {Name}: {Value}", p.Name, p.Value.ToString()); + } + } - // Add item to data as a JObject, JArray or native type. - if (p.Value is JObject) { - target.Data[dataType == null || dataType == typeof(JObject) ? dataKey : unknownTypeDataKey] = p.Value.ToObject(); - } else if (p.Value is JArray) { - target.Data[dataType == null || dataType == typeof(JArray) ? dataKey : unknownTypeDataKey] = p.Value.ToObject(); - } else if (p.Value is JValue && p.Value.Type != JTokenType.String) { - object value = ((JValue)p.Value).Value; - target.Data[dataType == null || dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; - } else { - string value = p.Value.ToString(); - var jsonType = value.GetJsonType(); - if (jsonType == JsonType.Object) { - if (value.TryFromJson(out JObject obj)) - target.Data[dataType == null || dataType == obj.GetType() ? dataKey : unknownTypeDataKey] = obj; - else - target.Data[dataType == null || dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; - } else if (jsonType == JsonType.Array) { - if (value.TryFromJson(out JArray obj)) - target.Data[dataType == null || dataType == obj.GetType() ? dataKey : unknownTypeDataKey] = obj; - else - target.Data[dataType == null || dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; - } else { + // Add item to data as a JObject, JArray or native type. + if (p.Value is JObject) { + target.Data[dataType == null || dataType == typeof(JObject) ? dataKey : unknownTypeDataKey] = p.Value.ToObject(); + } + else if (p.Value is JArray) { + target.Data[dataType == null || dataType == typeof(JArray) ? dataKey : unknownTypeDataKey] = p.Value.ToObject(); + } + else if (p.Value is JValue && p.Value.Type != JTokenType.String) { + object value = ((JValue)p.Value).Value; + target.Data[dataType == null || dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; + } + else { + string value = p.Value.ToString(); + var jsonType = value.GetJsonType(); + if (jsonType == JsonType.Object) { + if (value.TryFromJson(out JObject obj)) + target.Data[dataType == null || dataType == obj.GetType() ? dataKey : unknownTypeDataKey] = obj; + else + target.Data[dataType == null || dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; + } + else if (jsonType == JsonType.Array) { + if (value.TryFromJson(out JArray obj)) + target.Data[dataType == null || dataType == obj.GetType() ? dataKey : unknownTypeDataKey] = obj; + else target.Data[dataType == null || dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; - } + } + else { + target.Data[dataType == null || dataType == value.GetType() ? dataKey : unknownTypeDataKey] = value; } } + } - private string GetDataKey(DataDictionary data, string dataKey, bool isUnknownType = false) { - if (data.ContainsKey(dataKey) || isUnknownType) - dataKey = dataKey.StartsWith("@") ? "_" + dataKey : dataKey; + private string GetDataKey(DataDictionary data, string dataKey, bool isUnknownType = false) { + if (data.ContainsKey(dataKey) || isUnknownType) + dataKey = dataKey.StartsWith("@") ? "_" + dataKey : dataKey; - int count = 1; - string key = dataKey; - while (data.ContainsKey(key) || (isUnknownType && _dataTypeRegistry.ContainsKey(key))) - key = dataKey + count++; + int count = 1; + string key = dataKey; + while (data.ContainsKey(key) || (isUnknownType && _dataTypeRegistry.ContainsKey(key))) + key = dataKey + count++; - return key; - } + return key; + } - public override T Create(Type objectType) { - return new T(); - } + public override T Create(Type objectType) { + return new T(); + } - public override bool CanRead => true; + public override bool CanRead => true; - public override bool CanWrite => false; + public override bool CanWrite => false; - public override bool CanConvert(Type objectType) { - return objectType == _type; - } + public override bool CanConvert(Type objectType) { + return objectType == _type; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Serialization/DynamicTypeContractResolver.cs b/src/Exceptionless.Core/Serialization/DynamicTypeContractResolver.cs index d2251da109..87ef8932f0 100644 --- a/src/Exceptionless.Core/Serialization/DynamicTypeContractResolver.cs +++ b/src/Exceptionless.Core/Serialization/DynamicTypeContractResolver.cs @@ -1,36 +1,33 @@ -using System; -using System.Collections.Generic; using System.Reflection; -using Exceptionless.Core.Extensions; using Foundatio.Repositories.Extensions; using Newtonsoft.Json.Serialization; -namespace Exceptionless.Serializer { - public class DynamicTypeContractResolver : IContractResolver { - private readonly HashSet _assemblies = new HashSet(); - private readonly HashSet _types = new HashSet(); +namespace Exceptionless.Serializer; - private readonly IContractResolver _defaultResolver; - private readonly IContractResolver _resolver; +public class DynamicTypeContractResolver : IContractResolver { + private readonly HashSet _assemblies = new HashSet(); + private readonly HashSet _types = new HashSet(); - public DynamicTypeContractResolver(IContractResolver resolver, IContractResolver defaultResolver = null) { - _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); - _defaultResolver = defaultResolver ?? new DefaultContractResolver(); - } + private readonly IContractResolver _defaultResolver; + private readonly IContractResolver _resolver; - public void UseDefaultResolverFor(params Assembly[] assemblies) { - _assemblies.AddRange(assemblies); - } + public DynamicTypeContractResolver(IContractResolver resolver, IContractResolver defaultResolver = null) { + _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); + _defaultResolver = defaultResolver ?? new DefaultContractResolver(); + } + + public void UseDefaultResolverFor(params Assembly[] assemblies) { + _assemblies.AddRange(assemblies); + } - public void UseDefaultResolverFor(params Type[] types) { - _types.AddRange(types); - } + public void UseDefaultResolverFor(params Type[] types) { + _types.AddRange(types); + } - public JsonContract ResolveContract(Type type) { - if (_types.Contains(type) || _assemblies.Contains(type.Assembly)) - return _defaultResolver.ResolveContract(type); + public JsonContract ResolveContract(Type type) { + if (_types.Contains(type) || _assemblies.Contains(type.Assembly)) + return _defaultResolver.ResolveContract(type); - return _resolver.ResolveContract(type); - } + return _resolver.ResolveContract(type); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Serialization/ElasticConnectionSettingsAwareContractResolver.cs b/src/Exceptionless.Core/Serialization/ElasticConnectionSettingsAwareContractResolver.cs index ce4353a7f3..6038c4a960 100644 --- a/src/Exceptionless.Core/Serialization/ElasticConnectionSettingsAwareContractResolver.cs +++ b/src/Exceptionless.Core/Serialization/ElasticConnectionSettingsAwareContractResolver.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using Exceptionless.Core.Extensions; using Nest; @@ -6,26 +5,26 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Exceptionless.Core.Serialization { - public class ElasticConnectionSettingsAwareContractResolver : ConnectionSettingsAwareContractResolver { - public ElasticConnectionSettingsAwareContractResolver(IConnectionSettingsValues connectionSettings) : base(connectionSettings) { } +namespace Exceptionless.Core.Serialization; - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { - var property = base.CreateProperty(member, memberSerialization); +public class ElasticConnectionSettingsAwareContractResolver : ConnectionSettingsAwareContractResolver { + public ElasticConnectionSettingsAwareContractResolver(IConnectionSettingsValues connectionSettings) : base(connectionSettings) { } - var shouldSerialize = property.ShouldSerialize; - property.ShouldSerialize = obj => (shouldSerialize == null || shouldSerialize(obj)) && !property.IsValueEmptyCollection(obj); - return property; - } + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { + var property = base.CreateProperty(member, memberSerialization); - protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { - var contract = base.CreateDictionaryContract(objectType); - contract.DictionaryKeyResolver = propertyName => propertyName; - return contract; - } + var shouldSerialize = property.ShouldSerialize; + property.ShouldSerialize = obj => (shouldSerialize == null || shouldSerialize(obj)) && !property.IsValueEmptyCollection(obj); + return property; + } + + protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { + var contract = base.CreateDictionaryContract(objectType); + contract.DictionaryKeyResolver = propertyName => propertyName; + return contract; + } - protected override string ResolvePropertyName(string propertyName) { - return propertyName.ToLowerUnderscoredWords(); - } + protected override string ResolvePropertyName(string propertyName) { + return propertyName.ToLowerUnderscoredWords(); } } diff --git a/src/Exceptionless.Core/Serialization/ElasticJsonNetSerializer.cs b/src/Exceptionless.Core/Serialization/ElasticJsonNetSerializer.cs index 5603cb1792..1444b9221f 100644 --- a/src/Exceptionless.Core/Serialization/ElasticJsonNetSerializer.cs +++ b/src/Exceptionless.Core/Serialization/ElasticJsonNetSerializer.cs @@ -1,37 +1,36 @@ -using System.Linq; using Elasticsearch.Net; using Nest; using Nest.JsonNetSerializer; using Newtonsoft.Json; -namespace Exceptionless.Core.Serialization { - public class ElasticJsonNetSerializer : JsonNetSerializer { - public ElasticJsonNetSerializer( - IElasticsearchSerializer builtinSerializer, - IConnectionSettingsValues connectionSettings, - JsonSerializerSettings serializerSettings - ) : base( - builtinSerializer, - connectionSettings, - () => CreateJsonSerializerSettings(serializerSettings), - contractJsonConverters: serializerSettings.Converters.ToList() - ) { - } +namespace Exceptionless.Core.Serialization; - private static JsonSerializerSettings CreateJsonSerializerSettings(JsonSerializerSettings serializerSettings) { - return new JsonSerializerSettings { - DateParseHandling = serializerSettings.DateParseHandling, - DefaultValueHandling = serializerSettings.DefaultValueHandling, - MissingMemberHandling = serializerSettings.MissingMemberHandling, - NullValueHandling = serializerSettings.NullValueHandling - }; - } +public class ElasticJsonNetSerializer : JsonNetSerializer { + public ElasticJsonNetSerializer( + IElasticsearchSerializer builtinSerializer, + IConnectionSettingsValues connectionSettings, + JsonSerializerSettings serializerSettings + ) : base( + builtinSerializer, + connectionSettings, + () => CreateJsonSerializerSettings(serializerSettings), + contractJsonConverters: serializerSettings.Converters.ToList() + ) { + } + + private static JsonSerializerSettings CreateJsonSerializerSettings(JsonSerializerSettings serializerSettings) { + return new JsonSerializerSettings { + DateParseHandling = serializerSettings.DateParseHandling, + DefaultValueHandling = serializerSettings.DefaultValueHandling, + MissingMemberHandling = serializerSettings.MissingMemberHandling, + NullValueHandling = serializerSettings.NullValueHandling + }; + } - protected override ConnectionSettingsAwareContractResolver CreateContractResolver() { - // TODO: Verify we don't need to use the DynamicTypeContractResolver. - var resolver = new ElasticConnectionSettingsAwareContractResolver(ConnectionSettings); - ModifyContractResolver(resolver); - return resolver; - } + protected override ConnectionSettingsAwareContractResolver CreateContractResolver() { + // TODO: Verify we don't need to use the DynamicTypeContractResolver. + var resolver = new ElasticConnectionSettingsAwareContractResolver(ConnectionSettings); + ModifyContractResolver(resolver); + return resolver; } } diff --git a/src/Exceptionless.Core/Serialization/LowerCaseUnderscorePropertyNamesContractResolver.cs b/src/Exceptionless.Core/Serialization/LowerCaseUnderscorePropertyNamesContractResolver.cs index 495dc15a9f..8a18890259 100644 --- a/src/Exceptionless.Core/Serialization/LowerCaseUnderscorePropertyNamesContractResolver.cs +++ b/src/Exceptionless.Core/Serialization/LowerCaseUnderscorePropertyNamesContractResolver.cs @@ -1,27 +1,26 @@ -using System; -using System.Reflection; +using System.Reflection; using Exceptionless.Core.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Exceptionless.Core.Serialization { - public class LowerCaseUnderscorePropertyNamesContractResolver : DefaultContractResolver { - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { - var property = base.CreateProperty(member, memberSerialization); +namespace Exceptionless.Core.Serialization; - var shouldSerialize = property.ShouldSerialize; - property.ShouldSerialize = obj => (shouldSerialize == null || shouldSerialize(obj)) && !property.IsValueEmptyCollection(obj); - return property; - } +public class LowerCaseUnderscorePropertyNamesContractResolver : DefaultContractResolver { + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { + var property = base.CreateProperty(member, memberSerialization); - protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { - var contract = base.CreateDictionaryContract(objectType); - contract.DictionaryKeyResolver = propertyName => propertyName; - return contract; - } + var shouldSerialize = property.ShouldSerialize; + property.ShouldSerialize = obj => (shouldSerialize == null || shouldSerialize(obj)) && !property.IsValueEmptyCollection(obj); + return property; + } + + protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { + var contract = base.CreateDictionaryContract(objectType); + contract.DictionaryKeyResolver = propertyName => propertyName; + return contract; + } - protected override string ResolvePropertyName(string propertyName) { - return propertyName.ToLowerUnderscoredWords(); - } + protected override string ResolvePropertyName(string propertyName) { + return propertyName.ToLowerUnderscoredWords(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Services/EventPostService.cs b/src/Exceptionless.Core/Services/EventPostService.cs index 55a9470613..a61dfd9e1b 100644 --- a/src/Exceptionless.Core/Services/EventPostService.cs +++ b/src/Exceptionless.Core/Services/EventPostService.cs @@ -1,98 +1,97 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Queues.Models; using Foundatio.Queues; using Foundatio.Storage; using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Services { - public class EventPostService { - private readonly IQueue _queue; - private readonly IFileStorage _storage; - private readonly ILogger _logger; +namespace Exceptionless.Core.Services; - public EventPostService(IQueue queue, IFileStorage storage, ILoggerFactory loggerFactory) { - _queue = queue; - _storage = storage; - _logger = loggerFactory.CreateLogger(); - } +public class EventPostService { + private readonly IQueue _queue; + private readonly IFileStorage _storage; + private readonly ILogger _logger; - public async Task EnqueueAsync(EventPost data, Stream stream, CancellationToken cancellationToken = default) { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); + public EventPostService(IQueue queue, IFileStorage storage, ILoggerFactory loggerFactory) { + _queue = queue; + _storage = storage; + _logger = loggerFactory.CreateLogger(); + } - if (data.ShouldArchive) { - data.FilePath = GetArchivePath(SystemClock.UtcNow, data.ProjectId, $"{Guid.NewGuid():N}.json"); - } else { - string fileId = Guid.NewGuid().ToString("N"); - data.FilePath = Path.Combine("q", fileId.Substring(0, 3), $"{fileId}.json"); - } + public async Task EnqueueAsync(EventPost data, Stream stream, CancellationToken cancellationToken = default) { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); - var saveTask = data.ShouldArchive ? _storage.SaveObjectAsync(data.FilePath, (EventPostInfo)data, cancellationToken) : Task.FromResult(true); - var savePayloadTask = _storage.SaveFileAsync(Path.ChangeExtension(data.FilePath, ".payload"), stream, cancellationToken); + if (data.ShouldArchive) { + data.FilePath = GetArchivePath(SystemClock.UtcNow, data.ProjectId, $"{Guid.NewGuid():N}.json"); + } + else { + string fileId = Guid.NewGuid().ToString("N"); + data.FilePath = Path.Combine("q", fileId.Substring(0, 3), $"{fileId}.json"); + } - if (!await saveTask.AnyContext()) { - using (_logger.BeginScope(new ExceptionlessState().Organization(data.OrganizationId).Property(nameof(EventPostInfo), data))) - _logger.LogError("Unable to save event post info"); + var saveTask = data.ShouldArchive ? _storage.SaveObjectAsync(data.FilePath, (EventPostInfo)data, cancellationToken) : Task.FromResult(true); + var savePayloadTask = _storage.SaveFileAsync(Path.ChangeExtension(data.FilePath, ".payload"), stream, cancellationToken); - await savePayloadTask.AnyContext(); - return null; - } + if (!await saveTask.AnyContext()) { + using (_logger.BeginScope(new ExceptionlessState().Organization(data.OrganizationId).Property(nameof(EventPostInfo), data))) + _logger.LogError("Unable to save event post info"); - if (!await savePayloadTask.AnyContext()) { - using (_logger.BeginScope(new ExceptionlessState().Organization(data.OrganizationId).Property(nameof(EventPostInfo), data))) - _logger.LogError("Unable to save event post payload"); + await savePayloadTask.AnyContext(); + return null; + } - return null; - } + if (!await savePayloadTask.AnyContext()) { + using (_logger.BeginScope(new ExceptionlessState().Organization(data.OrganizationId).Property(nameof(EventPostInfo), data))) + _logger.LogError("Unable to save event post payload"); - return await _queue.EnqueueAsync(data).AnyContext(); + return null; } - public async Task GetEventPostPayloadAsync(string path) { - if (String.IsNullOrEmpty(path)) - return null; + return await _queue.EnqueueAsync(data).AnyContext(); + } - byte[] data; - try { - data = await _storage.GetFileContentsRawAsync(path).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error retrieving event post payload: {Path}.", path); - return null; - } + public async Task GetEventPostPayloadAsync(string path) { + if (String.IsNullOrEmpty(path)) + return null; - return data; + byte[] data; + try { + data = await _storage.GetFileContentsRawAsync(path).AnyContext(); } + catch (Exception ex) { + _logger.LogError(ex, "Error retrieving event post payload: {Path}.", path); + return null; + } + + return data; + } + + public async Task CompleteEventPostAsync(string path, string projectId, DateTime created, bool shouldArchive = true) { + if (String.IsNullOrEmpty(path)) + return false; + + // don't move files that are already in the archive + if (path.StartsWith("archive")) + return true; - public async Task CompleteEventPostAsync(string path, string projectId, DateTime created, bool shouldArchive = true) { - if (String.IsNullOrEmpty(path)) - return false; - - // don't move files that are already in the archive - if (path.StartsWith("archive")) - return true; - - try { - if (shouldArchive) { - string archivePath = GetArchivePath(created, projectId, Path.GetFileName(path)); - var renameTask = _storage.RenameFileAsync(path, archivePath); - var renamePayLoadTask = _storage.RenameFileAsync(Path.ChangeExtension(path, ".payload"), Path.ChangeExtension(archivePath, ".payload")); - return await renameTask.AnyContext() && await renamePayLoadTask.AnyContext(); - } - - return await _storage.DeleteFileAsync(Path.ChangeExtension(path, ".payload")).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error archiving event post data {Path}.", path); - return false; + try { + if (shouldArchive) { + string archivePath = GetArchivePath(created, projectId, Path.GetFileName(path)); + var renameTask = _storage.RenameFileAsync(path, archivePath); + var renamePayLoadTask = _storage.RenameFileAsync(Path.ChangeExtension(path, ".payload"), Path.ChangeExtension(archivePath, ".payload")); + return await renameTask.AnyContext() && await renamePayLoadTask.AnyContext(); } - } - private string GetArchivePath(DateTime createdUtc, string projectId, string fileName) { - return Path.Combine("archive", createdUtc.ToString("yy"), createdUtc.ToString("MM"), createdUtc.ToString("dd"), createdUtc.ToString("HH"), createdUtc.ToString("mm"), projectId, fileName); + return await _storage.DeleteFileAsync(Path.ChangeExtension(path, ".payload")).AnyContext(); + } + catch (Exception ex) { + _logger.LogError(ex, "Error archiving event post data {Path}.", path); + return false; } } -} \ No newline at end of file + + private string GetArchivePath(DateTime createdUtc, string projectId, string fileName) { + return Path.Combine("archive", createdUtc.ToString("yy"), createdUtc.ToString("MM"), createdUtc.ToString("dd"), createdUtc.ToString("HH"), createdUtc.ToString("mm"), projectId, fileName); + } +} diff --git a/src/Exceptionless.Core/Services/MessageService.cs b/src/Exceptionless.Core/Services/MessageService.cs index d374b36e7f..da12bcc2d1 100644 --- a/src/Exceptionless.Core/Services/MessageService.cs +++ b/src/Exceptionless.Core/Services/MessageService.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Messaging.Models; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; @@ -11,59 +8,59 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Exceptionless.Core.Services { - public class MessageService : IDisposable, IStartupAction { - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly IConnectionMapping _connectionMapping; - private readonly AppOptions _options; - private readonly ILogger _logger; +namespace Exceptionless.Core.Services; - public MessageService(IStackRepository stackRepository, IEventRepository eventRepository, IConnectionMapping connectionMapping, AppOptions options, ILoggerFactory loggerFactory) { - _stackRepository = stackRepository; - _eventRepository = eventRepository; - _connectionMapping = connectionMapping; - _options = options; - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - } +public class MessageService : IDisposable, IStartupAction { + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly IConnectionMapping _connectionMapping; + private readonly AppOptions _options; + private readonly ILogger _logger; - public Task RunAsync(CancellationToken shutdownToken = default) { - if (!_options.EnableRepositoryNotifications) - return Task.CompletedTask; - - if (_stackRepository is StackRepository sr) - sr.BeforePublishEntityChanged.AddHandler(BeforePublishStackEntityChanged); - if (_eventRepository is EventRepository er) - er.BeforePublishEntityChanged.AddHandler(BeforePublishEventEntityChanged); + public MessageService(IStackRepository stackRepository, IEventRepository eventRepository, IConnectionMapping connectionMapping, AppOptions options, ILoggerFactory loggerFactory) { + _stackRepository = stackRepository; + _eventRepository = eventRepository; + _connectionMapping = connectionMapping; + _options = options; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + } + public Task RunAsync(CancellationToken shutdownToken = default) { + if (!_options.EnableRepositoryNotifications) return Task.CompletedTask; - } - private async Task BeforePublishStackEntityChanged(object sender, BeforePublishEntityChangedEventArgs args) { - args.Cancel = await GetNumberOfListeners(args.Message).AnyContext() == 0; - if (args.Cancel) - _logger.LogTrace("Cancelled Stack Entity Changed Message: {@Message}", args.Message); - } + if (_stackRepository is StackRepository sr) + sr.BeforePublishEntityChanged.AddHandler(BeforePublishStackEntityChanged); + if (_eventRepository is EventRepository er) + er.BeforePublishEntityChanged.AddHandler(BeforePublishEventEntityChanged); + + return Task.CompletedTask; + } + + private async Task BeforePublishStackEntityChanged(object sender, BeforePublishEntityChangedEventArgs args) { + args.Cancel = await GetNumberOfListeners(args.Message).AnyContext() == 0; + if (args.Cancel) + _logger.LogTrace("Cancelled Stack Entity Changed Message: {@Message}", args.Message); + } - private async Task BeforePublishEventEntityChanged(object sender, BeforePublishEntityChangedEventArgs args) { - args.Cancel = await GetNumberOfListeners(args.Message).AnyContext() == 0; - if (args.Cancel) - _logger.LogTrace("Cancelled Persistent Event Entity Changed Message: {@Message}", args.Message); - } + private async Task BeforePublishEventEntityChanged(object sender, BeforePublishEntityChangedEventArgs args) { + args.Cancel = await GetNumberOfListeners(args.Message).AnyContext() == 0; + if (args.Cancel) + _logger.LogTrace("Cancelled Persistent Event Entity Changed Message: {@Message}", args.Message); + } - private Task GetNumberOfListeners(EntityChanged message) { - var entityChanged = ExtendedEntityChanged.Create(message, false); - if (String.IsNullOrEmpty(entityChanged.OrganizationId)) - return Task.FromResult(1); // Return 1 as we have no idea if people are listening. + private Task GetNumberOfListeners(EntityChanged message) { + var entityChanged = ExtendedEntityChanged.Create(message, false); + if (String.IsNullOrEmpty(entityChanged.OrganizationId)) + return Task.FromResult(1); // Return 1 as we have no idea if people are listening. - return _connectionMapping.GetGroupConnectionCountAsync(entityChanged.OrganizationId); - } + return _connectionMapping.GetGroupConnectionCountAsync(entityChanged.OrganizationId); + } - public void Dispose() { - if (_stackRepository is StackRepository sr) - sr.BeforePublishEntityChanged.RemoveHandler(BeforePublishStackEntityChanged); - if (_eventRepository is EventRepository er) - er.BeforePublishEntityChanged.RemoveHandler(BeforePublishEventEntityChanged); - } + public void Dispose() { + if (_stackRepository is StackRepository sr) + sr.BeforePublishEntityChanged.RemoveHandler(BeforePublishStackEntityChanged); + if (_eventRepository is EventRepository er) + er.BeforePublishEntityChanged.RemoveHandler(BeforePublishEventEntityChanged); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Services/OrganizationService.cs b/src/Exceptionless.Core/Services/OrganizationService.cs index d0c3dc7b05..dda0210c0d 100644 --- a/src/Exceptionless.Core/Services/OrganizationService.cs +++ b/src/Exceptionless.Core/Services/OrganizationService.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; @@ -9,81 +6,82 @@ using Microsoft.Extensions.Logging; using Stripe; -namespace Exceptionless.Core.Services { - public class OrganizationService { - private readonly IOrganizationRepository _organizationRepository; - private readonly ITokenRepository _tokenRepository; - private readonly IUserRepository _userRepository; - private readonly IWebHookRepository _webHookRepository; - private readonly StripeOptions _stripeOptions; - private readonly ILogger _logger; +namespace Exceptionless.Core.Services; - public OrganizationService(IOrganizationRepository organizationRepository, ITokenRepository tokenRepository, IUserRepository userRepository, IWebHookRepository webHookRepository, StripeOptions stripeOptions, ILoggerFactory loggerFactory = null) { - _organizationRepository = organizationRepository; - _tokenRepository = tokenRepository; - _userRepository = userRepository; - _webHookRepository = webHookRepository; - _stripeOptions = stripeOptions; - _logger = loggerFactory.CreateLogger(); +public class OrganizationService { + private readonly IOrganizationRepository _organizationRepository; + private readonly ITokenRepository _tokenRepository; + private readonly IUserRepository _userRepository; + private readonly IWebHookRepository _webHookRepository; + private readonly StripeOptions _stripeOptions; + private readonly ILogger _logger; + + public OrganizationService(IOrganizationRepository organizationRepository, ITokenRepository tokenRepository, IUserRepository userRepository, IWebHookRepository webHookRepository, StripeOptions stripeOptions, ILoggerFactory loggerFactory = null) { + _organizationRepository = organizationRepository; + _tokenRepository = tokenRepository; + _userRepository = userRepository; + _webHookRepository = webHookRepository; + _stripeOptions = stripeOptions; + _logger = loggerFactory.CreateLogger(); + } + + public async Task CancelSubscriptionsAsync(Organization organization) { + if (String.IsNullOrEmpty(organization.StripeCustomerId)) + return; + + var client = new StripeClient(_stripeOptions.StripeApiKey); + var subscriptionService = new SubscriptionService(client); + var subscriptions = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId }).AnyContext(); + foreach (var subscription in subscriptions.Where(s => !s.CanceledAt.HasValue)) { + _logger.LogInformation("Canceling stripe subscription ({SubscriptionId}) for {OrganizationName} ({organization})", subscription.Id, organization.Name, organization.Id); + await subscriptionService.CancelAsync(subscription.Id, new SubscriptionCancelOptions()).AnyContext(); + _logger.LogInformation("Canceled stripe subscription ({SubscriptionId}) for {OrganizationName} ({organization})", subscription.Id, organization.Name, organization.Id); } + } - public async Task CancelSubscriptionsAsync(Organization organization) { - if (String.IsNullOrEmpty(organization.StripeCustomerId)) - return; - - var client = new StripeClient(_stripeOptions.StripeApiKey); - var subscriptionService = new SubscriptionService(client); - var subscriptions = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId }).AnyContext(); - foreach (var subscription in subscriptions.Where(s => !s.CanceledAt.HasValue)) { - _logger.LogInformation("Canceling stripe subscription ({SubscriptionId}) for {OrganizationName} ({organization})", subscription.Id, organization.Name, organization.Id); - await subscriptionService.CancelAsync(subscription.Id, new SubscriptionCancelOptions()).AnyContext(); - _logger.LogInformation("Canceled stripe subscription ({SubscriptionId}) for {OrganizationName} ({organization})", subscription.Id, organization.Name, organization.Id); + public async Task RemoveUsersAsync(Organization organization, string currentUserId) { + var users = await _userRepository.GetByOrganizationIdAsync(organization.Id, o => o.PageLimit(1000)).AnyContext(); + foreach (var user in users.Documents) { + // delete the user if they are not associated to any other organizations and they are not the current user + if (user.OrganizationIds.All(oid => String.Equals(oid, organization.Id)) && !String.Equals(user.Id, currentUserId)) { + _logger.LogInformation("Removing user {User} as they do not belong to any other organizations.", user.Id); + await _userRepository.RemoveAsync(user.Id).AnyContext(); } - } + else { + _logger.LogInformation("Removing user {User} from organization: {OrganizationName} ({OrganizationId})", user.Id, organization.Name, organization.Id); + user.OrganizationIds.Remove(organization.Id); - public async Task RemoveUsersAsync(Organization organization, string currentUserId) { - var users = await _userRepository.GetByOrganizationIdAsync(organization.Id, o => o.PageLimit(1000)).AnyContext(); - foreach (var user in users.Documents) { - // delete the user if they are not associated to any other organizations and they are not the current user - if (user.OrganizationIds.All(oid => String.Equals(oid, organization.Id)) && !String.Equals(user.Id, currentUserId)) { - _logger.LogInformation("Removing user {User} as they do not belong to any other organizations.", user.Id); - await _userRepository.RemoveAsync(user.Id).AnyContext(); - } else { - _logger.LogInformation("Removing user {User} from organization: {OrganizationName} ({OrganizationId})", user.Id, organization.Name, organization.Id); - user.OrganizationIds.Remove(organization.Id); - - // Temp fix for old user records :\. - if (!user.IsEmailAddressVerified && String.IsNullOrEmpty(user.VerifyEmailAddressToken)) - user.CreateVerifyEmailAddressToken(); + // Temp fix for old user records :\. + if (!user.IsEmailAddressVerified && String.IsNullOrEmpty(user.VerifyEmailAddressToken)) + user.CreateVerifyEmailAddressToken(); - await _userRepository.SaveAsync(user, o => o.Cache()).AnyContext(); - } + await _userRepository.SaveAsync(user, o => o.Cache()).AnyContext(); } - - return users.Documents.Count; } - public Task RemoveTokensAsync(Organization organization) { - _logger.LogInformation("Removing tokens for {OrganizationName} ({OrganizationId})", organization.Name, organization.Id); - return _tokenRepository.RemoveAllByOrganizationIdAsync(organization.Id); - } - - public Task RemoveWebHooksAsync(Organization organization) { - _logger.LogInformation("Removing web hooks for {OrganizationName} ({OrganizationId})", organization.Name, organization.Id); - return _webHookRepository.RemoveAllByOrganizationIdAsync(organization.Id); - } - - public async Task SoftDeleteOrganizationAsync(Organization organization, string currentUserId) { - if (organization.IsDeleted) - return; - - await RemoveTokensAsync(organization).AnyContext(); - await RemoveWebHooksAsync(organization).AnyContext(); - await CancelSubscriptionsAsync(organization).AnyContext(); - await RemoveUsersAsync(organization, currentUserId).AnyContext(); + return users.Documents.Count; + } - organization.IsDeleted = true; - await _organizationRepository.SaveAsync(organization).AnyContext(); - } + public Task RemoveTokensAsync(Organization organization) { + _logger.LogInformation("Removing tokens for {OrganizationName} ({OrganizationId})", organization.Name, organization.Id); + return _tokenRepository.RemoveAllByOrganizationIdAsync(organization.Id); + } + + public Task RemoveWebHooksAsync(Organization organization) { + _logger.LogInformation("Removing web hooks for {OrganizationName} ({OrganizationId})", organization.Name, organization.Id); + return _webHookRepository.RemoveAllByOrganizationIdAsync(organization.Id); + } + + public async Task SoftDeleteOrganizationAsync(Organization organization, string currentUserId) { + if (organization.IsDeleted) + return; + + await RemoveTokensAsync(organization).AnyContext(); + await RemoveWebHooksAsync(organization).AnyContext(); + await CancelSubscriptionsAsync(organization).AnyContext(); + await RemoveUsersAsync(organization, currentUserId).AnyContext(); + + organization.IsDeleted = true; + await _organizationRepository.SaveAsync(organization).AnyContext(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Services/SlackService.cs b/src/Exceptionless.Core/Services/SlackService.cs index ff711369bc..ba822469bf 100644 --- a/src/Exceptionless.Core/Services/SlackService.cs +++ b/src/Exceptionless.Core/Services/SlackService.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.Formatting; using Exceptionless.Core.Queues.Models; @@ -11,144 +7,144 @@ using Microsoft.Extensions.Logging; // ReSharper disable InconsistentNaming -namespace Exceptionless.Core.Services { - public class SlackService { - private readonly HttpClient _client = new HttpClient(); - private readonly IQueue _webHookNotificationQueue; - private readonly FormattingPluginManager _pluginManager; - private readonly ISerializer _serializer; - private readonly AppOptions _appOptions; - private readonly ILogger _logger; - - public SlackService(IQueue webHookNotificationQueue, FormattingPluginManager pluginManager, ITextSerializer serializer, AppOptions appOptions, ILoggerFactory loggerFactory = null) { - _webHookNotificationQueue = webHookNotificationQueue; - _pluginManager = pluginManager; - _serializer = serializer; - _appOptions = appOptions; - _logger = loggerFactory.CreateLogger(); - } +namespace Exceptionless.Core.Services; + +public class SlackService { + private readonly HttpClient _client = new HttpClient(); + private readonly IQueue _webHookNotificationQueue; + private readonly FormattingPluginManager _pluginManager; + private readonly ISerializer _serializer; + private readonly AppOptions _appOptions; + private readonly ILogger _logger; + + public SlackService(IQueue webHookNotificationQueue, FormattingPluginManager pluginManager, ITextSerializer serializer, AppOptions appOptions, ILoggerFactory loggerFactory = null) { + _webHookNotificationQueue = webHookNotificationQueue; + _pluginManager = pluginManager; + _serializer = serializer; + _appOptions = appOptions; + _logger = loggerFactory.CreateLogger(); + } - public async Task GetAccessTokenAsync(string code) { - if (String.IsNullOrEmpty(code)) - throw new ArgumentNullException(nameof(code)); + public async Task GetAccessTokenAsync(string code) { + if (String.IsNullOrEmpty(code)) + throw new ArgumentNullException(nameof(code)); - var data = new Dictionary { + var data = new Dictionary { { "client_id", _appOptions.SlackOptions.SlackId }, { "client_secret", _appOptions.SlackOptions.SlackSecret }, { "code", code }, { "redirect_uri", new Uri(_appOptions.BaseURL).GetLeftPart(UriPartial.Authority) } }; - string url = $"https://slack.com/api/oauth.access?{data.ToQueryString()}"; - var response = await _client.PostAsync(url).AnyContext(); - byte[] body = await response.Content.ReadAsByteArrayAsync().AnyContext(); - var result = _serializer.Deserialize(body); - - if (!result.ok) { - _logger.LogWarning("Error getting access token: {Message}, Response: {Response}", result.error ?? result.warning, result); - return null; - } - - var token = new SlackToken { - AccessToken = result.access_token, - Scopes = result.scope?.Split(new [] {"," }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0], - UserId = result.user_id, - TeamId = result.team_id, - TeamName = result.team_name - }; + string url = $"https://slack.com/api/oauth.access?{data.ToQueryString()}"; + var response = await _client.PostAsync(url).AnyContext(); + byte[] body = await response.Content.ReadAsByteArrayAsync().AnyContext(); + var result = _serializer.Deserialize(body); - if (result.incoming_webhook != null) { - token.IncomingWebhook = new SlackToken.IncomingWebHook { - Channel = result.incoming_webhook.channel, - ChannelId = result.incoming_webhook.channel_id, - ConfigurationUrl = result.incoming_webhook.configuration_url, - Url = result.incoming_webhook.url - }; - } + if (!result.ok) { + _logger.LogWarning("Error getting access token: {Message}, Response: {Response}", result.error ?? result.warning, result); + return null; + } - return token; + var token = new SlackToken { + AccessToken = result.access_token, + Scopes = result.scope?.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0], + UserId = result.user_id, + TeamId = result.team_id, + TeamName = result.team_name + }; + + if (result.incoming_webhook != null) { + token.IncomingWebhook = new SlackToken.IncomingWebHook { + Channel = result.incoming_webhook.channel, + ChannelId = result.incoming_webhook.channel_id, + ConfigurationUrl = result.incoming_webhook.configuration_url, + Url = result.incoming_webhook.url + }; } - public async Task RevokeAccessTokenAsync(string token) { - if (String.IsNullOrEmpty(token)) - throw new ArgumentNullException(nameof(token)); + return token; + } - string url = $"https://slack.com/api/auth.revoke?token={token}"; - var response = await _client.PostAsync(url).AnyContext(); - byte[] body = await response.Content.ReadAsByteArrayAsync().AnyContext(); - var result = _serializer.Deserialize(body); + public async Task RevokeAccessTokenAsync(string token) { + if (String.IsNullOrEmpty(token)) + throw new ArgumentNullException(nameof(token)); - if (result.ok && result.revoked || String.Equals(result.error, "invalid_auth")) - return true; + string url = $"https://slack.com/api/auth.revoke?token={token}"; + var response = await _client.PostAsync(url).AnyContext(); + byte[] body = await response.Content.ReadAsByteArrayAsync().AnyContext(); + var result = _serializer.Deserialize(body); - _logger.LogWarning("Error revoking token: {Message}, Response: {Response}", result.error ?? result.warning, result); - return false; - } + if (result.ok && result.revoked || String.Equals(result.error, "invalid_auth")) + return true; - public Task SendMessageAsync(string organizationId, string projectId, string url, SlackMessage message) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); + _logger.LogWarning("Error revoking token: {Message}, Response: {Response}", result.error ?? result.warning, result); + return false; + } - if (String.IsNullOrEmpty(projectId)) - throw new ArgumentNullException(nameof(projectId)); + public Task SendMessageAsync(string organizationId, string projectId, string url, SlackMessage message) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); - if (String.IsNullOrEmpty(url)) - throw new ArgumentNullException(nameof(url)); + if (String.IsNullOrEmpty(projectId)) + throw new ArgumentNullException(nameof(projectId)); - if (message == null) - throw new ArgumentNullException(nameof(message)); + if (String.IsNullOrEmpty(url)) + throw new ArgumentNullException(nameof(url)); - var notification = new WebHookNotification { - OrganizationId = organizationId, - ProjectId = projectId, - Url = url, - Type = WebHookType.Slack, - Data = message - }; + if (message == null) + throw new ArgumentNullException(nameof(message)); - return _webHookNotificationQueue.EnqueueAsync(notification); - } + var notification = new WebHookNotification { + OrganizationId = organizationId, + ProjectId = projectId, + Url = url, + Type = WebHookType.Slack, + Data = message + }; - public async Task SendEventNoticeAsync(PersistentEvent ev, Project project, bool isNew, bool isRegression) { - var token = project.GetSlackToken(); - if (token?.IncomingWebhook?.Url == null) - return false; + return _webHookNotificationQueue.EnqueueAsync(notification); + } - bool isCritical = ev.IsCritical(); - var message = _pluginManager.GetSlackEventNotificationMessage(ev, project, isCritical, isNew, isRegression); - if (message == null) { - _logger.LogWarning("Unable to create event notification slack message for event {id}.", ev.Id); - return false; - } + public async Task SendEventNoticeAsync(PersistentEvent ev, Project project, bool isNew, bool isRegression) { + var token = project.GetSlackToken(); + if (token?.IncomingWebhook?.Url == null) + return false; - await SendMessageAsync(ev.OrganizationId, ev.ProjectId, token.IncomingWebhook.Url, message); - return true; + bool isCritical = ev.IsCritical(); + var message = _pluginManager.GetSlackEventNotificationMessage(ev, project, isCritical, isNew, isRegression); + if (message == null) { + _logger.LogWarning("Unable to create event notification slack message for event {id}.", ev.Id); + return false; } - private class Response { - public bool ok { get; set; } - public string warning { get; set; } - public string error { get; set; } - } + await SendMessageAsync(ev.OrganizationId, ev.ProjectId, token.IncomingWebhook.Url, message); + return true; + } - private class AuthRevokeResponse : Response { - public bool revoked { get; set; } - } + private class Response { + public bool ok { get; set; } + public string warning { get; set; } + public string error { get; set; } + } + + private class AuthRevokeResponse : Response { + public bool revoked { get; set; } + } - private class OAuthAccessResponse : Response { - public string access_token { get; set; } - public string scope { get; set; } - public string user_id { get; set; } - public string team_id { get; set; } - public string team_name { get; set; } - public IncomingWebHook incoming_webhook { get; set; } - - public class IncomingWebHook { - public string channel { get; set; } - public string channel_id { get; set; } - public string configuration_url { get; set; } - public string url { get; set; } - } + private class OAuthAccessResponse : Response { + public string access_token { get; set; } + public string scope { get; set; } + public string user_id { get; set; } + public string team_id { get; set; } + public string team_name { get; set; } + public IncomingWebHook incoming_webhook { get; set; } + + public class IncomingWebHook { + public string channel { get; set; } + public string channel_id { get; set; } + public string configuration_url { get; set; } + public string url { get; set; } } } } diff --git a/src/Exceptionless.Core/Services/StackService.cs b/src/Exceptionless.Core/Services/StackService.cs index 61c54672b8..68db071afc 100644 --- a/src/Exceptionless.Core/Services/StackService.cs +++ b/src/Exceptionless.Core/Services/StackService.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories; using Foundatio.Caching; @@ -8,106 +5,108 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Exceptionless.Core.Services { - public class StackService { - private readonly ILogger _logger; - private readonly IStackRepository _stackRepository; - private readonly ICacheClient _cache; - private readonly TimeSpan _expireTimeout = TimeSpan.FromHours(12); - - public StackService(IStackRepository stackRepository, ICacheClient cache, ILoggerFactory loggerFactory = null) { - _stackRepository = stackRepository; - _cache = cache; - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - } +namespace Exceptionless.Core.Services; + +public class StackService { + private readonly ILogger _logger; + private readonly IStackRepository _stackRepository; + private readonly ICacheClient _cache; + private readonly TimeSpan _expireTimeout = TimeSpan.FromHours(12); + + public StackService(IStackRepository stackRepository, ICacheClient cache, ILoggerFactory loggerFactory = null) { + _stackRepository = stackRepository; + _cache = cache; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + } + + public async Task IncrementStackUsageAsync(string organizationId, string projectId, string stackId, DateTime minOccurrenceDateUtc, DateTime maxOccurrenceDateUtc, int count) { + if (String.IsNullOrEmpty(organizationId)) + throw new ArgumentNullException(nameof(organizationId)); + if (String.IsNullOrEmpty(projectId)) + throw new ArgumentNullException(nameof(projectId)); + if (String.IsNullOrEmpty(stackId)) + throw new ArgumentNullException(nameof(stackId)); + if (count <= 0) + return; + + await Task.WhenAll( + _cache.ListAddAsync(GetStackOccurrenceSetCacheKey(), (organizationId, projectId, stackId)), + _cache.IncrementAsync(GetStackOccurrenceCountCacheKey(stackId), count, _expireTimeout), + _cache.SetIfLowerAsync(GetStackOccurrenceMinDateCacheKey(stackId), minOccurrenceDateUtc, _expireTimeout), + _cache.SetIfHigherAsync(GetStackOccurrenceMaxDateCacheKey(stackId), maxOccurrenceDateUtc, _expireTimeout) + ).AnyContext(); + } - public async Task IncrementStackUsageAsync(string organizationId, string projectId, string stackId, DateTime minOccurrenceDateUtc, DateTime maxOccurrenceDateUtc, int count) { - if (String.IsNullOrEmpty(organizationId)) - throw new ArgumentNullException(nameof(organizationId)); - if (String.IsNullOrEmpty(projectId)) - throw new ArgumentNullException(nameof(projectId)); - if (String.IsNullOrEmpty(stackId)) - throw new ArgumentNullException(nameof(stackId)); - if (count <= 0) - return; + public async Task SaveStackUsagesAsync(bool sendNotifications = true, CancellationToken cancellationToken = default) { + string occurrenceSetCacheKey = GetStackOccurrenceSetCacheKey(); + var stackUsageSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(occurrenceSetCacheKey).AnyContext(); + if (!stackUsageSet.HasValue) + return; + + foreach (var (organizationId, projectId, stackId) in stackUsageSet.Value) { + if (cancellationToken.IsCancellationRequested) + break; + + var removeFromSetTask = _cache.ListRemoveAsync(occurrenceSetCacheKey, (organizationId, projectId, stackId)); + string countCacheKey = GetStackOccurrenceCountCacheKey(stackId); + var countTask = _cache.GetAsync(countCacheKey, 0); + string minDateCacheKey = GetStackOccurrenceMinDateCacheKey(stackId); + var minDateTask = _cache.GetUnixTimeMillisecondsAsync(minDateCacheKey, SystemClock.UtcNow); + string maxDateCacheKey = GetStackOccurrenceMaxDateCacheKey(stackId); + var maxDateTask = _cache.GetUnixTimeMillisecondsAsync(maxDateCacheKey, SystemClock.UtcNow); await Task.WhenAll( - _cache.ListAddAsync(GetStackOccurrenceSetCacheKey(), (organizationId, projectId, stackId)), - _cache.IncrementAsync(GetStackOccurrenceCountCacheKey(stackId), count, _expireTimeout), - _cache.SetIfLowerAsync(GetStackOccurrenceMinDateCacheKey(stackId), minOccurrenceDateUtc, _expireTimeout), - _cache.SetIfHigherAsync(GetStackOccurrenceMaxDateCacheKey(stackId), maxOccurrenceDateUtc, _expireTimeout) + removeFromSetTask, + countTask, + minDateTask, + maxDateTask ).AnyContext(); - } - public async Task SaveStackUsagesAsync(bool sendNotifications = true, CancellationToken cancellationToken = default) { - string occurrenceSetCacheKey = GetStackOccurrenceSetCacheKey(); - var stackUsageSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(occurrenceSetCacheKey).AnyContext(); - if (!stackUsageSet.HasValue) - return; - - foreach (var (organizationId, projectId, stackId) in stackUsageSet.Value) { - if (cancellationToken.IsCancellationRequested) - break; - - var removeFromSetTask = _cache.ListRemoveAsync(occurrenceSetCacheKey, (organizationId, projectId, stackId)); - string countCacheKey = GetStackOccurrenceCountCacheKey(stackId); - var countTask = _cache.GetAsync(countCacheKey, 0); - string minDateCacheKey = GetStackOccurrenceMinDateCacheKey(stackId); - var minDateTask = _cache.GetUnixTimeMillisecondsAsync(minDateCacheKey, SystemClock.UtcNow); - string maxDateCacheKey = GetStackOccurrenceMaxDateCacheKey(stackId); - var maxDateTask = _cache.GetUnixTimeMillisecondsAsync(maxDateCacheKey, SystemClock.UtcNow); - - await Task.WhenAll( - removeFromSetTask, - countTask, - minDateTask, - maxDateTask - ).AnyContext(); - - int occurrenceCount = (int)countTask.Result; - if (occurrenceCount <= 0) { - await _cache.RemoveAllAsync(new[] { minDateCacheKey, maxDateCacheKey }).AnyContext(); - continue; - } + int occurrenceCount = (int)countTask.Result; + if (occurrenceCount <= 0) { + await _cache.RemoveAllAsync(new[] { minDateCacheKey, maxDateCacheKey }).AnyContext(); + continue; + } + + await Task.WhenAll( + _cache.RemoveAllAsync(new[] { minDateCacheKey, maxDateCacheKey }), + _cache.DecrementAsync(countCacheKey, occurrenceCount, _expireTimeout) + ).AnyContext(); - await Task.WhenAll( - _cache.RemoveAllAsync(new []{ minDateCacheKey, maxDateCacheKey }), - _cache.DecrementAsync(countCacheKey, occurrenceCount, _expireTimeout) - ).AnyContext(); - - var occurrenceMinDate = minDateTask.Result; - var occurrenceMaxDate = maxDateTask.Result; - bool shouldRetry = false; - try { - if (!await _stackRepository.IncrementEventCounterAsync(organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate, occurrenceCount, sendNotifications).AnyContext()) { - shouldRetry = true; - await IncrementStackUsageAsync(organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate, occurrenceCount).AnyContext(); - } else { - _logger.LogTrace("Increment event count {OccurrenceCount} for organization:{OrganizationId} project:{ProjectId} stack:{StackId} with Min Date:{OccurrenceMinDate} Max Date:{OccurrenceMaxDate}", occurrenceCount, organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate); - } - } catch(Exception ex) { - _logger.LogError(ex, "Error incrementing event count for organization: {OrganizationId} project:{ProjectId} stack:{StackId}", organizationId, projectId, stackId); - if (!shouldRetry) { - await IncrementStackUsageAsync(organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate, occurrenceCount).AnyContext(); - } + var occurrenceMinDate = minDateTask.Result; + var occurrenceMaxDate = maxDateTask.Result; + bool shouldRetry = false; + try { + if (!await _stackRepository.IncrementEventCounterAsync(organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate, occurrenceCount, sendNotifications).AnyContext()) { + shouldRetry = true; + await IncrementStackUsageAsync(organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate, occurrenceCount).AnyContext(); + } + else { + _logger.LogTrace("Increment event count {OccurrenceCount} for organization:{OrganizationId} project:{ProjectId} stack:{StackId} with Min Date:{OccurrenceMinDate} Max Date:{OccurrenceMaxDate}", occurrenceCount, organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate); + } + } + catch (Exception ex) { + _logger.LogError(ex, "Error incrementing event count for organization: {OrganizationId} project:{ProjectId} stack:{StackId}", organizationId, projectId, stackId); + if (!shouldRetry) { + await IncrementStackUsageAsync(organizationId, projectId, stackId, occurrenceMinDate, occurrenceMaxDate, occurrenceCount).AnyContext(); } } } + } - internal string GetStackOccurrenceSetCacheKey() { - return "usage:occurrences"; - } + internal string GetStackOccurrenceSetCacheKey() { + return "usage:occurrences"; + } - internal string GetStackOccurrenceCountCacheKey(string stackId) { - return String.Concat("usage:occurrences:count:", stackId); - } + internal string GetStackOccurrenceCountCacheKey(string stackId) { + return String.Concat("usage:occurrences:count:", stackId); + } - internal string GetStackOccurrenceMinDateCacheKey( string stackId) { - return String.Concat("usage:occurrences:min:", stackId); - } + internal string GetStackOccurrenceMinDateCacheKey(string stackId) { + return String.Concat("usage:occurrences:min:", stackId); + } - internal string GetStackOccurrenceMaxDateCacheKey(string stackId) { - return String.Concat("usage:occurrences:max:", stackId); - } + internal string GetStackOccurrenceMaxDateCacheKey(string stackId) { + return String.Concat("usage:occurrences:max:", stackId); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Services/UsageService.cs b/src/Exceptionless.Core/Services/UsageService.cs index e24760828d..8d214f9784 100644 --- a/src/Exceptionless.Core/Services/UsageService.cs +++ b/src/Exceptionless.Core/Services/UsageService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Threading.Tasks; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Messaging.Models; @@ -15,237 +12,239 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Services { - public sealed class UsageService { - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly ICacheClient _cache; - private readonly IMessagePublisher _messagePublisher; - private readonly BillingPlans _plans; - private readonly ILogger _logger; - - public UsageService(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, ICacheClient cache, IMessagePublisher messagePublisher, BillingPlans plans, ILoggerFactory loggerFactory = null) { - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _cache = cache; - _messagePublisher = messagePublisher; - _plans = plans; - _logger = loggerFactory.CreateLogger(); - } +namespace Exceptionless.Core.Services; + +public sealed class UsageService { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly ICacheClient _cache; + private readonly IMessagePublisher _messagePublisher; + private readonly BillingPlans _plans; + private readonly ILogger _logger; + + public UsageService(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, ICacheClient cache, IMessagePublisher messagePublisher, BillingPlans plans, ILoggerFactory loggerFactory = null) { + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _cache = cache; + _messagePublisher = messagePublisher; + _plans = plans; + _logger = loggerFactory.CreateLogger(); + } - public async Task IncrementUsageAsync(Organization organization, Project project, bool tooBig, int count = 1, bool applyHourlyLimit = true) { - if (organization == null || organization.MaxEventsPerMonth < 0 || project == null || count == 0) - return false; + public async Task IncrementUsageAsync(Organization organization, Project project, bool tooBig, int count = 1, bool applyHourlyLimit = true) { + if (organization == null || organization.MaxEventsPerMonth < 0 || project == null || count == 0) + return false; - var orgUsage = await GetUsageAsync(organization, tooBig, count).AnyContext(); - double totalBlocked = GetTotalBlocked(organization, count, orgUsage, applyHourlyLimit); - bool overLimit = totalBlocked > 0; - if (overLimit) { - orgUsage.HourlyBlocked = await _cache.IncrementAsync(GetHourlyBlockedCacheKey(organization.Id), (int)totalBlocked, TimeSpan.FromMinutes(61), (uint)orgUsage.HourlyBlocked).AnyContext(); - orgUsage.MonthlyBlocked = await _cache.IncrementAsync(GetMonthlyBlockedCacheKey(organization.Id), (int)totalBlocked, TimeSpan.FromDays(32), (uint)orgUsage.MonthlyBlocked).AnyContext(); - } + var orgUsage = await GetUsageAsync(organization, tooBig, count).AnyContext(); + double totalBlocked = GetTotalBlocked(organization, count, orgUsage, applyHourlyLimit); + bool overLimit = totalBlocked > 0; + if (overLimit) { + orgUsage.HourlyBlocked = await _cache.IncrementAsync(GetHourlyBlockedCacheKey(organization.Id), (int)totalBlocked, TimeSpan.FromMinutes(61), (uint)orgUsage.HourlyBlocked).AnyContext(); + orgUsage.MonthlyBlocked = await _cache.IncrementAsync(GetMonthlyBlockedCacheKey(organization.Id), (int)totalBlocked, TimeSpan.FromDays(32), (uint)orgUsage.MonthlyBlocked).AnyContext(); + } - bool justWentOverHourly = orgUsage.HourlyTotal > organization.GetHourlyEventLimit(_plans) && orgUsage.HourlyTotal <= organization.GetHourlyEventLimit(_plans) + count; - bool justWentOverMonthly = orgUsage.MonthlyTotal > organization.GetMaxEventsPerMonthWithBonus() && orgUsage.MonthlyTotal <= organization.GetMaxEventsPerMonthWithBonus() + count; - var projectUsage = await GetUsageAsync(organization, project, tooBig, count, overLimit, (int)totalBlocked).AnyContext(); + bool justWentOverHourly = orgUsage.HourlyTotal > organization.GetHourlyEventLimit(_plans) && orgUsage.HourlyTotal <= organization.GetHourlyEventLimit(_plans) + count; + bool justWentOverMonthly = orgUsage.MonthlyTotal > organization.GetMaxEventsPerMonthWithBonus() && orgUsage.MonthlyTotal <= organization.GetMaxEventsPerMonthWithBonus() + count; + var projectUsage = await GetUsageAsync(organization, project, tooBig, count, overLimit, (int)totalBlocked).AnyContext(); - var tasks = new List(3) { + var tasks = new List(3) { SaveUsageAsync(organization, justWentOverHourly, justWentOverMonthly, orgUsage), SaveUsageAsync(organization, project, justWentOverHourly, justWentOverMonthly, projectUsage) }; - if (justWentOverMonthly) - tasks.Add(_messagePublisher.PublishAsync(new PlanOverage { OrganizationId = organization.Id })); - else if (justWentOverHourly) - tasks.Add(_messagePublisher.PublishAsync(new PlanOverage { OrganizationId = organization.Id, IsHourly = true })); + if (justWentOverMonthly) + tasks.Add(_messagePublisher.PublishAsync(new PlanOverage { OrganizationId = organization.Id })); + else if (justWentOverHourly) + tasks.Add(_messagePublisher.PublishAsync(new PlanOverage { OrganizationId = organization.Id, IsHourly = true })); - await Task.WhenAll(tasks).AnyContext(); - return overLimit; - } + await Task.WhenAll(tasks).AnyContext(); + return overLimit; + } - private async Task GetUsageAsync(Organization org, bool tooBig, int count) { - var hourlyTotal = _cache.IncrementAsync(GetHourlyTotalCacheKey(org.Id), count, TimeSpan.FromMinutes(61), (uint)org.GetCurrentHourlyTotal()); - var monthlyTotal = _cache.IncrementAsync(GetMonthlyTotalCacheKey(org.Id), count, TimeSpan.FromDays(32), (uint)org.GetCurrentMonthlyTotal()); - var hourlyTooBig = _cache.IncrementIfAsync(GetHourlyTooBigCacheKey(org.Id), count, TimeSpan.FromMinutes(61), tooBig, (uint)org.GetCurrentHourlyTooBig()); - var monthlyTooBig = _cache.IncrementIfAsync(GetMonthlyTooBigCacheKey(org.Id), count, TimeSpan.FromDays(32), tooBig, (uint)org.GetCurrentMonthlyTooBig()); - var hourlyBlocked = _cache.GetAsync(GetHourlyBlockedCacheKey(org.Id), org.GetCurrentHourlyBlocked()); - var monthlyBlocked = _cache.GetAsync(GetMonthlyBlockedCacheKey(org.Id), org.GetCurrentMonthlyBlocked()); - await Task.WhenAll(hourlyTotal, monthlyTotal, hourlyTooBig, monthlyTooBig, hourlyBlocked, monthlyBlocked).AnyContext(); - - return new Usage { - HourlyTotal = hourlyTotal.Result, - MonthlyTotal = monthlyTotal.Result, - HourlyTooBig = hourlyTooBig.Result, - MonthlyTooBig = monthlyTooBig.Result, - HourlyBlocked = hourlyBlocked.Result, - MonthlyBlocked = monthlyBlocked.Result, - }; - } + private async Task GetUsageAsync(Organization org, bool tooBig, int count) { + var hourlyTotal = _cache.IncrementAsync(GetHourlyTotalCacheKey(org.Id), count, TimeSpan.FromMinutes(61), (uint)org.GetCurrentHourlyTotal()); + var monthlyTotal = _cache.IncrementAsync(GetMonthlyTotalCacheKey(org.Id), count, TimeSpan.FromDays(32), (uint)org.GetCurrentMonthlyTotal()); + var hourlyTooBig = _cache.IncrementIfAsync(GetHourlyTooBigCacheKey(org.Id), count, TimeSpan.FromMinutes(61), tooBig, (uint)org.GetCurrentHourlyTooBig()); + var monthlyTooBig = _cache.IncrementIfAsync(GetMonthlyTooBigCacheKey(org.Id), count, TimeSpan.FromDays(32), tooBig, (uint)org.GetCurrentMonthlyTooBig()); + var hourlyBlocked = _cache.GetAsync(GetHourlyBlockedCacheKey(org.Id), org.GetCurrentHourlyBlocked()); + var monthlyBlocked = _cache.GetAsync(GetMonthlyBlockedCacheKey(org.Id), org.GetCurrentMonthlyBlocked()); + await Task.WhenAll(hourlyTotal, monthlyTotal, hourlyTooBig, monthlyTooBig, hourlyBlocked, monthlyBlocked).AnyContext(); + + return new Usage { + HourlyTotal = hourlyTotal.Result, + MonthlyTotal = monthlyTotal.Result, + HourlyTooBig = hourlyTooBig.Result, + MonthlyTooBig = monthlyTooBig.Result, + HourlyBlocked = hourlyBlocked.Result, + MonthlyBlocked = monthlyBlocked.Result, + }; + } - private async Task GetUsageAsync(Organization org, Project project, bool tooBig, int count, bool overLimit, int totalBlocked) { - var hourlyTotal = _cache.IncrementAsync(GetHourlyTotalCacheKey(org.Id, project.Id), count, TimeSpan.FromMinutes(61), (uint)project.GetCurrentHourlyTotal()); - var monthlyTotal = _cache.IncrementAsync(GetMonthlyTotalCacheKey(org.Id, project.Id), count, TimeSpan.FromDays(32), (uint)project.GetCurrentMonthlyTotal()); - var hourlyTooBig = _cache.IncrementIfAsync(GetHourlyTooBigCacheKey(org.Id, project.Id), count, TimeSpan.FromMinutes(61), tooBig, (uint)project.GetCurrentHourlyTooBig()); - var monthlyTooBig = _cache.IncrementIfAsync(GetMonthlyTooBigCacheKey(org.Id, project.Id), count, TimeSpan.FromDays(32), tooBig, (uint)project.GetCurrentMonthlyTooBig()); - var hourlyBlocked = _cache.IncrementIfAsync(GetHourlyBlockedCacheKey(org.Id, project.Id), totalBlocked, TimeSpan.FromMinutes(61), overLimit, (uint)project.GetCurrentHourlyBlocked()); - var monthlyBlocked = _cache.IncrementIfAsync(GetMonthlyBlockedCacheKey(org.Id, project.Id), totalBlocked, TimeSpan.FromDays(32), overLimit, (uint)project.GetCurrentMonthlyBlocked()); - await Task.WhenAll(hourlyTotal, monthlyTotal, hourlyTooBig, monthlyTooBig, hourlyBlocked, monthlyBlocked).AnyContext(); - - return new Usage { - HourlyTotal = hourlyTotal.Result, - MonthlyTotal = monthlyTotal.Result, - HourlyTooBig = hourlyTooBig.Result, - MonthlyTooBig = monthlyTooBig.Result, - HourlyBlocked = hourlyBlocked.Result, - MonthlyBlocked = monthlyBlocked.Result, - }; - } + private async Task GetUsageAsync(Organization org, Project project, bool tooBig, int count, bool overLimit, int totalBlocked) { + var hourlyTotal = _cache.IncrementAsync(GetHourlyTotalCacheKey(org.Id, project.Id), count, TimeSpan.FromMinutes(61), (uint)project.GetCurrentHourlyTotal()); + var monthlyTotal = _cache.IncrementAsync(GetMonthlyTotalCacheKey(org.Id, project.Id), count, TimeSpan.FromDays(32), (uint)project.GetCurrentMonthlyTotal()); + var hourlyTooBig = _cache.IncrementIfAsync(GetHourlyTooBigCacheKey(org.Id, project.Id), count, TimeSpan.FromMinutes(61), tooBig, (uint)project.GetCurrentHourlyTooBig()); + var monthlyTooBig = _cache.IncrementIfAsync(GetMonthlyTooBigCacheKey(org.Id, project.Id), count, TimeSpan.FromDays(32), tooBig, (uint)project.GetCurrentMonthlyTooBig()); + var hourlyBlocked = _cache.IncrementIfAsync(GetHourlyBlockedCacheKey(org.Id, project.Id), totalBlocked, TimeSpan.FromMinutes(61), overLimit, (uint)project.GetCurrentHourlyBlocked()); + var monthlyBlocked = _cache.IncrementIfAsync(GetMonthlyBlockedCacheKey(org.Id, project.Id), totalBlocked, TimeSpan.FromDays(32), overLimit, (uint)project.GetCurrentMonthlyBlocked()); + await Task.WhenAll(hourlyTotal, monthlyTotal, hourlyTooBig, monthlyTooBig, hourlyBlocked, monthlyBlocked).AnyContext(); + + return new Usage { + HourlyTotal = hourlyTotal.Result, + MonthlyTotal = monthlyTotal.Result, + HourlyTooBig = hourlyTooBig.Result, + MonthlyTooBig = monthlyTooBig.Result, + HourlyBlocked = hourlyBlocked.Result, + MonthlyBlocked = monthlyBlocked.Result, + }; + } - private async Task SaveUsageAsync(Organization org, bool justWentOverHourly, bool justWentOverMonthly, Usage usage) { - bool shouldSaveUsage = await ShouldSaveUsageAsync(org, null, justWentOverHourly, justWentOverMonthly).AnyContext(); - if (!shouldSaveUsage) + private async Task SaveUsageAsync(Organization org, bool justWentOverHourly, bool justWentOverMonthly, Usage usage) { + bool shouldSaveUsage = await ShouldSaveUsageAsync(org, null, justWentOverHourly, justWentOverMonthly).AnyContext(); + if (!shouldSaveUsage) + return; + + string orgId = org.Id; + try { + org = await _organizationRepository.GetByIdAsync(orgId).AnyContext(); + if (org == null) return; - string orgId = org.Id; - try { - org = await _organizationRepository.GetByIdAsync(orgId).AnyContext(); - if (org == null) - return; - - org.LastEventDateUtc = SystemClock.UtcNow; - org.SetMonthlyUsage(usage.MonthlyTotal, usage.MonthlyBlocked, usage.MonthlyTooBig); - if (usage.HourlyBlocked > 0 || usage.HourlyTooBig > 0) - org.SetHourlyOverage(usage.HourlyTotal, usage.HourlyBlocked, usage.HourlyTooBig, _plans); - - await _organizationRepository.SaveAsync(org, o => o.Cache()).AnyContext(); - await _cache.SetAsync(GetUsageSavedCacheKey(orgId), SystemClock.UtcNow, TimeSpan.FromDays(32)).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error while saving organization {OrganizationId} usage data.", orgId); - - // Set the next document save for 5 seconds in the future. - await _cache.SetAsync(GetUsageSavedCacheKey(orgId), SystemClock.UtcNow.SubtractMinutes(4).SubtractSeconds(55), TimeSpan.FromDays(32)).AnyContext(); - } + org.LastEventDateUtc = SystemClock.UtcNow; + org.SetMonthlyUsage(usage.MonthlyTotal, usage.MonthlyBlocked, usage.MonthlyTooBig); + if (usage.HourlyBlocked > 0 || usage.HourlyTooBig > 0) + org.SetHourlyOverage(usage.HourlyTotal, usage.HourlyBlocked, usage.HourlyTooBig, _plans); + + await _organizationRepository.SaveAsync(org, o => o.Cache()).AnyContext(); + await _cache.SetAsync(GetUsageSavedCacheKey(orgId), SystemClock.UtcNow, TimeSpan.FromDays(32)).AnyContext(); } + catch (Exception ex) { + _logger.LogError(ex, "Error while saving organization {OrganizationId} usage data.", orgId); + + // Set the next document save for 5 seconds in the future. + await _cache.SetAsync(GetUsageSavedCacheKey(orgId), SystemClock.UtcNow.SubtractMinutes(4).SubtractSeconds(55), TimeSpan.FromDays(32)).AnyContext(); + } + } - private async Task SaveUsageAsync(Organization org, Project project, bool justWentOverHourly, bool justWentOverMonthly, Usage usage) { - bool shouldSaveUsage = await ShouldSaveUsageAsync(org, project, justWentOverHourly, justWentOverMonthly).AnyContext(); - if (!shouldSaveUsage) + private async Task SaveUsageAsync(Organization org, Project project, bool justWentOverHourly, bool justWentOverMonthly, Usage usage) { + bool shouldSaveUsage = await ShouldSaveUsageAsync(org, project, justWentOverHourly, justWentOverMonthly).AnyContext(); + if (!shouldSaveUsage) + return; + + string projectId = project.Id; + try { + project = await _projectRepository.GetByIdAsync(projectId).AnyContext(); + if (project == null) return; - string projectId = project.Id; - try { - project = await _projectRepository.GetByIdAsync(projectId).AnyContext(); - if (project == null) - return; - - project.LastEventDateUtc = SystemClock.UtcNow; - project.SetMonthlyUsage(usage.MonthlyTotal, usage.MonthlyBlocked, usage.MonthlyTooBig, org.GetMaxEventsPerMonthWithBonus()); - if (usage.HourlyBlocked > 0 || usage.HourlyTooBig > 0) - project.SetHourlyOverage(usage.HourlyTotal, usage.HourlyBlocked, usage.HourlyTooBig, org.GetHourlyEventLimit(_plans)); - - await _projectRepository.SaveAsync(project, o => o.Cache()).AnyContext(); - await _cache.SetAsync(GetUsageSavedCacheKey(org.Id, projectId), SystemClock.UtcNow, TimeSpan.FromDays(32)).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error while saving project {ProjectId} usage data.", projectId); - - // Set the next document save for 5 seconds in the future. - await _cache.SetAsync(GetUsageSavedCacheKey(org.Id, projectId), SystemClock.UtcNow.SubtractMinutes(4).SubtractSeconds(55), TimeSpan.FromDays(32)).AnyContext(); - } + project.LastEventDateUtc = SystemClock.UtcNow; + project.SetMonthlyUsage(usage.MonthlyTotal, usage.MonthlyBlocked, usage.MonthlyTooBig, org.GetMaxEventsPerMonthWithBonus()); + if (usage.HourlyBlocked > 0 || usage.HourlyTooBig > 0) + project.SetHourlyOverage(usage.HourlyTotal, usage.HourlyBlocked, usage.HourlyTooBig, org.GetHourlyEventLimit(_plans)); + + await _projectRepository.SaveAsync(project, o => o.Cache()).AnyContext(); + await _cache.SetAsync(GetUsageSavedCacheKey(org.Id, projectId), SystemClock.UtcNow, TimeSpan.FromDays(32)).AnyContext(); } + catch (Exception ex) { + _logger.LogError(ex, "Error while saving project {ProjectId} usage data.", projectId); - private async Task ShouldSaveUsageAsync(Organization organization, Project project, bool justWentOverHourly, bool justWentOverMonthly) { - // save usages if we just went over one of the limits - bool shouldSaveUsage = justWentOverHourly || justWentOverMonthly; - if (shouldSaveUsage) - return true; + // Set the next document save for 5 seconds in the future. + await _cache.SetAsync(GetUsageSavedCacheKey(org.Id, projectId), SystemClock.UtcNow.SubtractMinutes(4).SubtractSeconds(55), TimeSpan.FromDays(32)).AnyContext(); + } + } - var lastCounterSavedDate = await _cache.GetAsync(GetUsageSavedCacheKey(organization.Id, project?.Id)).AnyContext(); - // don't save on the 1st increment, but set the last saved date so we will save in 5 minutes - if (!lastCounterSavedDate.HasValue) - await _cache.SetAsync(GetUsageSavedCacheKey(organization.Id, project?.Id), SystemClock.UtcNow, TimeSpan.FromDays(32)).AnyContext(); + private async Task ShouldSaveUsageAsync(Organization organization, Project project, bool justWentOverHourly, bool justWentOverMonthly) { + // save usages if we just went over one of the limits + bool shouldSaveUsage = justWentOverHourly || justWentOverMonthly; + if (shouldSaveUsage) + return true; - // TODO: If the save period is in the next hour we will lose all data in the past five minutes. - // save usages if the last time we saved them is more than 5 minutes ago - if (lastCounterSavedDate.HasValue && SystemClock.UtcNow.Subtract(lastCounterSavedDate.Value).TotalMinutes >= 5) - shouldSaveUsage = true; + var lastCounterSavedDate = await _cache.GetAsync(GetUsageSavedCacheKey(organization.Id, project?.Id)).AnyContext(); + // don't save on the 1st increment, but set the last saved date so we will save in 5 minutes + if (!lastCounterSavedDate.HasValue) + await _cache.SetAsync(GetUsageSavedCacheKey(organization.Id, project?.Id), SystemClock.UtcNow, TimeSpan.FromDays(32)).AnyContext(); - return shouldSaveUsage; - } + // TODO: If the save period is in the next hour we will lose all data in the past five minutes. + // save usages if the last time we saved them is more than 5 minutes ago + if (lastCounterSavedDate.HasValue && SystemClock.UtcNow.Subtract(lastCounterSavedDate.Value).TotalMinutes >= 5) + shouldSaveUsage = true; - public async Task GetRemainingEventLimitAsync(Organization organization) { - if (organization == null || organization.MaxEventsPerMonth < 0) - return Int32.MaxValue; + return shouldSaveUsage; + } - string monthlyCacheKey = GetMonthlyTotalCacheKey(organization.Id); - long monthlyEventCount = await _cache.GetAsync(monthlyCacheKey, 0).AnyContext(); - return Math.Max(0, organization.GetMaxEventsPerMonthWithBonus() - (int)monthlyEventCount); - } + public async Task GetRemainingEventLimitAsync(Organization organization) { + if (organization == null || organization.MaxEventsPerMonth < 0) + return Int32.MaxValue; - private double GetTotalBlocked(Organization organization, int count, Usage usage, bool applyHourlyLimit) { - if (organization.IsSuspended) - return count; + string monthlyCacheKey = GetMonthlyTotalCacheKey(organization.Id); + long monthlyEventCount = await _cache.GetAsync(monthlyCacheKey, 0).AnyContext(); + return Math.Max(0, organization.GetMaxEventsPerMonthWithBonus() - (int)monthlyEventCount); + } - int hourlyEventLimit = organization.GetHourlyEventLimit(_plans); - int monthlyEventLimit = organization.GetMaxEventsPerMonthWithBonus(); - double originalAllowedMonthlyEventTotal = usage.MonthlyTotal - usage.MonthlyBlocked - count; + private double GetTotalBlocked(Organization organization, int count, Usage usage, bool applyHourlyLimit) { + if (organization.IsSuspended) + return count; - // If the original count is less than the max events per month and original count + hourly limit is greater than the max events per month then use the monthly limit. - if (originalAllowedMonthlyEventTotal < monthlyEventLimit && (originalAllowedMonthlyEventTotal + hourlyEventLimit) >= monthlyEventLimit) - return originalAllowedMonthlyEventTotal < monthlyEventLimit ? Math.Max(usage.MonthlyTotal - usage.MonthlyBlocked - monthlyEventLimit, 0) : count; + int hourlyEventLimit = organization.GetHourlyEventLimit(_plans); + int monthlyEventLimit = organization.GetMaxEventsPerMonthWithBonus(); + double originalAllowedMonthlyEventTotal = usage.MonthlyTotal - usage.MonthlyBlocked - count; - double originalAllowedHourlyEventTotal = usage.HourlyTotal - usage.HourlyBlocked - count; - if (applyHourlyLimit && (usage.HourlyTotal - usage.HourlyBlocked) > hourlyEventLimit) - return originalAllowedHourlyEventTotal < hourlyEventLimit ? Math.Max(usage.HourlyTotal - usage.HourlyBlocked - hourlyEventLimit, 0) : count; + // If the original count is less than the max events per month and original count + hourly limit is greater than the max events per month then use the monthly limit. + if (originalAllowedMonthlyEventTotal < monthlyEventLimit && (originalAllowedMonthlyEventTotal + hourlyEventLimit) >= monthlyEventLimit) + return originalAllowedMonthlyEventTotal < monthlyEventLimit ? Math.Max(usage.MonthlyTotal - usage.MonthlyBlocked - monthlyEventLimit, 0) : count; - if ((usage.MonthlyTotal - usage.MonthlyBlocked) > monthlyEventLimit) - return originalAllowedMonthlyEventTotal < monthlyEventLimit ? Math.Max(usage.MonthlyTotal - usage.MonthlyBlocked - monthlyEventLimit, 0) : count; + double originalAllowedHourlyEventTotal = usage.HourlyTotal - usage.HourlyBlocked - count; + if (applyHourlyLimit && (usage.HourlyTotal - usage.HourlyBlocked) > hourlyEventLimit) + return originalAllowedHourlyEventTotal < hourlyEventLimit ? Math.Max(usage.HourlyTotal - usage.HourlyBlocked - hourlyEventLimit, 0) : count; - return 0; - } + if ((usage.MonthlyTotal - usage.MonthlyBlocked) > monthlyEventLimit) + return originalAllowedMonthlyEventTotal < monthlyEventLimit ? Math.Max(usage.MonthlyTotal - usage.MonthlyBlocked - monthlyEventLimit, 0) : count; - private string GetHourlyBlockedCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } + return 0; + } - private string GetHourlyTotalCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:total", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } + private string GetHourlyBlockedCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } - private string GetHourlyTooBigCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:toobig", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } + private string GetHourlyTotalCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:total", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } - private string GetMonthlyBlockedCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } + private string GetHourlyTooBigCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:toobig", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } - private string GetMonthlyTotalCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:total", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } + private string GetMonthlyBlockedCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } - private string GetMonthlyTooBigCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:toobig", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } + private string GetMonthlyTotalCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:total", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } - private string GetUsageSavedCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:saved", ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } + private string GetMonthlyTooBigCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:toobig", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } - [DebuggerDisplay("MonthlyTotal: {MonthlyTotal}, HourlyTotal: {HourlyTotal}, MonthlyBlocked: {MonthlyBlocked}, HourlyBlocked: {HourlyBlocked}, MonthlyTooBig: {MonthlyTooBig}, HourlyTooBig: {HourlyTooBig}")] - private struct Usage { - public double MonthlyTotal { get; set; } - public double HourlyTotal { get; set; } - public double MonthlyBlocked { get; set; } - public double HourlyBlocked { get; set; } - public double MonthlyTooBig { get; set; } - public double HourlyTooBig { get; set; } - } + private string GetUsageSavedCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:saved", ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } + + [DebuggerDisplay("MonthlyTotal: {MonthlyTotal}, HourlyTotal: {HourlyTotal}, MonthlyBlocked: {MonthlyBlocked}, HourlyBlocked: {HourlyBlocked}, MonthlyTooBig: {MonthlyTooBig}, HourlyTooBig: {HourlyTooBig}")] + private struct Usage { + public double MonthlyTotal { get; set; } + public double HourlyTotal { get; set; } + public double MonthlyBlocked { get; set; } + public double HourlyBlocked { get; set; } + public double MonthlyTooBig { get; set; } + public double HourlyTooBig { get; set; } } } diff --git a/src/Exceptionless.Core/Utility/Apm/EventSourceEventFormatter.cs b/src/Exceptionless.Core/Utility/Apm/EventSourceEventFormatter.cs index 84774dedc6..faa0cbd374 100644 --- a/src/Exceptionless.Core/Utility/Apm/EventSourceEventFormatter.cs +++ b/src/Exceptionless.Core/Utility/Apm/EventSourceEventFormatter.cs @@ -1,40 +1,61 @@ -using System; -using System.Diagnostics.Tracing; +using System.Diagnostics.Tracing; using System.Globalization; -using System.Linq; -namespace OpenTelemetry.Internal { - internal static class EventSourceEventFormatter { - private static readonly object[] EmptyPayload = Array.Empty(); +namespace OpenTelemetry.Internal; - public static string Format(EventWrittenEventArgs eventData) { - var payloadCollection = eventData.Payload.ToArray() ?? EmptyPayload; +internal static class EventSourceEventFormatter { + private static readonly object[] EmptyPayload = Array.Empty(); - ProcessPayloadArray(payloadCollection); + public static string Format(EventWrittenEventArgs eventData) { + var payloadCollection = eventData.Payload.ToArray() ?? EmptyPayload; - if (eventData.Message != null) { - try { - return string.Format(CultureInfo.InvariantCulture, eventData.Message, payloadCollection); - } - catch (FormatException) { - } + ProcessPayloadArray(payloadCollection); + + if (eventData.Message != null) { + try { + return string.Format(CultureInfo.InvariantCulture, eventData.Message, payloadCollection); } + catch (FormatException) { + } + } - var stringBuilder = StringBuilderPool.Instance.Get(); + var stringBuilder = StringBuilderPool.Instance.Get(); - try { - stringBuilder.Append(eventData.EventName); + try { + stringBuilder.Append(eventData.EventName); - if (!string.IsNullOrWhiteSpace(eventData.Message)) { + if (!string.IsNullOrWhiteSpace(eventData.Message)) { + stringBuilder.AppendLine(); + stringBuilder.Append(nameof(eventData.Message)).Append(" = ").Append(eventData.Message); + } + + if (eventData.PayloadNames != null) { + for (int i = 0; i < eventData.PayloadNames.Count; i++) { stringBuilder.AppendLine(); - stringBuilder.Append(nameof(eventData.Message)).Append(" = ").Append(eventData.Message); + stringBuilder.Append(eventData.PayloadNames[i]).Append(" = ").Append(payloadCollection[i]); } + } + + return stringBuilder.ToString(); + } + finally { + StringBuilderPool.Instance.Return(stringBuilder); + } + } - if (eventData.PayloadNames != null) { - for (int i = 0; i < eventData.PayloadNames.Count; i++) { - stringBuilder.AppendLine(); - stringBuilder.Append(eventData.PayloadNames[i]).Append(" = ").Append(payloadCollection[i]); - } + private static void ProcessPayloadArray(object[] payloadArray) { + for (int i = 0; i < payloadArray.Length; i++) { + payloadArray[i] = FormatValue(payloadArray[i]); + } + } + + private static object FormatValue(object o) { + if (o is byte[] bytes) { + var stringBuilder = StringBuilderPool.Instance.Get(); + + try { + foreach (byte b in bytes) { + stringBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", b); } return stringBuilder.ToString(); @@ -44,29 +65,6 @@ public static string Format(EventWrittenEventArgs eventData) { } } - private static void ProcessPayloadArray(object[] payloadArray) { - for (int i = 0; i < payloadArray.Length; i++) { - payloadArray[i] = FormatValue(payloadArray[i]); - } - } - - private static object FormatValue(object o) { - if (o is byte[] bytes) { - var stringBuilder = StringBuilderPool.Instance.Get(); - - try { - foreach (byte b in bytes) { - stringBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", b); - } - - return stringBuilder.ToString(); - } - finally { - StringBuilderPool.Instance.Return(stringBuilder); - } - } - - return o; - } + return o; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsEventLogForwarder.cs b/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsEventLogForwarder.cs index 6de7182d4a..91be421ca1 100644 --- a/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsEventLogForwarder.cs +++ b/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsEventLogForwarder.cs @@ -1,166 +1,135 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.Tracing; using Microsoft.Extensions.Logging; -namespace OpenTelemetry.Internal -{ - /// - /// SelfDiagnosticsEventListener class enables the events from OpenTelemetry event sources - /// and write the events to an ILoggerFactory. - /// - internal class SelfDiagnosticsEventLogForwarder : EventListener - { - private const string EventSourceNamePrefix = "OpenTelemetry-"; - private readonly object lockObj = new object(); - private readonly EventLevel? minEventLevel; - private readonly List eventSources = new List(); - private readonly ILoggerFactory loggerFactory; - private readonly ConcurrentDictionary loggers = new ConcurrentDictionary(); - - private readonly Func formatMessage = FormatMessage; - - internal SelfDiagnosticsEventLogForwarder(ILoggerFactory loggerFactory, EventLevel? minEventLevel = null) - { - this.loggerFactory = loggerFactory; - this.minEventLevel = minEventLevel; - - // set initial levels on existing event sources - this.SetEventSourceLevels(); - } +namespace OpenTelemetry.Internal; - public override void Dispose() - { - this.StopForwarding(); - base.Dispose(); - } +/// +/// SelfDiagnosticsEventListener class enables the events from OpenTelemetry event sources +/// and write the events to an ILoggerFactory. +/// +internal class SelfDiagnosticsEventLogForwarder : EventListener { + private const string EventSourceNamePrefix = "OpenTelemetry-"; + private readonly object lockObj = new object(); + private readonly EventLevel? minEventLevel; + private readonly List eventSources = new List(); + private readonly ILoggerFactory loggerFactory; + private readonly ConcurrentDictionary loggers = new ConcurrentDictionary(); - protected override void OnEventSourceCreated(EventSource eventSource) - { - if (eventSource.Name.StartsWith(EventSourceNamePrefix, StringComparison.Ordinal)) - { - lock (this.lockObj) - { - this.eventSources.Add(eventSource); - } + private readonly Func formatMessage = FormatMessage; - this.SetEventSourceLevel(eventSource); - } + internal SelfDiagnosticsEventLogForwarder(ILoggerFactory loggerFactory, EventLevel? minEventLevel = null) { + this.loggerFactory = loggerFactory; + this.minEventLevel = minEventLevel; - base.OnEventSourceCreated(eventSource); - } + // set initial levels on existing event sources + this.SetEventSourceLevels(); + } - /// - /// This method records the events from event sources to the logging system. - /// - /// Data of the EventSource event. - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - if (this.loggerFactory == null) - { - return; - } + public override void Dispose() { + this.StopForwarding(); + base.Dispose(); + } - if (!this.loggers.TryGetValue(eventData.EventSource, out var logger)) - { - logger = this.loggers.GetOrAdd(eventData.EventSource, eventSource => this.loggerFactory.CreateLogger(ToLoggerName(eventSource.Name))); + protected override void OnEventSourceCreated(EventSource eventSource) { + if (eventSource.Name.StartsWith(EventSourceNamePrefix, StringComparison.Ordinal)) { + lock (this.lockObj) { + this.eventSources.Add(eventSource); } - logger.Log(MapLevel(eventData.Level), new EventId(eventData.EventId, eventData.EventName), new EventSourceEvent(eventData), null, this.formatMessage); + this.SetEventSourceLevel(eventSource); } - private static string ToLoggerName(string name) - { - return name.Replace('-', '.'); - } + base.OnEventSourceCreated(eventSource); + } - private static LogLevel MapLevel(EventLevel level) - { - return level switch - { - EventLevel.Critical => LogLevel.Critical, - EventLevel.Error => LogLevel.Error, - EventLevel.Informational => LogLevel.Information, - EventLevel.Verbose => LogLevel.Debug, - EventLevel.Warning => LogLevel.Warning, - EventLevel.LogAlways => LogLevel.Information, - _ => LogLevel.None, - }; + /// + /// This method records the events from event sources to the logging system. + /// + /// Data of the EventSource event. + protected override void OnEventWritten(EventWrittenEventArgs eventData) { + if (this.loggerFactory == null) { + return; } - private static string FormatMessage(EventSourceEvent eventSourceEvent, Exception exception) - { - return EventSourceEventFormatter.Format(eventSourceEvent.EventData); + if (!this.loggers.TryGetValue(eventData.EventSource, out var logger)) { + logger = this.loggers.GetOrAdd(eventData.EventSource, eventSource => this.loggerFactory.CreateLogger(ToLoggerName(eventSource.Name))); } - private EventLevel? GetEventLevel(string category) - { - return this.minEventLevel; - } + logger.Log(MapLevel(eventData.Level), new EventId(eventData.EventId, eventData.EventName), new EventSourceEvent(eventData), null, this.formatMessage); + } + + private static string ToLoggerName(string name) { + return name.Replace('-', '.'); + } - private void SetEventSourceLevels() - { - lock (this.lockObj) - { - foreach (var eventSource in this.eventSources) - { - this.SetEventSourceLevel(eventSource); - } + private static LogLevel MapLevel(EventLevel level) { + return level switch { + EventLevel.Critical => LogLevel.Critical, + EventLevel.Error => LogLevel.Error, + EventLevel.Informational => LogLevel.Information, + EventLevel.Verbose => LogLevel.Debug, + EventLevel.Warning => LogLevel.Warning, + EventLevel.LogAlways => LogLevel.Information, + _ => LogLevel.None, + }; + } + + private static string FormatMessage(EventSourceEvent eventSourceEvent, Exception exception) { + return EventSourceEventFormatter.Format(eventSourceEvent.EventData); + } + + private EventLevel? GetEventLevel(string category) { + return this.minEventLevel; + } + + private void SetEventSourceLevels() { + lock (this.lockObj) { + foreach (var eventSource in this.eventSources) { + this.SetEventSourceLevel(eventSource); } } + } - private void StopForwarding() - { - lock (this.lockObj) - { - foreach (var eventSource in this.eventSources) - { - this.DisableEvents(eventSource); - } + private void StopForwarding() { + lock (this.lockObj) { + foreach (var eventSource in this.eventSources) { + this.DisableEvents(eventSource); } } + } - private void SetEventSourceLevel(EventSource eventSource) - { - var eventLevel = this.GetEventLevel(ToLoggerName(eventSource.Name)); + private void SetEventSourceLevel(EventSource eventSource) { + var eventLevel = this.GetEventLevel(ToLoggerName(eventSource.Name)); - if (eventLevel.HasValue) - { - this.EnableEvents(eventSource, eventLevel.HasValue ? eventLevel.Value : EventLevel.Warning); - } - else - { - this.DisableEvents(eventSource); - } + if (eventLevel.HasValue) { + this.EnableEvents(eventSource, eventLevel.HasValue ? eventLevel.Value : EventLevel.Warning); } + else { + this.DisableEvents(eventSource); + } + } - private readonly struct EventSourceEvent : IReadOnlyList> - { - public EventSourceEvent(EventWrittenEventArgs eventData) - { - this.EventData = eventData; - } + private readonly struct EventSourceEvent : IReadOnlyList> { + public EventSourceEvent(EventWrittenEventArgs eventData) { + this.EventData = eventData; + } - public EventWrittenEventArgs EventData { get; } + public EventWrittenEventArgs EventData { get; } - public int Count => this.EventData.PayloadNames.Count; + public int Count => this.EventData.PayloadNames.Count; - public KeyValuePair this[int index] => new KeyValuePair(this.EventData.PayloadNames[index], this.EventData.Payload[index]); + public KeyValuePair this[int index] => new KeyValuePair(this.EventData.PayloadNames[index], this.EventData.Payload[index]); - public IEnumerator> GetEnumerator() - { - for (int i = 0; i < this.Count; i++) - { - yield return new KeyValuePair(this.EventData.PayloadNames[i], this.EventData.Payload[i]); - } + public IEnumerator> GetEnumerator() { + for (int i = 0; i < this.Count; i++) { + yield return new KeyValuePair(this.EventData.PayloadNames[i], this.EventData.Payload[i]); } + } - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() { + return this.GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsLoggingHostedService.cs b/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsLoggingHostedService.cs index f97be1632b..1bc74c3424 100644 --- a/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsLoggingHostedService.cs +++ b/src/Exceptionless.Core/Utility/Apm/SelfDiagnosticsLoggingHostedService.cs @@ -1,41 +1,32 @@ -using System; -using System.Diagnostics.Tracing; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics.Tracing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry.Internal; -namespace OpenTelemetry.Extensions.Hosting.Implementation -{ - public class SelfDiagnosticsLoggingHostedService : IHostedService, IDisposable - { - private readonly ILoggerFactory loggerFactory; - private readonly IDisposable forwarder; +namespace OpenTelemetry.Extensions.Hosting.Implementation; - public SelfDiagnosticsLoggingHostedService(ILoggerFactory loggerFactory, EventLevel? minEventLevel = null) - { - this.loggerFactory = loggerFactory; +public class SelfDiagnosticsLoggingHostedService : IHostedService, IDisposable { + private readonly ILoggerFactory loggerFactory; + private readonly IDisposable forwarder; - // The sole purpose of this HostedService is to - // start forwarding the self-diagnostics events - // to the logger factory - this.forwarder = new SelfDiagnosticsEventLogForwarder(this.loggerFactory, minEventLevel); - } + public SelfDiagnosticsLoggingHostedService(ILoggerFactory loggerFactory, EventLevel? minEventLevel = null) { + this.loggerFactory = loggerFactory; - public void Dispose() - { - this.forwarder?.Dispose(); - } + // The sole purpose of this HostedService is to + // start forwarding the self-diagnostics events + // to the logger factory + this.forwarder = new SelfDiagnosticsEventLogForwarder(this.loggerFactory, minEventLevel); + } + + public void Dispose() { + this.forwarder?.Dispose(); + } - public Task StartAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StartAsync(CancellationToken cancellationToken) { + return Task.CompletedTask; + } - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StopAsync(CancellationToken cancellationToken) { + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Apm/StringBuilderPool.cs b/src/Exceptionless.Core/Utility/Apm/StringBuilderPool.cs index 14514389cb..8d07b6cdb1 100644 --- a/src/Exceptionless.Core/Utility/Apm/StringBuilderPool.cs +++ b/src/Exceptionless.Core/Utility/Apm/StringBuilderPool.cs @@ -1,67 +1,65 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Text; -using System.Threading; -namespace OpenTelemetry.Internal { - // Based on Microsoft.Extensions.ObjectPool - // https://github.com/dotnet/aspnetcore/blob/main/src/ObjectPool/src/DefaultObjectPool.cs - internal class StringBuilderPool { - internal static StringBuilderPool Instance = new StringBuilderPool(); +namespace OpenTelemetry.Internal; - private protected readonly ObjectWrapper[] items; - private protected StringBuilder firstItem; +// Based on Microsoft.Extensions.ObjectPool +// https://github.com/dotnet/aspnetcore/blob/main/src/ObjectPool/src/DefaultObjectPool.cs +internal class StringBuilderPool { + internal static StringBuilderPool Instance = new StringBuilderPool(); - public StringBuilderPool() - : this(Environment.ProcessorCount * 2) { - } + private protected readonly ObjectWrapper[] items; + private protected StringBuilder firstItem; - public StringBuilderPool(int maximumRetained) { - // -1 due to _firstItem - this.items = new ObjectWrapper[maximumRetained - 1]; - } + public StringBuilderPool() + : this(Environment.ProcessorCount * 2) { + } + + public StringBuilderPool(int maximumRetained) { + // -1 due to _firstItem + this.items = new ObjectWrapper[maximumRetained - 1]; + } - public int MaximumRetainedCapacity { get; set; } = 4 * 1024; + public int MaximumRetainedCapacity { get; set; } = 4 * 1024; - public int InitialCapacity { get; set; } = 100; + public int InitialCapacity { get; set; } = 100; - public StringBuilder Get() { - var item = this.firstItem; - if (item == null || Interlocked.CompareExchange(ref this.firstItem, null, item) != item) { - var items = this.items; - for (var i = 0; i < items.Length; i++) { - item = items[i].Element; - if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item) { - return item; - } + public StringBuilder Get() { + var item = this.firstItem; + if (item == null || Interlocked.CompareExchange(ref this.firstItem, null, item) != item) { + var items = this.items; + for (var i = 0; i < items.Length; i++) { + item = items[i].Element; + if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item) { + return item; } - - item = new StringBuilder(this.InitialCapacity); } - return item; + item = new StringBuilder(this.InitialCapacity); } - public bool Return(StringBuilder item) { - if (item.Capacity > this.MaximumRetainedCapacity) { - // Too big. Discard this one. - return false; - } + return item; + } - item.Clear(); + public bool Return(StringBuilder item) { + if (item.Capacity > this.MaximumRetainedCapacity) { + // Too big. Discard this one. + return false; + } - if (this.firstItem != null || Interlocked.CompareExchange(ref this.firstItem, item, null) != null) { - var items = this.items; - for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, item, null) != null; ++i) { - } - } + item.Clear(); - return true; + if (this.firstItem != null || Interlocked.CompareExchange(ref this.firstItem, item, null) != null) { + var items = this.items; + for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, item, null) != null; ++i) { + } } - [DebuggerDisplay("{Element}")] - private protected struct ObjectWrapper { - public StringBuilder Element; - } + return true; + } + + [DebuggerDisplay("{Element}")] + private protected struct ObjectWrapper { + public StringBuilder Element; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/AssemblyDetail.cs b/src/Exceptionless.Core/Utility/AssemblyDetail.cs index 5c7cc4ac7b..5d82a7ad46 100644 --- a/src/Exceptionless.Core/Utility/AssemblyDetail.cs +++ b/src/Exceptionless.Core/Utility/AssemblyDetail.cs @@ -1,66 +1,64 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Reflection; using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Utility { - public class AssemblyDetail { - public string AssemblyName { get; private set; } +namespace Exceptionless.Core.Utility; - public string AssemblyTitle { get; private set; } +public class AssemblyDetail { + public string AssemblyName { get; private set; } - public string AssemblyDescription { get; private set; } + public string AssemblyTitle { get; private set; } - public string AssemblyProduct { get; private set; } + public string AssemblyDescription { get; private set; } - public string AssemblyCompany { get; private set; } + public string AssemblyProduct { get; private set; } - public string AssemblyCopyright { get; private set; } + public string AssemblyCompany { get; private set; } - public string AssemblyConfiguration { get; private set; } + public string AssemblyCopyright { get; private set; } - public string AssemblyVersion { get; private set; } + public string AssemblyConfiguration { get; private set; } - public string AssemblyFileVersion { get; private set; } + public string AssemblyVersion { get; private set; } - public string AssemblyInformationalVersion { get; private set; } + public string AssemblyFileVersion { get; private set; } + public string AssemblyInformationalVersion { get; private set; } - private static readonly ConcurrentDictionary _detailCache = new ConcurrentDictionary(); - public static AssemblyDetail Extract(Assembly assembly) { - var detail = _detailCache.GetOrAdd(assembly, a => { - var assemblyDetail = new AssemblyDetail(); - var assemblyName = a.GetName(); + private static readonly ConcurrentDictionary _detailCache = new ConcurrentDictionary(); - assemblyDetail.AssemblyName = assemblyName.Name; - assemblyDetail.AssemblyVersion = assemblyName.Version?.ToString(); + public static AssemblyDetail Extract(Assembly assembly) { + var detail = _detailCache.GetOrAdd(assembly, a => { + var assemblyDetail = new AssemblyDetail(); + var assemblyName = a.GetName(); - assemblyDetail.AssemblyTitle = a.GetCustomAttribute()?.Title; - assemblyDetail.AssemblyDescription = a.GetCustomAttribute()?.Description; - assemblyDetail.AssemblyProduct = a.GetCustomAttribute()?.Product; - assemblyDetail.AssemblyCompany = a.GetCustomAttribute()?.Company; - assemblyDetail.AssemblyCopyright = a.GetCustomAttribute()?.Copyright; - assemblyDetail.AssemblyConfiguration = a.GetCustomAttribute()?.Configuration; - assemblyDetail.AssemblyFileVersion = a.GetCustomAttribute()?.Version; - assemblyDetail.AssemblyInformationalVersion = a.GetCustomAttribute()?.InformationalVersion; + assemblyDetail.AssemblyName = assemblyName.Name; + assemblyDetail.AssemblyVersion = assemblyName.Version?.ToString(); - return assemblyDetail; - }); + assemblyDetail.AssemblyTitle = a.GetCustomAttribute()?.Title; + assemblyDetail.AssemblyDescription = a.GetCustomAttribute()?.Description; + assemblyDetail.AssemblyProduct = a.GetCustomAttribute()?.Product; + assemblyDetail.AssemblyCompany = a.GetCustomAttribute()?.Company; + assemblyDetail.AssemblyCopyright = a.GetCustomAttribute()?.Copyright; + assemblyDetail.AssemblyConfiguration = a.GetCustomAttribute()?.Configuration; + assemblyDetail.AssemblyFileVersion = a.GetCustomAttribute()?.Version; + assemblyDetail.AssemblyInformationalVersion = a.GetCustomAttribute()?.InformationalVersion; - return detail; - } + return assemblyDetail; + }); + + return detail; + } - public static IEnumerable ExtractAll(string filter = "Exceptionless*") { - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + public static IEnumerable ExtractAll(string filter = "Exceptionless*") { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var assembly in assemblies) { - if (!assembly.FullName.AnyWildcardMatches(new [] { filter })) - continue; + foreach (var assembly in assemblies) { + if (!assembly.FullName.AnyWildcardMatches(new[] { filter })) + continue; - yield return Extract(assembly); - } + yield return Extract(assembly); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/ErrorSignature.cs b/src/Exceptionless.Core/Utility/ErrorSignature.cs index 9f725923ed..1406e50bdc 100644 --- a/src/Exceptionless.Core/Utility/ErrorSignature.cs +++ b/src/Exceptionless.Core/Utility/ErrorSignature.cs @@ -1,176 +1,175 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; -namespace Exceptionless.Core.Utility { - public class ErrorSignature { - private readonly HashSet _userNamespaces; - private readonly HashSet _userCommonMethods; - private static readonly string[] _defaultNonUserNamespaces = { "System", "Microsoft" }; - // TODO: Add support for user public key token on signed assemblies +namespace Exceptionless.Core.Utility; - public ErrorSignature(Error error, IEnumerable userNamespaces = null, IEnumerable userCommonMethods = null, bool emptyNamespaceIsUserMethod = true, bool shouldFlagSignatureTarget = true) { - Error = error ?? throw new ArgumentNullException(nameof(error)); +public class ErrorSignature { + private readonly HashSet _userNamespaces; + private readonly HashSet _userCommonMethods; + private static readonly string[] _defaultNonUserNamespaces = { "System", "Microsoft" }; + // TODO: Add support for user public key token on signed assemblies - _userNamespaces = userNamespaces == null - ? new HashSet() - : new HashSet(userNamespaces); + public ErrorSignature(Error error, IEnumerable userNamespaces = null, IEnumerable userCommonMethods = null, bool emptyNamespaceIsUserMethod = true, bool shouldFlagSignatureTarget = true) { + Error = error ?? throw new ArgumentNullException(nameof(error)); - _userCommonMethods = userCommonMethods == null - ? new HashSet() - : new HashSet(userCommonMethods); + _userNamespaces = userNamespaces == null + ? new HashSet() + : new HashSet(userNamespaces); - EmptyNamespaceIsUserMethod = emptyNamespaceIsUserMethod; + _userCommonMethods = userCommonMethods == null + ? new HashSet() + : new HashSet(userCommonMethods); - SignatureInfo = new SettingsDictionary(); - ShouldFlagSignatureTarget = shouldFlagSignatureTarget; + EmptyNamespaceIsUserMethod = emptyNamespaceIsUserMethod; - Parse(); - } + SignatureInfo = new SettingsDictionary(); + ShouldFlagSignatureTarget = shouldFlagSignatureTarget; - public string[] UserNamespaces => _userNamespaces.ToArray(); + Parse(); + } - public string[] UserCommonMethods => _userCommonMethods.ToArray(); + public string[] UserNamespaces => _userNamespaces.ToArray(); - public Error Error { get; private set; } + public string[] UserCommonMethods => _userCommonMethods.ToArray(); - public bool EmptyNamespaceIsUserMethod { get; private set; } + public Error Error { get; private set; } - public SettingsDictionary SignatureInfo { get; private set; } + public bool EmptyNamespaceIsUserMethod { get; private set; } - public string SignatureHash { get; private set; } + public SettingsDictionary SignatureInfo { get; private set; } - public bool IsUser { get; private set; } - public bool ShouldFlagSignatureTarget { get; private set; } + public string SignatureHash { get; private set; } - private void Parse() { - SignatureInfo.Clear(); + public bool IsUser { get; private set; } + public bool ShouldFlagSignatureTarget { get; private set; } - // start at the inner most exception and work our way out until we find a user method - InnerError current = Error; - var errorStack = new List { current }; - while (current.Inner != null) { - current = current.Inner; - errorStack.Add(current); - } + private void Parse() { + SignatureInfo.Clear(); - errorStack.Reverse(); + // start at the inner most exception and work our way out until we find a user method + InnerError current = Error; + var errorStack = new List { current }; + while (current.Inner != null) { + current = current.Inner; + errorStack.Add(current); + } - // reset all flags before we figure out which method to tag as the new target. - if (ShouldFlagSignatureTarget) - errorStack.ForEach(es => es.StackTrace.ForEach(st => st.IsSignatureTarget = false)); - - foreach (var e in errorStack) { - var stackTrace = e.StackTrace; - if (stackTrace == null) - continue; - - foreach (var stackFrame in stackTrace.Where(IsUserFrame)) { - SignatureInfo.AddItemIfNotEmpty("ExceptionType", e.Type); - SignatureInfo.AddItemIfNotEmpty("Method", GetStackFrameSignature(stackFrame)); - if (ShouldFlagSignatureTarget) - stackFrame.IsSignatureTarget = true; - AddSpecialCaseDetails(e); - UpdateInfo(true); - return; - } - } + errorStack.Reverse(); - // We haven't found a user method yet, try some alternatives with the inner most error. - var innerMostError = errorStack[0]; + // reset all flags before we figure out which method to tag as the new target. + if (ShouldFlagSignatureTarget) + errorStack.ForEach(es => es.StackTrace.ForEach(st => st.IsSignatureTarget = false)); - if (innerMostError.TargetMethod != null) { - // Use the target method if it exists. - SignatureInfo.AddItemIfNotEmpty("ExceptionType", innerMostError.Type); - SignatureInfo.AddItemIfNotEmpty("Method", GetStackFrameSignature(innerMostError.TargetMethod)); - if (ShouldFlagSignatureTarget) - innerMostError.TargetMethod.IsSignatureTarget = true; - } else if (innerMostError.StackTrace != null && innerMostError.StackTrace.Count > 0) { - // Use the topmost stack frame. - SignatureInfo.AddItemIfNotEmpty("ExceptionType", innerMostError.Type); - SignatureInfo.AddItemIfNotEmpty("Method", GetStackFrameSignature(innerMostError.StackTrace[0])); + foreach (var e in errorStack) { + var stackTrace = e.StackTrace; + if (stackTrace == null) + continue; + + foreach (var stackFrame in stackTrace.Where(IsUserFrame)) { + SignatureInfo.AddItemIfNotEmpty("ExceptionType", e.Type); + SignatureInfo.AddItemIfNotEmpty("Method", GetStackFrameSignature(stackFrame)); if (ShouldFlagSignatureTarget) - innerMostError.StackTrace[0].IsSignatureTarget = true; - } else { - // All else failed, use the type. - SignatureInfo.AddItemIfNotEmpty("ExceptionType", innerMostError.Type); + stackFrame.IsSignatureTarget = true; + AddSpecialCaseDetails(e); + UpdateInfo(true); + return; } + } - AddSpecialCaseDetails(innerMostError); - if (SignatureInfo.Count == 0) - SignatureInfo.Add("NoStackingInformation", Guid.NewGuid().ToString()); + // We haven't found a user method yet, try some alternatives with the inner most error. + var innerMostError = errorStack[0]; - UpdateInfo(false); + if (innerMostError.TargetMethod != null) { + // Use the target method if it exists. + SignatureInfo.AddItemIfNotEmpty("ExceptionType", innerMostError.Type); + SignatureInfo.AddItemIfNotEmpty("Method", GetStackFrameSignature(innerMostError.TargetMethod)); + if (ShouldFlagSignatureTarget) + innerMostError.TargetMethod.IsSignatureTarget = true; } - - private void UpdateInfo(bool isUser) { - IsUser = isUser; - RecalculateHash(); + else if (innerMostError.StackTrace != null && innerMostError.StackTrace.Count > 0) { + // Use the topmost stack frame. + SignatureInfo.AddItemIfNotEmpty("ExceptionType", innerMostError.Type); + SignatureInfo.AddItemIfNotEmpty("Method", GetStackFrameSignature(innerMostError.StackTrace[0])); + if (ShouldFlagSignatureTarget) + innerMostError.StackTrace[0].IsSignatureTarget = true; } - - public void RecalculateHash() { - SignatureHash = SignatureInfo.Values.Any(v => v != null) ? SignatureInfo.Values.ToSHA1() : null; + else { + // All else failed, use the type. + SignatureInfo.AddItemIfNotEmpty("ExceptionType", innerMostError.Type); } - private string GetStackFrameSignature(Method method) { - var builder = new StringBuilder(255); + AddSpecialCaseDetails(innerMostError); + if (SignatureInfo.Count == 0) + SignatureInfo.Add("NoStackingInformation", Guid.NewGuid().ToString()); - if (method == null) - return builder.ToString(); + UpdateInfo(false); + } - builder.Append(method.GetSignature()); + private void UpdateInfo(bool isUser) { + IsUser = isUser; + RecalculateHash(); + } + public void RecalculateHash() { + SignatureHash = SignatureInfo.Values.Any(v => v != null) ? SignatureInfo.Values.ToSHA1() : null; + } + + private string GetStackFrameSignature(Method method) { + var builder = new StringBuilder(255); + + if (method == null) return builder.ToString(); - } - private bool IsUserFrame(StackFrame frame) { - if (frame == null) - throw new ArgumentNullException(nameof(frame)); + builder.Append(method.GetSignature()); - if (frame.Name == null) - return false; + return builder.ToString(); + } - // Assume user method if no namespace - bool isEmptyNamespaceMethod = EmptyNamespaceIsUserMethod && frame.DeclaringNamespace.IsNullOrEmpty(); - if (!isEmptyNamespaceMethod) { - bool isUserNamespace = IsUserNamespace(frame.DeclaringNamespace); - if (!isUserNamespace) - return false; - } + private bool IsUserFrame(StackFrame frame) { + if (frame == null) + throw new ArgumentNullException(nameof(frame)); - return !UserCommonMethods.Any(frame.GetSignature().Contains); - } + if (frame.Name == null) + return false; - private bool IsUserNamespace(string ns) { - if (String.IsNullOrEmpty(ns)) + // Assume user method if no namespace + bool isEmptyNamespaceMethod = EmptyNamespaceIsUserMethod && frame.DeclaringNamespace.IsNullOrEmpty(); + if (!isEmptyNamespaceMethod) { + bool isUserNamespace = IsUserNamespace(frame.DeclaringNamespace); + if (!isUserNamespace) return false; + } - // if no user namespaces were set, return any non-system namespace as true - if (UserNamespaces == null || _userNamespaces.Count == 0) - return !_defaultNonUserNamespaces.Any(ns.StartsWith); + return !UserCommonMethods.Any(frame.GetSignature().Contains); + } - return UserNamespaces.Any(ns.StartsWith); - } + private bool IsUserNamespace(string ns) { + if (String.IsNullOrEmpty(ns)) + return false; - private void AddSpecialCaseDetails(InnerError error) { - if (!error.Data.ContainsKey(Error.KnownDataKeys.ExtraProperties)) - return; + // if no user namespaces were set, return any non-system namespace as true + if (UserNamespaces == null || _userNamespaces.Count == 0) + return !_defaultNonUserNamespaces.Any(ns.StartsWith); - var extraProperties = error.Data.GetValue>(Error.KnownDataKeys.ExtraProperties); - if (extraProperties == null) { - error.Data.Remove(Error.KnownDataKeys.ExtraProperties); - return; - } + return UserNamespaces.Any(ns.StartsWith); + } - if (extraProperties.TryGetValue("Number", out object value)) - SignatureInfo.Add("Number", value.ToString()); + private void AddSpecialCaseDetails(InnerError error) { + if (!error.Data.ContainsKey(Error.KnownDataKeys.ExtraProperties)) + return; - if (extraProperties.TryGetValue("ErrorCode", out value)) - SignatureInfo.Add("ErrorCode", value.ToString()); + var extraProperties = error.Data.GetValue>(Error.KnownDataKeys.ExtraProperties); + if (extraProperties == null) { + error.Data.Remove(Error.KnownDataKeys.ExtraProperties); + return; } + + if (extraProperties.TryGetValue("Number", out object value)) + SignatureInfo.Add("Number", value.ToString()); + + if (extraProperties.TryGetValue("ErrorCode", out value)) + SignatureInfo.Add("ErrorCode", value.ToString()); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/ExtensibleObject.cs b/src/Exceptionless.Core/Utility/ExtensibleObject.cs index d048dd0e56..b574d1cc31 100644 --- a/src/Exceptionless.Core/Utility/ExtensibleObject.cs +++ b/src/Exceptionless.Core/Utility/ExtensibleObject.cs @@ -1,79 +1,77 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; using Exceptionless.Core.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Utility { - public interface IExtensibleObject { - void SetProperty(string name, T value); +namespace Exceptionless.Core.Utility; - T GetProperty(string name); +public interface IExtensibleObject { + void SetProperty(string name, T value); - object GetProperty(string name); + T GetProperty(string name); - bool HasProperty(string name); + object GetProperty(string name); - void RemoveProperty(string name); + bool HasProperty(string name); - IEnumerable> GetProperties(); - } + void RemoveProperty(string name); - public class ExtensibleObject : INotifyPropertyChanged, IExtensibleObject { - public ExtensibleObject() { - _extendedData = new Dictionary(); - } + IEnumerable> GetProperties(); +} - [JsonProperty] - private readonly Dictionary _extendedData; +public class ExtensibleObject : INotifyPropertyChanged, IExtensibleObject { + public ExtensibleObject() { + _extendedData = new Dictionary(); + } - public void SetProperty(string name, T value) { - if (_extendedData.ContainsKey(name)) - _extendedData[name] = value; - else - _extendedData.Add(name, value); + [JsonProperty] + private readonly Dictionary _extendedData; - NotifyPropertyChanged(name); - } + public void SetProperty(string name, T value) { + if (_extendedData.ContainsKey(name)) + _extendedData[name] = value; + else + _extendedData.Add(name, value); - public T GetProperty(string name) { - object value = GetProperty(name); - if (value == null) - throw new InvalidOperationException($"Property value \"{name}\" is null. Can't use generic method on null values."); + NotifyPropertyChanged(name); + } - if (value is T) - return (T)value; + public T GetProperty(string name) { + object value = GetProperty(name); + if (value == null) + throw new InvalidOperationException($"Property value \"{name}\" is null. Can't use generic method on null values."); - if (value is JContainer) - return ((JContainer)value).ToObject(); + if (value is T) + return (T)value; - return value.ToType(); - } + if (value is JContainer) + return ((JContainer)value).ToObject(); - public object GetProperty(string name) { - return _extendedData.TryGetValue(name, out object value) ? value : null; - } + return value.ToType(); + } - public IEnumerable> GetProperties() { - return _extendedData; - } + public object GetProperty(string name) { + return _extendedData.TryGetValue(name, out object value) ? value : null; + } - public bool HasProperty(string name) { - return _extendedData.ContainsKey(name); - } + public IEnumerable> GetProperties() { + return _extendedData; + } - public void RemoveProperty(string name) { - if (_extendedData.ContainsKey(name)) { - _extendedData.Remove(name); - NotifyPropertyChanged(name); - } + public bool HasProperty(string name) { + return _extendedData.ContainsKey(name); + } + + public void RemoveProperty(string name) { + if (_extendedData.ContainsKey(name)) { + _extendedData.Remove(name); + NotifyPropertyChanged(name); } + } - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler PropertyChanged; - protected void NotifyPropertyChanged(string name) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - } + protected void NotifyPropertyChanged(string name) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/IConnectionMapping.cs b/src/Exceptionless.Core/Utility/IConnectionMapping.cs index 4ec21843d1..a61cefe70d 100644 --- a/src/Exceptionless.Core/Utility/IConnectionMapping.cs +++ b/src/Exceptionless.Core/Utility/IConnectionMapping.cs @@ -1,100 +1,98 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Exceptionless.Core.Utility { - public interface IConnectionMapping { - Task AddAsync(string key, string connectionId); - Task> GetConnectionsAsync(string key); - Task GetConnectionCountAsync(string key); - Task RemoveAsync(string key, string connectionId); - } - public class ConnectionMapping : IConnectionMapping { - private readonly ConcurrentDictionary> _connections = new ConcurrentDictionary>(); +namespace Exceptionless.Core.Utility; - public Task AddAsync(string key, string connectionId) { - if (key == null) - return Task.CompletedTask; +public interface IConnectionMapping { + Task AddAsync(string key, string connectionId); + Task> GetConnectionsAsync(string key); + Task GetConnectionCountAsync(string key); + Task RemoveAsync(string key, string connectionId); +} - _connections.AddOrUpdate(key, new HashSet(new[] { connectionId }), (_, hs) => { - hs.Add(connectionId); - return hs; - }); +public class ConnectionMapping : IConnectionMapping { + private readonly ConcurrentDictionary> _connections = new ConcurrentDictionary>(); + public Task AddAsync(string key, string connectionId) { + if (key == null) return Task.CompletedTask; - } - public Task> GetConnectionsAsync(string key) { - if (key == null) - return Task.FromResult>(new List()); + _connections.AddOrUpdate(key, new HashSet(new[] { connectionId }), (_, hs) => { + hs.Add(connectionId); + return hs; + }); - return Task.FromResult>(_connections.GetOrAdd(key, new HashSet())); - } + return Task.CompletedTask; + } - public Task GetConnectionCountAsync(string key) { - if (key == null) - return Task.FromResult(0); + public Task> GetConnectionsAsync(string key) { + if (key == null) + return Task.FromResult>(new List()); - if (_connections.TryGetValue(key, out var connections)) - return Task.FromResult(connections.Count); + return Task.FromResult>(_connections.GetOrAdd(key, new HashSet())); + } + public Task GetConnectionCountAsync(string key) { + if (key == null) return Task.FromResult(0); - } - public Task RemoveAsync(string key, string connectionId) { - if (key == null) - return Task.CompletedTask; + if (_connections.TryGetValue(key, out var connections)) + return Task.FromResult(connections.Count); - bool shouldRemove = false; - _connections.AddOrUpdate(key, new HashSet(), (_, hs) => { - hs.Remove(connectionId); - if (hs.Count == 0) - shouldRemove = true; + return Task.FromResult(0); + } - return hs; - }); + public Task RemoveAsync(string key, string connectionId) { + if (key == null) + return Task.CompletedTask; - if (!shouldRemove) - return Task.CompletedTask; + bool shouldRemove = false; + _connections.AddOrUpdate(key, new HashSet(), (_, hs) => { + hs.Remove(connectionId); + if (hs.Count == 0) + shouldRemove = true; - if (_connections.TryRemove(key, out var connections) && connections.Count > 0) - _connections.TryAdd(key, connections); + return hs; + }); + if (!shouldRemove) return Task.CompletedTask; - } + + if (_connections.TryRemove(key, out var connections) && connections.Count > 0) + _connections.TryAdd(key, connections); + + return Task.CompletedTask; + } +} + +public static class ConnectionMappingExtensions { + public const string UserIdPrefix = "u-"; + public const string GroupPrefix = "g-"; + + public static Task GroupAddAsync(this IConnectionMapping map, string group, string connectionId) { + return map.AddAsync(GroupPrefix + group, connectionId); + } + + public static Task GroupRemoveAsync(this IConnectionMapping map, string group, string connectionId) { + return map.RemoveAsync(GroupPrefix + group, connectionId); + } + + public static Task> GetGroupConnectionsAsync(this IConnectionMapping map, string group) { + return map.GetConnectionsAsync(GroupPrefix + group); + } + + public static Task GetGroupConnectionCountAsync(this IConnectionMapping map, string group) { + return map.GetConnectionCountAsync(GroupPrefix + group); + } + + public static Task UserIdAddAsync(this IConnectionMapping map, string userId, string connectionId) { + return map.AddAsync(UserIdPrefix + userId, connectionId); + } + + public static Task UserIdRemoveAsync(this IConnectionMapping map, string userId, string connectionId) { + return map.RemoveAsync(UserIdPrefix + userId, connectionId); } - public static class ConnectionMappingExtensions { - public const string UserIdPrefix = "u-"; - public const string GroupPrefix = "g-"; - - public static Task GroupAddAsync(this IConnectionMapping map, string group, string connectionId) { - return map.AddAsync(GroupPrefix + group, connectionId); - } - - public static Task GroupRemoveAsync(this IConnectionMapping map, string group, string connectionId) { - return map.RemoveAsync(GroupPrefix + group, connectionId); - } - - public static Task> GetGroupConnectionsAsync(this IConnectionMapping map, string group) { - return map.GetConnectionsAsync(GroupPrefix + group); - } - - public static Task GetGroupConnectionCountAsync(this IConnectionMapping map, string group) { - return map.GetConnectionCountAsync(GroupPrefix + group); - } - - public static Task UserIdAddAsync(this IConnectionMapping map, string userId, string connectionId) { - return map.AddAsync(UserIdPrefix + userId, connectionId); - } - - public static Task UserIdRemoveAsync(this IConnectionMapping map, string userId, string connectionId) { - return map.RemoveAsync(UserIdPrefix + userId, connectionId); - } - - public static Task> GetUserIdConnectionsAsync(this IConnectionMapping map, string userId) { - return map.GetConnectionsAsync(UserIdPrefix + userId); - } + public static Task> GetUserIdConnectionsAsync(this IConnectionMapping map, string userId) { + return map.GetConnectionsAsync(UserIdPrefix + userId); } } diff --git a/src/Exceptionless.Core/Utility/IConnectionString.cs b/src/Exceptionless.Core/Utility/IConnectionString.cs index cb4371c8ee..ec92991dc9 100644 --- a/src/Exceptionless.Core/Utility/IConnectionString.cs +++ b/src/Exceptionless.Core/Utility/IConnectionString.cs @@ -1,21 +1,21 @@ -namespace Exceptionless.Core.Utility { - public interface IConnectionString { - string ConnectionString { get; } - } +namespace Exceptionless.Core.Utility; - public interface IElasticsearchConnectionString : IConnectionString { - bool EnableMapperSizePlugin { get; } - int FieldsLimit { get; } - int NumberOfReplicas { get; } - int NumberOfShards { get; } - string ServerUrl { get; } - } +public interface IConnectionString { + string ConnectionString { get; } +} - public class DefaultConnectionString : IConnectionString { - public DefaultConnectionString(string connectionString) { - ConnectionString = connectionString; - } +public interface IElasticsearchConnectionString : IConnectionString { + bool EnableMapperSizePlugin { get; } + int FieldsLimit { get; } + int NumberOfReplicas { get; } + int NumberOfShards { get; } + string ServerUrl { get; } +} - public string ConnectionString { get; } +public class DefaultConnectionString : IConnectionString { + public DefaultConnectionString(string connectionString) { + ConnectionString = connectionString; } + + public string ConnectionString { get; } } diff --git a/src/Exceptionless.Core/Utility/LastReferenceIdManager.cs b/src/Exceptionless.Core/Utility/LastReferenceIdManager.cs index 20273d38cd..50fe711357 100644 --- a/src/Exceptionless.Core/Utility/LastReferenceIdManager.cs +++ b/src/Exceptionless.Core/Utility/LastReferenceIdManager.cs @@ -1,11 +1,11 @@ -namespace Exceptionless.Core.Utility { - public class NullCoreLastReferenceIdManager : ICoreLastReferenceIdManager { - public string GetLastReferenceId() { - return null; - } - } +namespace Exceptionless.Core.Utility; - public interface ICoreLastReferenceIdManager { - string GetLastReferenceId(); +public class NullCoreLastReferenceIdManager : ICoreLastReferenceIdManager { + public string GetLastReferenceId() { + return null; } } + +public interface ICoreLastReferenceIdManager { + string GetLastReferenceId(); +} diff --git a/src/Exceptionless.Core/Utility/MetricNames.cs b/src/Exceptionless.Core/Utility/MetricNames.cs index 8375c66ac8..d152d8c040 100644 --- a/src/Exceptionless.Core/Utility/MetricNames.cs +++ b/src/Exceptionless.Core/Utility/MetricNames.cs @@ -1,34 +1,34 @@ -namespace Exceptionless.Core.AppStats { - public static class MetricNames { - public const string EventsSubmitted = "events.submitted"; - public const string EventsProcessed = "events.processed"; - public const string EventsProcessingTime = "events.processingtime"; - public const string EventsPaidProcessed = "events.paid.processed"; - public const string EventsProcessErrors = "events.processing.errors"; - public const string EventsDiscarded = "events.discarded"; - public const string EventsProcessCancelled = "events.processing.cancelled"; - public const string EventsRetryCount = "events.retry.count"; - public const string EventsRetryErrors = "events.retry.errors"; - public const string EventsFieldCount = "events.field.count"; +namespace Exceptionless.Core.AppStats; - public const string PostsParsed = "posts.parsed"; - public const string PostsEventCount = "posts.eventcount"; - public const string PostsSize = "posts.size"; - public const string PostsParseErrors = "posts.parse.errors"; - public const string PostsMarkFileActiveTime = "posts.markfileactivetime"; - public const string PostsParsingTime = "posts.parsingtime"; - public const string PostsRetryTime = "posts.retrytime"; - public const string PostsAbandonTime = "posts.abandontime"; - public const string PostsCompleteTime = "posts.completetime"; - public const string PostsDiscarded = "posts.discarded"; - public const string PostsBlocked = "posts.blocked"; +public static class MetricNames { + public const string EventsSubmitted = "events.submitted"; + public const string EventsProcessed = "events.processed"; + public const string EventsProcessingTime = "events.processingtime"; + public const string EventsPaidProcessed = "events.paid.processed"; + public const string EventsProcessErrors = "events.processing.errors"; + public const string EventsDiscarded = "events.discarded"; + public const string EventsProcessCancelled = "events.processing.cancelled"; + public const string EventsRetryCount = "events.retry.count"; + public const string EventsRetryErrors = "events.retry.errors"; + public const string EventsFieldCount = "events.field.count"; - public const string PostsMessageSize = "posts.message.size"; - public const string PostsCompressedSize = "posts.compressed.size"; - public const string PostsUncompressedSize = "posts.uncompressed.size"; - public const string PostsDecompressionTime = "posts.decompression.time"; - public const string PostsDecompressionErrors = "posts.decompression.errors"; + public const string PostsParsed = "posts.parsed"; + public const string PostsEventCount = "posts.eventcount"; + public const string PostsSize = "posts.size"; + public const string PostsParseErrors = "posts.parse.errors"; + public const string PostsMarkFileActiveTime = "posts.markfileactivetime"; + public const string PostsParsingTime = "posts.parsingtime"; + public const string PostsRetryTime = "posts.retrytime"; + public const string PostsAbandonTime = "posts.abandontime"; + public const string PostsCompleteTime = "posts.completetime"; + public const string PostsDiscarded = "posts.discarded"; + public const string PostsBlocked = "posts.blocked"; - public const string UsageGeocodingApi = "usage.geocoding"; - } -} \ No newline at end of file + public const string PostsMessageSize = "posts.message.size"; + public const string PostsCompressedSize = "posts.compressed.size"; + public const string PostsUncompressedSize = "posts.uncompressed.size"; + public const string PostsDecompressionTime = "posts.decompression.time"; + public const string PostsDecompressionErrors = "posts.decompression.errors"; + + public const string UsageGeocodingApi = "usage.geocoding"; +} diff --git a/src/Exceptionless.Core/Utility/PathHelper.cs b/src/Exceptionless.Core/Utility/PathHelper.cs index 60907fac2c..51ac566b06 100644 --- a/src/Exceptionless.Core/Utility/PathHelper.cs +++ b/src/Exceptionless.Core/Utility/PathHelper.cs @@ -1,62 +1,59 @@ -using System; -using System.IO; +namespace Exceptionless.Core.Utility; -namespace Exceptionless.Core.Utility { - public static class PathHelper { - private const string DATA_DIRECTORY = "|DataDirectory|"; +public static class PathHelper { + private const string DATA_DIRECTORY = "|DataDirectory|"; - /// - /// Expand the path, resolving the |DataDirectory| macro as appropriate. - /// - /// The path to expand - /// The expanded path - public static string ExpandPath(string path) { - if (String.IsNullOrEmpty(path)) - return path; + /// + /// Expand the path, resolving the |DataDirectory| macro as appropriate. + /// + /// The path to expand + /// The expanded path + public static string ExpandPath(string path) { + if (String.IsNullOrEmpty(path)) + return path; - path = path.Replace('\\', Path.DirectorySeparatorChar); - path = path.Replace('/', Path.DirectorySeparatorChar); + path = path.Replace('\\', Path.DirectorySeparatorChar); + path = path.Replace('/', Path.DirectorySeparatorChar); - if (!path.StartsWith(DATA_DIRECTORY, StringComparison.OrdinalIgnoreCase)) - return Path.GetFullPath(path); + if (!path.StartsWith(DATA_DIRECTORY, StringComparison.OrdinalIgnoreCase)) + return Path.GetFullPath(path); - string dataDirectory = GetDataDirectory(); - if (String.IsNullOrEmpty(dataDirectory)) - return path; + string dataDirectory = GetDataDirectory(); + if (String.IsNullOrEmpty(dataDirectory)) + return path; - int length = DATA_DIRECTORY.Length; + int length = DATA_DIRECTORY.Length; - if (path.Length <= length) - return dataDirectory; + if (path.Length <= length) + return dataDirectory; - string relativePath = path.Substring(length); - char c = relativePath[0]; + string relativePath = path.Substring(length); + char c = relativePath[0]; - if (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar) - relativePath = relativePath.Substring(1); + if (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar) + relativePath = relativePath.Substring(1); - string fullPath = Path.Combine(dataDirectory, relativePath); - fullPath = Path.GetFullPath(fullPath); + string fullPath = Path.Combine(dataDirectory, relativePath); + fullPath = Path.GetFullPath(fullPath); - return fullPath; - } + return fullPath; + } - /// - /// Gets the data directory for the |DataDirectory| macro. - /// - /// The DataDirectory path. - public static string GetDataDirectory() { - string dataDirectory = Environment.GetEnvironmentVariable("WEBROOT_PATH"); - if (!String.IsNullOrEmpty(dataDirectory)) - dataDirectory = Path.Combine(dataDirectory, "App_Data"); + /// + /// Gets the data directory for the |DataDirectory| macro. + /// + /// The DataDirectory path. + public static string GetDataDirectory() { + string dataDirectory = Environment.GetEnvironmentVariable("WEBROOT_PATH"); + if (!String.IsNullOrEmpty(dataDirectory)) + dataDirectory = Path.Combine(dataDirectory, "App_Data"); - if (String.IsNullOrEmpty(dataDirectory)) - dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory") as string; + if (String.IsNullOrEmpty(dataDirectory)) + dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory") as string; - if (String.IsNullOrEmpty(dataDirectory)) - dataDirectory = AppContext.BaseDirectory; + if (String.IsNullOrEmpty(dataDirectory)) + dataDirectory = AppContext.BaseDirectory; - return !String.IsNullOrEmpty(dataDirectory) ? Path.GetFullPath(dataDirectory) : null; - } + return !String.IsNullOrEmpty(dataDirectory) ? Path.GetFullPath(dataDirectory) : null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Reflection/DelegateFactory.cs b/src/Exceptionless.Core/Utility/Reflection/DelegateFactory.cs index 0b85f875b7..39c3ef7ad2 100644 --- a/src/Exceptionless.Core/Utility/Reflection/DelegateFactory.cs +++ b/src/Exceptionless.Core/Utility/Reflection/DelegateFactory.cs @@ -1,171 +1,158 @@ -using System; -using System.Reflection; +using System.Reflection; using System.Reflection.Emit; using Exceptionless.Core.Extensions; -namespace Exceptionless.Core.Reflection -{ - public delegate object LateBoundMethod(object target, params object[] arguments); - public delegate object LateBoundGet(object target); - public delegate void LateBoundSet(object target, object value); - public delegate object LateBoundConstructor(); - - public static class DelegateFactory - { - private static DynamicMethod CreateDynamicMethod(string name, Type returnType, Type[] parameterTypes, Type owner) - { - var dynamicMethod = !owner.IsInterface - ? new DynamicMethod(name, returnType, parameterTypes, owner, true) - : new DynamicMethod(name, returnType, parameterTypes, owner.Assembly.ManifestModule, true); - - return dynamicMethod; - } +namespace Exceptionless.Core.Reflection; - public static LateBoundMethod CreateMethod(MethodBase method) - { - var dynamicMethod = CreateDynamicMethod(method.ToString(), typeof(object), new[] { typeof(object), typeof(object[]) }, method.DeclaringType); - var generator = dynamicMethod.GetILGenerator(); +public delegate object LateBoundMethod(object target, params object[] arguments); +public delegate object LateBoundGet(object target); +public delegate void LateBoundSet(object target, object value); +public delegate object LateBoundConstructor(); - var args = method.GetParameters(); +public static class DelegateFactory { + private static DynamicMethod CreateDynamicMethod(string name, Type returnType, Type[] parameterTypes, Type owner) { + var dynamicMethod = !owner.IsInterface + ? new DynamicMethod(name, returnType, parameterTypes, owner, true) + : new DynamicMethod(name, returnType, parameterTypes, owner.Assembly.ManifestModule, true); - var argsOk = generator.DefineLabel(); + return dynamicMethod; + } - generator.Emit(OpCodes.Ldarg_1); - generator.Emit(OpCodes.Ldlen); - generator.Emit(OpCodes.Ldc_I4, args.Length); - generator.Emit(OpCodes.Beq, argsOk); + public static LateBoundMethod CreateMethod(MethodBase method) { + var dynamicMethod = CreateDynamicMethod(method.ToString(), typeof(object), new[] { typeof(object), typeof(object[]) }, method.DeclaringType); + var generator = dynamicMethod.GetILGenerator(); + + var args = method.GetParameters(); + + var argsOk = generator.DefineLabel(); + + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Ldlen); + generator.Emit(OpCodes.Ldc_I4, args.Length); + generator.Emit(OpCodes.Beq, argsOk); - generator.Emit(OpCodes.Newobj, typeof(TargetParameterCountException).GetConstructor(Type.EmptyTypes)); - generator.Emit(OpCodes.Throw); + generator.Emit(OpCodes.Newobj, typeof(TargetParameterCountException).GetConstructor(Type.EmptyTypes)); + generator.Emit(OpCodes.Throw); - generator.MarkLabel(argsOk); + generator.MarkLabel(argsOk); - if (!method.IsConstructor && !method.IsStatic) - generator.PushInstance(method.DeclaringType); + if (!method.IsConstructor && !method.IsStatic) + generator.PushInstance(method.DeclaringType); - for (int i = 0; i < args.Length; i++) - { - generator.Emit(OpCodes.Ldarg_1); - generator.Emit(OpCodes.Ldc_I4, i); - generator.Emit(OpCodes.Ldelem_Ref); + for (int i = 0; i < args.Length; i++) { + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Ldc_I4, i); + generator.Emit(OpCodes.Ldelem_Ref); + + generator.UnboxIfNeeded(args[i].ParameterType); + } - generator.UnboxIfNeeded(args[i].ParameterType); - } + if (method.IsConstructor) + generator.Emit(OpCodes.Newobj, (ConstructorInfo)method); + else if (method.IsFinal || !method.IsVirtual) + generator.CallMethod((MethodInfo)method); - if (method.IsConstructor) - generator.Emit(OpCodes.Newobj, (ConstructorInfo)method); - else if (method.IsFinal || !method.IsVirtual) - generator.CallMethod((MethodInfo)method); + var returnType = method.IsConstructor + ? method.DeclaringType + : ((MethodInfo)method).ReturnType; - var returnType = method.IsConstructor - ? method.DeclaringType - : ((MethodInfo)method).ReturnType; + if (returnType != typeof(void)) + generator.BoxIfNeeded(returnType); + else + generator.Emit(OpCodes.Ldnull); - if (returnType != typeof(void)) - generator.BoxIfNeeded(returnType); - else - generator.Emit(OpCodes.Ldnull); + generator.Return(); - generator.Return(); + return (LateBoundMethod)dynamicMethod.CreateDelegate(typeof(LateBoundMethod)); + } + + public static LateBoundConstructor CreateConstructor(Type type) { + var dynamicMethod = CreateDynamicMethod("Create" + type.FullName, typeof(object), Type.EmptyTypes, type); + dynamicMethod.InitLocals = true; + var generator = dynamicMethod.GetILGenerator(); - return (LateBoundMethod)dynamicMethod.CreateDelegate(typeof(LateBoundMethod)); + if (type.IsValueType) { + generator.DeclareLocal(type); + generator.Emit(OpCodes.Ldloc_0); + generator.Emit(OpCodes.Box, type); } + else { + var constructorInfo = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null); + if (constructorInfo == null) + throw new InvalidOperationException($"Could not get constructor for {type}."); - public static LateBoundConstructor CreateConstructor(Type type) - { - var dynamicMethod = CreateDynamicMethod("Create" + type.FullName, typeof(object), Type.EmptyTypes, type); - dynamicMethod.InitLocals = true; - var generator = dynamicMethod.GetILGenerator(); - - if (type.IsValueType) - { - generator.DeclareLocal(type); - generator.Emit(OpCodes.Ldloc_0); - generator.Emit(OpCodes.Box, type); - } - else - { - var constructorInfo = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null); - if (constructorInfo == null) - throw new InvalidOperationException($"Could not get constructor for {type}."); - - generator.Emit(OpCodes.Newobj, constructorInfo); - } - - generator.Return(); - - return (LateBoundConstructor)dynamicMethod.CreateDelegate(typeof(LateBoundConstructor)); + generator.Emit(OpCodes.Newobj, constructorInfo); } - public static LateBoundGet CreateGet(PropertyInfo propertyInfo) - { - var getMethod = propertyInfo.GetGetMethod(true); - if (getMethod == null) - throw new InvalidOperationException($"Property '{propertyInfo.Name}' does not have a getter."); + generator.Return(); - var dynamicMethod = CreateDynamicMethod("Get" + propertyInfo.Name, typeof(object), new[] { typeof(object) }, propertyInfo.DeclaringType); - var generator = dynamicMethod.GetILGenerator(); + return (LateBoundConstructor)dynamicMethod.CreateDelegate(typeof(LateBoundConstructor)); + } - if (!getMethod.IsStatic) - generator.PushInstance(propertyInfo.DeclaringType); + public static LateBoundGet CreateGet(PropertyInfo propertyInfo) { + var getMethod = propertyInfo.GetGetMethod(true); + if (getMethod == null) + throw new InvalidOperationException($"Property '{propertyInfo.Name}' does not have a getter."); - generator.CallMethod(getMethod); - generator.BoxIfNeeded(propertyInfo.PropertyType); - generator.Return(); + var dynamicMethod = CreateDynamicMethod("Get" + propertyInfo.Name, typeof(object), new[] { typeof(object) }, propertyInfo.DeclaringType); + var generator = dynamicMethod.GetILGenerator(); - return (LateBoundGet)dynamicMethod.CreateDelegate(typeof(LateBoundGet)); - } + if (!getMethod.IsStatic) + generator.PushInstance(propertyInfo.DeclaringType); - public static LateBoundGet CreateGet(FieldInfo fieldInfo) - { - var dynamicMethod = CreateDynamicMethod("Get" + fieldInfo.Name, typeof(object), new[] { typeof(object) }, fieldInfo.DeclaringType); + generator.CallMethod(getMethod); + generator.BoxIfNeeded(propertyInfo.PropertyType); + generator.Return(); - var generator = dynamicMethod.GetILGenerator(); + return (LateBoundGet)dynamicMethod.CreateDelegate(typeof(LateBoundGet)); + } - if (!fieldInfo.IsStatic) - generator.PushInstance(fieldInfo.DeclaringType); + public static LateBoundGet CreateGet(FieldInfo fieldInfo) { + var dynamicMethod = CreateDynamicMethod("Get" + fieldInfo.Name, typeof(object), new[] { typeof(object) }, fieldInfo.DeclaringType); - generator.Emit(OpCodes.Ldfld, fieldInfo); - generator.BoxIfNeeded(fieldInfo.FieldType); - generator.Return(); + var generator = dynamicMethod.GetILGenerator(); - return (LateBoundGet)dynamicMethod.CreateDelegate(typeof(LateBoundGet)); - } + if (!fieldInfo.IsStatic) + generator.PushInstance(fieldInfo.DeclaringType); - public static LateBoundSet CreateSet(PropertyInfo propertyInfo) - { - var setMethod = propertyInfo.GetSetMethod(true); - if (setMethod == null) - throw new InvalidOperationException($"Property '{propertyInfo.Name}' does not have a setter."); + generator.Emit(OpCodes.Ldfld, fieldInfo); + generator.BoxIfNeeded(fieldInfo.FieldType); + generator.Return(); - var dynamicMethod = CreateDynamicMethod("Set" + propertyInfo.Name, null, new[] { typeof(object), typeof(object) }, propertyInfo.DeclaringType); - var generator = dynamicMethod.GetILGenerator(); + return (LateBoundGet)dynamicMethod.CreateDelegate(typeof(LateBoundGet)); + } - if (!setMethod.IsStatic) - generator.PushInstance(propertyInfo.DeclaringType); + public static LateBoundSet CreateSet(PropertyInfo propertyInfo) { + var setMethod = propertyInfo.GetSetMethod(true); + if (setMethod == null) + throw new InvalidOperationException($"Property '{propertyInfo.Name}' does not have a setter."); - generator.Emit(OpCodes.Ldarg_1); - generator.UnboxIfNeeded(propertyInfo.PropertyType); - generator.CallMethod(setMethod); - generator.Return(); + var dynamicMethod = CreateDynamicMethod("Set" + propertyInfo.Name, null, new[] { typeof(object), typeof(object) }, propertyInfo.DeclaringType); + var generator = dynamicMethod.GetILGenerator(); - return (LateBoundSet)dynamicMethod.CreateDelegate(typeof(LateBoundSet)); - } + if (!setMethod.IsStatic) + generator.PushInstance(propertyInfo.DeclaringType); - public static LateBoundSet CreateSet(FieldInfo fieldInfo) - { - var dynamicMethod = CreateDynamicMethod("Set" + fieldInfo.Name, null, new[] { typeof(object), typeof(object) }, fieldInfo.DeclaringType); - var generator = dynamicMethod.GetILGenerator(); + generator.Emit(OpCodes.Ldarg_1); + generator.UnboxIfNeeded(propertyInfo.PropertyType); + generator.CallMethod(setMethod); + generator.Return(); - if (!fieldInfo.IsStatic) - generator.PushInstance(fieldInfo.DeclaringType); + return (LateBoundSet)dynamicMethod.CreateDelegate(typeof(LateBoundSet)); + } - generator.Emit(OpCodes.Ldarg_1); - generator.UnboxIfNeeded(fieldInfo.FieldType); - generator.Emit(OpCodes.Stfld, fieldInfo); - generator.Return(); + public static LateBoundSet CreateSet(FieldInfo fieldInfo) { + var dynamicMethod = CreateDynamicMethod("Set" + fieldInfo.Name, null, new[] { typeof(object), typeof(object) }, fieldInfo.DeclaringType); + var generator = dynamicMethod.GetILGenerator(); - return (LateBoundSet)dynamicMethod.CreateDelegate(typeof(LateBoundSet)); - } + if (!fieldInfo.IsStatic) + generator.PushInstance(fieldInfo.DeclaringType); + + generator.Emit(OpCodes.Ldarg_1); + generator.UnboxIfNeeded(fieldInfo.FieldType); + generator.Emit(OpCodes.Stfld, fieldInfo); + generator.Return(); + + return (LateBoundSet)dynamicMethod.CreateDelegate(typeof(LateBoundSet)); } } diff --git a/src/Exceptionless.Core/Utility/Reflection/FieldAccessor.cs b/src/Exceptionless.Core/Utility/Reflection/FieldAccessor.cs index 712536ac4f..3413646464 100644 --- a/src/Exceptionless.Core/Utility/Reflection/FieldAccessor.cs +++ b/src/Exceptionless.Core/Utility/Reflection/FieldAccessor.cs @@ -1,106 +1,100 @@ -using System; -using System.Reflection; +using System.Reflection; #if PFX_LEGACY_3_5 using CodeSmith.Core.Threading; #endif -namespace Exceptionless.Core.Reflection -{ +namespace Exceptionless.Core.Reflection; + +/// +/// An accessor class for . +/// +internal class FieldAccessor : MemberAccessor { + private readonly FieldInfo _fieldInfo; + private readonly string _name; + private readonly bool _hasGetter; + private readonly bool _hasSetter; + private readonly Type _memberType; + private readonly Lazy _lateBoundGet; + private readonly Lazy _lateBoundSet; + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use for this accessor. + public FieldAccessor(FieldInfo fieldInfo) { + _fieldInfo = fieldInfo; + _name = fieldInfo.Name; + _memberType = fieldInfo.FieldType; + + _hasGetter = true; + _lateBoundGet = new Lazy(() => DelegateFactory.CreateGet(_fieldInfo)); + + _hasSetter = !fieldInfo.IsInitOnly && !fieldInfo.IsLiteral; + if (_hasSetter) + _lateBoundSet = new Lazy(() => DelegateFactory.CreateSet(_fieldInfo)); + } + /// - /// An accessor class for . + /// Gets the type of the member. /// - internal class FieldAccessor : MemberAccessor - { - private readonly FieldInfo _fieldInfo; - private readonly string _name; - private readonly bool _hasGetter; - private readonly bool _hasSetter; - private readonly Type _memberType; - private readonly Lazy _lateBoundGet; - private readonly Lazy _lateBoundSet; - - /// - /// Initializes a new instance of the class. - /// - /// The instance to use for this accessor. - public FieldAccessor(FieldInfo fieldInfo) - { - _fieldInfo = fieldInfo; - _name = fieldInfo.Name; - _memberType = fieldInfo.FieldType; - - _hasGetter = true; - _lateBoundGet = new Lazy(() => DelegateFactory.CreateGet(_fieldInfo)); - - _hasSetter = !fieldInfo.IsInitOnly && !fieldInfo.IsLiteral; - if (_hasSetter) - _lateBoundSet = new Lazy(() => DelegateFactory.CreateSet(_fieldInfo)); - } - - /// - /// Gets the type of the member. - /// - /// The type of the member. - public override Type MemberType => _memberType; - - /// - /// Gets the member info. - /// - /// The member info. - public override MemberInfo MemberInfo => _fieldInfo; - - /// - /// Gets the name of the member. - /// - /// The name of the member. - public override string Name => _name; - - /// - /// Gets a value indicating whether this member has getter. - /// - /// true if this member has getter; otherwise, false. - public override bool HasGetter => _hasGetter; - - /// - /// Gets a value indicating whether this member has setter. - /// - /// true if this member has setter; otherwise, false. - public override bool HasSetter => _hasSetter; - - /// - /// Returns the value of the member. - /// - /// The object whose member value will be returned. - /// - /// The member value for the instance parameter. - /// - public override object GetValue(object instance) - { - if (_lateBoundGet == null || !HasGetter) - throw new InvalidOperationException($"Field '{Name}' does not have a getter."); - - var get = _lateBoundGet.Value; - if (get == null) - throw new InvalidOperationException($"Field '{Name}' does not have a getter."); - - return get(instance); - } - - /// - /// Sets the value of the member. - /// - /// The object whose member value will be set. - /// The new value for this member. - public override void SetValue(object instance, object value) - { - if (_lateBoundSet == null || !HasSetter) - throw new InvalidOperationException($"Field '{Name}' does not have a setter."); - - var set = _lateBoundSet.Value; - if (set == null) - throw new InvalidOperationException($"Field '{Name}' does not have a setter."); - - set(instance, value); - } + /// The type of the member. + public override Type MemberType => _memberType; + + /// + /// Gets the member info. + /// + /// The member info. + public override MemberInfo MemberInfo => _fieldInfo; + + /// + /// Gets the name of the member. + /// + /// The name of the member. + public override string Name => _name; + + /// + /// Gets a value indicating whether this member has getter. + /// + /// true if this member has getter; otherwise, false. + public override bool HasGetter => _hasGetter; + + /// + /// Gets a value indicating whether this member has setter. + /// + /// true if this member has setter; otherwise, false. + public override bool HasSetter => _hasSetter; + + /// + /// Returns the value of the member. + /// + /// The object whose member value will be returned. + /// + /// The member value for the instance parameter. + /// + public override object GetValue(object instance) { + if (_lateBoundGet == null || !HasGetter) + throw new InvalidOperationException($"Field '{Name}' does not have a getter."); + + var get = _lateBoundGet.Value; + if (get == null) + throw new InvalidOperationException($"Field '{Name}' does not have a getter."); + + return get(instance); + } + + /// + /// Sets the value of the member. + /// + /// The object whose member value will be set. + /// The new value for this member. + public override void SetValue(object instance, object value) { + if (_lateBoundSet == null || !HasSetter) + throw new InvalidOperationException($"Field '{Name}' does not have a setter."); + + var set = _lateBoundSet.Value; + if (set == null) + throw new InvalidOperationException($"Field '{Name}' does not have a setter."); + + set(instance, value); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Reflection/IMemberAccessor.cs b/src/Exceptionless.Core/Utility/Reflection/IMemberAccessor.cs index 030f5f3fba..23884f274a 100644 --- a/src/Exceptionless.Core/Utility/Reflection/IMemberAccessor.cs +++ b/src/Exceptionless.Core/Utility/Reflection/IMemberAccessor.cs @@ -1,55 +1,52 @@ -using System; -using System.Reflection; +using System.Reflection; -namespace Exceptionless.Core.Reflection -{ +namespace Exceptionless.Core.Reflection; + +/// +/// An interface for member accessors. +/// +public interface IMemberAccessor { + /// + /// Gets the type of the member. + /// + /// The type of the member. + Type MemberType { get; } + /// + /// Gets the member info. + /// + /// The member info. + MemberInfo MemberInfo { get; } + /// + /// Gets the name of the member. + /// + /// The name of the member. + string Name { get; } /// - /// An interface for member accessors. + /// Gets a value indicating whether this member has getter. /// - public interface IMemberAccessor - { - /// - /// Gets the type of the member. - /// - /// The type of the member. - Type MemberType { get; } - /// - /// Gets the member info. - /// - /// The member info. - MemberInfo MemberInfo { get; } - /// - /// Gets the name of the member. - /// - /// The name of the member. - string Name { get; } - /// - /// Gets a value indicating whether this member has getter. - /// - /// - /// true if this member has getter; otherwise, false. - /// - bool HasGetter { get; } - /// - /// Gets a value indicating whether this member has setter. - /// - /// - /// true if this member has setter; otherwise, false. - /// - bool HasSetter { get; } + /// + /// true if this member has getter; otherwise, false. + /// + bool HasGetter { get; } + /// + /// Gets a value indicating whether this member has setter. + /// + /// + /// true if this member has setter; otherwise, false. + /// + bool HasSetter { get; } - /// - /// Returns the value of the member. - /// - /// The object whose member value will be returned. - /// The member value for the instance parameter. - object GetValue(object instance); + /// + /// Returns the value of the member. + /// + /// The object whose member value will be returned. + /// The member value for the instance parameter. + object GetValue(object instance); - /// - /// Sets the value of the member. - /// - /// The object whose member value will be set. - /// The new value for this member. - void SetValue(object instance, object value); - } -} \ No newline at end of file + /// + /// Sets the value of the member. + /// + /// The object whose member value will be set. + /// The new value for this member. + void SetValue(object instance, object value); +} diff --git a/src/Exceptionless.Core/Utility/Reflection/LateBinder.cs b/src/Exceptionless.Core/Utility/Reflection/LateBinder.cs index c510cc7ea6..792038d627 100644 --- a/src/Exceptionless.Core/Utility/Reflection/LateBinder.cs +++ b/src/Exceptionless.Core/Utility/Reflection/LateBinder.cs @@ -1,270 +1,252 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Reflection; -namespace Exceptionless.Core.Reflection -{ +namespace Exceptionless.Core.Reflection; + +/// +/// A class for late bound operations on a type. +/// +public static class LateBinder { + private static readonly ConcurrentDictionary _accessorCache = new ConcurrentDictionary(); + /// - /// A class for late bound operations on a type. + /// Searches for the public property with the specified name. /// - public static class LateBinder - { - private static readonly ConcurrentDictionary _accessorCache = new ConcurrentDictionary(); - - /// - /// Searches for the public property with the specified name. - /// - /// The to search for the property in. - /// The name of the property to find. - /// - /// An instance for the property if found; otherwise null. - /// - public static IMemberAccessor FindProperty(Type type, string name) - { - return FindProperty(type, name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + /// The to search for the property in. + /// The name of the property to find. + /// + /// An instance for the property if found; otherwise null. + /// + public static IMemberAccessor FindProperty(Type type, string name) { + return FindProperty(type, name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + } + + /// + /// Searches for the specified property, using the specified binding constraints. + /// + /// The to search for the property in. + /// The name of the property to find. + /// A bitmask comprised of one or more that specify how the search is conducted. + /// + /// An instance for the property if found; otherwise null. + /// + public static IMemberAccessor FindProperty(Type type, string name, BindingFlags flags) { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var currentType = type; + TypeAccessor typeAccessor; + IMemberAccessor memberAccessor = null; + + // support nested property + string[] parts = name.Split('.'); + foreach (string part in parts) { + if (memberAccessor != null) + currentType = memberAccessor.MemberType; + + typeAccessor = GetAccessor(currentType); + memberAccessor = typeAccessor.FindProperty(part, flags); } - /// - /// Searches for the specified property, using the specified binding constraints. - /// - /// The to search for the property in. - /// The name of the property to find. - /// A bitmask comprised of one or more that specify how the search is conducted. - /// - /// An instance for the property if found; otherwise null. - /// - public static IMemberAccessor FindProperty(Type type, string name, BindingFlags flags) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - - var currentType = type; - TypeAccessor typeAccessor; - IMemberAccessor memberAccessor = null; - - // support nested property - string[] parts = name.Split('.'); - foreach (string part in parts) - { - if (memberAccessor != null) - currentType = memberAccessor.MemberType; - - typeAccessor = GetAccessor(currentType); - memberAccessor = typeAccessor.FindProperty(part, flags); - } + return memberAccessor; + } - return memberAccessor; - } + /// + /// Searches for the specified property, using the specified binding constraints. + /// + /// The property to create an accessor for. + /// + /// An instance for the property. + /// + public static IMemberAccessor GetPropertyAccessor(PropertyInfo property) { + if (property == null) + throw new ArgumentNullException(nameof(property)); + + return new PropertyAccessor(property); + } - /// - /// Searches for the specified property, using the specified binding constraints. - /// - /// The property to create an accessor for. - /// - /// An instance for the property. - /// - public static IMemberAccessor GetPropertyAccessor(PropertyInfo property) { - if (property == null) - throw new ArgumentNullException(nameof(property)); - - return new PropertyAccessor(property); - } + /// + /// Searches for the specified property, using the specified binding constraints. + /// + /// The field to create an accessor for. + /// + /// An instance for the property. + /// + public static IMemberAccessor GetFieldAccessor(FieldInfo field) { + if (field == null) + throw new ArgumentNullException(nameof(field)); + + return new FieldAccessor(field); + } - /// - /// Searches for the specified property, using the specified binding constraints. - /// - /// The field to create an accessor for. - /// - /// An instance for the property. - /// - public static IMemberAccessor GetFieldAccessor(FieldInfo field) { - if (field == null) - throw new ArgumentNullException(nameof(field)); - - return new FieldAccessor(field); - } + /// + /// Searches for the field with the specified name. + /// + /// The to search for the field in. + /// The name of the field to find. + /// + /// An instance for the field if found; otherwise null. + /// + public static IMemberAccessor FindField(Type type, string name) { + return FindField(type, name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } - /// - /// Searches for the field with the specified name. - /// - /// The to search for the field in. - /// The name of the field to find. - /// - /// An instance for the field if found; otherwise null. - /// - public static IMemberAccessor FindField(Type type, string name) - { - return FindField(type, name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - } + /// + /// Searches for the field, using the specified binding constraints. + /// + /// The to search for the field in. + /// The name of the field to find. + /// A bitmask comprised of one or more that specify how the search is conducted. + /// + /// An instance for the field if found; otherwise null. + /// + public static IMemberAccessor FindField(Type type, string name, BindingFlags flags) { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var typeAccessor = GetAccessor(type); + var memberAccessor = typeAccessor.FindField(name, flags); + + return memberAccessor; + } - /// - /// Searches for the field, using the specified binding constraints. - /// - /// The to search for the field in. - /// The name of the field to find. - /// A bitmask comprised of one or more that specify how the search is conducted. - /// - /// An instance for the field if found; otherwise null. - /// - public static IMemberAccessor FindField(Type type, string name, BindingFlags flags) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - - var typeAccessor = GetAccessor(type); - var memberAccessor = typeAccessor.FindField(name, flags); - - return memberAccessor; + /// + /// Sets the property value with the specified name. + /// + /// The object whose property value will be set. + /// The name of the property to set. + /// The new value to be set. + public static void SetProperty(object target, string name, object value) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var rootType = target.GetType(); + var currentType = rootType; + object currentTarget = target; + + TypeAccessor typeAccessor; + IMemberAccessor memberAccessor = null; + + // support nested property + string[] parts = name.Split('.'); + foreach (string part in parts) { + if (memberAccessor != null) { + currentTarget = memberAccessor.GetValue(currentTarget); + currentType = memberAccessor.MemberType; + } + + typeAccessor = GetAccessor(currentType); + memberAccessor = typeAccessor.FindProperty(part); } - /// - /// Sets the property value with the specified name. - /// - /// The object whose property value will be set. - /// The name of the property to set. - /// The new value to be set. - public static void SetProperty(object target, string name, object value) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - - var rootType = target.GetType(); - var currentType = rootType; - object currentTarget = target; - - TypeAccessor typeAccessor; - IMemberAccessor memberAccessor = null; - - // support nested property - string[] parts = name.Split('.'); - foreach (string part in parts) - { - if (memberAccessor != null) - { - currentTarget = memberAccessor.GetValue(currentTarget); - currentType = memberAccessor.MemberType; - } - - typeAccessor = GetAccessor(currentType); - memberAccessor = typeAccessor.FindProperty(part); - } + if (memberAccessor == null) + throw new InvalidOperationException($"Could not find property '{name}' in type '{rootType.Name}'."); - if (memberAccessor == null) - throw new InvalidOperationException($"Could not find property '{name}' in type '{rootType.Name}'."); + memberAccessor.SetValue(currentTarget, value); + } - memberAccessor.SetValue(currentTarget, value); - } + /// + /// Sets the field value with the specified name. + /// + /// The object whose field value will be set. + /// The name of the field to set. + /// The new value to be set. + public static void SetField(object target, string name, object value) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var rootType = target.GetType(); + var memberAccessor = FindField(rootType, name); + + if (memberAccessor == null) + throw new InvalidOperationException($"Could not find field '{name}' in type '{rootType.Name}'."); + + memberAccessor.SetValue(target, value); + } + + /// + /// Returns the value of the property with the specified name. + /// + /// The object whose property value will be returned. + /// The name of the property to read. + /// The value of the property. + public static object GetProperty(object target, string name) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var rootType = target.GetType(); + var currentType = rootType; + object currentTarget = target; + + TypeAccessor typeAccessor; + IMemberAccessor memberAccessor = null; + + // support nested property + string[] parts = name.Split('.'); + foreach (string part in parts) { + if (memberAccessor != null) { + currentTarget = memberAccessor.GetValue(currentTarget); + currentType = memberAccessor.MemberType; + } - /// - /// Sets the field value with the specified name. - /// - /// The object whose field value will be set. - /// The name of the field to set. - /// The new value to be set. - public static void SetField(object target, string name, object value) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - - var rootType = target.GetType(); - var memberAccessor = FindField(rootType, name); - - if (memberAccessor == null) - throw new InvalidOperationException($"Could not find field '{name}' in type '{rootType.Name}'."); - - memberAccessor.SetValue(target, value); + typeAccessor = GetAccessor(currentType); + memberAccessor = typeAccessor.FindProperty(part); } - /// - /// Returns the value of the property with the specified name. - /// - /// The object whose property value will be returned. - /// The name of the property to read. - /// The value of the property. - public static object GetProperty(object target, string name) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - - var rootType = target.GetType(); - var currentType = rootType; - object currentTarget = target; - - TypeAccessor typeAccessor; - IMemberAccessor memberAccessor = null; - - // support nested property - string[] parts = name.Split('.'); - foreach (string part in parts) - { - if (memberAccessor != null) - { - currentTarget = memberAccessor.GetValue(currentTarget); - currentType = memberAccessor.MemberType; - } - - typeAccessor = GetAccessor(currentType); - memberAccessor = typeAccessor.FindProperty(part); - } + if (memberAccessor == null) + throw new InvalidOperationException($"Could not find property '{name}' in type '{rootType.Name}'."); - if (memberAccessor == null) - throw new InvalidOperationException($"Could not find property '{name}' in type '{rootType.Name}'."); + return memberAccessor.GetValue(currentTarget); + } - return memberAccessor.GetValue(currentTarget); - } + /// + /// Returns the value of the field with the specified name. + /// + /// The object whose field value will be returned. + /// The name of the field to read. + /// The value of the field. + public static object GetField(object target, string name) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + if (String.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + var rootType = target.GetType(); + var memberAccessor = FindField(rootType, name); + if (memberAccessor == null) + throw new InvalidOperationException($"Could not find field '{name}' in type '{rootType.Name}'."); + + return memberAccessor.GetValue(target); + } - /// - /// Returns the value of the field with the specified name. - /// - /// The object whose field value will be returned. - /// The name of the field to read. - /// The value of the field. - public static object GetField(object target, string name) - { - if (target == null) - throw new ArgumentNullException(nameof(target)); - if (String.IsNullOrEmpty(name)) - throw new ArgumentNullException(nameof(name)); - - var rootType = target.GetType(); - var memberAccessor = FindField(rootType, name); - if (memberAccessor == null) - throw new InvalidOperationException($"Could not find field '{name}' in type '{rootType.Name}'."); - - return memberAccessor.GetValue(target); - } + /// + /// Creates an instance of the specified type. + /// + /// The type to create. + /// A new instance of the specified type. + public static object CreateInstance(Type type) { + if (type == null) + throw new ArgumentNullException(nameof(type)); - /// - /// Creates an instance of the specified type. - /// - /// The type to create. - /// A new instance of the specified type. - public static object CreateInstance(Type type) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - var typeAccessor = GetAccessor(type); - if (typeAccessor == null) - throw new InvalidOperationException($"Could not find constructor for {type.Name}."); - - return typeAccessor.Create(); - } + var typeAccessor = GetAccessor(type); + if (typeAccessor == null) + throw new InvalidOperationException($"Could not find constructor for {type.Name}."); - private static TypeAccessor GetAccessor(Type type) - { - return _accessorCache.GetOrAdd(type, t => new TypeAccessor(t)); - } + return typeAccessor.Create(); + } + + private static TypeAccessor GetAccessor(Type type) { + return _accessorCache.GetOrAdd(type, t => new TypeAccessor(t)); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Reflection/MemberAccessor.cs b/src/Exceptionless.Core/Utility/Reflection/MemberAccessor.cs index 0f71d81f86..33efe951af 100644 --- a/src/Exceptionless.Core/Utility/Reflection/MemberAccessor.cs +++ b/src/Exceptionless.Core/Utility/Reflection/MemberAccessor.cs @@ -1,99 +1,93 @@ -using System; -using System.Reflection; +using System.Reflection; -namespace Exceptionless.Core.Reflection -{ +namespace Exceptionless.Core.Reflection; + +/// +/// A base class for member accessors. +/// +internal abstract class MemberAccessor : IMemberAccessor, IEquatable { + /// + /// Gets the type of the member. + /// + /// The type of the member. + public abstract Type MemberType { get; } /// - /// A base class for member accessors. + /// Gets the member info. /// - internal abstract class MemberAccessor : IMemberAccessor, IEquatable - { - /// - /// Gets the type of the member. - /// - /// The type of the member. - public abstract Type MemberType { get; } - /// - /// Gets the member info. - /// - /// The member info. - public abstract MemberInfo MemberInfo { get; } - /// - /// Gets the name of the member. - /// - /// The name of the member. - public abstract string Name { get; } - /// - /// Gets a value indicating whether this member has getter. - /// - /// true if this member has getter; otherwise, false. - public abstract bool HasGetter { get; } - /// - /// Gets a value indicating whether this member has setter. - /// - /// true if this member has setter; otherwise, false. - public abstract bool HasSetter { get; } + /// The member info. + public abstract MemberInfo MemberInfo { get; } + /// + /// Gets the name of the member. + /// + /// The name of the member. + public abstract string Name { get; } + /// + /// Gets a value indicating whether this member has getter. + /// + /// true if this member has getter; otherwise, false. + public abstract bool HasGetter { get; } + /// + /// Gets a value indicating whether this member has setter. + /// + /// true if this member has setter; otherwise, false. + public abstract bool HasSetter { get; } - /// - /// Returns the value of the member. - /// - /// The object whose member value will be returned. - /// - /// The member value for the instance parameter. - /// - public abstract object GetValue(object instance); - /// - /// Sets the value of the member. - /// - /// The object whose member value will be set. - /// The new value for this member. - public abstract void SetValue(object instance, object value); + /// + /// Returns the value of the member. + /// + /// The object whose member value will be returned. + /// + /// The member value for the instance parameter. + /// + public abstract object GetValue(object instance); + /// + /// Sets the value of the member. + /// + /// The object whose member value will be set. + /// The new value for this member. + public abstract void SetValue(object instance, object value); - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public bool Equals(IMemberAccessor other) - { - if (other is null) - return false; - if (ReferenceEquals(this, other)) - return true; + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public bool Equals(IMemberAccessor other) { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; - return Equals(other.MemberInfo, MemberInfo); - } + return Equals(other.MemberInfo, MemberInfo); + } - /// - /// Determines whether the specified is equal to this instance. - /// - /// The to compare with this instance. - /// - /// true if the specified is equal to this instance; otherwise, false. - /// - public override bool Equals(object obj) - { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != typeof(MemberAccessor)) - return false; + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object obj) { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != typeof(MemberAccessor)) + return false; - return Equals((MemberAccessor)obj); - } + return Equals((MemberAccessor)obj); + } - /// - /// Returns a hash code for this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - return MemberInfo.GetHashCode(); - } + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() { + return MemberInfo.GetHashCode(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Reflection/PropertyAccessor.cs b/src/Exceptionless.Core/Utility/Reflection/PropertyAccessor.cs index 4741dbdd16..45fec70c69 100644 --- a/src/Exceptionless.Core/Utility/Reflection/PropertyAccessor.cs +++ b/src/Exceptionless.Core/Utility/Reflection/PropertyAccessor.cs @@ -1,107 +1,101 @@ -using System; -using System.Reflection; +using System.Reflection; #if PFX_LEGACY_3_5 using CodeSmith.Core.Threading; #endif -namespace Exceptionless.Core.Reflection -{ +namespace Exceptionless.Core.Reflection; + +/// +/// An accessor class for . +/// +internal class PropertyAccessor : MemberAccessor { + private readonly PropertyInfo _propertyInfo; + private readonly string _name; + private readonly bool _hasGetter; + private readonly bool _hasSetter; + private readonly Type _memberType; + private readonly Lazy _lateBoundGet; + private readonly Lazy _lateBoundSet; + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use for this accessor. + public PropertyAccessor(PropertyInfo propertyInfo) { + _propertyInfo = propertyInfo; + _name = _propertyInfo.Name; + _memberType = _propertyInfo.PropertyType; + + _hasGetter = _propertyInfo.GetGetMethod(true) != null; + if (_hasGetter) + _lateBoundGet = new Lazy(() => DelegateFactory.CreateGet(_propertyInfo)); + + _hasSetter = propertyInfo.GetSetMethod(true) != null; + if (_hasSetter) + _lateBoundSet = new Lazy(() => DelegateFactory.CreateSet(_propertyInfo)); + } + /// - /// An accessor class for . + /// Gets the type of the member. /// - internal class PropertyAccessor : MemberAccessor - { - private readonly PropertyInfo _propertyInfo; - private readonly string _name; - private readonly bool _hasGetter; - private readonly bool _hasSetter; - private readonly Type _memberType; - private readonly Lazy _lateBoundGet; - private readonly Lazy _lateBoundSet; - - /// - /// Initializes a new instance of the class. - /// - /// The instance to use for this accessor. - public PropertyAccessor(PropertyInfo propertyInfo) - { - _propertyInfo = propertyInfo; - _name = _propertyInfo.Name; - _memberType = _propertyInfo.PropertyType; - - _hasGetter = _propertyInfo.GetGetMethod(true) != null; - if (_hasGetter) - _lateBoundGet = new Lazy(() => DelegateFactory.CreateGet(_propertyInfo)); - - _hasSetter = propertyInfo.GetSetMethod(true) != null; - if (_hasSetter) - _lateBoundSet = new Lazy(() => DelegateFactory.CreateSet(_propertyInfo)); - } - - /// - /// Gets the type of the member. - /// - /// The type of the member. - public override Type MemberType => _memberType; - - /// - /// Gets the member info. - /// - /// The member info. - public override MemberInfo MemberInfo => _propertyInfo; - - /// - /// Gets the name of the member. - /// - /// The name of the member. - public override string Name => _name; - - /// - /// Gets a value indicating whether this member has getter. - /// - /// true if this member has getter; otherwise, false. - public override bool HasGetter => _hasGetter; - - /// - /// Gets a value indicating whether this member has setter. - /// - /// true if this member has setter; otherwise, false. - public override bool HasSetter => _hasSetter; - - /// - /// Returns the value of the member. - /// - /// The object whose member value will be returned. - /// - /// The member value for the instance parameter. - /// - public override object GetValue(object instance) - { - if (_lateBoundGet == null || !HasGetter) - throw new InvalidOperationException($"Property '{Name}' does not have a getter."); - - var get = _lateBoundGet.Value; - if (get == null) - throw new InvalidOperationException($"Property '{Name}' does not have a getter."); - - return get(instance); - } - - /// - /// Sets the value of the member. - /// - /// The object whose member value will be set. - /// The new value for this member. - public override void SetValue(object instance, object value) - { - if (_lateBoundSet == null || !HasSetter) - throw new InvalidOperationException($"Property '{Name}' does not have a setter."); - - var set = _lateBoundSet.Value; - if (set == null) - throw new InvalidOperationException($"Property '{Name}' does not have a setter."); - - set(instance, value); - } + /// The type of the member. + public override Type MemberType => _memberType; + + /// + /// Gets the member info. + /// + /// The member info. + public override MemberInfo MemberInfo => _propertyInfo; + + /// + /// Gets the name of the member. + /// + /// The name of the member. + public override string Name => _name; + + /// + /// Gets a value indicating whether this member has getter. + /// + /// true if this member has getter; otherwise, false. + public override bool HasGetter => _hasGetter; + + /// + /// Gets a value indicating whether this member has setter. + /// + /// true if this member has setter; otherwise, false. + public override bool HasSetter => _hasSetter; + + /// + /// Returns the value of the member. + /// + /// The object whose member value will be returned. + /// + /// The member value for the instance parameter. + /// + public override object GetValue(object instance) { + if (_lateBoundGet == null || !HasGetter) + throw new InvalidOperationException($"Property '{Name}' does not have a getter."); + + var get = _lateBoundGet.Value; + if (get == null) + throw new InvalidOperationException($"Property '{Name}' does not have a getter."); + + return get(instance); + } + + /// + /// Sets the value of the member. + /// + /// The object whose member value will be set. + /// The new value for this member. + public override void SetValue(object instance, object value) { + if (_lateBoundSet == null || !HasSetter) + throw new InvalidOperationException($"Property '{Name}' does not have a setter."); + + var set = _lateBoundSet.Value; + if (set == null) + throw new InvalidOperationException($"Property '{Name}' does not have a setter."); + + set(instance, value); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Utility/Reflection/TypeAccessor.cs b/src/Exceptionless.Core/Utility/Reflection/TypeAccessor.cs index 33b9364692..2f13ff2dbb 100644 --- a/src/Exceptionless.Core/Utility/Reflection/TypeAccessor.cs +++ b/src/Exceptionless.Core/Utility/Reflection/TypeAccessor.cs @@ -1,152 +1,136 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Reflection; -namespace Exceptionless.Core.Reflection -{ +namespace Exceptionless.Core.Reflection; + +/// +/// A class holding all the accessors for a . +/// +internal class TypeAccessor { + private readonly ConcurrentDictionary _memberCache = new ConcurrentDictionary(); + private readonly Lazy _lateBoundConstructor; + private readonly Type _type; + + /// + /// Initializes a new instance of the class. + /// + /// The this accessor is for. + public TypeAccessor(Type type) { + _type = type; + _lateBoundConstructor = new Lazy(() => DelegateFactory.CreateConstructor(_type)); + } + + /// + /// Gets the this accessor is for. + /// + /// The this accessor is for. + public Type Type => _type; + + /// + /// Creates a new instance of accessors type. + /// + /// A new instance of accessors type. + public object Create() { + var constructor = _lateBoundConstructor.Value; + if (constructor == null) + throw new InvalidOperationException($"Could not find constructor for '{Type.Name}'."); + + return constructor.Invoke(); + } + + #region FindProperty + /// + /// Searches for the public property with the specified name. + /// + /// The name of the property to find. + /// An instance for the property if found; otherwise null. + public IMemberAccessor FindProperty(string name) { + return FindProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + } + + /// + /// Searches for the specified property, using the specified binding constraints. + /// + /// The name of the property to find. + /// A bitmask comprised of one or more that specify how the search is conducted. + /// + /// An instance for the property if found; otherwise null. + /// + public IMemberAccessor FindProperty(string name, BindingFlags flags) { + return _memberCache.GetOrAdd(name, n => CreatePropertyAccessor(n, flags)); + } + + private IMemberAccessor CreatePropertyAccessor(string name, BindingFlags flags) { + var info = FindProperty(Type, name, flags); + return info == null ? null : GetMemberAccessor(info); + } + + private static PropertyInfo FindProperty(Type type, string name, BindingFlags flags) { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (name == null) + throw new ArgumentNullException(nameof(name)); + + // first try GetProperty + var property = type.GetProperty(name, flags); + if (property != null) + return property; + + // if not found, search while ignoring case + return type.GetProperties(flags).FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + + private static IMemberAccessor GetMemberAccessor(PropertyInfo propertyInfo) { + return propertyInfo == null ? null : new PropertyAccessor(propertyInfo); + } + #endregion + + #region FindField /// - /// A class holding all the accessors for a . + /// Searches for the specified field with the specified name. /// - internal class TypeAccessor - { - private readonly ConcurrentDictionary _memberCache = new ConcurrentDictionary(); - private readonly Lazy _lateBoundConstructor; - private readonly Type _type; - - /// - /// Initializes a new instance of the class. - /// - /// The this accessor is for. - public TypeAccessor(Type type) - { - _type = type; - _lateBoundConstructor = new Lazy(() => DelegateFactory.CreateConstructor(_type)); - } - - /// - /// Gets the this accessor is for. - /// - /// The this accessor is for. - public Type Type => _type; - - /// - /// Creates a new instance of accessors type. - /// - /// A new instance of accessors type. - public object Create() - { - var constructor = _lateBoundConstructor.Value; - if (constructor == null) - throw new InvalidOperationException($"Could not find constructor for '{Type.Name}'."); - - return constructor.Invoke(); - } - - #region FindProperty - /// - /// Searches for the public property with the specified name. - /// - /// The name of the property to find. - /// An instance for the property if found; otherwise null. - public IMemberAccessor FindProperty(string name) - { - return FindProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); - } - - /// - /// Searches for the specified property, using the specified binding constraints. - /// - /// The name of the property to find. - /// A bitmask comprised of one or more that specify how the search is conducted. - /// - /// An instance for the property if found; otherwise null. - /// - public IMemberAccessor FindProperty(string name, BindingFlags flags) - { - return _memberCache.GetOrAdd(name, n => CreatePropertyAccessor(n, flags)); - } - - private IMemberAccessor CreatePropertyAccessor(string name, BindingFlags flags) - { - var info = FindProperty(Type, name, flags); - return info == null ? null : GetMemberAccessor(info); - } - - private static PropertyInfo FindProperty(Type type, string name, BindingFlags flags) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (name == null) - throw new ArgumentNullException(nameof(name)); - - // first try GetProperty - var property = type.GetProperty(name, flags); - if (property != null) - return property; - - // if not found, search while ignoring case - return type.GetProperties(flags).FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - } - - private static IMemberAccessor GetMemberAccessor(PropertyInfo propertyInfo) - { - return propertyInfo == null ? null : new PropertyAccessor(propertyInfo); - } - #endregion - - #region FindField - /// - /// Searches for the specified field with the specified name. - /// - /// The name of the field to find. - /// - /// An instance for the field if found; otherwise null. - /// - public IMemberAccessor FindField(string name) - { - return FindField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - } - - /// - /// Searches for the specified field, using the specified binding constraints. - /// - /// The name of the field to find. - /// A bitmask comprised of one or more that specify how the search is conducted. - /// - /// An instance for the field if found; otherwise null. - /// - public IMemberAccessor FindField(string name, BindingFlags flags) - { - return _memberCache.GetOrAdd(name, n => CreateFieldAccessor(n, flags)); - } - - private IMemberAccessor CreateFieldAccessor(string name, BindingFlags flags) - { - var info = FindField(Type, name, flags); - return info == null ? null : GetMemberAccessor(info); - } - - private static FieldInfo FindField(Type type, string name, BindingFlags flags) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (name == null) - throw new ArgumentNullException(nameof(name)); - - // first try GetField - var field = type.GetField(name, flags); - if (field != null) - return field; - - // if not found, search while ignoring case - return type.GetFields(flags).FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - } - - private static IMemberAccessor GetMemberAccessor(FieldInfo fieldInfo) - { - return fieldInfo == null ? null : new FieldAccessor(fieldInfo); - } - #endregion + /// The name of the field to find. + /// + /// An instance for the field if found; otherwise null. + /// + public IMemberAccessor FindField(string name) { + return FindField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } + + /// + /// Searches for the specified field, using the specified binding constraints. + /// + /// The name of the field to find. + /// A bitmask comprised of one or more that specify how the search is conducted. + /// + /// An instance for the field if found; otherwise null. + /// + public IMemberAccessor FindField(string name, BindingFlags flags) { + return _memberCache.GetOrAdd(name, n => CreateFieldAccessor(n, flags)); + } + + private IMemberAccessor CreateFieldAccessor(string name, BindingFlags flags) { + var info = FindField(Type, name, flags); + return info == null ? null : GetMemberAccessor(info); + } + + private static FieldInfo FindField(Type type, string name, BindingFlags flags) { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (name == null) + throw new ArgumentNullException(nameof(name)); + + // first try GetField + var field = type.GetField(name, flags); + if (field != null) + return field; + + // if not found, search while ignoring case + return type.GetFields(flags).FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + + private static IMemberAccessor GetMemberAccessor(FieldInfo fieldInfo) { + return fieldInfo == null ? null : new FieldAccessor(fieldInfo); } -} \ No newline at end of file + #endregion +} diff --git a/src/Exceptionless.Core/Utility/SampleDataService.cs b/src/Exceptionless.Core/Utility/SampleDataService.cs index 7ee989f046..af8af03630 100644 --- a/src/Exceptionless.Core/Utility/SampleDataService.cs +++ b/src/Exceptionless.Core/Utility/SampleDataService.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Authorization; +using Exceptionless.Core.Authorization; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories; @@ -9,118 +7,119 @@ using Foundatio.Utility; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Utility { - public class SampleDataService { - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly ITokenRepository _tokenRepository; - private readonly BillingManager _billingManager; - private readonly BillingPlans _billingPlans; - private readonly IUserRepository _userRepository; - private readonly ILogger _logger; - - public const string TEST_USER_EMAIL = "test@localhost"; - public const string TEST_USER_PASSWORD = "tester"; - public const string TEST_ORG_ID = "537650f3b77efe23a47914f3"; - public const string TEST_PROJECT_ID = "537650f3b77efe23a47914f4"; - public const string TEST_API_KEY = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; - public const string TEST_USER_API_KEY = "5f8aT5j0M1SdWCMOiJKCrlDNHMI38LjCH4LTTest"; - public const string TEST_ORG_USER_EMAIL = "org@localhost"; - public const string TEST_ORG_USER_PASSWORD = "tester"; - public const string FREE_USER_EMAIL = "free@localhost"; - public const string FREE_USER_PASSWORD = "tester"; - public const string FREE_ORG_ID = "537650f3b77efe23a47914f5"; - public const string FREE_PROJECT_ID = "537650f3b77efe23a47914f6"; - public const string FREE_API_KEY = "LhhP1C9gijpSKCslHHCvwdSIz298twx271n1Free"; - public const string FREE_USER_API_KEY = "5f8aT5j0M1SdWCMOiJKCrlDNHMI37LjCH4LTFree"; - public const string INTERNAL_API_KEY = "Bx7JgglstPG544R34Tw9T7RlCed3OIwtYXVeyhT2"; - public const string INTERNAL_PROJECT_ID = "54b56e480ef9605a88a13153"; - - public SampleDataService( - IOrganizationRepository organizationRepository, - IProjectRepository projectRepository, - IUserRepository userRepository, - ITokenRepository tokenRepository, - BillingManager billingManager, - BillingPlans billingPlans, - ILoggerFactory loggerFactory - ) { - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _userRepository = userRepository; - _tokenRepository = tokenRepository; - _billingManager = billingManager; - _billingPlans = billingPlans; - _logger = loggerFactory.CreateLogger(); - } - - public async Task CreateDataAsync() { - if (await _userRepository.GetByEmailAddressAsync(TEST_USER_EMAIL).AnyContext() != null) - return; - - var user = new User { - FullName = "Test User", - EmailAddress = TEST_USER_EMAIL - }; - - user.CreateVerifyEmailAddressToken(); - user.Roles.Add(AuthorizationRoles.Client); - user.Roles.Add(AuthorizationRoles.User); - user.Roles.Add(AuthorizationRoles.GlobalAdmin); - - user.Salt = StringExtensions.GetRandomString(16); - user.Password = TEST_USER_PASSWORD.ToSaltedHash(user.Salt); - - user = await _userRepository.AddAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); - _logger.LogDebug("Created Global Admin {FullName} - {EmailAddress}", user.FullName, user.EmailAddress); - await CreateOrganizationAndProjectAsync(user).AnyContext(); - await CreateInternalOrganizationAndProjectAsync(user.Id).AnyContext(); - await CreateOrganizationAdminUserAsync().AnyContext(); - await CreateFreeOrganizationAndProjectAsync().AnyContext(); - } - - public async Task CreateOrganizationAdminUserAsync() { - if (await _userRepository.GetByEmailAddressAsync(TEST_ORG_USER_EMAIL).AnyContext() != null) - return; - - var user = new User { - FullName = "Test Org User", - EmailAddress = TEST_ORG_USER_EMAIL - }; - - user.CreateVerifyEmailAddressToken(); - user.Roles.Add(AuthorizationRoles.Client); - user.Roles.Add(AuthorizationRoles.User); - - user.Salt = StringExtensions.GetRandomString(16); - user.Password = TEST_ORG_USER_PASSWORD.ToSaltedHash(user.Salt); - - user.OrganizationIds.Add(TEST_ORG_ID); - - user = await _userRepository.AddAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); - _logger.LogDebug("Created Org Admin {FullName} - {EmailAddress}", user.FullName, user.EmailAddress); - } - - public async Task CreateOrganizationAndProjectAsync(User user) { - if (await _tokenRepository.ExistsAsync(TEST_API_KEY).AnyContext()) - return; - - var organization = new Organization { Id = TEST_ORG_ID, Name = "Acme" }; - _billingManager.ApplyBillingPlan(organization, _billingPlans.UnlimitedPlan, user); - organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()).AnyContext(); - - var project = new Project { - Id = TEST_PROJECT_ID, - Name = "Disintegrating Pistol", - OrganizationId = organization.Id, - NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks - }; - project.Configuration.Settings.Add("IncludeConditionalData", "true"); - project.AddDefaultNotificationSettings(user.Id); - project = await _projectRepository.AddAsync(project, o => o.ImmediateConsistency().Cache()).AnyContext(); - - await _tokenRepository.AddAsync(new List() - { +namespace Exceptionless.Core.Utility; + +public class SampleDataService { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly ITokenRepository _tokenRepository; + private readonly BillingManager _billingManager; + private readonly BillingPlans _billingPlans; + private readonly IUserRepository _userRepository; + private readonly ILogger _logger; + + public const string TEST_USER_EMAIL = "test@localhost"; + public const string TEST_USER_PASSWORD = "tester"; + public const string TEST_ORG_ID = "537650f3b77efe23a47914f3"; + public const string TEST_PROJECT_ID = "537650f3b77efe23a47914f4"; + public const string TEST_API_KEY = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; + public const string TEST_USER_API_KEY = "5f8aT5j0M1SdWCMOiJKCrlDNHMI38LjCH4LTTest"; + public const string TEST_ORG_USER_EMAIL = "org@localhost"; + public const string TEST_ORG_USER_PASSWORD = "tester"; + public const string FREE_USER_EMAIL = "free@localhost"; + public const string FREE_USER_PASSWORD = "tester"; + public const string FREE_ORG_ID = "537650f3b77efe23a47914f5"; + public const string FREE_PROJECT_ID = "537650f3b77efe23a47914f6"; + public const string FREE_API_KEY = "LhhP1C9gijpSKCslHHCvwdSIz298twx271n1Free"; + public const string FREE_USER_API_KEY = "5f8aT5j0M1SdWCMOiJKCrlDNHMI37LjCH4LTFree"; + public const string INTERNAL_API_KEY = "Bx7JgglstPG544R34Tw9T7RlCed3OIwtYXVeyhT2"; + public const string INTERNAL_PROJECT_ID = "54b56e480ef9605a88a13153"; + + public SampleDataService( + IOrganizationRepository organizationRepository, + IProjectRepository projectRepository, + IUserRepository userRepository, + ITokenRepository tokenRepository, + BillingManager billingManager, + BillingPlans billingPlans, + ILoggerFactory loggerFactory + ) { + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _userRepository = userRepository; + _tokenRepository = tokenRepository; + _billingManager = billingManager; + _billingPlans = billingPlans; + _logger = loggerFactory.CreateLogger(); + } + + public async Task CreateDataAsync() { + if (await _userRepository.GetByEmailAddressAsync(TEST_USER_EMAIL).AnyContext() != null) + return; + + var user = new User { + FullName = "Test User", + EmailAddress = TEST_USER_EMAIL + }; + + user.CreateVerifyEmailAddressToken(); + user.Roles.Add(AuthorizationRoles.Client); + user.Roles.Add(AuthorizationRoles.User); + user.Roles.Add(AuthorizationRoles.GlobalAdmin); + + user.Salt = StringExtensions.GetRandomString(16); + user.Password = TEST_USER_PASSWORD.ToSaltedHash(user.Salt); + + user = await _userRepository.AddAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); + _logger.LogDebug("Created Global Admin {FullName} - {EmailAddress}", user.FullName, user.EmailAddress); + await CreateOrganizationAndProjectAsync(user).AnyContext(); + await CreateInternalOrganizationAndProjectAsync(user.Id).AnyContext(); + await CreateOrganizationAdminUserAsync().AnyContext(); + await CreateFreeOrganizationAndProjectAsync().AnyContext(); + } + + public async Task CreateOrganizationAdminUserAsync() { + if (await _userRepository.GetByEmailAddressAsync(TEST_ORG_USER_EMAIL).AnyContext() != null) + return; + + var user = new User { + FullName = "Test Org User", + EmailAddress = TEST_ORG_USER_EMAIL + }; + + user.CreateVerifyEmailAddressToken(); + user.Roles.Add(AuthorizationRoles.Client); + user.Roles.Add(AuthorizationRoles.User); + + user.Salt = StringExtensions.GetRandomString(16); + user.Password = TEST_ORG_USER_PASSWORD.ToSaltedHash(user.Salt); + + user.OrganizationIds.Add(TEST_ORG_ID); + + user = await _userRepository.AddAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); + _logger.LogDebug("Created Org Admin {FullName} - {EmailAddress}", user.FullName, user.EmailAddress); + } + + public async Task CreateOrganizationAndProjectAsync(User user) { + if (await _tokenRepository.ExistsAsync(TEST_API_KEY).AnyContext()) + return; + + var organization = new Organization { Id = TEST_ORG_ID, Name = "Acme" }; + _billingManager.ApplyBillingPlan(organization, _billingPlans.UnlimitedPlan, user); + organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()).AnyContext(); + + var project = new Project { + Id = TEST_PROJECT_ID, + Name = "Disintegrating Pistol", + OrganizationId = organization.Id, + NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks + }; + project.Configuration.Settings.Add("IncludeConditionalData", "true"); + project.AddDefaultNotificationSettings(user.Id); + project = await _projectRepository.AddAsync(project, o => o.ImmediateConsistency().Cache()).AnyContext(); + + await _tokenRepository.AddAsync(new List() + { new Token { Id = TEST_API_KEY, OrganizationId = organization.Id, @@ -138,48 +137,48 @@ await _tokenRepository.AddAsync(new List() } }, o => o.ImmediateConsistency().Cache()).AnyContext(); - user.OrganizationIds.Add(organization.Id); - await _userRepository.SaveAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); - _logger.LogDebug("Created Organization {OrganizationName} and Project {ProjectName}", organization.Name, project.Name); - } - - public async Task CreateFreeOrganizationAndProjectAsync() { - if (await _userRepository.GetByEmailAddressAsync(FREE_USER_EMAIL).AnyContext() != null) - return; - - var user = new User { - FullName = "Free User", - EmailAddress = FREE_USER_EMAIL - }; - - user.CreateVerifyEmailAddressToken(); - user.Roles.Add(AuthorizationRoles.Client); - user.Roles.Add(AuthorizationRoles.User); - - user.Salt = StringExtensions.GetRandomString(16); - user.Password = FREE_USER_PASSWORD.ToSaltedHash(user.Salt); - - user = await _userRepository.AddAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); - - if (await _tokenRepository.ExistsAsync(FREE_API_KEY).AnyContext()) - return; - - var organization = new Organization { Id = FREE_ORG_ID, Name = "Free Plan Organization" }; - _billingManager.ApplyBillingPlan(organization, _billingPlans.FreePlan, user); - organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()).AnyContext(); - - var project = new Project { - Id = FREE_PROJECT_ID, - Name = "Free Plan Project", - OrganizationId = organization.Id, - NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks - }; - project.Configuration.Settings.Add("IncludeConditionalData", "true"); - project.AddDefaultNotificationSettings(user.Id); - project = await _projectRepository.AddAsync(project, o => o.ImmediateConsistency().Cache()).AnyContext(); - - await _tokenRepository.AddAsync(new List() - { + user.OrganizationIds.Add(organization.Id); + await _userRepository.SaveAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); + _logger.LogDebug("Created Organization {OrganizationName} and Project {ProjectName}", organization.Name, project.Name); + } + + public async Task CreateFreeOrganizationAndProjectAsync() { + if (await _userRepository.GetByEmailAddressAsync(FREE_USER_EMAIL).AnyContext() != null) + return; + + var user = new User { + FullName = "Free User", + EmailAddress = FREE_USER_EMAIL + }; + + user.CreateVerifyEmailAddressToken(); + user.Roles.Add(AuthorizationRoles.Client); + user.Roles.Add(AuthorizationRoles.User); + + user.Salt = StringExtensions.GetRandomString(16); + user.Password = FREE_USER_PASSWORD.ToSaltedHash(user.Salt); + + user = await _userRepository.AddAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); + + if (await _tokenRepository.ExistsAsync(FREE_API_KEY).AnyContext()) + return; + + var organization = new Organization { Id = FREE_ORG_ID, Name = "Free Plan Organization" }; + _billingManager.ApplyBillingPlan(organization, _billingPlans.FreePlan, user); + organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()).AnyContext(); + + var project = new Project { + Id = FREE_PROJECT_ID, + Name = "Free Plan Project", + OrganizationId = organization.Id, + NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks + }; + project.Configuration.Settings.Add("IncludeConditionalData", "true"); + project.AddDefaultNotificationSettings(user.Id); + project = await _projectRepository.AddAsync(project, o => o.ImmediateConsistency().Cache()).AnyContext(); + + await _tokenRepository.AddAsync(new List() + { new Token { Id = FREE_API_KEY, OrganizationId = organization.Id, @@ -197,41 +196,40 @@ await _tokenRepository.AddAsync(new List() } }, o => o.ImmediateConsistency().Cache()).AnyContext(); - user.OrganizationIds.Add(organization.Id); - await _userRepository.SaveAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); - _logger.LogDebug("Created Free Organization {OrganizationName} and Project {ProjectName}", organization.Name, project.Name); - } - - public async Task CreateInternalOrganizationAndProjectAsync(string userId) { - if (await _tokenRepository.GetByIdAsync(INTERNAL_API_KEY).AnyContext() != null) - return; - - var user = await _userRepository.GetByIdAsync(userId, o => o.Cache()).AnyContext(); - var organization = new Organization { Name = "Exceptionless" }; - _billingManager.ApplyBillingPlan(organization, _billingPlans.UnlimitedPlan, user); - organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()).AnyContext(); - - var project = new Project { - Id = INTERNAL_PROJECT_ID, - Name = "API", - OrganizationId = organization.Id, - NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks - }; - project.AddDefaultNotificationSettings(userId); - project = await _projectRepository.AddAsync(project, o => o.ImmediateConsistency().Cache()).AnyContext(); - - await _tokenRepository.AddAsync(new Token { - Id = INTERNAL_API_KEY, - OrganizationId = organization.Id, - ProjectId = project.Id, - CreatedUtc = SystemClock.UtcNow, - UpdatedUtc = SystemClock.UtcNow, - Type = TokenType.Access - }, o => o.ImmediateConsistency()).AnyContext(); - - user.OrganizationIds.Add(organization.Id); - await _userRepository.SaveAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); - _logger.LogDebug("Created Internal Organization {OrganizationName} and Project {ProjectName}", organization.Name, project.Name); - } + user.OrganizationIds.Add(organization.Id); + await _userRepository.SaveAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); + _logger.LogDebug("Created Free Organization {OrganizationName} and Project {ProjectName}", organization.Name, project.Name); + } + + public async Task CreateInternalOrganizationAndProjectAsync(string userId) { + if (await _tokenRepository.GetByIdAsync(INTERNAL_API_KEY).AnyContext() != null) + return; + + var user = await _userRepository.GetByIdAsync(userId, o => o.Cache()).AnyContext(); + var organization = new Organization { Name = "Exceptionless" }; + _billingManager.ApplyBillingPlan(organization, _billingPlans.UnlimitedPlan, user); + organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()).AnyContext(); + + var project = new Project { + Id = INTERNAL_PROJECT_ID, + Name = "API", + OrganizationId = organization.Id, + NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks + }; + project.AddDefaultNotificationSettings(userId); + project = await _projectRepository.AddAsync(project, o => o.ImmediateConsistency().Cache()).AnyContext(); + + await _tokenRepository.AddAsync(new Token { + Id = INTERNAL_API_KEY, + OrganizationId = organization.Id, + ProjectId = project.Id, + CreatedUtc = SystemClock.UtcNow, + UpdatedUtc = SystemClock.UtcNow, + Type = TokenType.Access + }, o => o.ImmediateConsistency()).AnyContext(); + + user.OrganizationIds.Add(organization.Id); + await _userRepository.SaveAsync(user, o => o.ImmediateConsistency().Cache()).AnyContext(); + _logger.LogDebug("Created Internal Organization {OrganizationName} and Project {ProjectName}", organization.Name, project.Name); } } diff --git a/src/Exceptionless.Core/Utility/SemanticVersionParser.cs b/src/Exceptionless.Core/Utility/SemanticVersionParser.cs index bb00be96ad..29b7224493 100644 --- a/src/Exceptionless.Core/Utility/SemanticVersionParser.cs +++ b/src/Exceptionless.Core/Utility/SemanticVersionParser.cs @@ -1,56 +1,53 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Caching; using McSherry.SemanticVersioning; using Microsoft.Extensions.Logging; -namespace Exceptionless.Core.Utility { - public class SemanticVersionParser { - private static readonly IReadOnlyCollection EmptyIdentifiers = new List(0).AsReadOnly(); - private readonly InMemoryCacheClient _localCache; - private readonly ILogger _logger; +namespace Exceptionless.Core.Utility; - public SemanticVersionParser(ILoggerFactory loggerFactory) { - _localCache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory, MaxItems = 250, CloneValues = true }); - _logger = loggerFactory.CreateLogger(); - } +public class SemanticVersionParser { + private static readonly IReadOnlyCollection EmptyIdentifiers = new List(0).AsReadOnly(); + private readonly InMemoryCacheClient _localCache; + private readonly ILogger _logger; + + public SemanticVersionParser(ILoggerFactory loggerFactory) { + _localCache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory, MaxItems = 250, CloneValues = true }); + _logger = loggerFactory.CreateLogger(); + } + + public SemanticVersion Default { get; } = new SemanticVersion(0, 0); + + public async Task ParseAsync(string version) { + version = version?.Trim(); + if (String.IsNullOrEmpty(version)) + return null; - public SemanticVersion Default { get; } = new SemanticVersion(0, 0); - - public async Task ParseAsync(string version) { - version = version?.Trim(); - if (String.IsNullOrEmpty(version)) - return null; - - var cacheValue = await _localCache.GetAsync(version).AnyContext(); - if (cacheValue.HasValue) - return cacheValue.Value; - - int spaceIndex = version.IndexOf(" ", StringComparison.OrdinalIgnoreCase); - if (spaceIndex > 0) - version = version.Substring(0, spaceIndex).Trim(); - - int wildCardIndex = version.IndexOf("*", StringComparison.OrdinalIgnoreCase); - if (wildCardIndex > 0) - version = version.Replace(".*", String.Empty).Replace("*", String.Empty); - - SemanticVersion semanticVersion = null; - if (version.Length >= 5 && SemanticVersion.TryParse(version, out semanticVersion)) { - await _localCache.SetAsync(version, semanticVersion).AnyContext(); - return semanticVersion; - } - - if (version.Length >= 3 && Version.TryParse(version, out var v)) - semanticVersion = new SemanticVersion(v.Major > 0 ? v.Major : 0, v.Minor > 0 ? v.Minor : 0, v.Build > 0 ? v.Build : 0, v.Revision >= 0 ? new[] { v.Revision.ToString() } : EmptyIdentifiers); - else if (Int32.TryParse(version, out int major)) - semanticVersion = new SemanticVersion(major, 0); - else - _logger.LogInformation("Unable to parse version: {Version}", version); + var cacheValue = await _localCache.GetAsync(version).AnyContext(); + if (cacheValue.HasValue) + return cacheValue.Value; + int spaceIndex = version.IndexOf(" ", StringComparison.OrdinalIgnoreCase); + if (spaceIndex > 0) + version = version.Substring(0, spaceIndex).Trim(); + + int wildCardIndex = version.IndexOf("*", StringComparison.OrdinalIgnoreCase); + if (wildCardIndex > 0) + version = version.Replace(".*", String.Empty).Replace("*", String.Empty); + + SemanticVersion semanticVersion = null; + if (version.Length >= 5 && SemanticVersion.TryParse(version, out semanticVersion)) { await _localCache.SetAsync(version, semanticVersion).AnyContext(); return semanticVersion; } + + if (version.Length >= 3 && Version.TryParse(version, out var v)) + semanticVersion = new SemanticVersion(v.Major > 0 ? v.Major : 0, v.Minor > 0 ? v.Minor : 0, v.Build > 0 ? v.Build : 0, v.Revision >= 0 ? new[] { v.Revision.ToString() } : EmptyIdentifiers); + else if (Int32.TryParse(version, out int major)) + semanticVersion = new SemanticVersion(major, 0); + else + _logger.LogInformation("Unable to parse version: {Version}", version); + + await _localCache.SetAsync(version, semanticVersion).AnyContext(); + return semanticVersion; } } diff --git a/src/Exceptionless.Core/Utility/SmtpUri.cs b/src/Exceptionless.Core/Utility/SmtpUri.cs index dc528b8c2f..961ab5e29c 100644 --- a/src/Exceptionless.Core/Utility/SmtpUri.cs +++ b/src/Exceptionless.Core/Utility/SmtpUri.cs @@ -1,38 +1,41 @@ -using System; -using System.Net; +using System.Net; -namespace Exceptionless.Core.Utility { - public class SmtpUri { - public SmtpUri(string uri) : this(new Uri(uri)) { } +namespace Exceptionless.Core.Utility; - public SmtpUri(Uri uri) { - if (String.Equals(uri.Scheme, "smtps", StringComparison.OrdinalIgnoreCase)) { - IsSecure = true; - Port = uri.IsDefaultPort ? 465 : uri.Port; - } else if (String.Equals(uri.Scheme, "smtp", StringComparison.OrdinalIgnoreCase)) { - Port = uri.IsDefaultPort ? 25 : uri.Port; - } else { - throw new ArgumentException("Invalid SMTP scheme", nameof(uri.Scheme)); - } +public class SmtpUri { + public SmtpUri(string uri) : this(new Uri(uri)) { } - string[] parts = uri.UserInfo.Split(new [] { ":" }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 1) { - User = WebUtility.UrlDecode(parts[0]); - } else if (parts.Length == 2) { - User = WebUtility.UrlDecode(parts[0]); - Password = WebUtility.UrlDecode(parts[1]); - } else if (parts.Length > 2) { - throw new ArgumentException("Unable to parse SMTP user info", nameof(uri.UserInfo)); - } - - Host = uri.Host; + public SmtpUri(Uri uri) { + if (String.Equals(uri.Scheme, "smtps", StringComparison.OrdinalIgnoreCase)) { + IsSecure = true; + Port = uri.IsDefaultPort ? 465 : uri.Port; + } + else if (String.Equals(uri.Scheme, "smtp", StringComparison.OrdinalIgnoreCase)) { Port = uri.IsDefaultPort ? 25 : uri.Port; } - - public string Host { get; } - public int Port { get; } - public bool IsSecure { get; } - public string User { get; } - public string Password { get; } + else { + throw new ArgumentException("Invalid SMTP scheme", nameof(uri.Scheme)); + } + + string[] parts = uri.UserInfo.Split(new[] { ":" }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 1) { + User = WebUtility.UrlDecode(parts[0]); + } + else if (parts.Length == 2) { + User = WebUtility.UrlDecode(parts[0]); + Password = WebUtility.UrlDecode(parts[1]); + } + else if (parts.Length > 2) { + throw new ArgumentException("Unable to parse SMTP user info", nameof(uri.UserInfo)); + } + + Host = uri.Host; + Port = uri.IsDefaultPort ? 25 : uri.Port; } -} \ No newline at end of file + + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public string User { get; } + public string Password { get; } +} diff --git a/src/Exceptionless.Core/Utility/TypeHelper.cs b/src/Exceptionless.Core/Utility/TypeHelper.cs index 5e8234e557..78512656a1 100644 --- a/src/Exceptionless.Core/Utility/TypeHelper.cs +++ b/src/Exceptionless.Core/Utility/TypeHelper.cs @@ -1,128 +1,127 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Reflection; using Newtonsoft.Json.Linq; -namespace Exceptionless.Core.Helpers { - public static class TypeHelper { - public static readonly Type ObjectType = typeof(object); - public static readonly Type StringType = typeof(string); - public static readonly Type CharType = typeof(char); - public static readonly Type NullableCharType = typeof(char?); - public static readonly Type DateTimeType = typeof(DateTime); - public static readonly Type NullableDateTimeType = typeof(DateTime?); - public static readonly Type BoolType = typeof(bool); - public static readonly Type NullableBoolType = typeof(bool?); - public static readonly Type ByteArrayType = typeof(byte[]); - public static readonly Type ByteType = typeof(byte); - public static readonly Type SByteType = typeof(sbyte); - public static readonly Type SingleType = typeof(float); - public static readonly Type DecimalType = typeof(decimal); - public static readonly Type Int16Type = typeof(short); - public static readonly Type UInt16Type = typeof(ushort); - public static readonly Type Int32Type = typeof(int); - public static readonly Type UInt32Type = typeof(uint); - public static readonly Type Int64Type = typeof(long); - public static readonly Type UInt64Type = typeof(ulong); - public static readonly Type DoubleType = typeof(double); - - public static string GetTypeName(string assemblyQualifiedName) { - if (String.IsNullOrEmpty(assemblyQualifiedName)) - return null; - - string[] parts = assemblyQualifiedName.Split(','); - int i = parts[0].LastIndexOf('.'); - if (i < 0) - return null; - - return parts[0].Substring(i + 1); - } +namespace Exceptionless.Core.Helpers; + +public static class TypeHelper { + public static readonly Type ObjectType = typeof(object); + public static readonly Type StringType = typeof(string); + public static readonly Type CharType = typeof(char); + public static readonly Type NullableCharType = typeof(char?); + public static readonly Type DateTimeType = typeof(DateTime); + public static readonly Type NullableDateTimeType = typeof(DateTime?); + public static readonly Type BoolType = typeof(bool); + public static readonly Type NullableBoolType = typeof(bool?); + public static readonly Type ByteArrayType = typeof(byte[]); + public static readonly Type ByteType = typeof(byte); + public static readonly Type SByteType = typeof(sbyte); + public static readonly Type SingleType = typeof(float); + public static readonly Type DecimalType = typeof(decimal); + public static readonly Type Int16Type = typeof(short); + public static readonly Type UInt16Type = typeof(ushort); + public static readonly Type Int32Type = typeof(int); + public static readonly Type UInt32Type = typeof(uint); + public static readonly Type Int64Type = typeof(long); + public static readonly Type UInt64Type = typeof(ulong); + public static readonly Type DoubleType = typeof(double); + + public static string GetTypeName(string assemblyQualifiedName) { + if (String.IsNullOrEmpty(assemblyQualifiedName)) + return null; + + string[] parts = assemblyQualifiedName.Split(','); + int i = parts[0].LastIndexOf('.'); + if (i < 0) + return null; + + return parts[0].Substring(i + 1); + } - public static bool AreSameValue(object a, object b) { - if (a.GetType() != b.GetType()) { - try { - b = ChangeType(b, a.GetType()); - } catch { } + public static bool AreSameValue(object a, object b) { + if (a.GetType() != b.GetType()) { + try { + b = ChangeType(b, a.GetType()); } + catch { } + } - if (a is JToken && b is JToken) - return a.ToString().Equals(b.ToString()); + if (a is JToken && b is JToken) + return a.ToString().Equals(b.ToString()); - if (a != b && !a.Equals(b)) - return false; + if (a != b && !a.Equals(b)) + return false; - return true; - } + return true; + } - public static T ChangeType(object v) { - return (T)ChangeType(v, typeof(T)); - } + public static T ChangeType(object v) { + return (T)ChangeType(v, typeof(T)); + } - public static object ChangeType(object v, Type desiredType) { - var currentType = v.GetType(); + public static object ChangeType(object v, Type desiredType) { + var currentType = v.GetType(); - if (desiredType == currentType) - return v; - if (desiredType.IsEnum && currentType == typeof(string)) - return Enum.Parse(desiredType, v.ToString()); - if (desiredType == typeof(bool)) - return ToBoolean(v); + if (desiredType == currentType) + return v; + if (desiredType.IsEnum && currentType == typeof(string)) + return Enum.Parse(desiredType, v.ToString()); + if (desiredType == typeof(bool)) + return ToBoolean(v); - // Must use InvariantCulture otherwise we run into localization issues. - return Convert.ChangeType(v, desiredType, CultureInfo.InvariantCulture); - } + // Must use InvariantCulture otherwise we run into localization issues. + return Convert.ChangeType(v, desiredType, CultureInfo.InvariantCulture); + } - private static object ToBoolean(object value) { - if (Boolean.TryParse(value.ToString(), out bool b)) - return b; + private static object ToBoolean(object value) { + if (Boolean.TryParse(value.ToString(), out bool b)) + return b; - if (Int32.TryParse(value.ToString(), out int i)) - return Convert.ToBoolean(i); + if (Int32.TryParse(value.ToString(), out int i)) + return Convert.ToBoolean(i); - return Convert.ToBoolean(value); - } + return Convert.ToBoolean(value); + } - public static IEnumerable GetDerivedTypes() { - return GetDerivedTypes(typeof(TAction)); - } + public static IEnumerable GetDerivedTypes() { + return GetDerivedTypes(typeof(TAction)); + } - public static IEnumerable GetDerivedTypes(Type type, IEnumerable assemblies = null) { - if (assemblies == null) - assemblies = AppDomain.CurrentDomain.GetAssemblies(); - - var types = new List(); - foreach (var assembly in assemblies) { - try { - types.AddRange(from implementingType in assembly.GetTypes() where implementingType.IsClass && !implementingType.IsNotPublic && !implementingType.IsAbstract && type.IsAssignableFrom(implementingType) select implementingType); - } catch (ReflectionTypeLoadException ex) { - string loaderMessages = String.Join(", ", ex.LoaderExceptions.ToList().Select(le => le.Message)); - Trace.TraceInformation("Unable to search types from assembly '{0}' for plugins of type '{1}': {2}", assembly.FullName, type.Name, loaderMessages); - } - } + public static IEnumerable GetDerivedTypes(Type type, IEnumerable assemblies = null) { + if (assemblies == null) + assemblies = AppDomain.CurrentDomain.GetAssemblies(); - return types; + var types = new List(); + foreach (var assembly in assemblies) { + try { + types.AddRange(from implementingType in assembly.GetTypes() where implementingType.IsClass && !implementingType.IsNotPublic && !implementingType.IsAbstract && type.IsAssignableFrom(implementingType) select implementingType); + } + catch (ReflectionTypeLoadException ex) { + string loaderMessages = String.Join(", ", ex.LoaderExceptions.ToList().Select(le => le.Message)); + Trace.TraceInformation("Unable to search types from assembly '{0}' for plugins of type '{1}': {2}", assembly.FullName, type.Name, loaderMessages); + } } - public static IEnumerable GetAllTypesImplementingOpenGenericType(Type openGenericType, IEnumerable assemblies = null) { - if (assemblies == null) - assemblies = AppDomain.CurrentDomain.GetAssemblies(); - - var implementingTypes = new List(); - foreach (var assembly in assemblies) - implementingTypes.AddRange( - from x in assembly.GetTypes() - from z in x.GetInterfaces() - let y = x.BaseType - where - (y != null && y.IsGenericType && openGenericType.IsAssignableFrom(y.GetGenericTypeDefinition())) - || (z.IsGenericType && openGenericType.IsAssignableFrom(z.GetGenericTypeDefinition())) - select x - ); - - return implementingTypes; - } + return types; + } + + public static IEnumerable GetAllTypesImplementingOpenGenericType(Type openGenericType, IEnumerable assemblies = null) { + if (assemblies == null) + assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + var implementingTypes = new List(); + foreach (var assembly in assemblies) + implementingTypes.AddRange( + from x in assembly.GetTypes() + from z in x.GetInterfaces() + let y = x.BaseType + where + (y != null && y.IsGenericType && openGenericType.IsAssignableFrom(y.GetGenericTypeDefinition())) + || (z.IsGenericType && openGenericType.IsAssignableFrom(z.GetGenericTypeDefinition())) + select x + ); + + return implementingTypes; } } diff --git a/src/Exceptionless.Core/Utility/UserAgentParser.cs b/src/Exceptionless.Core/Utility/UserAgentParser.cs index d1dfcff647..3003fead9a 100644 --- a/src/Exceptionless.Core/Utility/UserAgentParser.cs +++ b/src/Exceptionless.Core/Utility/UserAgentParser.cs @@ -1,38 +1,37 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Foundatio.Caching; using Microsoft.Extensions.Logging; using UAParser; -namespace Exceptionless.Core.Utility { - public sealed class UserAgentParser { - private static readonly Lazy _parser = new Lazy(() => Parser.GetDefault()); - private readonly InMemoryCacheClient _localCache; - private readonly ILogger _logger; +namespace Exceptionless.Core.Utility; - public UserAgentParser(ILoggerFactory loggerFactory) { - _localCache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory, MaxItems = 250, CloneValues = true }); - _logger = loggerFactory.CreateLogger(); - } +public sealed class UserAgentParser { + private static readonly Lazy _parser = new Lazy(() => Parser.GetDefault()); + private readonly InMemoryCacheClient _localCache; + private readonly ILogger _logger; - public async Task ParseAsync(string userAgent) { - if (String.IsNullOrEmpty(userAgent)) - return null; + public UserAgentParser(ILoggerFactory loggerFactory) { + _localCache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory, MaxItems = 250, CloneValues = true }); + _logger = loggerFactory.CreateLogger(); + } - var cacheValue = await _localCache.GetAsync(userAgent).AnyContext(); - if (cacheValue.HasValue) - return cacheValue.Value; + public async Task ParseAsync(string userAgent) { + if (String.IsNullOrEmpty(userAgent)) + return null; - ClientInfo info = null; - try { - info = _parser.Value.Parse(userAgent); - } catch (Exception ex) { - _logger.LogWarning("Unable to parse user agent {UserAgent}. Exception: {Message}", userAgent, ex.Message); - } + var cacheValue = await _localCache.GetAsync(userAgent).AnyContext(); + if (cacheValue.HasValue) + return cacheValue.Value; - await _localCache.SetAsync(userAgent, info).AnyContext(); - return info; + ClientInfo info = null; + try { + info = _parser.Value.Parse(userAgent); } + catch (Exception ex) { + _logger.LogWarning("Unable to parse user agent {UserAgent}. Exception: {Message}", userAgent, ex.Message); + } + + await _localCache.SetAsync(userAgent, info).AnyContext(); + return info; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Validation/IsObjectIdValidator.cs b/src/Exceptionless.Core/Validation/IsObjectIdValidator.cs index b1e85332b8..35ec0fc19e 100644 --- a/src/Exceptionless.Core/Validation/IsObjectIdValidator.cs +++ b/src/Exceptionless.Core/Validation/IsObjectIdValidator.cs @@ -1,22 +1,21 @@ -using System; -using FluentValidation; +using FluentValidation; using FluentValidation.Validators; -namespace Exceptionless.Core.Validation { - public class IsObjectIdValidator : PropertyValidator { - public override string Name => "IsObjectIdValidator"; +namespace Exceptionless.Core.Validation; - public override bool IsValid(ValidationContext context, TProperty value) { - if (value is not string stringValue) - return false; +public class IsObjectIdValidator : PropertyValidator { + public override string Name => "IsObjectIdValidator"; - if (String.IsNullOrEmpty(stringValue)) - return false; + public override bool IsValid(ValidationContext context, TProperty value) { + if (value is not string stringValue) + return false; - return stringValue.Length == 24; - } + if (String.IsNullOrEmpty(stringValue)) + return false; - protected override string GetDefaultMessageTemplate(string errorCode) - => "Value for {PropertyName} is not a valid object id."; + return stringValue.Length == 24; } -} \ No newline at end of file + + protected override string GetDefaultMessageTemplate(string errorCode) + => "Value for {PropertyName} is not a valid object id."; +} diff --git a/src/Exceptionless.Core/Validation/OrganizationValidator.cs b/src/Exceptionless.Core/Validation/OrganizationValidator.cs index b16f562682..be71e11fb2 100644 --- a/src/Exceptionless.Core/Validation/OrganizationValidator.cs +++ b/src/Exceptionless.Core/Validation/OrganizationValidator.cs @@ -1,29 +1,28 @@ -using System; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using FluentValidation; -namespace Exceptionless.Core.Validation { - public class OrganizationValidator : AbstractValidator { - public OrganizationValidator(BillingPlans plans) { - RuleFor(o => o.Name).NotEmpty().WithMessage("Please specify a valid name."); - RuleFor(o => o.PlanId).NotEmpty().WithMessage("Please specify a valid plan id."); - RuleFor(o => o.HasPremiumFeatures).Equal(false).When(o => o.PlanId == plans.FreePlan.Id).WithMessage("Premium features cannot be enabled on the free plan."); +namespace Exceptionless.Core.Validation; - RuleFor(o => o.StripeCustomerId).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The stripe customer should be set on paid plans."); - RuleFor(o => o.CardLast4).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The card last four should be set on paid plans."); - RuleFor(o => o.SubscribeDate).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The subscribe date should be set on paid plans."); - RuleFor(o => o.BillingChangeDate).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The billing change date should be set on paid plans."); - RuleFor(o => o.BillingChangedByUserId).IsObjectId().When(o => o.BillingPrice > 0).WithMessage("The billing changed by user id should be set on paid plans."); +public class OrganizationValidator : AbstractValidator { + public OrganizationValidator(BillingPlans plans) { + RuleFor(o => o.Name).NotEmpty().WithMessage("Please specify a valid name."); + RuleFor(o => o.PlanId).NotEmpty().WithMessage("Please specify a valid plan id."); + RuleFor(o => o.HasPremiumFeatures).Equal(false).When(o => o.PlanId == plans.FreePlan.Id).WithMessage("Premium features cannot be enabled on the free plan."); - RuleFor(o => o.SuspensionCode).NotEmpty().When(o => o.IsSuspended).WithMessage("Please specify a valid suspension code."); - RuleFor(o => o.SuspensionCode).Equal((SuspensionCode?)null).Unless(o => o.IsSuspended).WithMessage("The suspension code cannot be set while an organization is not suspended."); - RuleFor(o => o.SuspensionDate).NotEmpty().When(o => o.IsSuspended).WithMessage("Please specify a valid suspension date."); - RuleFor(o => o.SuspensionDate).Equal((DateTime?)null).Unless(o => o.IsSuspended).WithMessage("The suspension date cannot be set while an organization is not suspended."); - RuleFor(o => o.SuspendedByUserId).NotEmpty().When(o => o.IsSuspended).WithMessage("Please specify a user id of user that suspended this organization."); - RuleFor(o => o.SuspendedByUserId).Equal((string)null).Unless(o => o.IsSuspended).WithMessage("The suspended by user id cannot be set while an organization is not suspended."); - RuleFor(o => o.SuspensionNotes).NotEmpty().When(o => o.IsSuspended && o.SuspensionCode.HasValue && o.SuspensionCode == SuspensionCode.Other).WithMessage("Please specify a suspension note."); - } + RuleFor(o => o.StripeCustomerId).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The stripe customer should be set on paid plans."); + RuleFor(o => o.CardLast4).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The card last four should be set on paid plans."); + RuleFor(o => o.SubscribeDate).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The subscribe date should be set on paid plans."); + RuleFor(o => o.BillingChangeDate).NotEmpty().When(o => o.BillingPrice > 0).WithMessage("The billing change date should be set on paid plans."); + RuleFor(o => o.BillingChangedByUserId).IsObjectId().When(o => o.BillingPrice > 0).WithMessage("The billing changed by user id should be set on paid plans."); + + RuleFor(o => o.SuspensionCode).NotEmpty().When(o => o.IsSuspended).WithMessage("Please specify a valid suspension code."); + RuleFor(o => o.SuspensionCode).Equal((SuspensionCode?)null).Unless(o => o.IsSuspended).WithMessage("The suspension code cannot be set while an organization is not suspended."); + RuleFor(o => o.SuspensionDate).NotEmpty().When(o => o.IsSuspended).WithMessage("Please specify a valid suspension date."); + RuleFor(o => o.SuspensionDate).Equal((DateTime?)null).Unless(o => o.IsSuspended).WithMessage("The suspension date cannot be set while an organization is not suspended."); + RuleFor(o => o.SuspendedByUserId).NotEmpty().When(o => o.IsSuspended).WithMessage("Please specify a user id of user that suspended this organization."); + RuleFor(o => o.SuspendedByUserId).Equal((string)null).Unless(o => o.IsSuspended).WithMessage("The suspended by user id cannot be set while an organization is not suspended."); + RuleFor(o => o.SuspensionNotes).NotEmpty().When(o => o.IsSuspended && o.SuspensionCode.HasValue && o.SuspensionCode == SuspensionCode.Other).WithMessage("Please specify a suspension note."); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Validation/PersistentEventValidator.cs b/src/Exceptionless.Core/Validation/PersistentEventValidator.cs index 95f3b814a4..50d1afacc5 100644 --- a/src/Exceptionless.Core/Validation/PersistentEventValidator.cs +++ b/src/Exceptionless.Core/Validation/PersistentEventValidator.cs @@ -1,72 +1,69 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Models; using FluentValidation; using FluentValidation.Results; using Foundatio.Utility; -namespace Exceptionless.Core.Validation { - public class PersistentEventValidator : AbstractValidator { - public override ValidationResult Validate(ValidationContext context) { - var result = new ValidationResult(); - var ev = context.InstanceToValidate; +namespace Exceptionless.Core.Validation; - if (!IsObjectId(ev.Id)) - result.Errors.Add(new ValidationFailure("Id", "Please specify a valid id.")); +public class PersistentEventValidator : AbstractValidator { + public override ValidationResult Validate(ValidationContext context) { + var result = new ValidationResult(); + var ev = context.InstanceToValidate; - if (!IsObjectId(ev.OrganizationId)) - result.Errors.Add(new ValidationFailure("OrganizationId", "Please specify a valid organization id.")); + if (!IsObjectId(ev.Id)) + result.Errors.Add(new ValidationFailure("Id", "Please specify a valid id.")); - if (!IsObjectId(ev.ProjectId)) - result.Errors.Add(new ValidationFailure("ProjectId", "Please specify a valid project id.")); + if (!IsObjectId(ev.OrganizationId)) + result.Errors.Add(new ValidationFailure("OrganizationId", "Please specify a valid organization id.")); - if (!IsObjectId(ev.StackId)) - result.Errors.Add(new ValidationFailure("StackId", "Please specify a valid stack id.")); + if (!IsObjectId(ev.ProjectId)) + result.Errors.Add(new ValidationFailure("ProjectId", "Please specify a valid project id.")); - if (ev.Date == DateTimeOffset.MinValue) - result.Errors.Add(new ValidationFailure("Date", "Date must be specified.")); + if (!IsObjectId(ev.StackId)) + result.Errors.Add(new ValidationFailure("StackId", "Please specify a valid stack id.")); - if (ev.Date.UtcDateTime > SystemClock.UtcNow.AddHours(1)) - result.Errors.Add(new ValidationFailure("Date", "Date cannot be in the future.")); + if (ev.Date == DateTimeOffset.MinValue) + result.Errors.Add(new ValidationFailure("Date", "Date must be specified.")); - if (String.IsNullOrEmpty(ev.Type)) - result.Errors.Add(new ValidationFailure("Type", "Type must be specified")); - else if (ev.Type.Length > 100) - result.Errors.Add(new ValidationFailure("Type", "Type cannot be longer than 100 characters.")); + if (ev.Date.UtcDateTime > SystemClock.UtcNow.AddHours(1)) + result.Errors.Add(new ValidationFailure("Date", "Date cannot be in the future.")); - if (ev.Message != null && (ev.Message.Length < 1 || ev.Message.Length > 2000)) - result.Errors.Add(new ValidationFailure("Message", "Message cannot be longer than 2000 characters.")); + if (String.IsNullOrEmpty(ev.Type)) + result.Errors.Add(new ValidationFailure("Type", "Type must be specified")); + else if (ev.Type.Length > 100) + result.Errors.Add(new ValidationFailure("Type", "Type cannot be longer than 100 characters.")); - if (ev.Source != null && (ev.Source.Length < 1 || ev.Source.Length > 2000)) - result.Errors.Add(new ValidationFailure("Source", "Source cannot be longer than 2000 characters.")); + if (ev.Message != null && (ev.Message.Length < 1 || ev.Message.Length > 2000)) + result.Errors.Add(new ValidationFailure("Message", "Message cannot be longer than 2000 characters.")); - if (!ev.HasValidReferenceId()) - result.Errors.Add(new ValidationFailure("ReferenceId", "ReferenceId must contain between 8 and 100 alphanumeric or '-' characters.")); + if (ev.Source != null && (ev.Source.Length < 1 || ev.Source.Length > 2000)) + result.Errors.Add(new ValidationFailure("Source", "Source cannot be longer than 2000 characters.")); - // NOTE: We need to write a migration to cleanup all old events of 50 or more tags so there never is an error while saving. - //if (ev.Tags.Count > 50) - // result.Errors.Add(new ValidationFailure("Tags", "Tags can't include more than 50 tags.")); + if (!ev.HasValidReferenceId()) + result.Errors.Add(new ValidationFailure("ReferenceId", "ReferenceId must contain between 8 and 100 alphanumeric or '-' characters.")); - foreach (string tag in ev.Tags) { - if (String.IsNullOrEmpty(tag)) - result.Errors.Add(new ValidationFailure("Tags", "Tags can't be empty.")); - else if (tag.Length > 255) - result.Errors.Add(new ValidationFailure("Tags", "A tag cannot be longer than 255 characters.")); - } + // NOTE: We need to write a migration to cleanup all old events of 50 or more tags so there never is an error while saving. + //if (ev.Tags.Count > 50) + // result.Errors.Add(new ValidationFailure("Tags", "Tags can't include more than 50 tags.")); - return result; + foreach (string tag in ev.Tags) { + if (String.IsNullOrEmpty(tag)) + result.Errors.Add(new ValidationFailure("Tags", "Tags can't be empty.")); + else if (tag.Length > 255) + result.Errors.Add(new ValidationFailure("Tags", "A tag cannot be longer than 255 characters.")); } - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new CancellationToken()) { - return Task.FromResult(Validate(context.InstanceToValidate)); - } + return result; + } - private bool IsObjectId(string value) { - if (String.IsNullOrEmpty(value)) - return false; + public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new CancellationToken()) { + return Task.FromResult(Validate(context.InstanceToValidate)); + } - return value.Length == 24; - } + private bool IsObjectId(string value) { + if (String.IsNullOrEmpty(value)) + return false; + + return value.Length == 24; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Validation/ProjectValidator.cs b/src/Exceptionless.Core/Validation/ProjectValidator.cs index 6674472956..1e9448ee0f 100644 --- a/src/Exceptionless.Core/Validation/ProjectValidator.cs +++ b/src/Exceptionless.Core/Validation/ProjectValidator.cs @@ -2,12 +2,12 @@ using Exceptionless.Core.Models; using FluentValidation; -namespace Exceptionless.Core.Validation { - public class ProjectValidator : AbstractValidator { - public ProjectValidator() { - RuleFor(p => p.OrganizationId).IsObjectId().WithMessage("Please specify a valid organization id."); - RuleFor(p => p.Name).NotEmpty().WithMessage("Please specify a valid name."); - RuleFor(p => p.NextSummaryEndOfDayTicks).NotEmpty().WithMessage("Please specify a valid next summary end of day ticks."); - } +namespace Exceptionless.Core.Validation; + +public class ProjectValidator : AbstractValidator { + public ProjectValidator() { + RuleFor(p => p.OrganizationId).IsObjectId().WithMessage("Please specify a valid organization id."); + RuleFor(p => p.Name).NotEmpty().WithMessage("Please specify a valid name."); + RuleFor(p => p.NextSummaryEndOfDayTicks).NotEmpty().WithMessage("Please specify a valid next summary end of day ticks."); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Validation/StackValidator.cs b/src/Exceptionless.Core/Validation/StackValidator.cs index bfef9fa262..5f92c82513 100644 --- a/src/Exceptionless.Core/Validation/StackValidator.cs +++ b/src/Exceptionless.Core/Validation/StackValidator.cs @@ -1,60 +1,57 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Models; using FluentValidation; using FluentValidation.Results; -namespace Exceptionless.Core.Validation { - public class StackValidator : AbstractValidator { - public override ValidationResult Validate(ValidationContext context) { - var result = new ValidationResult(); - var stack = context.InstanceToValidate; +namespace Exceptionless.Core.Validation; - if (!IsObjectId(stack.Id)) - result.Errors.Add(new ValidationFailure("Id", "Please specify a valid id.")); +public class StackValidator : AbstractValidator { + public override ValidationResult Validate(ValidationContext context) { + var result = new ValidationResult(); + var stack = context.InstanceToValidate; - if (!IsObjectId(stack.OrganizationId)) - result.Errors.Add(new ValidationFailure("OrganizationId", "Please specify a valid organization id.")); + if (!IsObjectId(stack.Id)) + result.Errors.Add(new ValidationFailure("Id", "Please specify a valid id.")); - if (!IsObjectId(stack.ProjectId)) - result.Errors.Add(new ValidationFailure("ProjectId", "Please specify a valid project id.")); + if (!IsObjectId(stack.OrganizationId)) + result.Errors.Add(new ValidationFailure("OrganizationId", "Please specify a valid organization id.")); - if (stack.Title != null && stack.Title.Length > 1000) - result.Errors.Add(new ValidationFailure("Title", "Title cannot be longer than 1000 characters.")); + if (!IsObjectId(stack.ProjectId)) + result.Errors.Add(new ValidationFailure("ProjectId", "Please specify a valid project id.")); - if (stack.Type != null && (stack.Type.Length < 1 || stack.Type.Length > 100)) - result.Errors.Add(new ValidationFailure("Type", "Type must be specified and cannot be longer than 100 characters.")); + if (stack.Title != null && stack.Title.Length > 1000) + result.Errors.Add(new ValidationFailure("Title", "Title cannot be longer than 1000 characters.")); - // NOTE: We need to write a migration to cleanup all old stacks of 50 or more tags so there never is an error while saving. - //if (stack.Tags.Count > 50) - // result.Errors.Add(new ValidationFailure("Tags", "Tags can't include more than 50 tags.")); + if (stack.Type != null && (stack.Type.Length < 1 || stack.Type.Length > 100)) + result.Errors.Add(new ValidationFailure("Type", "Type must be specified and cannot be longer than 100 characters.")); - foreach (string tag in stack.Tags) { - if (String.IsNullOrEmpty(tag)) - result.Errors.Add(new ValidationFailure("Tags", "Tags can't be empty.")); - else if (tag.Length > 255) - result.Errors.Add(new ValidationFailure("Tags", "A tag cannot be longer than 255 characters.")); - } + // NOTE: We need to write a migration to cleanup all old stacks of 50 or more tags so there never is an error while saving. + //if (stack.Tags.Count > 50) + // result.Errors.Add(new ValidationFailure("Tags", "Tags can't include more than 50 tags.")); - if (String.IsNullOrEmpty(stack.SignatureHash)) - result.Errors.Add(new ValidationFailure("SignatureHash", "Please specify a valid signature hash.")); + foreach (string tag in stack.Tags) { + if (String.IsNullOrEmpty(tag)) + result.Errors.Add(new ValidationFailure("Tags", "Tags can't be empty.")); + else if (tag.Length > 255) + result.Errors.Add(new ValidationFailure("Tags", "A tag cannot be longer than 255 characters.")); + } - if (stack.SignatureInfo == null) - result.Errors.Add(new ValidationFailure("SignatureInfo", "Please specify a valid signature info.")); + if (String.IsNullOrEmpty(stack.SignatureHash)) + result.Errors.Add(new ValidationFailure("SignatureHash", "Please specify a valid signature hash.")); - return result; - } + if (stack.SignatureInfo == null) + result.Errors.Add(new ValidationFailure("SignatureInfo", "Please specify a valid signature info.")); - public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new CancellationToken()) { - return Task.FromResult(Validate(context.InstanceToValidate)); - } + return result; + } - private bool IsObjectId(string value) { - if (String.IsNullOrEmpty(value)) - return false; + public override Task ValidateAsync(ValidationContext context, CancellationToken cancellation = new CancellationToken()) { + return Task.FromResult(Validate(context.InstanceToValidate)); + } - return value.Length == 24; - } + private bool IsObjectId(string value) { + if (String.IsNullOrEmpty(value)) + return false; + + return value.Length == 24; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Validation/TokenValidator.cs b/src/Exceptionless.Core/Validation/TokenValidator.cs index f2ede021cf..47c60a4b05 100644 --- a/src/Exceptionless.Core/Validation/TokenValidator.cs +++ b/src/Exceptionless.Core/Validation/TokenValidator.cs @@ -1,23 +1,22 @@ -using System; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using FluentValidation; -namespace Exceptionless.Core.Validation { - public class TokenValidator : AbstractValidator { - public TokenValidator() { - RuleFor(t => t.Id).NotEmpty().WithMessage("Please specify a valid id."); - RuleFor(t => t.OrganizationId).IsObjectId().When(t => !String.IsNullOrEmpty(t.OrganizationId)).WithMessage("Please specify a valid organization id."); - RuleFor(t => t.OrganizationId).NotEmpty().When(t => !String.IsNullOrEmpty(t.ProjectId) || String.IsNullOrEmpty(t.UserId)).WithMessage("Please specify a valid organization id."); - RuleFor(t => t.CreatedUtc).NotEmpty().WithMessage("Please specify a valid created date."); - RuleFor(t => t.UpdatedUtc).NotEmpty().WithMessage("Please specify a valid updated date."); +namespace Exceptionless.Core.Validation; - RuleFor(t => t.ProjectId).IsObjectId().When(t => !String.IsNullOrEmpty(t.ProjectId)).WithMessage("Please specify a valid project id."); - RuleFor(t => t.DefaultProjectId).Must(String.IsNullOrEmpty).When(t => !String.IsNullOrEmpty(t.ProjectId)).WithMessage("Default project id cannot be set when a project id is defined."); - RuleFor(t => t.DefaultProjectId).IsObjectId().When(t => !String.IsNullOrEmpty(t.DefaultProjectId)).WithMessage("Please specify a valid default project id."); - RuleFor(t => t.UserId).Must(String.IsNullOrEmpty).When(t => !String.IsNullOrEmpty(t.ProjectId)).WithMessage("Can't set both user id and project id."); - - RuleFor(t => t.IsDisabled).Equal(false).When(t => t.Type != TokenType.Access).WithMessage("Only access tokens can be disabled"); - } +public class TokenValidator : AbstractValidator { + public TokenValidator() { + RuleFor(t => t.Id).NotEmpty().WithMessage("Please specify a valid id."); + RuleFor(t => t.OrganizationId).IsObjectId().When(t => !String.IsNullOrEmpty(t.OrganizationId)).WithMessage("Please specify a valid organization id."); + RuleFor(t => t.OrganizationId).NotEmpty().When(t => !String.IsNullOrEmpty(t.ProjectId) || String.IsNullOrEmpty(t.UserId)).WithMessage("Please specify a valid organization id."); + RuleFor(t => t.CreatedUtc).NotEmpty().WithMessage("Please specify a valid created date."); + RuleFor(t => t.UpdatedUtc).NotEmpty().WithMessage("Please specify a valid updated date."); + + RuleFor(t => t.ProjectId).IsObjectId().When(t => !String.IsNullOrEmpty(t.ProjectId)).WithMessage("Please specify a valid project id."); + RuleFor(t => t.DefaultProjectId).Must(String.IsNullOrEmpty).When(t => !String.IsNullOrEmpty(t.ProjectId)).WithMessage("Default project id cannot be set when a project id is defined."); + RuleFor(t => t.DefaultProjectId).IsObjectId().When(t => !String.IsNullOrEmpty(t.DefaultProjectId)).WithMessage("Please specify a valid default project id."); + RuleFor(t => t.UserId).Must(String.IsNullOrEmpty).When(t => !String.IsNullOrEmpty(t.ProjectId)).WithMessage("Can't set both user id and project id."); + + RuleFor(t => t.IsDisabled).Equal(false).When(t => t.Type != TokenType.Access).WithMessage("Only access tokens can be disabled"); } } diff --git a/src/Exceptionless.Core/Validation/UserDescriptionValidator.cs b/src/Exceptionless.Core/Validation/UserDescriptionValidator.cs index 0efc6276af..d74629f691 100644 --- a/src/Exceptionless.Core/Validation/UserDescriptionValidator.cs +++ b/src/Exceptionless.Core/Validation/UserDescriptionValidator.cs @@ -1,18 +1,17 @@ -using System; -using Exceptionless.Core.Models.Data; +using Exceptionless.Core.Models.Data; using FluentValidation; -namespace Exceptionless.Core.Validation { - public class UserDescriptionValidator : AbstractValidator { - public UserDescriptionValidator() { - RuleFor(u => u.EmailAddress) - .EmailAddress() - .Unless(u => String.IsNullOrEmpty(u.EmailAddress)) - .WithMessage("Please specify a valid email address."); +namespace Exceptionless.Core.Validation; - RuleFor(u => u.Description) - .NotEmpty() - .WithMessage("Please specify a description."); - } +public class UserDescriptionValidator : AbstractValidator { + public UserDescriptionValidator() { + RuleFor(u => u.EmailAddress) + .EmailAddress() + .Unless(u => String.IsNullOrEmpty(u.EmailAddress)) + .WithMessage("Please specify a valid email address."); + + RuleFor(u => u.Description) + .NotEmpty() + .WithMessage("Please specify a description."); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Validation/UserValidator.cs b/src/Exceptionless.Core/Validation/UserValidator.cs index 0484812b54..5b379cf966 100644 --- a/src/Exceptionless.Core/Validation/UserValidator.cs +++ b/src/Exceptionless.Core/Validation/UserValidator.cs @@ -1,15 +1,14 @@ -using System; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using FluentValidation; -namespace Exceptionless.Core.Validation { - public class UserValidator : AbstractValidator { - public UserValidator() { - RuleFor(u => u.FullName).NotEmpty().WithMessage("Please specify a valid full name."); - RuleFor(u => u.EmailAddress).NotEmpty().EmailAddress().WithMessage("Please specify a valid email address."); - RuleFor(u => u.IsEmailAddressVerified).Equal(false).When(u => String.IsNullOrEmpty(u.EmailAddress)).WithMessage("An email address cannot be verified if it doesn't exist"); - RuleFor(u => u.VerifyEmailAddressToken).NotEmpty().When(u => !u.IsEmailAddressVerified).WithMessage("A verify email address token must be set if the email address has not been verified."); - RuleFor(u => u.VerifyEmailAddressTokenExpiration).NotEmpty().When(u => !u.IsEmailAddressVerified).WithMessage("A verify email address token expiration must be set if the email address has not been verified."); - } +namespace Exceptionless.Core.Validation; + +public class UserValidator : AbstractValidator { + public UserValidator() { + RuleFor(u => u.FullName).NotEmpty().WithMessage("Please specify a valid full name."); + RuleFor(u => u.EmailAddress).NotEmpty().EmailAddress().WithMessage("Please specify a valid email address."); + RuleFor(u => u.IsEmailAddressVerified).Equal(false).When(u => String.IsNullOrEmpty(u.EmailAddress)).WithMessage("An email address cannot be verified if it doesn't exist"); + RuleFor(u => u.VerifyEmailAddressToken).NotEmpty().When(u => !u.IsEmailAddressVerified).WithMessage("A verify email address token must be set if the email address has not been verified."); + RuleFor(u => u.VerifyEmailAddressTokenExpiration).NotEmpty().When(u => !u.IsEmailAddressVerified).WithMessage("A verify email address token expiration must be set if the email address has not been verified."); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Core/Validation/WebHookValidator.cs b/src/Exceptionless.Core/Validation/WebHookValidator.cs index 2977dcbce8..9b73a3d2ef 100644 --- a/src/Exceptionless.Core/Validation/WebHookValidator.cs +++ b/src/Exceptionless.Core/Validation/WebHookValidator.cs @@ -1,16 +1,15 @@ -using System; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using FluentValidation; -namespace Exceptionless.Core.Validation { - public class WebHookValidator : AbstractValidator { - public WebHookValidator() { - RuleFor(w => w.OrganizationId).IsObjectId().WithMessage("Please specify a valid organization id."); - RuleFor(w => w.ProjectId).IsObjectId().When(p => String.IsNullOrEmpty(p.OrganizationId)).WithMessage("Please specify a valid project id."); - RuleFor(w => w.Url).NotEmpty().WithMessage("Please specify a valid url."); - RuleFor(w => w.EventTypes).NotEmpty().WithMessage("Please specify one or more event types."); - RuleFor(w => w.Version).NotEmpty().WithMessage("Please specify a valid version."); - } +namespace Exceptionless.Core.Validation; + +public class WebHookValidator : AbstractValidator { + public WebHookValidator() { + RuleFor(w => w.OrganizationId).IsObjectId().WithMessage("Please specify a valid organization id."); + RuleFor(w => w.ProjectId).IsObjectId().When(p => String.IsNullOrEmpty(p.OrganizationId)).WithMessage("Please specify a valid project id."); + RuleFor(w => w.Url).NotEmpty().WithMessage("Please specify a valid url."); + RuleFor(w => w.EventTypes).NotEmpty().WithMessage("Please specify one or more event types."); + RuleFor(w => w.Version).NotEmpty().WithMessage("Please specify a valid version."); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/Bootstrapper.cs b/src/Exceptionless.Insulation/Bootstrapper.cs index 49d8a756a8..783dfaaa9d 100644 --- a/src/Exceptionless.Insulation/Bootstrapper.cs +++ b/src/Exceptionless.Insulation/Bootstrapper.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Amazon; using Amazon.Runtime; using App.Metrics; @@ -38,306 +35,314 @@ using StackExchange.Redis; using QueueOptions = Exceptionless.Core.Configuration.QueueOptions; -namespace Exceptionless.Insulation { - public class Bootstrapper { - public static void RegisterServices(IServiceCollection services, AppOptions appOptions, bool runMaintenanceTasks) { - if (!String.IsNullOrEmpty(appOptions.ExceptionlessApiKey) && !String.IsNullOrEmpty(appOptions.ExceptionlessServerUrl)) { - var client = ExceptionlessClient.Default; - client.Configuration.ServerUrl = appOptions.ExceptionlessServerUrl; - client.Configuration.ApiKey = appOptions.ExceptionlessApiKey; - - client.Configuration.SetDefaultMinLogLevel(Logging.LogLevel.Warn); - client.Configuration.UseLogger(new SelfLogLogger()); - client.Configuration.SetVersion(appOptions.Version); - if (String.IsNullOrEmpty(appOptions.InternalProjectId)) - client.Configuration.Enabled = false; - - client.Configuration.UseInMemoryStorage(); - client.Configuration.UseReferenceIds(); - - services.ReplaceSingleton(); - services.AddSingleton(client); - } - - if (!String.IsNullOrEmpty(appOptions.GoogleGeocodingApiKey)) - services.ReplaceSingleton(s => new GoogleGeocodeService(appOptions.GoogleGeocodingApiKey)); - - if (!String.IsNullOrEmpty(appOptions.MaxMindGeoIpKey)) - services.ReplaceSingleton(); - - RegisterCache(services, appOptions.CacheOptions); - RegisterMessageBus(services, appOptions.MessageBusOptions); - RegisterMetric(services, appOptions.MetricOptions); - RegisterQueue(services, appOptions.QueueOptions, runMaintenanceTasks); - RegisterStorage(services, appOptions.StorageOptions); - - var healthCheckBuilder = RegisterHealthChecks(services, appOptions); - - if (!String.IsNullOrEmpty(appOptions.EmailOptions.SmtpHost)) { - services.ReplaceSingleton(); - healthCheckBuilder.Add(new HealthCheckRegistration("Mail", s => s.GetRequiredService() as MailKitMailSender, null, new[] { "Mail", "MailMessage", "AllJobs" })); - } +namespace Exceptionless.Insulation; + +public class Bootstrapper { + public static void RegisterServices(IServiceCollection services, AppOptions appOptions, bool runMaintenanceTasks) { + if (!String.IsNullOrEmpty(appOptions.ExceptionlessApiKey) && !String.IsNullOrEmpty(appOptions.ExceptionlessServerUrl)) { + var client = ExceptionlessClient.Default; + client.Configuration.ServerUrl = appOptions.ExceptionlessServerUrl; + client.Configuration.ApiKey = appOptions.ExceptionlessApiKey; + + client.Configuration.SetDefaultMinLogLevel(Logging.LogLevel.Warn); + client.Configuration.UseLogger(new SelfLogLogger()); + client.Configuration.SetVersion(appOptions.Version); + if (String.IsNullOrEmpty(appOptions.InternalProjectId)) + client.Configuration.Enabled = false; + + client.Configuration.UseInMemoryStorage(); + client.Configuration.UseReferenceIds(); + + services.ReplaceSingleton(); + services.AddSingleton(client); } - private static IHealthChecksBuilder RegisterHealthChecks(IServiceCollection services, AppOptions appOptions) { - services.AddStartupActionToWaitForHealthChecks("Critical"); - - return services.AddHealthChecks() - .AddCheckForStartupActions("Critical") - - .AddAutoNamedCheck("Critical") - .AddAutoNamedCheck("Critical") - .AddAutoNamedCheck("EventPosts", "AllJobs") - - .AddAutoNamedCheck>("EventPosts", "AllJobs") - .AddAutoNamedCheck>("EventUserDescriptions", "AllJobs") - .AddAutoNamedCheck>("EventNotifications", "AllJobs") - .AddAutoNamedCheck>("WebHooks", "AllJobs") - .AddAutoNamedCheck>("AllJobs") - .AddAutoNamedCheck>("WorkItem", "AllJobs") - - .AddAutoNamedCheck("AllJobs") - .AddAutoNamedCheck("AllJobs") - .AddAutoNamedCheck("AllJobs") - .AddAutoNamedCheck("AllJobs") - .AddAutoNamedCheck("AllJobs") - .AddAutoNamedCheck("AllJobs") - .AddAutoNamedCheck("AllJobs"); + if (!String.IsNullOrEmpty(appOptions.GoogleGeocodingApiKey)) + services.ReplaceSingleton(s => new GoogleGeocodeService(appOptions.GoogleGeocodingApiKey)); + + if (!String.IsNullOrEmpty(appOptions.MaxMindGeoIpKey)) + services.ReplaceSingleton(); + + RegisterCache(services, appOptions.CacheOptions); + RegisterMessageBus(services, appOptions.MessageBusOptions); + RegisterMetric(services, appOptions.MetricOptions); + RegisterQueue(services, appOptions.QueueOptions, runMaintenanceTasks); + RegisterStorage(services, appOptions.StorageOptions); + + var healthCheckBuilder = RegisterHealthChecks(services, appOptions); + + if (!String.IsNullOrEmpty(appOptions.EmailOptions.SmtpHost)) { + services.ReplaceSingleton(); + healthCheckBuilder.Add(new HealthCheckRegistration("Mail", s => s.GetRequiredService() as MailKitMailSender, null, new[] { "Mail", "MailMessage", "AllJobs" })); } + } + + private static IHealthChecksBuilder RegisterHealthChecks(IServiceCollection services, AppOptions appOptions) { + services.AddStartupActionToWaitForHealthChecks("Critical"); - private static void RegisterCache(IServiceCollection container, CacheOptions options) { - if (String.Equals(options.Provider, "redis")) { - container.ReplaceSingleton(s => GetRedisConnection(options.Data)); + return services.AddHealthChecks() + .AddCheckForStartupActions("Critical") - if (!String.IsNullOrEmpty(options.Scope)) - container.ReplaceSingleton(s => new ScopedCacheClient(CreateRedisCacheClient(s), options.Scope)); - else - container.ReplaceSingleton(CreateRedisCacheClient); + .AddAutoNamedCheck("Critical") + .AddAutoNamedCheck("Critical") + .AddAutoNamedCheck("EventPosts", "AllJobs") + + .AddAutoNamedCheck>("EventPosts", "AllJobs") + .AddAutoNamedCheck>("EventUserDescriptions", "AllJobs") + .AddAutoNamedCheck>("EventNotifications", "AllJobs") + .AddAutoNamedCheck>("WebHooks", "AllJobs") + .AddAutoNamedCheck>("AllJobs") + .AddAutoNamedCheck>("WorkItem", "AllJobs") + + .AddAutoNamedCheck("AllJobs") + .AddAutoNamedCheck("AllJobs") + .AddAutoNamedCheck("AllJobs") + .AddAutoNamedCheck("AllJobs") + .AddAutoNamedCheck("AllJobs") + .AddAutoNamedCheck("AllJobs") + .AddAutoNamedCheck("AllJobs"); + } - container.ReplaceSingleton(); - } + private static void RegisterCache(IServiceCollection container, CacheOptions options) { + if (String.Equals(options.Provider, "redis")) { + container.ReplaceSingleton(s => GetRedisConnection(options.Data)); + + if (!String.IsNullOrEmpty(options.Scope)) + container.ReplaceSingleton(s => new ScopedCacheClient(CreateRedisCacheClient(s), options.Scope)); + else + container.ReplaceSingleton(CreateRedisCacheClient); + + container.ReplaceSingleton(); } + } - private static void RegisterMessageBus(IServiceCollection container, MessageBusOptions options) { - if (String.Equals(options.Provider, "redis")) { - container.ReplaceSingleton(s => GetRedisConnection(options.Data)); - - container.ReplaceSingleton(s => new RedisMessageBus(new RedisMessageBusOptions { - Subscriber = s.GetRequiredService().GetSubscriber(), - Topic = options.Topic, - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - } else if (String.Equals(options.Provider, "rabbitmq")) { - container.ReplaceSingleton(s => new RabbitMQMessageBus(new RabbitMQMessageBusOptions { - ConnectionString = options.ConnectionString, - Topic = options.Topic, - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - } + private static void RegisterMessageBus(IServiceCollection container, MessageBusOptions options) { + if (String.Equals(options.Provider, "redis")) { + container.ReplaceSingleton(s => GetRedisConnection(options.Data)); + + container.ReplaceSingleton(s => new RedisMessageBus(new RedisMessageBusOptions { + Subscriber = s.GetRequiredService().GetSubscriber(), + Topic = options.Topic, + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); + } + else if (String.Equals(options.Provider, "rabbitmq")) { + container.ReplaceSingleton(s => new RabbitMQMessageBus(new RabbitMQMessageBusOptions { + ConnectionString = options.ConnectionString, + Topic = options.Topic, + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); } + } + + private static IConnectionMultiplexer GetRedisConnection(Dictionary options) { + // TODO: Remove this extra config parse step when sentinel bug is fixed + var config = ConfigurationOptions.Parse(options.GetString("server")); + return ConnectionMultiplexer.Connect(config); + } - private static IConnectionMultiplexer GetRedisConnection(Dictionary options) { - // TODO: Remove this extra config parse step when sentinel bug is fixed - var config = ConfigurationOptions.Parse(options.GetString("server")); - return ConnectionMultiplexer.Connect(config); + private static void RegisterMetric(IServiceCollection container, MetricOptions options) { + if (String.Equals(options.Provider, "statsd")) { + container.ReplaceSingleton(s => new StatsDMetricsClient(new StatsDMetricsClientOptions { + ServerName = options.Data.GetString("server", "127.0.0.1"), + Port = options.Data.GetValueOrDefault("port", 8125), + Prefix = "ex", + LoggerFactory = s.GetRequiredService() + })); } + else { + var metrics = BuildAppMetrics(options); + if (metrics == null) + return; - private static void RegisterMetric(IServiceCollection container, MetricOptions options) { - if (String.Equals(options.Provider, "statsd")) { - container.ReplaceSingleton(s => new StatsDMetricsClient(new StatsDMetricsClientOptions { - ServerName = options.Data.GetString("server", "127.0.0.1"), - Port = options.Data.GetValueOrDefault("port", 8125), - Prefix = "ex", - LoggerFactory = s.GetRequiredService() - })); - } else { - var metrics = BuildAppMetrics(options); - if (metrics == null) - return; - - container.ReplaceSingleton(metrics.Clock); - container.ReplaceSingleton(metrics.Filter); - container.ReplaceSingleton(metrics.DefaultOutputMetricsFormatter); - container.ReplaceSingleton(metrics.OutputMetricsFormatters); - container.ReplaceSingleton(metrics.DefaultOutputEnvFormatter); - container.ReplaceSingleton(metrics.OutputEnvFormatters); - container.TryAddSingleton(); - container.ReplaceSingleton(metrics); - container.ReplaceSingleton(metrics); - container.ReplaceSingleton(metrics.Options); - container.ReplaceSingleton(metrics.Reporters); - container.ReplaceSingleton(metrics.ReportRunner); - container.TryAddSingleton(); - container.ReplaceSingleton(); - } + container.ReplaceSingleton(metrics.Clock); + container.ReplaceSingleton(metrics.Filter); + container.ReplaceSingleton(metrics.DefaultOutputMetricsFormatter); + container.ReplaceSingleton(metrics.OutputMetricsFormatters); + container.ReplaceSingleton(metrics.DefaultOutputEnvFormatter); + container.ReplaceSingleton(metrics.OutputEnvFormatters); + container.TryAddSingleton(); + container.ReplaceSingleton(metrics); + container.ReplaceSingleton(metrics); + container.ReplaceSingleton(metrics.Options); + container.ReplaceSingleton(metrics.Reporters); + container.ReplaceSingleton(metrics.ReportRunner); + container.TryAddSingleton(); + container.ReplaceSingleton(); } + } - private static IMetricsRoot BuildAppMetrics(MetricOptions options) { - var metricsBuilder = AppMetrics.CreateDefaultBuilder(); - switch (options.Provider) { - case "graphite": - metricsBuilder.Report.ToGraphite(new MetricsReportingGraphiteOptions { - Graphite = { + private static IMetricsRoot BuildAppMetrics(MetricOptions options) { + var metricsBuilder = AppMetrics.CreateDefaultBuilder(); + switch (options.Provider) { + case "graphite": + metricsBuilder.Report.ToGraphite(new MetricsReportingGraphiteOptions { + Graphite = { BaseUri = new Uri(options.Data.GetString("server")) } - }); - break; - case "http": - metricsBuilder.Report.OverHttp(new MetricsReportingHttpOptions { - HttpSettings = { + }); + break; + case "http": + metricsBuilder.Report.OverHttp(new MetricsReportingHttpOptions { + HttpSettings = { RequestUri = new Uri(options.Data.GetString("server")), UserName = options.Data.GetString("username"), Password = options.Data.GetString("password"), } - }); - break; - case "influxdb": - metricsBuilder.Report.ToInfluxDb(new MetricsReportingInfluxDbOptions { - InfluxDb = { + }); + break; + case "influxdb": + metricsBuilder.Report.ToInfluxDb(new MetricsReportingInfluxDbOptions { + InfluxDb = { BaseUri = new Uri(options.Data.GetString("server")), UserName = options.Data.GetString("username"), Password = options.Data.GetString("password"), Database = options.Data.GetString("database", "exceptionless") } - }); - break; - default: - return null; - } - - return metricsBuilder.Build(); + }); + break; + default: + return null; } - private static void RegisterQueue(IServiceCollection container, QueueOptions options, bool runMaintenanceTasks) { - if (String.Equals(options.Provider, "azurestorage")) { - container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options, retries: 1)); - container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); - container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); - container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); - container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); - container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options, workItemTimeout: TimeSpan.FromHours(1))); - } else if (String.Equals(options.Provider, "redis")) { - container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks, retries: 1)); - container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); - container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); - container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); - container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); - container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks, workItemTimeout: TimeSpan.FromHours(1))); - } else if (String.Equals(options.Provider, "sqs")) { - container.ReplaceSingleton(s => CreateSQSQueue(s, options, retries: 1)); - container.ReplaceSingleton(s => CreateSQSQueue(s, options)); - container.ReplaceSingleton(s => CreateSQSQueue(s, options)); - container.ReplaceSingleton(s => CreateSQSQueue(s, options)); - container.ReplaceSingleton(s => CreateSQSQueue(s, options)); - container.ReplaceSingleton(s => CreateSQSQueue(s, options, workItemTimeout: TimeSpan.FromHours(1))); - } - } + return metricsBuilder.Build(); + } - private static void RegisterStorage(IServiceCollection container, StorageOptions options) { - if (String.Equals(options.Provider, "aliyun")) { - container.ReplaceSingleton(s => new AliyunFileStorage(new AliyunFileStorageOptions { - ConnectionString = options.ConnectionString, - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - } else if (String.Equals(options.Provider, "azurestorage")) { - container.ReplaceSingleton(s => new AzureFileStorage(new AzureFileStorageOptions { - ConnectionString = options.ConnectionString, - ContainerName = $"{options.ScopePrefix}ex-events", - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - } else if (String.Equals(options.Provider, "folder")) { - string path = options.Data.GetString("path", "|DataDirectory|\\storage"); - container.AddSingleton(s => new FolderFileStorage(new FolderFileStorageOptions { - Folder = PathHelper.ExpandPath(path), - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - } else if (String.Equals(options.Provider, "minio")) { - container.ReplaceSingleton(s => new MinioFileStorage(new MinioFileStorageOptions { - ConnectionString = options.ConnectionString, - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - } else if (String.Equals(options.Provider, "s3")) { - container.ReplaceSingleton(s => new S3FileStorage(new S3FileStorageOptions { - ConnectionString = options.ConnectionString, - Credentials = GetAWSCredentials(options.Data), - Region = GetAWSRegionEndpoint(options.Data), - Bucket = $"{options.ScopePrefix}{options.Data.GetString("bucket", "ex-events")}", - Serializer = s.GetRequiredService(), - LoggerFactory = s.GetRequiredService() - })); - } + private static void RegisterQueue(IServiceCollection container, QueueOptions options, bool runMaintenanceTasks) { + if (String.Equals(options.Provider, "azurestorage")) { + container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options, retries: 1)); + container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); + container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); + container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); + container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options)); + container.ReplaceSingleton(s => CreateAzureStorageQueue(s, options, workItemTimeout: TimeSpan.FromHours(1))); } + else if (String.Equals(options.Provider, "redis")) { + container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks, retries: 1)); + container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); + container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); + container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); + container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks)); + container.ReplaceSingleton(s => CreateRedisQueue(s, options, runMaintenanceTasks, workItemTimeout: TimeSpan.FromHours(1))); + } + else if (String.Equals(options.Provider, "sqs")) { + container.ReplaceSingleton(s => CreateSQSQueue(s, options, retries: 1)); + container.ReplaceSingleton(s => CreateSQSQueue(s, options)); + container.ReplaceSingleton(s => CreateSQSQueue(s, options)); + container.ReplaceSingleton(s => CreateSQSQueue(s, options)); + container.ReplaceSingleton(s => CreateSQSQueue(s, options)); + container.ReplaceSingleton(s => CreateSQSQueue(s, options, workItemTimeout: TimeSpan.FromHours(1))); + } + } - private static IQueue CreateAzureStorageQueue(IServiceProvider container, QueueOptions options, int retries = 2, TimeSpan? workItemTimeout = null) where T : class { - return new AzureStorageQueue(new AzureStorageQueueOptions { + private static void RegisterStorage(IServiceCollection container, StorageOptions options) { + if (String.Equals(options.Provider, "aliyun")) { + container.ReplaceSingleton(s => new AliyunFileStorage(new AliyunFileStorageOptions { ConnectionString = options.ConnectionString, - Name = GetQueueName(options).ToLowerInvariant(), - Retries = retries, - Behaviors = container.GetServices>().ToList(), - WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), - Serializer = container.GetRequiredService(), - LoggerFactory = container.GetRequiredService() - }); + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); } - - private static IQueue CreateRedisQueue(IServiceProvider container, QueueOptions options, bool runMaintenanceTasks, int retries = 2, TimeSpan? workItemTimeout = null) where T : class { - return new RedisQueue(new RedisQueueOptions { - ConnectionMultiplexer = container.GetRequiredService(), - Name = GetQueueName(options), - Retries = retries, - Behaviors = container.GetServices>().ToList(), - WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), - RunMaintenanceTasks = runMaintenanceTasks, - Serializer = container.GetRequiredService(), - LoggerFactory = container.GetRequiredService() - }); + else if (String.Equals(options.Provider, "azurestorage")) { + container.ReplaceSingleton(s => new AzureFileStorage(new AzureFileStorageOptions { + ConnectionString = options.ConnectionString, + ContainerName = $"{options.ScopePrefix}ex-events", + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); } - - private static RedisCacheClient CreateRedisCacheClient(IServiceProvider container) { - return new RedisCacheClient(new RedisCacheClientOptions { - ConnectionMultiplexer = container.GetRequiredService(), - Serializer = container.GetRequiredService(), - LoggerFactory = container.GetRequiredService() - }); + else if (String.Equals(options.Provider, "folder")) { + string path = options.Data.GetString("path", "|DataDirectory|\\storage"); + container.AddSingleton(s => new FolderFileStorage(new FolderFileStorageOptions { + Folder = PathHelper.ExpandPath(path), + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); } - - private static IQueue CreateSQSQueue(IServiceProvider container, QueueOptions options, int retries = 2, TimeSpan? workItemTimeout = null) where T : class { - return new SQSQueue(new SQSQueueOptions { - Name = GetQueueName(options), + else if (String.Equals(options.Provider, "minio")) { + container.ReplaceSingleton(s => new MinioFileStorage(new MinioFileStorageOptions { + ConnectionString = options.ConnectionString, + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); + } + else if (String.Equals(options.Provider, "s3")) { + container.ReplaceSingleton(s => new S3FileStorage(new S3FileStorageOptions { + ConnectionString = options.ConnectionString, Credentials = GetAWSCredentials(options.Data), Region = GetAWSRegionEndpoint(options.Data), - CanCreateQueue = false, - Retries = retries, - Behaviors = container.GetServices>().ToList(), - WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), - Serializer = container.GetRequiredService(), - LoggerFactory = container.GetRequiredService() - }); + Bucket = $"{options.ScopePrefix}{options.Data.GetString("bucket", "ex-events")}", + Serializer = s.GetRequiredService(), + LoggerFactory = s.GetRequiredService() + })); } + } - private static string GetQueueName(QueueOptions options) { - return String.Concat(options.ScopePrefix, typeof(T).Name); - } - - private static RegionEndpoint GetAWSRegionEndpoint(IDictionary data) { - string region = data.GetString("region"); - return RegionEndpoint.GetBySystemName(String.IsNullOrEmpty(region) ? "us-east-1" : region); - } + private static IQueue CreateAzureStorageQueue(IServiceProvider container, QueueOptions options, int retries = 2, TimeSpan? workItemTimeout = null) where T : class { + return new AzureStorageQueue(new AzureStorageQueueOptions { + ConnectionString = options.ConnectionString, + Name = GetQueueName(options).ToLowerInvariant(), + Retries = retries, + Behaviors = container.GetServices>().ToList(), + WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), + Serializer = container.GetRequiredService(), + LoggerFactory = container.GetRequiredService() + }); + } - private static AWSCredentials GetAWSCredentials(IDictionary data) { - string accessKey = data.GetString("accesskey"); - string secretKey = data.GetString("secretkey"); - if (String.IsNullOrEmpty(accessKey) - || String.IsNullOrEmpty(secretKey)) - return FallbackCredentialsFactory.GetCredentials(); + private static IQueue CreateRedisQueue(IServiceProvider container, QueueOptions options, bool runMaintenanceTasks, int retries = 2, TimeSpan? workItemTimeout = null) where T : class { + return new RedisQueue(new RedisQueueOptions { + ConnectionMultiplexer = container.GetRequiredService(), + Name = GetQueueName(options), + Retries = retries, + Behaviors = container.GetServices>().ToList(), + WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), + RunMaintenanceTasks = runMaintenanceTasks, + Serializer = container.GetRequiredService(), + LoggerFactory = container.GetRequiredService() + }); + } - return new BasicAWSCredentials(accessKey, secretKey); - } + private static RedisCacheClient CreateRedisCacheClient(IServiceProvider container) { + return new RedisCacheClient(new RedisCacheClientOptions { + ConnectionMultiplexer = container.GetRequiredService(), + Serializer = container.GetRequiredService(), + LoggerFactory = container.GetRequiredService() + }); + } + + private static IQueue CreateSQSQueue(IServiceProvider container, QueueOptions options, int retries = 2, TimeSpan? workItemTimeout = null) where T : class { + return new SQSQueue(new SQSQueueOptions { + Name = GetQueueName(options), + Credentials = GetAWSCredentials(options.Data), + Region = GetAWSRegionEndpoint(options.Data), + CanCreateQueue = false, + Retries = retries, + Behaviors = container.GetServices>().ToList(), + WorkItemTimeout = workItemTimeout.GetValueOrDefault(TimeSpan.FromMinutes(5.0)), + Serializer = container.GetRequiredService(), + LoggerFactory = container.GetRequiredService() + }); + } + + private static string GetQueueName(QueueOptions options) { + return String.Concat(options.ScopePrefix, typeof(T).Name); + } + + private static RegionEndpoint GetAWSRegionEndpoint(IDictionary data) { + string region = data.GetString("region"); + return RegionEndpoint.GetBySystemName(String.IsNullOrEmpty(region) ? "us-east-1" : region); + } + + private static AWSCredentials GetAWSCredentials(IDictionary data) { + string accessKey = data.GetString("accesskey"); + string secretKey = data.GetString("secretkey"); + if (String.IsNullOrEmpty(accessKey) + || String.IsNullOrEmpty(secretKey)) + return FallbackCredentialsFactory.GetCredentials(); + + return new BasicAWSCredentials(accessKey, secretKey); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/Configuration/YamlConfigurationExtensions.cs b/src/Exceptionless.Insulation/Configuration/YamlConfigurationExtensions.cs index bac04d35c4..c358206ce6 100644 --- a/src/Exceptionless.Insulation/Configuration/YamlConfigurationExtensions.cs +++ b/src/Exceptionless.Insulation/Configuration/YamlConfigurationExtensions.cs @@ -1,127 +1,126 @@ -using System; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.FileProviders; -namespace Exceptionless.Insulation.Configuration { +namespace Exceptionless.Insulation.Configuration; + +/// +/// Extension methods for adding . +/// +public static class YamlConfigurationExtensions { /// - /// Extension methods for adding . + /// Adds the YAML configuration provider at to . /// - public static class YamlConfigurationExtensions { - /// - /// Adds the YAML configuration provider at to . - /// - /// - /// The to add to. - /// - /// - /// Path relative to the base path stored in - /// of . - /// - /// - /// The . - /// - public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path) { - return AddYamlFile(builder, null, path, false, false); - } + /// + /// The to add to. + /// + /// + /// Path relative to the base path stored in + /// of . + /// + /// + /// The . + /// + public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path) { + return AddYamlFile(builder, null, path, false, false); + } - /// - /// Adds the YAML configuration provider at to . - /// - /// - /// The to add to. - /// - /// - /// Path relative to the base path stored in - /// of . - /// - /// - /// Whether the file is optional. - /// - /// - /// The . - /// - public static IConfigurationBuilder - AddYamlFile(this IConfigurationBuilder builder, string path, bool optional) { - return AddYamlFile(builder, null, path, optional, false); - } + /// + /// Adds the YAML configuration provider at to . + /// + /// + /// The to add to. + /// + /// + /// Path relative to the base path stored in + /// of . + /// + /// + /// Whether the file is optional. + /// + /// + /// The . + /// + public static IConfigurationBuilder + AddYamlFile(this IConfigurationBuilder builder, string path, bool optional) { + return AddYamlFile(builder, null, path, optional, false); + } - /// - /// Adds the YAML configuration provider at to . - /// - /// - /// The to add to. - /// - /// - /// Path relative to the base path stored in - /// of . - /// - /// - /// Whether the file is optional. - /// - /// - /// Whether the configuration should be reloaded if the file changes. - /// - /// - /// The . - /// - public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) { - return AddYamlFile(builder, null, path, optional, reloadOnChange); - } + /// + /// Adds the YAML configuration provider at to . + /// + /// + /// The to add to. + /// + /// + /// Path relative to the base path stored in + /// of . + /// + /// + /// Whether the file is optional. + /// + /// + /// Whether the configuration should be reloaded if the file changes. + /// + /// + /// The . + /// + public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) { + return AddYamlFile(builder, null, path, optional, reloadOnChange); + } - /// - /// Adds a YAML configuration source to . - /// - /// - /// The to add to. - /// - /// - /// The to use to access the file. - /// - /// - /// Path relative to the base path stored in - /// of . - /// - /// - /// Whether the file is optional. - /// - /// - /// Whether the configuration should be reloaded if the file changes. - /// - /// - /// The . - /// - public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange) { - if (builder == null) - throw new ArgumentNullException(nameof(builder)); + /// + /// Adds a YAML configuration source to . + /// + /// + /// The to add to. + /// + /// + /// The to use to access the file. + /// + /// + /// Path relative to the base path stored in + /// of . + /// + /// + /// Whether the file is optional. + /// + /// + /// Whether the configuration should be reloaded if the file changes. + /// + /// + /// The . + /// + public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange) { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); - if (String.IsNullOrEmpty(path)) - throw new ArgumentException("File path must be a non-empty string.", nameof(path)); + if (String.IsNullOrEmpty(path)) + throw new ArgumentException("File path must be a non-empty string.", nameof(path)); - return builder.AddYamlFile(s => { - s.FileProvider = provider; - s.Path = path; - s.Optional = optional; - s.ReloadOnChange = reloadOnChange; - s.ResolveFileProvider(); - }); - } + return builder.AddYamlFile(s => { + s.FileProvider = provider; + s.Path = path; + s.Optional = optional; + s.ReloadOnChange = reloadOnChange; + s.ResolveFileProvider(); + }); + } - /// - /// Adds a YAML configuration source to . - /// - /// - /// The to add to. - /// - /// - /// Configures the source. - /// - /// - /// The . - /// - public static IConfigurationBuilder AddYamlFile( - this IConfigurationBuilder builder, - Action configureSource) { - return builder.Add(configureSource); - } + /// + /// Adds a YAML configuration source to . + /// + /// + /// The to add to. + /// + /// + /// Configures the source. + /// + /// + /// The . + /// + public static IConfigurationBuilder AddYamlFile( + this IConfigurationBuilder builder, + Action configureSource) { + return builder.Add(configureSource); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/Configuration/YamlConfigurationFileParser.cs b/src/Exceptionless.Insulation/Configuration/YamlConfigurationFileParser.cs index 726fafacf6..be52123eb4 100644 --- a/src/Exceptionless.Insulation/Configuration/YamlConfigurationFileParser.cs +++ b/src/Exceptionless.Insulation/Configuration/YamlConfigurationFileParser.cs @@ -1,97 +1,93 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using YamlDotNet.RepresentationModel; -namespace Exceptionless.Insulation.Configuration { - internal class YamlConfigurationFileParser { - private readonly Stack _context = new Stack(); +namespace Exceptionless.Insulation.Configuration; - private readonly IDictionary _data = - new SortedDictionary(StringComparer.OrdinalIgnoreCase); +internal class YamlConfigurationFileParser { + private readonly Stack _context = new Stack(); - private string _currentPath; + private readonly IDictionary _data = + new SortedDictionary(StringComparer.OrdinalIgnoreCase); - public IDictionary Parse(Stream stream) { - _data.Clear(); + private string _currentPath; - var yamlStream = new YamlStream(); - yamlStream.Load(new StreamReader(stream)); + public IDictionary Parse(Stream stream) { + _data.Clear(); - if (!yamlStream.Documents.Any()) - return _data; + var yamlStream = new YamlStream(); + yamlStream.Load(new StreamReader(stream)); - if (!(yamlStream.Documents[0].RootNode is YamlMappingNode mappingNode)) - return _data; - - foreach (var nodePair in mappingNode.Children) { - string context = ((YamlScalarNode) nodePair.Key).Value; - VisitYamlNode(context, nodePair.Value); - } + if (!yamlStream.Documents.Any()) + return _data; + if (!(yamlStream.Documents[0].RootNode is YamlMappingNode mappingNode)) return _data; - } - private void VisitYamlNode(string context, YamlNode node) { - switch (node) { - case YamlScalarNode scalarNode: - VisitYamlScalarNode(context, scalarNode); - break; - case YamlMappingNode mappingNode: - VisitYamlMappingNode(context, mappingNode); - break; - case YamlSequenceNode sequenceNode: - VisitYamlSequenceNode(context, sequenceNode); - break; - default: - throw new ArgumentOutOfRangeException( - nameof(node), - $"Unsupported YAML node type '{node.GetType().Name} was found. " + - $"Path '{_currentPath}', line {node.Start.Line} position {node.Start.Column}."); - } + foreach (var nodePair in mappingNode.Children) { + string context = ((YamlScalarNode)nodePair.Key).Value; + VisitYamlNode(context, nodePair.Value); } - private void VisitYamlScalarNode(string context, YamlScalarNode scalarNode) { - EnterContext(context); - string currentKey = _currentPath; - - if (_data.ContainsKey(currentKey)) - throw new FormatException($"A duplicate key '{currentKey}' was found."); + return _data; + } - _data[currentKey] = scalarNode.Value; - ExitContext(); + private void VisitYamlNode(string context, YamlNode node) { + switch (node) { + case YamlScalarNode scalarNode: + VisitYamlScalarNode(context, scalarNode); + break; + case YamlMappingNode mappingNode: + VisitYamlMappingNode(context, mappingNode); + break; + case YamlSequenceNode sequenceNode: + VisitYamlSequenceNode(context, sequenceNode); + break; + default: + throw new ArgumentOutOfRangeException( + nameof(node), + $"Unsupported YAML node type '{node.GetType().Name} was found. " + + $"Path '{_currentPath}', line {node.Start.Line} position {node.Start.Column}."); } + } + + private void VisitYamlScalarNode(string context, YamlScalarNode scalarNode) { + EnterContext(context); + string currentKey = _currentPath; - private void VisitYamlMappingNode(string context, YamlMappingNode mappingNode) { - EnterContext(context); + if (_data.ContainsKey(currentKey)) + throw new FormatException($"A duplicate key '{currentKey}' was found."); + + _data[currentKey] = scalarNode.Value; + ExitContext(); + } - foreach (var nodePair in mappingNode.Children) { - string innerContext = ((YamlScalarNode) nodePair.Key).Value; - VisitYamlNode(innerContext, nodePair.Value); - } + private void VisitYamlMappingNode(string context, YamlMappingNode mappingNode) { + EnterContext(context); - ExitContext(); + foreach (var nodePair in mappingNode.Children) { + string innerContext = ((YamlScalarNode)nodePair.Key).Value; + VisitYamlNode(innerContext, nodePair.Value); } - private void VisitYamlSequenceNode(string context, YamlSequenceNode sequenceNode) { - EnterContext(context); + ExitContext(); + } - for (int i = 0; i < sequenceNode.Children.Count; ++i) - VisitYamlNode(i.ToString(), sequenceNode.Children[i]); + private void VisitYamlSequenceNode(string context, YamlSequenceNode sequenceNode) { + EnterContext(context); - ExitContext(); - } + for (int i = 0; i < sequenceNode.Children.Count; ++i) + VisitYamlNode(i.ToString(), sequenceNode.Children[i]); - private void EnterContext(string context) { - _context.Push(context); - _currentPath = ConfigurationPath.Combine(_context.Reverse()); - } + ExitContext(); + } - private void ExitContext() { - _context.Pop(); - _currentPath = ConfigurationPath.Combine(_context.Reverse()); - } + private void EnterContext(string context) { + _context.Push(context); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + + private void ExitContext() { + _context.Pop(); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/Configuration/YamlConfigurationProvider.cs b/src/Exceptionless.Insulation/Configuration/YamlConfigurationProvider.cs index 69cd9d961f..073d1e6f5a 100644 --- a/src/Exceptionless.Insulation/Configuration/YamlConfigurationProvider.cs +++ b/src/Exceptionless.Insulation/Configuration/YamlConfigurationProvider.cs @@ -1,49 +1,45 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using YamlDotNet.Core; -namespace Exceptionless.Insulation.Configuration { - public class YamlConfigurationProvider : FileConfigurationProvider { - public YamlConfigurationProvider(YamlConfigurationSource source) - : base(source) { - } +namespace Exceptionless.Insulation.Configuration; - public override void Load(Stream stream) { - var parser = new YamlConfigurationFileParser(); - try { - Data = parser.Parse(stream); - } - catch (YamlException ex) { - string errorLine = String.Empty; - if (stream.CanSeek) { - stream.Seek(0, SeekOrigin.Begin); +public class YamlConfigurationProvider : FileConfigurationProvider { + public YamlConfigurationProvider(YamlConfigurationSource source) + : base(source) { + } - using (var streamReader = new StreamReader(stream)) { - var fileContent = ReadLines(streamReader); - errorLine = RetrieveErrorContext(ex, fileContent); - } - } + public override void Load(Stream stream) { + var parser = new YamlConfigurationFileParser(); + try { + Data = parser.Parse(stream); + } + catch (YamlException ex) { + string errorLine = String.Empty; + if (stream.CanSeek) { + stream.Seek(0, SeekOrigin.Begin); - throw new FormatException( - "Could not parse the YAML file. " + - $"Error on line number '{ex.Start.Line}': '{errorLine}'.", ex); + using (var streamReader = new StreamReader(stream)) { + var fileContent = ReadLines(streamReader); + errorLine = RetrieveErrorContext(ex, fileContent); + } } - } - private static string RetrieveErrorContext(YamlException ex, IEnumerable fileContent) { - string possibleLineContent = fileContent.Skip(ex.Start.Line - 1).FirstOrDefault(); - return possibleLineContent ?? String.Empty; + throw new FormatException( + "Could not parse the YAML file. " + + $"Error on line number '{ex.Start.Line}': '{errorLine}'.", ex); } + } - private static IEnumerable ReadLines(StreamReader streamReader) { - string line; - do { - line = streamReader.ReadLine(); - yield return line; - } while (line != null); - } + private static string RetrieveErrorContext(YamlException ex, IEnumerable fileContent) { + string possibleLineContent = fileContent.Skip(ex.Start.Line - 1).FirstOrDefault(); + return possibleLineContent ?? String.Empty; + } + + private static IEnumerable ReadLines(StreamReader streamReader) { + string line; + do { + line = streamReader.ReadLine(); + yield return line; + } while (line != null); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/Configuration/YamlConfigurationSource.cs b/src/Exceptionless.Insulation/Configuration/YamlConfigurationSource.cs index 10e380d540..8112faf80f 100644 --- a/src/Exceptionless.Insulation/Configuration/YamlConfigurationSource.cs +++ b/src/Exceptionless.Insulation/Configuration/YamlConfigurationSource.cs @@ -1,10 +1,10 @@ using Microsoft.Extensions.Configuration; -namespace Exceptionless.Insulation.Configuration { - public class YamlConfigurationSource : FileConfigurationSource { - public override IConfigurationProvider Build(IConfigurationBuilder builder) { - EnsureDefaults(builder); - return new YamlConfigurationProvider(this); - } +namespace Exceptionless.Insulation.Configuration; + +public class YamlConfigurationSource : FileConfigurationSource { + public override IConfigurationProvider Build(IConfigurationBuilder builder) { + EnsureDefaults(builder); + return new YamlConfigurationProvider(this); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/ExceptionlessClientLastReferenceIdManager.cs b/src/Exceptionless.Insulation/ExceptionlessClientLastReferenceIdManager.cs index ee534272cc..ba94a5d77c 100644 --- a/src/Exceptionless.Insulation/ExceptionlessClientLastReferenceIdManager.cs +++ b/src/Exceptionless.Insulation/ExceptionlessClientLastReferenceIdManager.cs @@ -1,15 +1,15 @@ using Exceptionless.Core.Utility; -namespace Exceptionless.Insulation { - public class ExceptionlessClientCoreLastReferenceIdManager : ICoreLastReferenceIdManager { - private readonly ExceptionlessClient _client; +namespace Exceptionless.Insulation; - public ExceptionlessClientCoreLastReferenceIdManager(ExceptionlessClient client) { - _client = client; - } +public class ExceptionlessClientCoreLastReferenceIdManager : ICoreLastReferenceIdManager { + private readonly ExceptionlessClient _client; - public string GetLastReferenceId() { - return _client.GetLastReferenceId(); - } + public ExceptionlessClientCoreLastReferenceIdManager(ExceptionlessClient client) { + _client = client; + } + + public string GetLastReferenceId() { + return _client.GetLastReferenceId(); } } diff --git a/src/Exceptionless.Insulation/Geo/GoogleGeocodeService.cs b/src/Exceptionless.Insulation/Geo/GoogleGeocodeService.cs index e8918f7750..7b607c6305 100644 --- a/src/Exceptionless.Insulation/Geo/GoogleGeocodeService.cs +++ b/src/Exceptionless.Insulation/Geo/GoogleGeocodeService.cs @@ -1,35 +1,31 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Geo; using Geocoding.Google; -namespace Exceptionless.Insulation.Geo { - public class GoogleGeocodeService : IGeocodeService { - private readonly GoogleGeocoder _geocoder; - public GoogleGeocodeService(string apiKey) { - if (String.IsNullOrEmpty(apiKey)) - throw new ArgumentNullException(nameof(apiKey)); +namespace Exceptionless.Insulation.Geo; - _geocoder = new GoogleGeocoder(apiKey); - } +public class GoogleGeocodeService : IGeocodeService { + private readonly GoogleGeocoder _geocoder; + public GoogleGeocodeService(string apiKey) { + if (String.IsNullOrEmpty(apiKey)) + throw new ArgumentNullException(nameof(apiKey)); - public async Task ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default) { - var addresses = await _geocoder.ReverseGeocodeAsync(latitude, longitude).AnyContext(); - var address = addresses.FirstOrDefault(); - if (address == null) - return null; + _geocoder = new GoogleGeocoder(apiKey); + } + + public async Task ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default) { + var addresses = await _geocoder.ReverseGeocodeAsync(latitude, longitude).AnyContext(); + var address = addresses.FirstOrDefault(); + if (address == null) + return null; - return new GeoResult { - Country = address[GoogleAddressType.Country]?.ShortName, - Level1 = address[GoogleAddressType.AdministrativeAreaLevel1]?.ShortName, - Level2 = address[GoogleAddressType.AdministrativeAreaLevel2]?.ShortName, - Locality = address[GoogleAddressType.Locality]?.ShortName, - Latitude = latitude, - Longitude = longitude - }; - } + return new GeoResult { + Country = address[GoogleAddressType.Country]?.ShortName, + Level1 = address[GoogleAddressType.AdministrativeAreaLevel1]?.ShortName, + Level2 = address[GoogleAddressType.AdministrativeAreaLevel2]?.ShortName, + Locality = address[GoogleAddressType.Locality]?.ShortName, + Latitude = latitude, + Longitude = longitude + }; } } diff --git a/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs b/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs index 1f43ee2359..5a8da06c80 100644 --- a/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs +++ b/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Geo; using Exceptionless.Core.Jobs; using Exceptionless.DateTimeExtensions; @@ -12,99 +9,102 @@ using MaxMind.GeoIP2.Exceptions; using Microsoft.Extensions.Logging; -namespace Exceptionless.Insulation.Geo { - public class MaxMindGeoIpService : IGeoIpService, IDisposable { - private readonly InMemoryCacheClient _localCache; - private readonly IFileStorage _storage; - private readonly ILogger _logger; - private DatabaseReader _database; - private DateTime? _databaseLastChecked; - - public MaxMindGeoIpService(IFileStorage storage, ILoggerFactory loggerFactory) { - _storage = storage; - _localCache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory, MaxItems = 250, CloneValues = true }); - _logger = loggerFactory.CreateLogger(); - } +namespace Exceptionless.Insulation.Geo; + +public class MaxMindGeoIpService : IGeoIpService, IDisposable { + private readonly InMemoryCacheClient _localCache; + private readonly IFileStorage _storage; + private readonly ILogger _logger; + private DatabaseReader _database; + private DateTime? _databaseLastChecked; + + public MaxMindGeoIpService(IFileStorage storage, ILoggerFactory loggerFactory) { + _storage = storage; + _localCache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = loggerFactory, MaxItems = 250, CloneValues = true }); + _logger = loggerFactory.CreateLogger(); + } - public async Task ResolveIpAsync(string ip, CancellationToken cancellationToken = new CancellationToken()) { - if (String.IsNullOrEmpty(ip) || (!ip.Contains(".") && !ip.Contains(":"))) - return null; - - ip = ip.Trim(); - if (ip.IsPrivateNetwork()) - return null; - - var cacheValue = await _localCache.GetAsync(ip).AnyContext(); - if (cacheValue.HasValue) - return cacheValue.Value; - - GeoResult result = null; - var database = await GetDatabaseAsync(cancellationToken).AnyContext(); - if (database == null) - return null; - - try { - if (database.TryCity(ip, out var city) && city?.Location != null) { - result = new GeoResult { - Latitude = city.Location.Latitude, - Longitude = city.Location.Longitude, - Country = city.Country.IsoCode, - Level1 = city.MostSpecificSubdivision.IsoCode, - Locality = city.City.Name - }; - } - - await _localCache.SetAsync(ip, result).AnyContext(); - return result; - } catch (Exception ex) { - if (ex is GeoIP2Exception) { - _logger.LogTrace(ex, ex.Message); - await _localCache.SetAsync(ip, null).AnyContext(); - } else { - _logger.LogError(ex, "Unable to resolve geo location for ip: {IP}", ip); - } - - return null; + public async Task ResolveIpAsync(string ip, CancellationToken cancellationToken = new CancellationToken()) { + if (String.IsNullOrEmpty(ip) || (!ip.Contains(".") && !ip.Contains(":"))) + return null; + + ip = ip.Trim(); + if (ip.IsPrivateNetwork()) + return null; + + var cacheValue = await _localCache.GetAsync(ip).AnyContext(); + if (cacheValue.HasValue) + return cacheValue.Value; + + GeoResult result = null; + var database = await GetDatabaseAsync(cancellationToken).AnyContext(); + if (database == null) + return null; + + try { + if (database.TryCity(ip, out var city) && city?.Location != null) { + result = new GeoResult { + Latitude = city.Location.Latitude, + Longitude = city.Location.Longitude, + Country = city.Country.IsoCode, + Level1 = city.MostSpecificSubdivision.IsoCode, + Locality = city.City.Name + }; } - } - private async Task GetDatabaseAsync(CancellationToken cancellationToken) { - // Try to load the new database from disk if the current one is a day old. - if (_database != null && _databaseLastChecked.HasValue && _databaseLastChecked.Value < SystemClock.UtcNow.SubtractDays(1)) { - _database.Dispose(); - _database = null; + await _localCache.SetAsync(ip, result).AnyContext(); + return result; + } + catch (Exception ex) { + if (ex is GeoIP2Exception) { + _logger.LogTrace(ex, ex.Message); + await _localCache.SetAsync(ip, null).AnyContext(); } + else { + _logger.LogError(ex, "Unable to resolve geo location for ip: {IP}", ip); + } + + return null; + } + } - if (_database != null) - return _database; + private async Task GetDatabaseAsync(CancellationToken cancellationToken) { + // Try to load the new database from disk if the current one is a day old. + if (_database != null && _databaseLastChecked.HasValue && _databaseLastChecked.Value < SystemClock.UtcNow.SubtractDays(1)) { + _database.Dispose(); + _database = null; + } - if (_databaseLastChecked.HasValue && _databaseLastChecked.Value >= SystemClock.UtcNow.SubtractSeconds(30)) - return null; + if (_database != null) + return _database; - _databaseLastChecked = SystemClock.UtcNow; + if (_databaseLastChecked.HasValue && _databaseLastChecked.Value >= SystemClock.UtcNow.SubtractSeconds(30)) + return null; - if (!await _storage.ExistsAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH).AnyContext()) { - _logger.LogWarning("No GeoIP database was found."); - return null; - } + _databaseLastChecked = SystemClock.UtcNow; - _logger.LogInformation("Loading GeoIP database."); - try { - using (var stream = await _storage.GetFileStreamAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH, cancellationToken).AnyContext()) - _database = new DatabaseReader(stream); - } catch (Exception ex) { - _logger.LogError(ex, "Unable to open GeoIP database."); - } + if (!await _storage.ExistsAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH).AnyContext()) { + _logger.LogWarning("No GeoIP database was found."); + return null; + } - return _database; + _logger.LogInformation("Loading GeoIP database."); + try { + using (var stream = await _storage.GetFileStreamAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH, cancellationToken).AnyContext()) + _database = new DatabaseReader(stream); + } + catch (Exception ex) { + _logger.LogError(ex, "Unable to open GeoIP database."); } - public void Dispose() { - if (_database == null) - return; + return _database; + } - _database.Dispose(); - _database = null; - } + public void Dispose() { + if (_database == null) + return; + + _database.Dispose(); + _database = null; } } diff --git a/src/Exceptionless.Insulation/HealthChecks/CacheHealthCheck.cs b/src/Exceptionless.Insulation/HealthChecks/CacheHealthCheck.cs index 227f90d30b..42eba77275 100644 --- a/src/Exceptionless.Insulation/HealthChecks/CacheHealthCheck.cs +++ b/src/Exceptionless.Insulation/HealthChecks/CacheHealthCheck.cs @@ -1,37 +1,36 @@ -using System; using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Foundatio.Caching; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Insulation.HealthChecks { - public class CacheHealthCheck : IHealthCheck { - private readonly ICacheClient _cache; - private readonly ILogger _logger; +namespace Exceptionless.Insulation.HealthChecks; - public CacheHealthCheck(ICacheClient cache, ILoggerFactory loggerFactory) { - _cache = cache; - _logger = loggerFactory.CreateLogger(); +public class CacheHealthCheck : IHealthCheck { + private readonly ICacheClient _cache; + private readonly ILogger _logger; + + public CacheHealthCheck(ICacheClient cache, ILoggerFactory loggerFactory) { + _cache = cache; + _logger = loggerFactory.CreateLogger(); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + var sw = Stopwatch.StartNew(); + try { + var cache = new ScopedCacheClient(_cache, "health"); + var cacheValue = await cache.GetAsync("__PING__").AnyContext(); + if (cacheValue.HasValue) + return HealthCheckResult.Unhealthy("Cache Not Working"); } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var sw = Stopwatch.StartNew(); - try { - var cache = new ScopedCacheClient(_cache, "health"); - var cacheValue = await cache.GetAsync("__PING__").AnyContext(); - if (cacheValue.HasValue) - return HealthCheckResult.Unhealthy("Cache Not Working"); - } catch (Exception ex) { - return HealthCheckResult.Unhealthy("Cache Not Working.", ex); - } finally { - sw.Stop(); - _logger.LogTrace("Checking cache took {Duration:g}", sw.Elapsed); - } - - return HealthCheckResult.Healthy(); + catch (Exception ex) { + return HealthCheckResult.Unhealthy("Cache Not Working.", ex); } + finally { + sw.Stop(); + _logger.LogTrace("Checking cache took {Duration:g}", sw.Elapsed); + } + + return HealthCheckResult.Healthy(); } } diff --git a/src/Exceptionless.Insulation/HealthChecks/ElasticsearchHealthCheck.cs b/src/Exceptionless.Insulation/HealthChecks/ElasticsearchHealthCheck.cs index 2fa845469a..fb13596ded 100644 --- a/src/Exceptionless.Insulation/HealthChecks/ElasticsearchHealthCheck.cs +++ b/src/Exceptionless.Insulation/HealthChecks/ElasticsearchHealthCheck.cs @@ -1,41 +1,40 @@ -using System; using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Elasticsearch.Net; using Exceptionless.Core.Repositories.Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; using Nest; -namespace Exceptionless.Insulation.HealthChecks { - public class ElasticsearchHealthCheck : IHealthCheck { - private readonly ExceptionlessElasticConfiguration _config; - private readonly ILogger _logger; +namespace Exceptionless.Insulation.HealthChecks; - public ElasticsearchHealthCheck(ExceptionlessElasticConfiguration config, ILoggerFactory loggerFactory) { - _config = config; - _logger = loggerFactory.CreateLogger(); - } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var sw = Stopwatch.StartNew(); +public class ElasticsearchHealthCheck : IHealthCheck { + private readonly ExceptionlessElasticConfiguration _config; + private readonly ILogger _logger; + + public ElasticsearchHealthCheck(ExceptionlessElasticConfiguration config, ILoggerFactory loggerFactory) { + _config = config; + _logger = loggerFactory.CreateLogger(); + } - try { - var pingResult = await _config.Client.LowLevel.PingAsync(ctx: cancellationToken, requestParameters: new PingRequestParameters { - RequestConfiguration = new RequestConfiguration { - RequestTimeout = TimeSpan.FromSeconds(60) // 60 seconds is default for NEST - } - }); - bool isSuccess = pingResult.ApiCall.HttpStatusCode == 200; + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + var sw = Stopwatch.StartNew(); - return isSuccess ? HealthCheckResult.Healthy() : new HealthCheckResult(context.Registration.FailureStatus); - } catch (Exception ex) { - return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); - } finally { - sw.Stop(); - _logger.LogTrace("Checking Elasticsearch took {Duration:g}", sw.Elapsed); - } + try { + var pingResult = await _config.Client.LowLevel.PingAsync(ctx: cancellationToken, requestParameters: new PingRequestParameters { + RequestConfiguration = new RequestConfiguration { + RequestTimeout = TimeSpan.FromSeconds(60) // 60 seconds is default for NEST + } + }); + bool isSuccess = pingResult.ApiCall.HttpStatusCode == 200; + + return isSuccess ? HealthCheckResult.Healthy() : new HealthCheckResult(context.Registration.FailureStatus); + } + catch (Exception ex) { + return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + } + finally { + sw.Stop(); + _logger.LogTrace("Checking Elasticsearch took {Duration:g}", sw.Elapsed); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/HealthChecks/HealthCheckExtensions.cs b/src/Exceptionless.Insulation/HealthChecks/HealthCheckExtensions.cs index 01e8d71a17..af6d25fdba 100644 --- a/src/Exceptionless.Insulation/HealthChecks/HealthCheckExtensions.cs +++ b/src/Exceptionless.Insulation/HealthChecks/HealthCheckExtensions.cs @@ -1,25 +1,23 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Exceptionless.Insulation.HealthChecks { - public static class HealthCheckExtensions { - public static IHealthChecksBuilder AddAutoNamedCheck(this IHealthChecksBuilder builder, params string[] tags) where T : class, IHealthCheck { - var checkType = typeof(T); - string name = checkType.Name; - if (checkType.IsConstructedGenericType && checkType.GenericTypeArguments.Length == 1) - name = checkType.GenericTypeArguments[0].Name; +namespace Exceptionless.Insulation.HealthChecks; - if (name.EndsWith("HealthCheck", StringComparison.OrdinalIgnoreCase)) - name = name.Substring(0, name.Length - 11); - if (name.EndsWith("Check", StringComparison.OrdinalIgnoreCase)) - name = name.Substring(0, name.Length - 5); - if (name.EndsWith("Job", StringComparison.OrdinalIgnoreCase)) - name = name.Substring(0, name.Length - 3); +public static class HealthCheckExtensions { + public static IHealthChecksBuilder AddAutoNamedCheck(this IHealthChecksBuilder builder, params string[] tags) where T : class, IHealthCheck { + var checkType = typeof(T); + string name = checkType.Name; + if (checkType.IsConstructedGenericType && checkType.GenericTypeArguments.Length == 1) + name = checkType.GenericTypeArguments[0].Name; - var allTags = new List(tags) { name }; - return builder.AddCheck(name, null, allTags); - } + if (name.EndsWith("HealthCheck", StringComparison.OrdinalIgnoreCase)) + name = name.Substring(0, name.Length - 11); + if (name.EndsWith("Check", StringComparison.OrdinalIgnoreCase)) + name = name.Substring(0, name.Length - 5); + if (name.EndsWith("Job", StringComparison.OrdinalIgnoreCase)) + name = name.Substring(0, name.Length - 3); + + var allTags = new List(tags) { name }; + return builder.AddCheck(name, null, allTags); } } diff --git a/src/Exceptionless.Insulation/HealthChecks/QueueHealthCheck.cs b/src/Exceptionless.Insulation/HealthChecks/QueueHealthCheck.cs index 18f7200eda..1234e1694a 100644 --- a/src/Exceptionless.Insulation/HealthChecks/QueueHealthCheck.cs +++ b/src/Exceptionless.Insulation/HealthChecks/QueueHealthCheck.cs @@ -1,7 +1,4 @@ -using System; using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.DateTimeExtensions; using Foundatio.Queues; @@ -9,34 +6,36 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Insulation.HealthChecks { - public class QueueHealthCheck : IHealthCheck where T : class { - private readonly IQueue _queue; - private readonly ILogger _logger; +namespace Exceptionless.Insulation.HealthChecks; - public QueueHealthCheck(IQueue queue, ILoggerFactory loggerFactory) { - _queue = queue; - _logger = loggerFactory.CreateLogger(); +public class QueueHealthCheck : IHealthCheck where T : class { + private readonly IQueue _queue; + private readonly ILogger _logger; + + public QueueHealthCheck(IQueue queue, ILoggerFactory loggerFactory) { + _queue = queue; + _logger = loggerFactory.CreateLogger(); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (_queue is IQueueActivity qa) { + if (qa.LastDequeueActivity.HasValue && qa.LastDequeueActivity.Value.IsBefore(SystemClock.UtcNow.SubtractMinutes(1))) + return HealthCheckResult.Unhealthy("Last Dequeue was over a minute ago"); + + return HealthCheckResult.Healthy(); + } + + var sw = Stopwatch.StartNew(); + try { + await _queue.GetQueueStatsAsync().AnyContext(); + return HealthCheckResult.Healthy(); + } + catch (Exception ex) { + return HealthCheckResult.Unhealthy("Unable to get queue stats.", ex); } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (_queue is IQueueActivity qa) { - if (qa.LastDequeueActivity.HasValue && qa.LastDequeueActivity.Value.IsBefore(SystemClock.UtcNow.SubtractMinutes(1))) - return HealthCheckResult.Unhealthy("Last Dequeue was over a minute ago"); - - return HealthCheckResult.Healthy(); - } - - var sw = Stopwatch.StartNew(); - try { - await _queue.GetQueueStatsAsync().AnyContext(); - return HealthCheckResult.Healthy(); - } catch (Exception ex) { - return HealthCheckResult.Unhealthy("Unable to get queue stats.", ex); - } finally { - sw.Stop(); - _logger.LogTrace("Checking queue took {Duration:g}", sw.Elapsed); - } + finally { + sw.Stop(); + _logger.LogTrace("Checking queue took {Duration:g}", sw.Elapsed); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/HealthChecks/StorageHealthCheck.cs b/src/Exceptionless.Insulation/HealthChecks/StorageHealthCheck.cs index 37c7888bd5..14f92e3c0b 100644 --- a/src/Exceptionless.Insulation/HealthChecks/StorageHealthCheck.cs +++ b/src/Exceptionless.Insulation/HealthChecks/StorageHealthCheck.cs @@ -1,35 +1,34 @@ -using System; using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Foundatio.Storage; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -namespace Exceptionless.Insulation.HealthChecks { - public class StorageHealthCheck : IHealthCheck { - private readonly IFileStorage _storage; - private readonly ILogger _logger; +namespace Exceptionless.Insulation.HealthChecks; - public StorageHealthCheck(IFileStorage storage, ILoggerFactory loggerFactory) { - _storage = storage; - _logger = loggerFactory.CreateLogger(); +public class StorageHealthCheck : IHealthCheck { + private readonly IFileStorage _storage; + private readonly ILogger _logger; + + public StorageHealthCheck(IFileStorage storage, ILoggerFactory loggerFactory) { + _storage = storage; + _logger = loggerFactory.CreateLogger(); + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + var sw = Stopwatch.StartNew(); + + try { + await _storage.GetPagedFileListAsync(1, cancellationToken: cancellationToken).AnyContext(); + } + catch (Exception ex) { + return HealthCheckResult.Unhealthy("Storage Not Working.", ex); } - - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var sw = Stopwatch.StartNew(); - - try { - await _storage.GetPagedFileListAsync(1, cancellationToken: cancellationToken).AnyContext(); - } catch (Exception ex) { - return HealthCheckResult.Unhealthy("Storage Not Working.", ex); - } finally { - sw.Stop(); - _logger.LogTrace("Checking storage took {Duration:g}", sw.Elapsed); - } - - return HealthCheckResult.Healthy(); + finally { + sw.Stop(); + _logger.LogTrace("Checking storage took {Duration:g}", sw.Elapsed); } + + return HealthCheckResult.Healthy(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs b/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs index a8a3edd02c..19c2a765d7 100644 --- a/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs +++ b/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs @@ -1,63 +1,62 @@ -using System; using System.Text; using MailKit; using Microsoft.Extensions.Logging; -namespace Exceptionless.Insulation.Mail { - public class ExtensionsProtocolLogger : IProtocolLogger { - private const string CLIENT_PREFIX = "Client: "; - private const string SERVER_PREFIX = "Server: "; +namespace Exceptionless.Insulation.Mail; - private readonly ILogger _logger; +public class ExtensionsProtocolLogger : IProtocolLogger { + private const string CLIENT_PREFIX = "Client: "; + private const string SERVER_PREFIX = "Server: "; - public IAuthenticationSecretDetector AuthenticationSecretDetector { get; set; } + private readonly ILogger _logger; - public ExtensionsProtocolLogger(ILogger logger) { - _logger = logger; - } + public IAuthenticationSecretDetector AuthenticationSecretDetector { get; set; } - public void LogConnect(Uri uri) { - _logger.LogTrace("Connected to {URI}", uri); - } + public ExtensionsProtocolLogger(ILogger logger) { + _logger = logger; + } - public void LogClient(byte[] buffer, int offset, int count) { - LogMessage(CLIENT_PREFIX, buffer, offset, count); - } + public void LogConnect(Uri uri) { + _logger.LogTrace("Connected to {URI}", uri); + } - public void LogServer(byte[] buffer, int offset, int count) { - LogMessage(SERVER_PREFIX, buffer, offset, count); - } + public void LogClient(byte[] buffer, int offset, int count) { + LogMessage(CLIENT_PREFIX, buffer, offset, count); + } + + public void LogServer(byte[] buffer, int offset, int count) { + LogMessage(SERVER_PREFIX, buffer, offset, count); + } - private void LogMessage(string prefix, byte[] buffer, int offset, int count) { - if (!_logger.IsEnabled(LogLevel.Trace)) - return; - - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset < 0 || offset > buffer.Length) - throw new ArgumentOutOfRangeException(nameof(offset)); - - if (count < 0 || count > (buffer.Length - offset)) - throw new ArgumentOutOfRangeException(nameof(count)); - - int endIndex = offset + count; - int index = offset; - - while (index < endIndex) { - int start = index; - while (index < endIndex && buffer[index] != (byte)'\n') { - index++; - } - - if (index < endIndex && buffer[index] == (byte)'\n') { - index++; - } - - _logger.LogTrace(prefix + Encoding.Default.GetString(buffer, start, index - start).Trim()); + private void LogMessage(string prefix, byte[] buffer, int offset, int count) { + if (!_logger.IsEnabled(LogLevel.Trace)) + return; + + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + + if (count < 0 || count > (buffer.Length - offset)) + throw new ArgumentOutOfRangeException(nameof(count)); + + int endIndex = offset + count; + int index = offset; + + while (index < endIndex) { + int start = index; + while (index < endIndex && buffer[index] != (byte)'\n') { + index++; } + + if (index < endIndex && buffer[index] == (byte)'\n') { + index++; + } + + _logger.LogTrace(prefix + Encoding.Default.GetString(buffer, start, index - start).Trim()); } - - public void Dispose() { } } + + public void Dispose() { } } diff --git a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs index 4a4429b963..bbf490ba26 100644 --- a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs +++ b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs @@ -1,7 +1,4 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; using Exceptionless.Core.Mail; @@ -14,125 +11,127 @@ using MimeKit; using MailMessage = Exceptionless.Core.Queues.Models.MailMessage; -namespace Exceptionless.Insulation.Mail { - public class MailKitMailSender : IMailSender, IHealthCheck { - private readonly EmailOptions _emailOptions; - private readonly ILogger _logger; - private DateTime _lastSuccessfulConnection = DateTime.MinValue; +namespace Exceptionless.Insulation.Mail; - public MailKitMailSender(EmailOptions emailOptions, ILoggerFactory loggerFactory) { - _emailOptions = emailOptions; - _logger = loggerFactory.CreateLogger(); +public class MailKitMailSender : IMailSender, IHealthCheck { + private readonly EmailOptions _emailOptions; + private readonly ILogger _logger; + private DateTime _lastSuccessfulConnection = DateTime.MinValue; + + public MailKitMailSender(EmailOptions emailOptions, ILoggerFactory loggerFactory) { + _emailOptions = emailOptions; + _logger = loggerFactory.CreateLogger(); + } + + public async Task SendAsync(MailMessage model) { + _logger.LogTrace("Creating Mail Message from model"); + + var message = CreateMailMessage(model); + message.Headers.Add("X-Mailer-Machine", Environment.MachineName); + message.Headers.Add("X-Mailer-Date", SystemClock.UtcNow.ToString()); + message.Headers.Add("X-Auto-Response-Suppress", "All"); + message.Headers.Add("Auto-Submitted", "auto-generated"); + + using (var client = new SmtpClient(new ExtensionsProtocolLogger(_logger))) { + string host = _emailOptions.SmtpHost; + int port = _emailOptions.SmtpPort; + var encryption = GetSecureSocketOption(_emailOptions.SmtpEncryption); + _logger.LogTrace("Connecting to SMTP server: {SmtpHost}:{SmtpPort} using {Encryption}", host, port, encryption); + + var sw = Stopwatch.StartNew(); + await client.ConnectAsync(host, port, encryption).AnyContext(); + _logger.LogTrace("Connected to SMTP server took {Duration:g}", sw.Elapsed); + + // Note: since we don't have an OAuth2 token, disable the XOAUTH2 authentication mechanism. + client.AuthenticationMechanisms.Remove("XOAUTH2"); + + string user = _emailOptions.SmtpUser; + if (!String.IsNullOrEmpty(user)) { + _logger.LogTrace("Authenticating {SmtpUser} to SMTP server", user); + sw.Restart(); + await client.AuthenticateAsync(user, _emailOptions.SmtpPassword).AnyContext(); + _logger.LogTrace("Authenticated to SMTP server took {Duration:g}", user, sw.Elapsed); + } + + _logger.LogTrace("Sending message: to={To} subject={Subject}", message.Subject, message.To); + sw.Restart(); + await client.SendAsync(message).AnyContext(); + _logger.LogTrace("Sent Message took {Duration:g}", sw.Elapsed); + + sw.Restart(); + await client.DisconnectAsync(true).AnyContext(); + _logger.LogTrace("Disconnected from SMTP server took {Duration:g}", sw.Elapsed); + sw.Stop(); + } + + _lastSuccessfulConnection = SystemClock.UtcNow; + } + + private SecureSocketOptions GetSecureSocketOption(SmtpEncryption encryption) { + switch (encryption) { + case SmtpEncryption.StartTLS: + return SecureSocketOptions.StartTls; + case SmtpEncryption.SSL: + return SecureSocketOptions.SslOnConnect; + default: + return SecureSocketOptions.None; } + } + + private MimeMessage CreateMailMessage(MailMessage notification) { + var message = new MimeMessage { Subject = notification.Subject }; + var builder = new BodyBuilder(); + + if (!String.IsNullOrEmpty(notification.To)) + message.To.AddRange(InternetAddressList.Parse(notification.To)); + + if (!String.IsNullOrEmpty(notification.From)) + message.From.AddRange(InternetAddressList.Parse(notification.From)); + else + message.From.AddRange(InternetAddressList.Parse(_emailOptions.SmtpFrom)); - public async Task SendAsync(MailMessage model) { - _logger.LogTrace("Creating Mail Message from model"); - - var message = CreateMailMessage(model); - message.Headers.Add("X-Mailer-Machine", Environment.MachineName); - message.Headers.Add("X-Mailer-Date", SystemClock.UtcNow.ToString()); - message.Headers.Add("X-Auto-Response-Suppress", "All"); - message.Headers.Add("Auto-Submitted", "auto-generated"); + if (!String.IsNullOrEmpty(notification.Body)) + builder.HtmlBody = notification.Body; + message.Body = builder.ToMessageBody(); + return message; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { + if (_lastSuccessfulConnection.IsAfter(SystemClock.UtcNow.Subtract(TimeSpan.FromMinutes(5)))) + return HealthCheckResult.Healthy(); + + var sw = Stopwatch.StartNew(); + + try { using (var client = new SmtpClient(new ExtensionsProtocolLogger(_logger))) { string host = _emailOptions.SmtpHost; int port = _emailOptions.SmtpPort; var encryption = GetSecureSocketOption(_emailOptions.SmtpEncryption); - _logger.LogTrace("Connecting to SMTP server: {SmtpHost}:{SmtpPort} using {Encryption}", host, port, encryption); - var sw = Stopwatch.StartNew(); await client.ConnectAsync(host, port, encryption).AnyContext(); - _logger.LogTrace("Connected to SMTP server took {Duration:g}", sw.Elapsed); // Note: since we don't have an OAuth2 token, disable the XOAUTH2 authentication mechanism. client.AuthenticationMechanisms.Remove("XOAUTH2"); string user = _emailOptions.SmtpUser; if (!String.IsNullOrEmpty(user)) { - _logger.LogTrace("Authenticating {SmtpUser} to SMTP server", user); - sw.Restart(); await client.AuthenticateAsync(user, _emailOptions.SmtpPassword).AnyContext(); - _logger.LogTrace("Authenticated to SMTP server took {Duration:g}", user, sw.Elapsed); } - _logger.LogTrace("Sending message: to={To} subject={Subject}", message.Subject, message.To); - sw.Restart(); - await client.SendAsync(message).AnyContext(); - _logger.LogTrace("Sent Message took {Duration:g}", sw.Elapsed); - - sw.Restart(); await client.DisconnectAsync(true).AnyContext(); - _logger.LogTrace("Disconnected from SMTP server took {Duration:g}", sw.Elapsed); - sw.Stop(); } _lastSuccessfulConnection = SystemClock.UtcNow; } - - private SecureSocketOptions GetSecureSocketOption(SmtpEncryption encryption) { - switch (encryption) { - case SmtpEncryption.StartTLS: - return SecureSocketOptions.StartTls; - case SmtpEncryption.SSL: - return SecureSocketOptions.SslOnConnect; - default: - return SecureSocketOptions.None; - } + catch (Exception ex) { + return HealthCheckResult.Unhealthy("Email Not Working.", ex); } - - private MimeMessage CreateMailMessage(MailMessage notification) { - var message = new MimeMessage { Subject = notification.Subject }; - var builder = new BodyBuilder(); - - if (!String.IsNullOrEmpty(notification.To)) - message.To.AddRange(InternetAddressList.Parse(notification.To)); - - if (!String.IsNullOrEmpty(notification.From)) - message.From.AddRange(InternetAddressList.Parse(notification.From)); - else - message.From.AddRange(InternetAddressList.Parse(_emailOptions.SmtpFrom)); - - if (!String.IsNullOrEmpty(notification.Body)) - builder.HtmlBody = notification.Body; - - message.Body = builder.ToMessageBody(); - return message; + finally { + sw.Stop(); + _logger.LogTrace("Checking email took {Duration:g}", sw.Elapsed); } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - if (_lastSuccessfulConnection.IsAfter(SystemClock.UtcNow.Subtract(TimeSpan.FromMinutes(5)))) - return HealthCheckResult.Healthy(); - - var sw = Stopwatch.StartNew(); - - try { - using (var client = new SmtpClient(new ExtensionsProtocolLogger(_logger))) { - string host = _emailOptions.SmtpHost; - int port = _emailOptions.SmtpPort; - var encryption = GetSecureSocketOption(_emailOptions.SmtpEncryption); - - await client.ConnectAsync(host, port, encryption).AnyContext(); - - // Note: since we don't have an OAuth2 token, disable the XOAUTH2 authentication mechanism. - client.AuthenticationMechanisms.Remove("XOAUTH2"); - - string user = _emailOptions.SmtpUser; - if (!String.IsNullOrEmpty(user)) { - await client.AuthenticateAsync(user, _emailOptions.SmtpPassword).AnyContext(); - } - - await client.DisconnectAsync(true).AnyContext(); - } - - _lastSuccessfulConnection = SystemClock.UtcNow; - } catch (Exception ex) { - return HealthCheckResult.Unhealthy("Email Not Working.", ex); - } finally { - sw.Stop(); - _logger.LogTrace("Checking email took {Duration:g}", sw.Elapsed); - } - - return HealthCheckResult.Healthy(); - } + return HealthCheckResult.Healthy(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Insulation/Redis/RedisConnectionMap.cs b/src/Exceptionless.Insulation/Redis/RedisConnectionMap.cs index 1ba8fa60bb..aecd35e392 100644 --- a/src/Exceptionless.Insulation/Redis/RedisConnectionMap.cs +++ b/src/Exceptionless.Insulation/Redis/RedisConnectionMap.cs @@ -1,49 +1,45 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Utility; using StackExchange.Redis; -namespace Exceptionless.Insulation.Redis { - public sealed class RedisConnectionMapping : IConnectionMapping { - private const string KeyPrefix = "Hub:"; - private readonly IConnectionMultiplexer _muxer; +namespace Exceptionless.Insulation.Redis; - public RedisConnectionMapping(IConnectionMultiplexer muxer) { - _muxer = muxer; - } +public sealed class RedisConnectionMapping : IConnectionMapping { + private const string KeyPrefix = "Hub:"; + private readonly IConnectionMultiplexer _muxer; - public Task AddAsync(string key, string connectionId) { - if (key == null) - return Task.CompletedTask; + public RedisConnectionMapping(IConnectionMultiplexer muxer) { + _muxer = muxer; + } - return Database.SetAddAsync(String.Concat(KeyPrefix, key), connectionId); - } + public Task AddAsync(string key, string connectionId) { + if (key == null) + return Task.CompletedTask; - private IDatabase Database => _muxer.GetDatabase(); + return Database.SetAddAsync(String.Concat(KeyPrefix, key), connectionId); + } - public async Task> GetConnectionsAsync(string key) { - if (key == null) - return new List(); + private IDatabase Database => _muxer.GetDatabase(); - var values = await Database.SetMembersAsync(String.Concat(KeyPrefix, key)).AnyContext(); - return values.Select(v => v.ToString()).ToList(); - } + public async Task> GetConnectionsAsync(string key) { + if (key == null) + return new List(); - public async Task GetConnectionCountAsync(string key) { - if (key == null) - return 0; + var values = await Database.SetMembersAsync(String.Concat(KeyPrefix, key)).AnyContext(); + return values.Select(v => v.ToString()).ToList(); + } - return (int)await Database.SetLengthAsync(String.Concat(KeyPrefix, key)).AnyContext(); - } + public async Task GetConnectionCountAsync(string key) { + if (key == null) + return 0; + + return (int)await Database.SetLengthAsync(String.Concat(KeyPrefix, key)).AnyContext(); + } - public Task RemoveAsync(string key, string connectionId) { - if (key == null) - return Task.CompletedTask; + public Task RemoveAsync(string key, string connectionId) { + if (key == null) + return Task.CompletedTask; - return Database.SetRemoveAsync(String.Concat(KeyPrefix, key), connectionId); - } + return Database.SetRemoveAsync(String.Concat(KeyPrefix, key), connectionId); } } diff --git a/src/Exceptionless.Job/JobRunnerOptions.cs b/src/Exceptionless.Job/JobRunnerOptions.cs index 447df9ef94..f04ff6079e 100644 --- a/src/Exceptionless.Job/JobRunnerOptions.cs +++ b/src/Exceptionless.Job/JobRunnerOptions.cs @@ -1,93 +1,90 @@ -using System; -using System.Linq; - -namespace Exceptionless.Job { - public class JobRunnerOptions { - public JobRunnerOptions(string[] args) { - if (args.Length > 1) - throw new ArgumentException("More than one job argument specified. You must either specify 1 named job or don't pass any arguments to run all jobs."); - - CleanupData = args.Length == 0 || args.Contains(nameof(CleanupData), StringComparer.OrdinalIgnoreCase); - if (CleanupData && args.Length != 0) - JobName = nameof(CleanupData); - - CleanupOrphanedData = args.Length == 0 || args.Contains(nameof(CleanupOrphanedData), StringComparer.OrdinalIgnoreCase); - if (CleanupOrphanedData && args.Length != 0) - JobName = nameof(CleanupOrphanedData); - - CloseInactiveSessions = args.Length == 0 || args.Contains(nameof(CloseInactiveSessions), StringComparer.OrdinalIgnoreCase); - if (CloseInactiveSessions && args.Length != 0) - JobName = nameof(CloseInactiveSessions); - - DailySummary = args.Length == 0 || args.Contains(nameof(DailySummary), StringComparer.OrdinalIgnoreCase); - if (DailySummary && args.Length != 0) - JobName = nameof(DailySummary); - - DataMigration = args.Contains(nameof(DataMigration), StringComparer.OrdinalIgnoreCase); - if (DataMigration && args.Length != 0) - JobName = nameof(DataMigration); - - DownloadGeoIPDatabase = args.Length == 0 || args.Contains(nameof(DownloadGeoIPDatabase), StringComparer.OrdinalIgnoreCase); - if (DownloadGeoIPDatabase && args.Length != 0) - JobName = nameof(DownloadGeoIPDatabase); - - EventNotifications = args.Length == 0 || args.Contains(nameof(EventNotifications), StringComparer.OrdinalIgnoreCase); - if (EventNotifications && args.Length != 0) - JobName = nameof(EventNotifications); - - EventPosts = args.Length == 0 || args.Contains(nameof(EventPosts), StringComparer.OrdinalIgnoreCase); - if (EventPosts && args.Length != 0) - JobName = nameof(EventPosts); - - EventUserDescriptions = args.Length == 0 || args.Contains(nameof(EventUserDescriptions), StringComparer.OrdinalIgnoreCase); - if (EventUserDescriptions && args.Length != 0) - JobName = nameof(EventUserDescriptions); - - MailMessage = args.Length == 0 || args.Contains(nameof(MailMessage), StringComparer.OrdinalIgnoreCase); - if (MailMessage && args.Length != 0) - JobName = nameof(MailMessage); - - MaintainIndexes = args.Length == 0 || args.Contains(nameof(MaintainIndexes), StringComparer.OrdinalIgnoreCase); - if (MaintainIndexes && args.Length != 0) - JobName = nameof(MaintainIndexes); - - Migration = args.Length == 0 || args.Contains(nameof(Migration), StringComparer.OrdinalIgnoreCase); - if (Migration && args.Length != 0) - JobName = nameof(Migration); - - StackStatus = args.Length == 0 || args.Contains(nameof(StackStatus), StringComparer.OrdinalIgnoreCase); - if (StackStatus && args.Length != 0) - JobName = nameof(StackStatus); - - StackEventCount = args.Length == 0 || args.Contains(nameof(StackEventCount), StringComparer.OrdinalIgnoreCase); - if (StackEventCount && args.Length != 0) - JobName = nameof(StackEventCount); - - WebHooks = args.Length == 0 || args.Contains(nameof(WebHooks), StringComparer.OrdinalIgnoreCase); - if (WebHooks && args.Length != 0) - JobName = nameof(WebHooks); - - WorkItem = args.Length == 0 || args.Contains(nameof(WorkItem), StringComparer.OrdinalIgnoreCase); - if (WorkItem && args.Length != 0) - JobName = nameof(WorkItem); - } - - public string JobName { get; } - public bool CleanupData { get; } - public bool CleanupOrphanedData { get; } - public bool CloseInactiveSessions { get; } - public bool DailySummary { get; } - public bool DataMigration { get; } - public bool DownloadGeoIPDatabase { get; } - public bool EventNotifications { get; } - public bool EventPosts { get; } - public bool EventUserDescriptions { get; } - public bool MailMessage { get; } - public bool MaintainIndexes { get; } - public bool Migration { get; } - public bool StackStatus { get; } - public bool StackEventCount { get; } - public bool WebHooks { get; } - public bool WorkItem { get; } +namespace Exceptionless.Job; + +public class JobRunnerOptions { + public JobRunnerOptions(string[] args) { + if (args.Length > 1) + throw new ArgumentException("More than one job argument specified. You must either specify 1 named job or don't pass any arguments to run all jobs."); + + CleanupData = args.Length == 0 || args.Contains(nameof(CleanupData), StringComparer.OrdinalIgnoreCase); + if (CleanupData && args.Length != 0) + JobName = nameof(CleanupData); + + CleanupOrphanedData = args.Length == 0 || args.Contains(nameof(CleanupOrphanedData), StringComparer.OrdinalIgnoreCase); + if (CleanupOrphanedData && args.Length != 0) + JobName = nameof(CleanupOrphanedData); + + CloseInactiveSessions = args.Length == 0 || args.Contains(nameof(CloseInactiveSessions), StringComparer.OrdinalIgnoreCase); + if (CloseInactiveSessions && args.Length != 0) + JobName = nameof(CloseInactiveSessions); + + DailySummary = args.Length == 0 || args.Contains(nameof(DailySummary), StringComparer.OrdinalIgnoreCase); + if (DailySummary && args.Length != 0) + JobName = nameof(DailySummary); + + DataMigration = args.Contains(nameof(DataMigration), StringComparer.OrdinalIgnoreCase); + if (DataMigration && args.Length != 0) + JobName = nameof(DataMigration); + + DownloadGeoIPDatabase = args.Length == 0 || args.Contains(nameof(DownloadGeoIPDatabase), StringComparer.OrdinalIgnoreCase); + if (DownloadGeoIPDatabase && args.Length != 0) + JobName = nameof(DownloadGeoIPDatabase); + + EventNotifications = args.Length == 0 || args.Contains(nameof(EventNotifications), StringComparer.OrdinalIgnoreCase); + if (EventNotifications && args.Length != 0) + JobName = nameof(EventNotifications); + + EventPosts = args.Length == 0 || args.Contains(nameof(EventPosts), StringComparer.OrdinalIgnoreCase); + if (EventPosts && args.Length != 0) + JobName = nameof(EventPosts); + + EventUserDescriptions = args.Length == 0 || args.Contains(nameof(EventUserDescriptions), StringComparer.OrdinalIgnoreCase); + if (EventUserDescriptions && args.Length != 0) + JobName = nameof(EventUserDescriptions); + + MailMessage = args.Length == 0 || args.Contains(nameof(MailMessage), StringComparer.OrdinalIgnoreCase); + if (MailMessage && args.Length != 0) + JobName = nameof(MailMessage); + + MaintainIndexes = args.Length == 0 || args.Contains(nameof(MaintainIndexes), StringComparer.OrdinalIgnoreCase); + if (MaintainIndexes && args.Length != 0) + JobName = nameof(MaintainIndexes); + + Migration = args.Length == 0 || args.Contains(nameof(Migration), StringComparer.OrdinalIgnoreCase); + if (Migration && args.Length != 0) + JobName = nameof(Migration); + + StackStatus = args.Length == 0 || args.Contains(nameof(StackStatus), StringComparer.OrdinalIgnoreCase); + if (StackStatus && args.Length != 0) + JobName = nameof(StackStatus); + + StackEventCount = args.Length == 0 || args.Contains(nameof(StackEventCount), StringComparer.OrdinalIgnoreCase); + if (StackEventCount && args.Length != 0) + JobName = nameof(StackEventCount); + + WebHooks = args.Length == 0 || args.Contains(nameof(WebHooks), StringComparer.OrdinalIgnoreCase); + if (WebHooks && args.Length != 0) + JobName = nameof(WebHooks); + + WorkItem = args.Length == 0 || args.Contains(nameof(WorkItem), StringComparer.OrdinalIgnoreCase); + if (WorkItem && args.Length != 0) + JobName = nameof(WorkItem); } + + public string JobName { get; } + public bool CleanupData { get; } + public bool CleanupOrphanedData { get; } + public bool CloseInactiveSessions { get; } + public bool DailySummary { get; } + public bool DataMigration { get; } + public bool DownloadGeoIPDatabase { get; } + public bool EventNotifications { get; } + public bool EventPosts { get; } + public bool EventUserDescriptions { get; } + public bool MailMessage { get; } + public bool MaintainIndexes { get; } + public bool Migration { get; } + public bool StackStatus { get; } + public bool StackEventCount { get; } + public bool WebHooks { get; } + public bool WorkItem { get; } } diff --git a/src/Exceptionless.Job/Program.cs b/src/Exceptionless.Job/Program.cs index c78c8a108d..942756d04d 100644 --- a/src/Exceptionless.Job/Program.cs +++ b/src/Exceptionless.Job/Program.cs @@ -1,7 +1,4 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; +using System.Diagnostics; using App.Metrics; using App.Metrics.AspNetCore; using App.Metrics.Formatters; @@ -15,176 +12,171 @@ using Foundatio.Extensions.Hosting.Jobs; using Foundatio.Extensions.Hosting.Startup; using Foundatio.Jobs; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using OpenTelemetry; using Serilog; using Serilog.Enrichers.Span; using Serilog.Events; using Serilog.Sinks.Exceptionless; -namespace Exceptionless.Job { - public class Program { - public static async Task Main(string[] args) { - try { - await CreateHostBuilder(args).Build().RunAsync(); - return 0; - } catch (Exception ex) { - Log.Fatal(ex, "Job host terminated unexpectedly"); - return 1; - } finally { - Log.CloseAndFlush(); - await ExceptionlessClient.Default.ProcessQueueAsync(); - - if (Debugger.IsAttached) - Console.ReadKey(); - } - } - - public static IHostBuilder CreateHostBuilder(string[] args) { - var jobOptions = new JobRunnerOptions(args); - - Console.Title = jobOptions.JobName != null ? $"Exceptionless {jobOptions.JobName} Job" : "Exceptionless Jobs"; - - string environment = Environment.GetEnvironmentVariable("EX_AppMode"); - if (String.IsNullOrWhiteSpace(environment)) - environment = "Production"; - - var config = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddYamlFile("appsettings.yml", optional: true, reloadOnChange: true) - .AddYamlFile($"appsettings.{environment}.yml", optional: true, reloadOnChange: true) - .AddEnvironmentVariables("EX_") - .AddEnvironmentVariables("ASPNETCORE_") - .AddCommandLine(args) - .Build(); +namespace Exceptionless.Job; - var options = AppOptions.ReadFromConfiguration(config); - - var loggerConfig = new LoggerConfiguration().ReadFrom.Configuration(config) - .Enrich.FromLogContext() - .Enrich.WithMachineName() - .Enrich.WithSpan(); - - if (!String.IsNullOrEmpty(options.ExceptionlessApiKey)) - loggerConfig.WriteTo.Sink(new ExceptionlessSink(), LogEventLevel.Information); - - Log.Logger = loggerConfig.CreateLogger(); - var configDictionary = config.ToDictionary("Serilog"); - Log.Information("Bootstrapping Exceptionless {JobName} job(s) in {AppMode} mode ({InformationalVersion}) on {MachineName} with settings {@Settings}", jobOptions.JobName ?? "All", environment, options.InformationalVersion, Environment.MachineName, configDictionary); - - var builder = Host.CreateDefaultBuilder() - .UseEnvironment(environment) - .UseSerilog() - .ConfigureWebHostDefaults(webBuilder => { - webBuilder - .UseConfiguration(config) - .Configure(app => { - app.UseSerilogRequestLogging(o => { - o.MessageTemplate = "traceID={TraceId} HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; - o.GetLevel = (context, duration, ex) => { - if (ex != null || context.Response.StatusCode > 499) - return LogEventLevel.Error; - - return duration < 1000 && context.Response.StatusCode < 400 ? LogEventLevel.Debug : LogEventLevel.Information; - }; - }); - }) - .Configure(app => { - Bootstrapper.LogConfiguration(app.ApplicationServices, options, app.ApplicationServices.GetService>()); - - if (!String.IsNullOrEmpty(options.ExceptionlessApiKey) && !String.IsNullOrEmpty(options.ExceptionlessServerUrl)) - app.UseExceptionless(ExceptionlessClient.Default); - - app.UseHealthChecks("/health", new HealthCheckOptions { - Predicate = hcr => !String.IsNullOrEmpty(jobOptions.JobName) ? hcr.Tags.Contains(jobOptions.JobName) : hcr.Tags.Contains("AllJobs") - }); - - app.UseHealthChecks("/ready", new HealthCheckOptions { - Predicate = hcr => hcr.Tags.Contains("Critical") - }); - - app.UseWaitForStartupActionsBeforeServingRequests(); - app.Run(async context => - { - await context.Response.WriteAsync($"Running Job: {jobOptions.JobName}"); - }); - }); - }) - .ConfigureServices((ctx, services) => { - AddJobs(services, jobOptions); - services.AddAppOptions(options); - - Bootstrapper.RegisterServices(services); - Insulation.Bootstrapper.RegisterServices(services, options, true); - - services.AddApm(new ApmConfig(config, "Exceptionless.Job", "Exceptionless", options.InformationalVersion, options.CacheOptions.Provider == "redis")); - }); - - if (!String.IsNullOrEmpty(options.MetricOptions.Provider)) - ConfigureMetricsReporting(builder, options.MetricOptions); - - return builder; +public class Program { + public static async Task Main(string[] args) { + try { + await CreateHostBuilder(args).Build().RunAsync(); + return 0; } + catch (Exception ex) { + Log.Fatal(ex, "Job host terminated unexpectedly"); + return 1; + } + finally { + Log.CloseAndFlush(); + await ExceptionlessClient.Default.ProcessQueueAsync(); - private static void AddJobs(IServiceCollection services, JobRunnerOptions options) { - services.AddJobLifetimeService(); - - if (options.CleanupData) - services.AddJob(); - if (options.CleanupOrphanedData) - services.AddJob(); - if (options.CloseInactiveSessions) - services.AddJob(true); - if (options.DailySummary) - services.AddJob(true); - if (options.DataMigration) - services.AddJob(true); - if (options.DownloadGeoIPDatabase) - services.AddJob(true); - if (options.EventNotifications) - services.AddJob(true); - if (options.EventPosts) - services.AddJob(true); - if (options.EventUserDescriptions) - services.AddJob(true); - if (options.MailMessage) - services.AddJob(true); - if (options.MaintainIndexes) - services.AddJob(); - if (options.Migration) - services.AddJob(true); - if (options.StackStatus) - services.AddJob(true); - if (options.StackEventCount) - services.AddJob(true); - if (options.WebHooks) - services.AddJob(true); - if (options.WorkItem) - services.AddJob(true); + if (Debugger.IsAttached) + Console.ReadKey(); } + } + + public static IHostBuilder CreateHostBuilder(string[] args) { + var jobOptions = new JobRunnerOptions(args); + + Console.Title = jobOptions.JobName != null ? $"Exceptionless {jobOptions.JobName} Job" : "Exceptionless Jobs"; + + string environment = Environment.GetEnvironmentVariable("EX_AppMode"); + if (String.IsNullOrWhiteSpace(environment)) + environment = "Production"; + + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddYamlFile("appsettings.yml", optional: true, reloadOnChange: true) + .AddYamlFile($"appsettings.{environment}.yml", optional: true, reloadOnChange: true) + .AddEnvironmentVariables("EX_") + .AddEnvironmentVariables("ASPNETCORE_") + .AddCommandLine(args) + .Build(); + + var options = AppOptions.ReadFromConfiguration(config); + + var loggerConfig = new LoggerConfiguration().ReadFrom.Configuration(config) + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .Enrich.WithSpan(); + + if (!String.IsNullOrEmpty(options.ExceptionlessApiKey)) + loggerConfig.WriteTo.Sink(new ExceptionlessSink(), LogEventLevel.Information); + + Log.Logger = loggerConfig.CreateLogger(); + var configDictionary = config.ToDictionary("Serilog"); + Log.Information("Bootstrapping Exceptionless {JobName} job(s) in {AppMode} mode ({InformationalVersion}) on {MachineName} with settings {@Settings}", jobOptions.JobName ?? "All", environment, options.InformationalVersion, Environment.MachineName, configDictionary); + + var builder = Host.CreateDefaultBuilder() + .UseEnvironment(environment) + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => { + webBuilder + .UseConfiguration(config) + .Configure(app => { + app.UseSerilogRequestLogging(o => { + o.MessageTemplate = "traceID={TraceId} HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; + o.GetLevel = (context, duration, ex) => { + if (ex != null || context.Response.StatusCode > 499) + return LogEventLevel.Error; + + return duration < 1000 && context.Response.StatusCode < 400 ? LogEventLevel.Debug : LogEventLevel.Information; + }; + }); + }) + .Configure(app => { + Bootstrapper.LogConfiguration(app.ApplicationServices, options, app.ApplicationServices.GetService>()); + + if (!String.IsNullOrEmpty(options.ExceptionlessApiKey) && !String.IsNullOrEmpty(options.ExceptionlessServerUrl)) + app.UseExceptionless(ExceptionlessClient.Default); - private static void ConfigureMetricsReporting(IHostBuilder builder, MetricOptions options) { - if (String.Equals(options.Provider, "prometheus")) { - var metrics = AppMetrics.CreateDefaultBuilder() - .OutputMetrics.AsPrometheusPlainText() - .OutputMetrics.AsPrometheusProtobuf() - .Build(); - builder.ConfigureMetrics(metrics).UseMetrics(o => { - o.EndpointOptions = endpointsOptions => { - endpointsOptions.MetricsTextEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); - endpointsOptions.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); - }; - }); - } else if (!String.Equals(options.Provider, "statsd")) { - builder.UseMetrics(); - } + app.UseHealthChecks("/health", new HealthCheckOptions { + Predicate = hcr => !String.IsNullOrEmpty(jobOptions.JobName) ? hcr.Tags.Contains(jobOptions.JobName) : hcr.Tags.Contains("AllJobs") + }); + + app.UseHealthChecks("/ready", new HealthCheckOptions { + Predicate = hcr => hcr.Tags.Contains("Critical") + }); + + app.UseWaitForStartupActionsBeforeServingRequests(); + app.Run(async context => { + await context.Response.WriteAsync($"Running Job: {jobOptions.JobName}"); + }); + }); + }) + .ConfigureServices((ctx, services) => { + AddJobs(services, jobOptions); + services.AddAppOptions(options); + + Bootstrapper.RegisterServices(services); + Insulation.Bootstrapper.RegisterServices(services, options, true); + + services.AddApm(new ApmConfig(config, "Exceptionless.Job", "Exceptionless", options.InformationalVersion, options.CacheOptions.Provider == "redis")); + }); + + if (!String.IsNullOrEmpty(options.MetricOptions.Provider)) + ConfigureMetricsReporting(builder, options.MetricOptions); + + return builder; + } + + private static void AddJobs(IServiceCollection services, JobRunnerOptions options) { + services.AddJobLifetimeService(); + + if (options.CleanupData) + services.AddJob(); + if (options.CleanupOrphanedData) + services.AddJob(); + if (options.CloseInactiveSessions) + services.AddJob(true); + if (options.DailySummary) + services.AddJob(true); + if (options.DataMigration) + services.AddJob(true); + if (options.DownloadGeoIPDatabase) + services.AddJob(true); + if (options.EventNotifications) + services.AddJob(true); + if (options.EventPosts) + services.AddJob(true); + if (options.EventUserDescriptions) + services.AddJob(true); + if (options.MailMessage) + services.AddJob(true); + if (options.MaintainIndexes) + services.AddJob(); + if (options.Migration) + services.AddJob(true); + if (options.StackStatus) + services.AddJob(true); + if (options.StackEventCount) + services.AddJob(true); + if (options.WebHooks) + services.AddJob(true); + if (options.WorkItem) + services.AddJob(true); + } + + private static void ConfigureMetricsReporting(IHostBuilder builder, MetricOptions options) { + if (String.Equals(options.Provider, "prometheus")) { + var metrics = AppMetrics.CreateDefaultBuilder() + .OutputMetrics.AsPrometheusPlainText() + .OutputMetrics.AsPrometheusProtobuf() + .Build(); + builder.ConfigureMetrics(metrics).UseMetrics(o => { + o.EndpointOptions = endpointsOptions => { + endpointsOptions.MetricsTextEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); + endpointsOptions.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); + }; + }); + } + else if (!String.Equals(options.Provider, "statsd")) { + builder.UseMetrics(); } } } diff --git a/src/Exceptionless.Web/ApmExtensions.cs b/src/Exceptionless.Web/ApmExtensions.cs index 4221018423..0997ae7af0 100644 --- a/src/Exceptionless.Web/ApmExtensions.cs +++ b/src/Exceptionless.Web/ApmExtensions.cs @@ -1,111 +1,105 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.Tracing; -using System.Net.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using System.Diagnostics.Tracing; using OpenTelemetry.Extensions.Hosting.Implementation; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; -namespace OpenTelemetry { - public static class ApmExtensions { - public static IServiceCollection AddApm(this IServiceCollection services, ApmConfig config) { - if (!config.Enabled) - return services; +namespace OpenTelemetry; - string apiKey = config.ApiKey; - if (!String.IsNullOrEmpty(apiKey) && apiKey.Length > 6) - apiKey = apiKey.Substring(0, 6) + "***"; +public static class ApmExtensions { + public static IServiceCollection AddApm(this IServiceCollection services, ApmConfig config) { + if (!config.Enabled) + return services; + + string apiKey = config.ApiKey; + if (!String.IsNullOrEmpty(apiKey) && apiKey.Length > 6) + apiKey = apiKey.Substring(0, 6) + "***"; - Log.Information("Configuring APM: Endpoint={Endpoint} ApiKey={ApiKey} FullDetails={FullDetails} EnableRedis={EnableRedis} SampleRate={SampleRate}", - config.Endpoint, apiKey, config.FullDetails, config.EnableRedis, config.SampleRate); + Log.Information("Configuring APM: Endpoint={Endpoint} ApiKey={ApiKey} FullDetails={FullDetails} EnableRedis={EnableRedis} SampleRate={SampleRate}", + config.Endpoint, apiKey, config.FullDetails, config.EnableRedis, config.SampleRate); - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - services.AddHostedService(sp => new SelfDiagnosticsLoggingHostedService(sp.GetRequiredService(), config.Debug ? EventLevel.Verbose : null)); + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + services.AddHostedService(sp => new SelfDiagnosticsLoggingHostedService(sp.GetRequiredService(), config.Debug ? EventLevel.Verbose : null)); - services.AddOpenTelemetryTracing(b => { - b.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(config.ServiceName).AddAttributes(new[] { + services.AddOpenTelemetryTracing(b => { + b.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(config.ServiceName).AddAttributes(new[] { new KeyValuePair("service.namespace", config.ServiceNamespace), new KeyValuePair("service.version", config.ServiceVersion) })); - b.AddAspNetCoreInstrumentation(); - b.AddElasticsearchClientInstrumentation(c => { - c.SuppressDownstreamInstrumentation = true; - c.ParseAndFormatRequest = config.FullDetails; - c.Enrich = (activity, source, data) => { - activity.SetTag("service.name", "Elasticsearch"); - if (activity.DisplayName.StartsWith("Elasticsearch ")) - activity.DisplayName = activity.DisplayName.Substring(14); - }; - }); - b.AddHttpClientInstrumentation(o => { - o.Enrich = (activity, source, data) => { - if (data is HttpRequestMessage request) { - if (request.RequestUri.Host.EndsWith("amazonaws.com")) { - if (request.RequestUri.Host.StartsWith("sqs")) - activity.SetTag("service.name", "AWS SQS"); - else if (request.RequestUri.Host.Contains("s3")) - activity.SetTag("service.name", "AWS S3"); - else - activity.SetTag("service.name", "AWS"); - } else { - activity.SetTag("service.name", "External HTTP"); - } + b.AddAspNetCoreInstrumentation(); + b.AddElasticsearchClientInstrumentation(c => { + c.SuppressDownstreamInstrumentation = true; + c.ParseAndFormatRequest = config.FullDetails; + c.Enrich = (activity, source, data) => { + activity.SetTag("service.name", "Elasticsearch"); + if (activity.DisplayName.StartsWith("Elasticsearch ")) + activity.DisplayName = activity.DisplayName.Substring(14); + }; + }); + b.AddHttpClientInstrumentation(o => { + o.Enrich = (activity, source, data) => { + if (data is HttpRequestMessage request) { + if (request.RequestUri.Host.EndsWith("amazonaws.com")) { + if (request.RequestUri.Host.StartsWith("sqs")) + activity.SetTag("service.name", "AWS SQS"); + else if (request.RequestUri.Host.Contains("s3")) + activity.SetTag("service.name", "AWS S3"); + else + activity.SetTag("service.name", "AWS"); + } else { + activity.SetTag("service.name", "External HTTP"); } + } + }; + }); + b.AddSource("Foundatio"); + if (config.EnableRedis) + b.AddRedisInstrumentation(null, c => { + c.SetVerboseDatabaseStatements = config.FullDetails; + c.Enrich = (activity, command) => { + activity.SetTag("service.name", "Redis"); }; }); - b.AddSource("Foundatio"); - if (config.EnableRedis) - b.AddRedisInstrumentation(null, c => { - c.SetVerboseDatabaseStatements = config.FullDetails; - c.Enrich = (activity, command) => { - activity.SetTag("service.name", "Redis"); - }; - }); - b.SetSampler(new TraceIdRatioBasedSampler(config.SampleRate)); - b.AddOtlpExporter(c => { - if (!String.IsNullOrEmpty(config.Endpoint)) - c.Endpoint = new Uri(config.Endpoint); - if (!String.IsNullOrEmpty(config.ApiKey)) - c.Headers = $"api-key={config.ApiKey}"; - }); + b.SetSampler(new TraceIdRatioBasedSampler(config.SampleRate)); + b.AddOtlpExporter(c => { + if (!String.IsNullOrEmpty(config.Endpoint)) + c.Endpoint = new Uri(config.Endpoint); + if (!String.IsNullOrEmpty(config.ApiKey)) + c.Headers = $"api-key={config.ApiKey}"; }); + }); - //services.Configure(options => { - // options.Filter = (req) => { - // return req.Request.Host != null; - // }; - //}); + //services.Configure(options => { + // options.Filter = (req) => { + // return req.Request.Host != null; + // }; + //}); - return services; - } + return services; } +} - public class ApmConfig { - private readonly IConfiguration config; - - public ApmConfig(IConfigurationRoot config, string serviceName, string serviceNamespace, string serviceVersion, bool enableRedis) { - this.config = config.GetSection("Apm"); - ServiceName = serviceName; - ServiceNamespace = serviceNamespace; - ServiceVersion = serviceVersion; - EnableRedis = enableRedis; - } +public class ApmConfig { + private readonly IConfiguration config; - public bool Enabled => config.GetValue("Enabled", false); - public string ServiceName { get; } - public string ServiceNamespace { get; } - public string ServiceVersion { get; } - public string Endpoint => config.GetValue("Endpoint", String.Empty); - public string ApiKey => config.GetValue("ApiKey", String.Empty); - public bool FullDetails => config.GetValue("FullDetails", false); - public double SampleRate => config.GetValue("SampleRate", 1.0); - public bool EnableRedis { get; } - public bool Debug => config.GetValue("Debug", false); + public ApmConfig(IConfigurationRoot config, string serviceName, string serviceNamespace, string serviceVersion, bool enableRedis) { + this.config = config.GetSection("Apm"); + ServiceName = serviceName; + ServiceNamespace = serviceNamespace; + ServiceVersion = serviceVersion; + EnableRedis = enableRedis; } + + public bool Enabled => config.GetValue("Enabled", false); + public string ServiceName { get; } + public string ServiceNamespace { get; } + public string ServiceVersion { get; } + public string Endpoint => config.GetValue("Endpoint", String.Empty); + public string ApiKey => config.GetValue("ApiKey", String.Empty); + public bool FullDetails => config.GetValue("FullDetails", false); + public double SampleRate => config.GetValue("SampleRate", 1.0); + public bool EnableRedis { get; } + public bool Debug => config.GetValue("Debug", false); } diff --git a/src/Exceptionless.Web/Bootstrapper.cs b/src/Exceptionless.Web/Bootstrapper.cs index b961332a96..f924811b1c 100644 --- a/src/Exceptionless.Web/Bootstrapper.cs +++ b/src/Exceptionless.Web/Bootstrapper.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Core; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; @@ -12,65 +11,63 @@ using Foundatio.Extensions.Hosting.Startup; using Foundatio.Jobs; using Foundatio.Messaging; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Token = Exceptionless.Core.Models.Token; -namespace Exceptionless.Web { - public class Bootstrapper { - public static void RegisterServices(IServiceCollection services, AppOptions appOptions, ILoggerFactory loggerFactory) { - services.AddSingleton(); - services.AddSingleton(); +namespace Exceptionless.Web; - services.AddTransient(); +public class Bootstrapper { + public static void RegisterServices(IServiceCollection services, AppOptions appOptions, ILoggerFactory loggerFactory) { + services.AddSingleton(); + services.AddSingleton(); - Core.Bootstrapper.RegisterServices(services); - Insulation.Bootstrapper.RegisterServices(services, appOptions, appOptions.RunJobsInProcess); + services.AddTransient(); - if (appOptions.RunJobsInProcess) - Core.Bootstrapper.AddHostedJobs(services, loggerFactory); + Core.Bootstrapper.RegisterServices(services); + Insulation.Bootstrapper.RegisterServices(services, appOptions, appOptions.RunJobsInProcess); - var logger = loggerFactory.CreateLogger(); - services.AddStartupAction(); - services.AddStartupAction("Subscribe to Log Work Item Progress", (sp, ct) => { - var subscriber = sp.GetRequiredService(); - return subscriber.SubscribeAsync(workItemStatus => { - if (logger.IsEnabled(LogLevel.Trace)) - logger.LogTrace("WorkItem id:{WorkItemId} message:{Message} progress:{Progress}", workItemStatus.WorkItemId ?? "", workItemStatus.Message ?? "", workItemStatus.Progress); + if (appOptions.RunJobsInProcess) + Core.Bootstrapper.AddHostedJobs(services, loggerFactory); - return Task.CompletedTask; - }, ct); - }); + var logger = loggerFactory.CreateLogger(); + services.AddStartupAction(); + services.AddStartupAction("Subscribe to Log Work Item Progress", (sp, ct) => { + var subscriber = sp.GetRequiredService(); + return subscriber.SubscribeAsync(workItemStatus => { + if (logger.IsEnabled(LogLevel.Trace)) + logger.LogTrace("WorkItem id:{WorkItemId} message:{Message} progress:{Progress}", workItemStatus.WorkItemId ?? "", workItemStatus.Message ?? "", workItemStatus.Progress); - services.AddSingleton(); - services.AddStartupAction(); - } + return Task.CompletedTask; + }, ct); + }); + + services.AddSingleton(); + services.AddStartupAction(); + } - public class ApiMappings : Profile { - public ApiMappings(BillingPlans plans) { - CreateMap(); + public class ApiMappings : Profile { + public ApiMappings(BillingPlans plans) { + CreateMap(); - CreateMap(); - CreateMap().AfterMap((o, vo) => { - vo.IsOverHourlyLimit = o.IsOverHourlyLimit(plans); - vo.IsOverMonthlyLimit = o.IsOverMonthlyLimit(); - }); + CreateMap(); + CreateMap().AfterMap((o, vo) => { + vo.IsOverHourlyLimit = o.IsOverHourlyLimit(plans); + vo.IsOverMonthlyLimit = o.IsOverMonthlyLimit(); + }); - CreateMap().AfterMap((si, igm) => { - igm.Id = igm.Id.Substring(3); - igm.Date = si.Created; - }); + CreateMap().AfterMap((si, igm) => { + igm.Id = igm.Id.Substring(3); + igm.Date = si.Created; + }); - CreateMap(); - CreateMap().AfterMap((p, vp) => vp.HasSlackIntegration = p.Data.ContainsKey(Project.KnownDataKeys.SlackToken)); + CreateMap(); + CreateMap().AfterMap((p, vp) => vp.HasSlackIntegration = p.Data.ContainsKey(Project.KnownDataKeys.SlackToken)); - CreateMap().ForMember(m => m.Type, m => m.Ignore()); - CreateMap(); + CreateMap().ForMember(m => m.Type, m => m.Ignore()); + CreateMap(); - CreateMap(); + CreateMap(); - CreateMap(); - } + CreateMap(); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Controllers/AdminController.cs b/src/Exceptionless.Web/Controllers/AdminController.cs index fac7c33089..2039fd3f35 100644 --- a/src/Exceptionless.Web/Controllers/AdminController.cs +++ b/src/Exceptionless.Web/Controllers/AdminController.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Exceptionless.Core; using Exceptionless.Core.Authorization; using Exceptionless.Core.Billing; @@ -21,147 +18,147 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/admin")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public class AdminController : ExceptionlessApiController { - private readonly ExceptionlessElasticConfiguration _configuration; - private readonly IFileStorage _fileStorage; - private readonly IMessagePublisher _messagePublisher; - private readonly IOrganizationRepository _organizationRepository; - private readonly IQueue _eventPostQueue; - private readonly IQueue _workItemQueue; - private readonly AppOptions _appOptions; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - - public AdminController( - ExceptionlessElasticConfiguration configuration, - IFileStorage fileStorage, - IMessagePublisher messagePublisher, - IOrganizationRepository organizationRepository, - IQueue eventPostQueue, - IQueue workItemQueue, - AppOptions appOptions, - BillingManager billingManager, - BillingPlans plans) { - _configuration = configuration; - _fileStorage = fileStorage; - _messagePublisher = messagePublisher; - _organizationRepository = organizationRepository; - _eventPostQueue = eventPostQueue; - _workItemQueue = workItemQueue; - _appOptions = appOptions; - _billingManager = billingManager; - _plans = plans; - } +namespace Exceptionless.Web.Controllers; + +[Route(API_PREFIX + "/admin")] +[Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] +[ApiExplorerSettings(IgnoreApi = true)] +public class AdminController : ExceptionlessApiController { + private readonly ExceptionlessElasticConfiguration _configuration; + private readonly IFileStorage _fileStorage; + private readonly IMessagePublisher _messagePublisher; + private readonly IOrganizationRepository _organizationRepository; + private readonly IQueue _eventPostQueue; + private readonly IQueue _workItemQueue; + private readonly AppOptions _appOptions; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + + public AdminController( + ExceptionlessElasticConfiguration configuration, + IFileStorage fileStorage, + IMessagePublisher messagePublisher, + IOrganizationRepository organizationRepository, + IQueue eventPostQueue, + IQueue workItemQueue, + AppOptions appOptions, + BillingManager billingManager, + BillingPlans plans) { + _configuration = configuration; + _fileStorage = fileStorage; + _messagePublisher = messagePublisher; + _organizationRepository = organizationRepository; + _eventPostQueue = eventPostQueue; + _workItemQueue = workItemQueue; + _appOptions = appOptions; + _billingManager = billingManager; + _plans = plans; + } - [HttpGet("settings")] - public ActionResult SettingsRequest() { - return Ok(_appOptions); - } + [HttpGet("settings")] + public ActionResult SettingsRequest() { + return Ok(_appOptions); + } - [HttpGet("echo")] - public ActionResult EchoRequest() { - return Ok(new { - Request.Headers, - IpAddress = Request.GetClientIpAddress() - }); - } - - [HttpGet("assemblies")] - public ActionResult> Assemblies() { - var details = AssemblyDetail.ExtractAll(); - return Ok(details); - } + [HttpGet("echo")] + public ActionResult EchoRequest() { + return Ok(new { + Request.Headers, + IpAddress = Request.GetClientIpAddress() + }); + } - [Consumes("application/json")] - [HttpPost("change-plan")] - public async Task ChangePlanAsync(string organizationId, string planId) { - if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) - return Ok(new { Success = false, Message = "Invalid Organization Id." }); + [HttpGet("assemblies")] + public ActionResult> Assemblies() { + var details = AssemblyDetail.ExtractAll(); + return Ok(details); + } - var organization = await _organizationRepository.GetByIdAsync(organizationId); - if (organization == null) - return Ok(new { Success = false, Message = "Invalid Organization Id." }); + [Consumes("application/json")] + [HttpPost("change-plan")] + public async Task ChangePlanAsync(string organizationId, string planId) { + if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) + return Ok(new { Success = false, Message = "Invalid Organization Id." }); - var plan = _billingManager.GetBillingPlan(planId); - if (plan == null) - return Ok(new { Success = false, Message = "Invalid PlanId." }); + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization == null) + return Ok(new { Success = false, Message = "Invalid Organization Id." }); - organization.BillingStatus = !String.Equals(plan.Id, _plans.FreePlan.Id) ? BillingStatus.Active : BillingStatus.Trialing; - organization.RemoveSuspension(); - _billingManager.ApplyBillingPlan(organization, plan, CurrentUser, false); + var plan = _billingManager.GetBillingPlan(planId); + if (plan == null) + return Ok(new { Success = false, Message = "Invalid PlanId." }); - await _organizationRepository.SaveAsync(organization, o => o.Cache()); - await _messagePublisher.PublishAsync(new PlanChanged { - OrganizationId = organization.Id - }); + organization.BillingStatus = !String.Equals(plan.Id, _plans.FreePlan.Id) ? BillingStatus.Active : BillingStatus.Trialing; + organization.RemoveSuspension(); + _billingManager.ApplyBillingPlan(organization, plan, CurrentUser, false); - return Ok(new { Success = true }); - } + await _organizationRepository.SaveAsync(organization, o => o.Cache()); + await _messagePublisher.PublishAsync(new PlanChanged { + OrganizationId = organization.Id + }); - [Consumes("application/json")] - [HttpPost("set-bonus")] - public async Task SetBonusAsync(string organizationId, int bonusEvents, DateTime? expires = null) { - if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) - return Ok(new { Success = false, Message = "Invalid Organization Id." }); + return Ok(new { Success = true }); + } - var organization = await _organizationRepository.GetByIdAsync(organizationId); - if (organization == null) - return Ok(new { Success = false, Message = "Invalid Organization Id." }); + [Consumes("application/json")] + [HttpPost("set-bonus")] + public async Task SetBonusAsync(string organizationId, int bonusEvents, DateTime? expires = null) { + if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) + return Ok(new { Success = false, Message = "Invalid Organization Id." }); - _billingManager.ApplyBonus(organization, bonusEvents, expires); - await _organizationRepository.SaveAsync(organization, o => o.Cache()); + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization == null) + return Ok(new { Success = false, Message = "Invalid Organization Id." }); - return Ok(new { Success = true }); - } + _billingManager.ApplyBonus(organization, bonusEvents, expires); + await _organizationRepository.SaveAsync(organization, o => o.Cache()); - [HttpGet("requeue")] - public async Task RequeueAsync(string path = null, bool archive = false) { - if (String.IsNullOrEmpty(path)) - path = @"q\*"; + return Ok(new { Success = true }); + } - int enqueued = 0; - foreach (var file in await _fileStorage.GetFileListAsync(path)) { - await _eventPostQueue.EnqueueAsync(new EventPost(_appOptions.EnableArchive && archive) { FilePath = file.Path }); - enqueued++; - } + [HttpGet("requeue")] + public async Task RequeueAsync(string path = null, bool archive = false) { + if (String.IsNullOrEmpty(path)) + path = @"q\*"; - return Ok(new { Enqueued = enqueued }); + int enqueued = 0; + foreach (var file in await _fileStorage.GetFileListAsync(path)) { + await _eventPostQueue.EnqueueAsync(new EventPost(_appOptions.EnableArchive && archive) { FilePath = file.Path }); + enqueued++; } - [HttpGet("maintenance/{name:minlength(1)}")] - public async Task RunJobAsync(string name) { - switch (name.ToLowerInvariant()) { - case "indexes": - if (!_appOptions.ElasticsearchOptions.DisableIndexConfiguration) - await _configuration.ConfigureIndexesAsync(beginReindexingOutdated: false); - break; - case "update-organization-plans": - await _workItemQueue.EnqueueAsync(new OrganizationMaintenanceWorkItem { UpgradePlans = true }); - break; - case "remove-old-organization-usage": - await _workItemQueue.EnqueueAsync(new OrganizationMaintenanceWorkItem { RemoveOldUsageStats = true }); - break; - case "update-project-default-bot-lists": - await _workItemQueue.EnqueueAsync(new ProjectMaintenanceWorkItem { UpdateDefaultBotList = true, IncrementConfigurationVersion = true }); - break; - case "increment-project-configuration-version": - await _workItemQueue.EnqueueAsync(new ProjectMaintenanceWorkItem { IncrementConfigurationVersion = true }); - break; - case "remove-old-project-usage": - await _workItemQueue.EnqueueAsync(new ProjectMaintenanceWorkItem { RemoveOldUsageStats = true }); - break; - case "normalize-user-email-address": - await _workItemQueue.EnqueueAsync(new UserMaintenanceWorkItem { Normalize = true }); - break; - default: - return NotFound(); - } - - return Ok(); + return Ok(new { Enqueued = enqueued }); + } + + [HttpGet("maintenance/{name:minlength(1)}")] + public async Task RunJobAsync(string name) { + switch (name.ToLowerInvariant()) { + case "indexes": + if (!_appOptions.ElasticsearchOptions.DisableIndexConfiguration) + await _configuration.ConfigureIndexesAsync(beginReindexingOutdated: false); + break; + case "update-organization-plans": + await _workItemQueue.EnqueueAsync(new OrganizationMaintenanceWorkItem { UpgradePlans = true }); + break; + case "remove-old-organization-usage": + await _workItemQueue.EnqueueAsync(new OrganizationMaintenanceWorkItem { RemoveOldUsageStats = true }); + break; + case "update-project-default-bot-lists": + await _workItemQueue.EnqueueAsync(new ProjectMaintenanceWorkItem { UpdateDefaultBotList = true, IncrementConfigurationVersion = true }); + break; + case "increment-project-configuration-version": + await _workItemQueue.EnqueueAsync(new ProjectMaintenanceWorkItem { IncrementConfigurationVersion = true }); + break; + case "remove-old-project-usage": + await _workItemQueue.EnqueueAsync(new ProjectMaintenanceWorkItem { RemoveOldUsageStats = true }); + break; + case "normalize-user-email-address": + await _workItemQueue.EnqueueAsync(new UserMaintenanceWorkItem { Normalize = true }); + break; + default: + return NotFound(); } + + return Ok(); } } diff --git a/src/Exceptionless.Web/Controllers/AuthController.cs b/src/Exceptionless.Web/Controllers/AuthController.cs index c99934e525..e222f432f1 100644 --- a/src/Exceptionless.Web/Controllers/AuthController.cs +++ b/src/Exceptionless.Web/Controllers/AuthController.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Web.Extensions; +using Exceptionless.Web.Extensions; using Exceptionless.Core.Authentication; using Exceptionless.Core.Authorization; using Exceptionless.Core.Configuration; @@ -16,9 +13,7 @@ using Foundatio.Repositories; using Foundatio.Utility; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using OAuth2.Client; using OAuth2.Client.Impl; @@ -26,761 +21,774 @@ using OAuth2.Infrastructure; using OAuth2.Models; -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/auth")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public class AuthController : ExceptionlessApiController { - private readonly AuthOptions _authOptions; - private readonly IDomainLoginProvider _domainLoginProvider; - private readonly IOrganizationRepository _organizationRepository; - private readonly IUserRepository _userRepository; - private readonly ITokenRepository _tokenRepository; - private readonly ICacheClient _cache; - private readonly IMailer _mailer; - private readonly ILogger _logger; - - private static bool _isFirstUserChecked; - - public AuthController(AuthOptions authOptions, IOrganizationRepository organizationRepository, IUserRepository userRepository, ITokenRepository tokenRepository, ICacheClient cacheClient, IMailer mailer, ILogger logger, IDomainLoginProvider domainLoginProvider) { - _authOptions = authOptions; - _domainLoginProvider = domainLoginProvider; - _organizationRepository = organizationRepository; - _userRepository = userRepository; - _tokenRepository = tokenRepository; - _cache = new ScopedCacheClient(cacheClient, "Auth"); - _mailer = mailer; - _logger = logger; - } +namespace Exceptionless.Web.Controllers; + +[Route(API_PREFIX + "/auth")] +[Authorize(Policy = AuthorizationRoles.UserPolicy)] +public class AuthController : ExceptionlessApiController { + private readonly AuthOptions _authOptions; + private readonly IDomainLoginProvider _domainLoginProvider; + private readonly IOrganizationRepository _organizationRepository; + private readonly IUserRepository _userRepository; + private readonly ITokenRepository _tokenRepository; + private readonly ICacheClient _cache; + private readonly IMailer _mailer; + private readonly ILogger _logger; + + private static bool _isFirstUserChecked; + + public AuthController(AuthOptions authOptions, IOrganizationRepository organizationRepository, IUserRepository userRepository, ITokenRepository tokenRepository, ICacheClient cacheClient, IMailer mailer, ILogger logger, IDomainLoginProvider domainLoginProvider) { + _authOptions = authOptions; + _domainLoginProvider = domainLoginProvider; + _organizationRepository = organizationRepository; + _userRepository = userRepository; + _tokenRepository = tokenRepository; + _cache = new ScopedCacheClient(cacheClient, "Auth"); + _mailer = mailer; + _logger = logger; + } - /// - /// Login - /// - /// - /// Log in with your email address and password to generate a token scoped with your users roles. - /// - /// { "email": "noreply@exceptionless.io", "password": "exceptionless" } - /// - /// This token can then be used to access the api. You can use this token in the header (bearer authentication) - /// or append it onto the query string: ?access_token=MY_TOKEN - /// - /// Please note that you can also use this token on the documentation site by placing it in the - /// headers api_key input box. - /// - /// The login model. - /// The login model is invalid. - /// Login failed. - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("login")] - public async Task> LoginAsync(LoginModel model) { - string email = model?.Email?.Trim().ToLowerInvariant(); - using (_logger.BeginScope(new ExceptionlessState().Tag("Login").Identity(email).SetHttpContext(HttpContext))) { - if (String.IsNullOrEmpty(email)) { - _logger.LogError("Login failed: Email Address is required."); - return BadRequest("Email Address is required."); - } + /// + /// Login + /// + /// + /// Log in with your email address and password to generate a token scoped with your users roles. + /// + /// { "email": "noreply@exceptionless.io", "password": "exceptionless" } + /// + /// This token can then be used to access the api. You can use this token in the header (bearer authentication) + /// or append it onto the query string: ?access_token=MY_TOKEN + /// + /// Please note that you can also use this token on the documentation site by placing it in the + /// headers api_key input box. + /// + /// The login model. + /// The login model is invalid. + /// Login failed. + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("login")] + public async Task> LoginAsync(LoginModel model) { + string email = model?.Email?.Trim().ToLowerInvariant(); + using (_logger.BeginScope(new ExceptionlessState().Tag("Login").Identity(email).SetHttpContext(HttpContext))) { + if (String.IsNullOrEmpty(email)) { + _logger.LogError("Login failed: Email Address is required."); + return BadRequest("Email Address is required."); + } - if (String.IsNullOrWhiteSpace(model.Password)) { - _logger.LogError("Login failed for {EmailAddress}: Password is required.", email); - return BadRequest("Password is required."); - } + if (String.IsNullOrWhiteSpace(model.Password)) { + _logger.LogError("Login failed for {EmailAddress}: Password is required.", email); + return BadRequest("Password is required."); + } - // Only allow 5 password attempts per 15 minute period. - string userLoginAttemptsCacheKey = $"user:{email}:attempts"; - long userLoginAttempts = await _cache.IncrementAsync(userLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); + // Only allow 5 password attempts per 15 minute period. + string userLoginAttemptsCacheKey = $"user:{email}:attempts"; + long userLoginAttempts = await _cache.IncrementAsync(userLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); - // Only allow 15 login attempts per 15 minute period by a single ip. - string ipLoginAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:attempts"; - long ipLoginAttempts = await _cache.IncrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); + // Only allow 15 login attempts per 15 minute period by a single ip. + string ipLoginAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:attempts"; + long ipLoginAttempts = await _cache.IncrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); - if (userLoginAttempts > 5) { - _logger.LogError("Login denied for {EmailAddress} for the {UserLoginAttempts} time.", email, userLoginAttempts); - return Unauthorized(); - } + if (userLoginAttempts > 5) { + _logger.LogError("Login denied for {EmailAddress} for the {UserLoginAttempts} time.", email, userLoginAttempts); + return Unauthorized(); + } - if (ipLoginAttempts > 15) { - _logger.LogError("Login denied for {EmailAddress} for the {IPLoginAttempts} time.", Request.GetClientIpAddress(), ipLoginAttempts); - return Unauthorized(); - } + if (ipLoginAttempts > 15) { + _logger.LogError("Login denied for {EmailAddress} for the {IPLoginAttempts} time.", Request.GetClientIpAddress(), ipLoginAttempts); + return Unauthorized(); + } + + User user; + try { + user = await _userRepository.GetByEmailAddressAsync(email); + } + catch (Exception ex) { + _logger.LogCritical(ex, "Login failed for {EmailAddress}: {Message}", email, ex.Message); + return Unauthorized(); + } + + if (user == null) { + _logger.LogError("Login failed for {EmailAddress}: User not found.", email); + return Unauthorized(); + } + + if (!user.IsActive) { + _logger.LogError("Login failed for {EmailAddress}: The user is inactive.", user.EmailAddress); + return Unauthorized(); + } - User user; - try { - user = await _userRepository.GetByEmailAddressAsync(email); - } catch (Exception ex) { - _logger.LogCritical(ex, "Login failed for {EmailAddress}: {Message}", email, ex.Message); + if (!_authOptions.EnableActiveDirectoryAuth) { + if (String.IsNullOrEmpty(user.Salt)) { + _logger.LogError("Login failed for {EmailAddress}: The user has no salt defined.", user.EmailAddress); return Unauthorized(); } - if (user == null) { - _logger.LogError("Login failed for {EmailAddress}: User not found.", email); + if (!user.IsCorrectPassword(model.Password)) { + _logger.LogError("Login failed for {EmailAddress}: Invalid Password.", user.EmailAddress); return Unauthorized(); } - if (!user.IsActive) { - _logger.LogError("Login failed for {EmailAddress}: The user is inactive.", user.EmailAddress); + if (!PasswordMeetsRequirements(model.Password)) { + _logger.LogError("Login denied for {EmailAddress} for invalid password.", email); + return StatusCode(423, "Password requirements have changed. Password needs to be reset to meet the new requirements."); + } + } + else { + if (!IsValidActiveDirectoryLogin(email, model.Password)) { + _logger.LogError("Domain login failed for {EmailAddress}: Invalid Password or Account.", user.EmailAddress); return Unauthorized(); } + } - if (!_authOptions.EnableActiveDirectoryAuth) { - if (String.IsNullOrEmpty(user.Salt)) { - _logger.LogError("Login failed for {EmailAddress}: The user has no salt defined.", user.EmailAddress); - return Unauthorized(); - } + if (!String.IsNullOrEmpty(model.InviteToken)) + await AddInvitedUserToOrganizationAsync(model.InviteToken, user); - if (!user.IsCorrectPassword(model.Password)) { - _logger.LogError("Login failed for {EmailAddress}: Invalid Password.", user.EmailAddress); - return Unauthorized(); - } + await _cache.RemoveAsync(userLoginAttemptsCacheKey); + await _cache.DecrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); - if (!PasswordMeetsRequirements(model.Password)) { - _logger.LogError("Login denied for {EmailAddress} for invalid password.", email); - return StatusCode(423, "Password requirements have changed. Password needs to be reset to meet the new requirements."); - } - } else { - if (!IsValidActiveDirectoryLogin(email, model.Password)) { - _logger.LogError("Domain login failed for {EmailAddress}: Invalid Password or Account.", user.EmailAddress); - return Unauthorized(); - } - } + _logger.UserLoggedIn(user.EmailAddress); + return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(user) }); + } + } - if (!String.IsNullOrEmpty(model.InviteToken)) - await AddInvitedUserToOrganizationAsync(model.InviteToken, user); + [ApiExplorerSettings(IgnoreApi = true)] + [HttpGet("logout")] + public async Task LogoutAsync() { + if (User.IsTokenAuthType()) + return Ok(); - await _cache.RemoveAsync(userLoginAttemptsCacheKey); - await _cache.DecrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); + string id = User.GetLoggedInUsersTokenId(); + if (String.IsNullOrEmpty(id)) + return Ok(); - _logger.UserLoggedIn(user.EmailAddress); - return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(user) }); - } + try { + await _tokenRepository.RemoveAsync(id); + } + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Tag("Logout").Identity(CurrentUser.EmailAddress).SetHttpContext(HttpContext))) + _logger.LogCritical(ex, "Logout failed for {EmailAddress}: {Message}", CurrentUser.EmailAddress, ex.Message); } - [ApiExplorerSettings(IgnoreApi = true)] - [HttpGet("logout")] - public async Task LogoutAsync() { - if (User.IsTokenAuthType()) - return Ok(); - - string id = User.GetLoggedInUsersTokenId(); - if (String.IsNullOrEmpty(id)) - return Ok(); + return Ok(); + } - try { - await _tokenRepository.RemoveAsync(id); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Tag("Logout").Identity(CurrentUser.EmailAddress).SetHttpContext(HttpContext))) - _logger.LogCritical(ex, "Logout failed for {EmailAddress}: {Message}", CurrentUser.EmailAddress, ex.Message); + /// + /// Sign up + /// + /// The sign up model. + /// The sign up model is invalid. + /// Sign up failed. + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("signup")] + public async Task> SignupAsync(SignupModel model) { + bool valid = await IsAccountCreationEnabledAsync(model?.InviteToken); + if (!valid) + return BadRequest("Account Creation is currently disabled."); + + string email = model?.Email?.Trim().ToLowerInvariant(); + using (_logger.BeginScope(new ExceptionlessState().Tag("Signup").Identity(email).Property("Name", model != null ? model.Name : "").Property("Password Length", model?.Password?.Length ?? 0).SetHttpContext(HttpContext))) { + if (String.IsNullOrEmpty(email)) { + _logger.LogError("Signup failed: Email Address is required."); + return BadRequest("Email Address is required."); } - return Ok(); - } - - /// - /// Sign up - /// - /// The sign up model. - /// The sign up model is invalid. - /// Sign up failed. - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("signup")] - public async Task> SignupAsync(SignupModel model) { - bool valid = await IsAccountCreationEnabledAsync(model?.InviteToken); - if (!valid) - return BadRequest("Account Creation is currently disabled."); + if (String.IsNullOrWhiteSpace(model.Name)) { + _logger.LogError("Signup failed for {EmailAddress}: Name is required.", email); + return BadRequest("Name is required."); + } - string email = model?.Email?.Trim().ToLowerInvariant(); - using (_logger.BeginScope(new ExceptionlessState().Tag("Signup").Identity(email).Property("Name", model != null ? model.Name : "").Property("Password Length", model?.Password?.Length ?? 0).SetHttpContext(HttpContext))) { - if (String.IsNullOrEmpty(email)) { - _logger.LogError("Signup failed: Email Address is required."); - return BadRequest("Email Address is required."); - } + if (!PasswordMeetsRequirements(model.Password)) { + _logger.LogError("Signup failed for {EmailAddress}: Invalid Password", email); + return BadRequest("Password must be at least 6 characters long."); + } - if (String.IsNullOrWhiteSpace(model.Name)) { - _logger.LogError("Signup failed for {EmailAddress}: Name is required.", email); - return BadRequest("Name is required."); - } + User user; + try { + user = await _userRepository.GetByEmailAddressAsync(email); + } + catch (Exception ex) { + _logger.LogCritical(ex, "Signup failed for {EmailAddress}: {Message}", email, ex.Message); + return BadRequest(); + } - if (!PasswordMeetsRequirements(model.Password)) { - _logger.LogError("Signup failed for {EmailAddress}: Invalid Password", email); - return BadRequest("Password must be at least 6 characters long."); - } + if (user != null) + return await LoginAsync(model); - User user; - try { - user = await _userRepository.GetByEmailAddressAsync(email); - } catch (Exception ex) { - _logger.LogCritical(ex, "Signup failed for {EmailAddress}: {Message}", email, ex.Message); + string ipSignupAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:signup:attempts"; + bool hasValidInviteToken = !String.IsNullOrWhiteSpace(model.InviteToken) && await _organizationRepository.GetByInviteTokenAsync(model.InviteToken) != null; + if (!hasValidInviteToken) { + // Only allow 10 sign ups per hour period by a single ip. + long ipSignupAttempts = await _cache.IncrementAsync(ipSignupAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); + if (ipSignupAttempts > 10) { + _logger.LogError("Signup denied for {EmailAddress} for the {IPSignupAttempts} time.", email, ipSignupAttempts); return BadRequest(); } + } - if (user != null) - return await LoginAsync(model); - - string ipSignupAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:signup:attempts"; - bool hasValidInviteToken = !String.IsNullOrWhiteSpace(model.InviteToken) && await _organizationRepository.GetByInviteTokenAsync(model.InviteToken) != null; - if (!hasValidInviteToken) { - // Only allow 10 sign ups per hour period by a single ip. - long ipSignupAttempts = await _cache.IncrementAsync(ipSignupAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); - if (ipSignupAttempts > 10) { - _logger.LogError("Signup denied for {EmailAddress} for the {IPSignupAttempts} time.", email, ipSignupAttempts); - return BadRequest(); - } - } + if (_authOptions.EnableActiveDirectoryAuth && !IsValidActiveDirectoryLogin(email, model.Password)) { + _logger.LogError("Signup failed for {EmailAddress}: Active Directory authentication failed.", email); + return BadRequest(); + } - if (_authOptions.EnableActiveDirectoryAuth && !IsValidActiveDirectoryLogin(email, model.Password)) { - _logger.LogError("Signup failed for {EmailAddress}: Active Directory authentication failed.", email); - return BadRequest(); - } + user = new User { + IsActive = true, + FullName = model.Name.Trim(), + EmailAddress = email, + IsEmailAddressVerified = _authOptions.EnableActiveDirectoryAuth + }; + user.CreateVerifyEmailAddressToken(); + user.Roles.Add(AuthorizationRoles.Client); + user.Roles.Add(AuthorizationRoles.User); + await AddGlobalAdminRoleIfFirstUserAsync(user); + + if (!_authOptions.EnableActiveDirectoryAuth) { + user.Salt = Core.Extensions.StringExtensions.GetRandomString(16); + user.Password = model.Password.ToSaltedHash(user.Salt); + } - user = new User { - IsActive = true, - FullName = model.Name.Trim(), - EmailAddress = email, - IsEmailAddressVerified = _authOptions.EnableActiveDirectoryAuth - }; - user.CreateVerifyEmailAddressToken(); - user.Roles.Add(AuthorizationRoles.Client); - user.Roles.Add(AuthorizationRoles.User); - await AddGlobalAdminRoleIfFirstUserAsync(user); - - if (!_authOptions.EnableActiveDirectoryAuth) { - user.Salt = Core.Extensions.StringExtensions.GetRandomString(16); - user.Password = model.Password.ToSaltedHash(user.Salt); - } + try { + user = await _userRepository.AddAsync(user, o => o.Cache()); + } + catch (ValidationException ex) { + string errors = String.Join(", ", ex.Errors); + _logger.LogCritical(ex, "Signup failed for {EmailAddress}: {Message}", email, errors); + return BadRequest(errors); + } + catch (Exception ex) { + _logger.LogCritical(ex, "Signup failed for {EmailAddress}: {Message}", email, ex.Message); + return BadRequest("An error occurred."); + } - try { - user = await _userRepository.AddAsync(user, o => o.Cache()); - } catch (ValidationException ex) { - string errors = String.Join(", ", ex.Errors); - _logger.LogCritical(ex, "Signup failed for {EmailAddress}: {Message}", email, errors); - return BadRequest(errors); - } catch (Exception ex) { - _logger.LogCritical(ex, "Signup failed for {EmailAddress}: {Message}", email, ex.Message); - return BadRequest("An error occurred."); - } + if (hasValidInviteToken) + await AddInvitedUserToOrganizationAsync(model.InviteToken, user); - if (hasValidInviteToken) - await AddInvitedUserToOrganizationAsync(model.InviteToken, user); + if (!user.IsEmailAddressVerified) + await _mailer.SendUserEmailVerifyAsync(user); - if (!user.IsEmailAddressVerified) - await _mailer.SendUserEmailVerifyAsync(user); + _logger.UserSignedUp(user.EmailAddress); + return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(user) }); + } + } - _logger.UserSignedUp(user.EmailAddress); - return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(user) }); + [ApiExplorerSettings(IgnoreApi = true)] + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("github")] + public Task> GitHubAsync(JObject value) { + return ExternalLoginAsync(value.ToObject(), + _authOptions.GitHubId, + _authOptions.GitHubSecret, + (f, c) => { + c.Scope = "user:email"; + return new GitHubClient(f, c); } - } + ); + } - [ApiExplorerSettings(IgnoreApi = true)] - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("github")] - public Task> GitHubAsync(JObject value) { - return ExternalLoginAsync(value.ToObject(), - _authOptions.GitHubId, - _authOptions.GitHubSecret, - (f, c) => { - c.Scope = "user:email"; - return new GitHubClient(f, c); - } - ); - } + [ApiExplorerSettings(IgnoreApi = true)] + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("google")] + public Task> GoogleAsync(JObject value) { + return ExternalLoginAsync(value.ToObject(), + _authOptions.GoogleId, + _authOptions.GoogleSecret, + (f, c) => { + c.Scope = "profile email"; + return new GoogleClient(f, c); + } + ); + } - [ApiExplorerSettings(IgnoreApi = true)] - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("google")] - public Task> GoogleAsync(JObject value) { - return ExternalLoginAsync(value.ToObject(), - _authOptions.GoogleId, - _authOptions.GoogleSecret, - (f, c) => { - c.Scope = "profile email"; - return new GoogleClient(f, c); - } - ); - } + [ApiExplorerSettings(IgnoreApi = true)] + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("facebook")] + public Task> FacebookAsync(JObject value) { + return ExternalLoginAsync(value.ToObject(), + _authOptions.FacebookId, + _authOptions.FacebookSecret, + (f, c) => { + c.Scope = "email"; + return new FacebookClient(f, c); + } + ); + } - [ApiExplorerSettings(IgnoreApi = true)] - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("facebook")] - public Task> FacebookAsync(JObject value) { - return ExternalLoginAsync(value.ToObject(), - _authOptions.FacebookId, - _authOptions.FacebookSecret, - (f, c) => { - c.Scope = "email"; - return new FacebookClient(f, c); - } - ); - } + [ApiExplorerSettings(IgnoreApi = true)] + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("live")] + public Task> LiveAsync(JObject value) { + return ExternalLoginAsync(value.ToObject(), + _authOptions.MicrosoftId, + _authOptions.MicrosoftSecret, + (f, c) => { + c.Scope = "wl.emails"; + return new WindowsLiveClient(f, c); + } + ); + } - [ApiExplorerSettings(IgnoreApi = true)] - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("live")] - public Task> LiveAsync(JObject value) { - return ExternalLoginAsync(value.ToObject(), - _authOptions.MicrosoftId, - _authOptions.MicrosoftSecret, - (f, c) => { - c.Scope = "wl.emails"; - return new WindowsLiveClient(f, c); - } - ); - } + /// + /// Removes an external login provider from the account + /// + /// The provider name. + /// The provider user id. + /// Invalid provider name. + /// An error while saving the user account. + [ApiExplorerSettings(IgnoreApi = true)] + [Consumes("application/json")] + [HttpPost("unlink/{providerName:minlength(1)}")] + public async Task> RemoveExternalLoginAsync(string providerName, ValueFromBody providerUserId) { + using (_logger.BeginScope(new ExceptionlessState().Tag("External Login").Tag(providerName).Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).Property("Provider User Id", providerUserId?.Value).SetHttpContext(HttpContext))) { + if (String.IsNullOrWhiteSpace(providerName) || String.IsNullOrWhiteSpace(providerUserId?.Value)) { + _logger.LogError("Remove external login failed for {EmailAddress}: Invalid Provider Name or Provider User Id.", CurrentUser.EmailAddress); + return BadRequest("Invalid Provider Name or Provider User Id."); + } - /// - /// Removes an external login provider from the account - /// - /// The provider name. - /// The provider user id. - /// Invalid provider name. - /// An error while saving the user account. - [ApiExplorerSettings(IgnoreApi = true)] - [Consumes("application/json")] - [HttpPost("unlink/{providerName:minlength(1)}")] - public async Task> RemoveExternalLoginAsync(string providerName, ValueFromBody providerUserId) { - using (_logger.BeginScope(new ExceptionlessState().Tag("External Login").Tag(providerName).Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).Property("Provider User Id", providerUserId?.Value).SetHttpContext(HttpContext))) { - if (String.IsNullOrWhiteSpace(providerName) || String.IsNullOrWhiteSpace(providerUserId?.Value)) { - _logger.LogError("Remove external login failed for {EmailAddress}: Invalid Provider Name or Provider User Id.", CurrentUser.EmailAddress); - return BadRequest("Invalid Provider Name or Provider User Id."); - } + if (CurrentUser.OAuthAccounts.Count <= 1 && String.IsNullOrEmpty(CurrentUser.Password)) { + _logger.LogError("Remove external login failed for {EmailAddress}: You must set a local password before removing your external login.", CurrentUser.EmailAddress); + return BadRequest("You must set a local password before removing your external login."); + } - if (CurrentUser.OAuthAccounts.Count <= 1 && String.IsNullOrEmpty(CurrentUser.Password)) { - _logger.LogError("Remove external login failed for {EmailAddress}: You must set a local password before removing your external login.", CurrentUser.EmailAddress); - return BadRequest("You must set a local password before removing your external login."); - } + try { + if (CurrentUser.RemoveOAuthAccount(providerName, providerUserId.Value)) + await _userRepository.SaveAsync(CurrentUser, o => o.Cache()); + } + catch (Exception ex) { + _logger.LogCritical(ex, "Error removing external login for {EmailAddress}: {Message}", CurrentUser.EmailAddress, ex.Message); + throw; + } - try { - if (CurrentUser.RemoveOAuthAccount(providerName, providerUserId.Value)) - await _userRepository.SaveAsync(CurrentUser, o => o.Cache()); - } catch (Exception ex) { - _logger.LogCritical(ex, "Error removing external login for {EmailAddress}: {Message}", CurrentUser.EmailAddress, ex.Message); - throw; - } + await ResetUserTokensAsync(CurrentUser, nameof(RemoveExternalLoginAsync)); - await ResetUserTokensAsync(CurrentUser, nameof(RemoveExternalLoginAsync)); + _logger.UserRemovedExternalLogin(CurrentUser.EmailAddress, providerName); + return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(CurrentUser) }); + } + } - _logger.UserRemovedExternalLogin(CurrentUser.EmailAddress, providerName); - return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(CurrentUser) }); + /// + /// Change password + /// + /// The change password model. + /// Invalid change password model. + [Consumes("application/json")] + [HttpPost("change-password")] + public async Task> ChangePasswordAsync(ChangePasswordModel model) { + using (_logger.BeginScope(new ExceptionlessState().Tag("Change Password").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).Property("Password Length", model?.Password?.Length ?? 0).SetHttpContext(HttpContext))) { + if (model == null || !PasswordMeetsRequirements(model.Password)) { + _logger.LogError("Change password failed for {EmailAddress}: The New Password must be at least 6 characters long.", CurrentUser.EmailAddress); + return BadRequest("The New Password must be at least 6 characters long."); } - } - /// - /// Change password - /// - /// The change password model. - /// Invalid change password model. - [Consumes("application/json")] - [HttpPost("change-password")] - public async Task> ChangePasswordAsync(ChangePasswordModel model) { - using (_logger.BeginScope(new ExceptionlessState().Tag("Change Password").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).Property("Password Length", model?.Password?.Length ?? 0).SetHttpContext(HttpContext))) { - if (model == null || !PasswordMeetsRequirements(model.Password)) { - _logger.LogError("Change password failed for {EmailAddress}: The New Password must be at least 6 characters long.", CurrentUser.EmailAddress); - return BadRequest("The New Password must be at least 6 characters long."); + // User has a local account.. + if (!String.IsNullOrWhiteSpace(CurrentUser.Password)) { + if (String.IsNullOrWhiteSpace(model.CurrentPassword)) { + _logger.LogError("Change password failed for {EmailAddress}: The current password is incorrect.", CurrentUser.EmailAddress); + return BadRequest("The current password is incorrect."); } - // User has a local account.. - if (!String.IsNullOrWhiteSpace(CurrentUser.Password)) { - if (String.IsNullOrWhiteSpace(model.CurrentPassword)) { - _logger.LogError("Change password failed for {EmailAddress}: The current password is incorrect.", CurrentUser.EmailAddress); - return BadRequest("The current password is incorrect."); - } - - string encodedPassword = model.CurrentPassword.ToSaltedHash(CurrentUser.Salt); - if (!String.Equals(encodedPassword, CurrentUser.Password)) { - _logger.LogError("Change password failed for {EmailAddress}: The current password is incorrect.", CurrentUser.EmailAddress); - return BadRequest("The current password is incorrect."); - } + string encodedPassword = model.CurrentPassword.ToSaltedHash(CurrentUser.Salt); + if (!String.Equals(encodedPassword, CurrentUser.Password)) { + _logger.LogError("Change password failed for {EmailAddress}: The current password is incorrect.", CurrentUser.EmailAddress); + return BadRequest("The current password is incorrect."); + } - string newPasswordHash = model.Password.ToSaltedHash(CurrentUser.Salt); - if (String.Equals(newPasswordHash, CurrentUser.Password)) { - _logger.LogError("Change password failed for {EmailAddress}: The new password is the same as the current password.", CurrentUser.EmailAddress); - return BadRequest("The new password must be different than the previous password."); - } + string newPasswordHash = model.Password.ToSaltedHash(CurrentUser.Salt); + if (String.Equals(newPasswordHash, CurrentUser.Password)) { + _logger.LogError("Change password failed for {EmailAddress}: The new password is the same as the current password.", CurrentUser.EmailAddress); + return BadRequest("The new password must be different than the previous password."); } + } - await ChangePasswordAsync(CurrentUser, model.Password, nameof(ChangePasswordAsync)); - await ResetUserTokensAsync(CurrentUser, nameof(ChangePasswordAsync)); + await ChangePasswordAsync(CurrentUser, model.Password, nameof(ChangePasswordAsync)); + await ResetUserTokensAsync(CurrentUser, nameof(ChangePasswordAsync)); - string userLoginAttemptsCacheKey = $"user:{CurrentUser.EmailAddress}:attempts"; - await _cache.RemoveAsync(userLoginAttemptsCacheKey); + string userLoginAttemptsCacheKey = $"user:{CurrentUser.EmailAddress}:attempts"; + await _cache.RemoveAsync(userLoginAttemptsCacheKey); - string ipLoginAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:attempts"; - long attempts = await _cache.DecrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); - if (attempts <= 0) - await _cache.RemoveAsync(ipLoginAttemptsCacheKey); + string ipLoginAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:attempts"; + long attempts = await _cache.DecrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); + if (attempts <= 0) + await _cache.RemoveAsync(ipLoginAttemptsCacheKey); - _logger.UserChangedPassword(CurrentUser.EmailAddress); - return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(CurrentUser) }); - } + _logger.UserChangedPassword(CurrentUser.EmailAddress); + return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(CurrentUser) }); } + } - [ApiExplorerSettings(IgnoreApi = true)] - [AllowAnonymous] - [HttpGet("check-email-address/{email:minlength(1)}")] - public async Task IsEmailAddressAvailableAsync(string email) { - if (String.IsNullOrWhiteSpace(email)) - return StatusCode(StatusCodes.Status204NoContent); - - email = email.Trim().ToLowerInvariant(); - if (CurrentUser != null && String.Equals(CurrentUser.EmailAddress, email, StringComparison.InvariantCultureIgnoreCase)) - return StatusCode(StatusCodes.Status201Created); - - // Only allow 3 checks attempts per hour period by a single ip. - string ipEmailAddressAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:email:attempts"; - long attempts = await _cache.IncrementAsync(ipEmailAddressAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); - - if (attempts > 3 || await _userRepository.GetByEmailAddressAsync(email) == null) - return StatusCode(StatusCodes.Status204NoContent); + [ApiExplorerSettings(IgnoreApi = true)] + [AllowAnonymous] + [HttpGet("check-email-address/{email:minlength(1)}")] + public async Task IsEmailAddressAvailableAsync(string email) { + if (String.IsNullOrWhiteSpace(email)) + return StatusCode(StatusCodes.Status204NoContent); + email = email.Trim().ToLowerInvariant(); + if (CurrentUser != null && String.Equals(CurrentUser.EmailAddress, email, StringComparison.InvariantCultureIgnoreCase)) return StatusCode(StatusCodes.Status201Created); - } - /// - /// Forgot password - /// - /// The email address. - /// Invalid email address. - [AllowAnonymous] - [HttpGet("forgot-password/{email:minlength(1)}")] - public async Task ForgotPasswordAsync(string email) { - using (_logger.BeginScope(new ExceptionlessState().Tag("Forgot Password").Identity(email).SetHttpContext(HttpContext))) { - if (String.IsNullOrWhiteSpace(email)) { - _logger.LogError("Forgot password failed: Please specify a valid Email Address."); - return BadRequest("Please specify a valid Email Address."); - } + // Only allow 3 checks attempts per hour period by a single ip. + string ipEmailAddressAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:email:attempts"; + long attempts = await _cache.IncrementAsync(ipEmailAddressAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); - // Only allow 3 checks attempts per hour period by a single ip. - string ipResetPasswordAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:password:attempts"; - long attempts = await _cache.IncrementAsync(ipResetPasswordAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); - if (attempts > 3) { - _logger.LogError("Login denied for {EmailAddress} for the {ResetPasswordAttempts} time.", email, attempts); - return Ok(); - } + if (attempts > 3 || await _userRepository.GetByEmailAddressAsync(email) == null) + return StatusCode(StatusCodes.Status204NoContent); - email = email.Trim().ToLowerInvariant(); - var user = await _userRepository.GetByEmailAddressAsync(email); - if (user == null) { - _logger.LogError("Forgot password failed for {EmailAddress}: No user was found.", email); - return Ok(); - } + return StatusCode(StatusCodes.Status201Created); + } - user.CreatePasswordResetToken(); - await _userRepository.SaveAsync(user, o => o.Cache()); + /// + /// Forgot password + /// + /// The email address. + /// Invalid email address. + [AllowAnonymous] + [HttpGet("forgot-password/{email:minlength(1)}")] + public async Task ForgotPasswordAsync(string email) { + using (_logger.BeginScope(new ExceptionlessState().Tag("Forgot Password").Identity(email).SetHttpContext(HttpContext))) { + if (String.IsNullOrWhiteSpace(email)) { + _logger.LogError("Forgot password failed: Please specify a valid Email Address."); + return BadRequest("Please specify a valid Email Address."); + } + + // Only allow 3 checks attempts per hour period by a single ip. + string ipResetPasswordAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:password:attempts"; + long attempts = await _cache.IncrementAsync(ipResetPasswordAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); + if (attempts > 3) { + _logger.LogError("Login denied for {EmailAddress} for the {ResetPasswordAttempts} time.", email, attempts); + return Ok(); + } - await _mailer.SendUserPasswordResetAsync(user); - _logger.UserForgotPassword(user.EmailAddress); + email = email.Trim().ToLowerInvariant(); + var user = await _userRepository.GetByEmailAddressAsync(email); + if (user == null) { + _logger.LogError("Forgot password failed for {EmailAddress}: No user was found.", email); return Ok(); } + + user.CreatePasswordResetToken(); + await _userRepository.SaveAsync(user, o => o.Cache()); + + await _mailer.SendUserPasswordResetAsync(user); + _logger.UserForgotPassword(user.EmailAddress); + return Ok(); + } + } + + /// + /// Reset password + /// + /// The reset password model. + /// Invalid reset password model. + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("reset-password")] + public async Task ResetPasswordAsync(ResetPasswordModel model) { + if (String.IsNullOrEmpty(model?.PasswordResetToken)) { + using (_logger.BeginScope(new ExceptionlessState().Tag("Reset Password").SetHttpContext(HttpContext))) + _logger.LogError("Reset password failed: Invalid Password Reset Token."); + return BadRequest("Invalid Password Reset Token."); } - /// - /// Reset password - /// - /// The reset password model. - /// Invalid reset password model. - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("reset-password")] - public async Task ResetPasswordAsync(ResetPasswordModel model) { - if (String.IsNullOrEmpty(model?.PasswordResetToken)) { - using (_logger.BeginScope(new ExceptionlessState().Tag("Reset Password").SetHttpContext(HttpContext))) - _logger.LogError("Reset password failed: Invalid Password Reset Token."); + var user = await _userRepository.GetByPasswordResetTokenAsync(model.PasswordResetToken); + using (_logger.BeginScope(new ExceptionlessState().Tag("Reset Password").Identity(user?.EmailAddress).Property("User", user).Property("Password Length", model.Password?.Length ?? 0).SetHttpContext(HttpContext))) { + if (user == null) { + _logger.LogError("Reset password failed: Invalid Password Reset Token."); return BadRequest("Invalid Password Reset Token."); } - var user = await _userRepository.GetByPasswordResetTokenAsync(model.PasswordResetToken); - using (_logger.BeginScope(new ExceptionlessState().Tag("Reset Password").Identity(user?.EmailAddress).Property("User", user).Property("Password Length", model.Password?.Length ?? 0).SetHttpContext(HttpContext))) { - if (user == null) { - _logger.LogError("Reset password failed: Invalid Password Reset Token."); - return BadRequest("Invalid Password Reset Token."); - } + if (!user.HasValidPasswordResetTokenExpiration()) { + _logger.LogError("Reset password failed for {EmailAddress}: Password Reset Token has expired.", user.EmailAddress); + return BadRequest("Password Reset Token has expired."); + } - if (!user.HasValidPasswordResetTokenExpiration()) { - _logger.LogError("Reset password failed for {EmailAddress}: Password Reset Token has expired.", user.EmailAddress); - return BadRequest("Password Reset Token has expired."); - } + if (!PasswordMeetsRequirements(model.Password)) { + _logger.LogError("Reset password failed for {EmailAddress}: The New Password must be at least 6 characters long.", user.EmailAddress); + return BadRequest("The New Password must be at least 6 characters long."); + } - if (!PasswordMeetsRequirements(model.Password)) { - _logger.LogError("Reset password failed for {EmailAddress}: The New Password must be at least 6 characters long.", user.EmailAddress); - return BadRequest("The New Password must be at least 6 characters long."); + // User has a local account.. + if (!String.IsNullOrWhiteSpace(user.Password)) { + string newPasswordHash = model.Password.ToSaltedHash(user.Salt); + if (String.Equals(newPasswordHash, user.Password)) { + _logger.LogError("Reset password failed for {EmailAddress}: The new password is the same as the current password.", user.EmailAddress); + return BadRequest("The new password must be different than the previous password."); } + } - // User has a local account.. - if (!String.IsNullOrWhiteSpace(user.Password)) { - string newPasswordHash = model.Password.ToSaltedHash(user.Salt); - if (String.Equals(newPasswordHash, user.Password)) { - _logger.LogError("Reset password failed for {EmailAddress}: The new password is the same as the current password.", user.EmailAddress); - return BadRequest("The new password must be different than the previous password."); - } - } + user.MarkEmailAddressVerified(); + await ChangePasswordAsync(user, model.Password, nameof(ResetPasswordAsync)); + await ResetUserTokensAsync(user, nameof(ResetPasswordAsync)); - user.MarkEmailAddressVerified(); - await ChangePasswordAsync(user, model.Password, nameof(ResetPasswordAsync)); - await ResetUserTokensAsync(user, nameof(ResetPasswordAsync)); + string userLoginAttemptsCacheKey = $"user:{user.EmailAddress}:attempts"; + await _cache.RemoveAsync(userLoginAttemptsCacheKey); - string userLoginAttemptsCacheKey = $"user:{user.EmailAddress}:attempts"; - await _cache.RemoveAsync(userLoginAttemptsCacheKey); + string ipLoginAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:attempts"; + long attempts = await _cache.DecrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); + if (attempts <= 0) + await _cache.RemoveAsync(ipLoginAttemptsCacheKey); - string ipLoginAttemptsCacheKey = $"ip:{Request.GetClientIpAddress()}:attempts"; - long attempts = await _cache.DecrementAsync(ipLoginAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromMinutes(15))); - if (attempts <= 0) - await _cache.RemoveAsync(ipLoginAttemptsCacheKey); + _logger.UserResetPassword(user.EmailAddress); + return Ok(); + } + } - _logger.UserResetPassword(user.EmailAddress); - return Ok(); - } + /// + /// Cancel reset password + /// + /// The password reset token. + /// Invalid password reset token. + [AllowAnonymous] + [Consumes("application/json")] + [HttpPost("cancel-reset-password/{token:minlength(1)}")] + public async Task CancelResetPasswordAsync(string token) { + if (String.IsNullOrEmpty(token)) { + using (_logger.BeginScope(new ExceptionlessState().Tag("Cancel Reset Password").SetHttpContext(HttpContext))) + _logger.LogError("Cancel reset password failed: Invalid Password Reset Token."); + return BadRequest("Invalid password reset token."); } - /// - /// Cancel reset password - /// - /// The password reset token. - /// Invalid password reset token. - [AllowAnonymous] - [Consumes("application/json")] - [HttpPost("cancel-reset-password/{token:minlength(1)}")] - public async Task CancelResetPasswordAsync(string token) { - if (String.IsNullOrEmpty(token)) { - using (_logger.BeginScope(new ExceptionlessState().Tag("Cancel Reset Password").SetHttpContext(HttpContext))) - _logger.LogError("Cancel reset password failed: Invalid Password Reset Token."); - return BadRequest("Invalid password reset token."); - } - - var user = await _userRepository.GetByPasswordResetTokenAsync(token); - if (user == null) - return Ok(); + var user = await _userRepository.GetByPasswordResetTokenAsync(token); + if (user == null) + return Ok(); - user.ResetPasswordResetToken(); - await _userRepository.SaveAsync(user, o => o.Cache()); + user.ResetPasswordResetToken(); + await _userRepository.SaveAsync(user, o => o.Cache()); - using (_logger.BeginScope(new ExceptionlessState().Tag("Cancel Reset Password").Identity(user.EmailAddress).Property("User", user).SetHttpContext(HttpContext))) - _logger.UserCanceledResetPassword(user.EmailAddress); + using (_logger.BeginScope(new ExceptionlessState().Tag("Cancel Reset Password").Identity(user.EmailAddress).Property("User", user).SetHttpContext(HttpContext))) + _logger.UserCanceledResetPassword(user.EmailAddress); - return Ok(); - } + return Ok(); + } - private async Task AddGlobalAdminRoleIfFirstUserAsync(User user) { - if (_isFirstUserChecked) - return; + private async Task AddGlobalAdminRoleIfFirstUserAsync(User user) { + if (_isFirstUserChecked) + return; - bool isFirstUser = await _userRepository.CountAsync() == 0; - if (isFirstUser) - user.Roles.Add(AuthorizationRoles.GlobalAdmin); + bool isFirstUser = await _userRepository.CountAsync() == 0; + if (isFirstUser) + user.Roles.Add(AuthorizationRoles.GlobalAdmin); - _isFirstUserChecked = true; - } + _isFirstUserChecked = true; + } - private async Task> ExternalLoginAsync(ExternalAuthInfo authInfo, string appId, string appSecret, Func createClient) where TClient : OAuth2Client { - using (_logger.BeginScope(new ExceptionlessState().Tag("External Login").Property("Auth Info", authInfo).SetHttpContext(HttpContext))) { - if (String.IsNullOrEmpty(authInfo?.Code)) { - _logger.LogError("External login failed: Unable to get auth info."); - return NotFound(); - } + private async Task> ExternalLoginAsync(ExternalAuthInfo authInfo, string appId, string appSecret, Func createClient) where TClient : OAuth2Client { + using (_logger.BeginScope(new ExceptionlessState().Tag("External Login").Property("Auth Info", authInfo).SetHttpContext(HttpContext))) { + if (String.IsNullOrEmpty(authInfo?.Code)) { + _logger.LogError("External login failed: Unable to get auth info."); + return NotFound(); + } - if (String.IsNullOrEmpty(appId) || String.IsNullOrEmpty(appSecret)) - return NotFound(); - - var client = createClient(new RequestFactory(), new OAuth2.Configuration.ClientConfiguration { - ClientId = appId, - ClientSecret = appSecret, - RedirectUri = authInfo.RedirectUri - }); - - UserInfo userInfo; - try { - userInfo = await client.GetUserInfoAsync(authInfo.Code, authInfo.RedirectUri); - } catch (Exception ex) { - _logger.LogCritical(ex, "External login failed: {Message}", ex.Message); - return BadRequest("Unable to get user info."); - } + if (String.IsNullOrEmpty(appId) || String.IsNullOrEmpty(appSecret)) + return NotFound(); - User user; - try { - user = await FromExternalLoginAsync(userInfo); - } catch (ApplicationException ex) { - _logger.LogCritical(ex, "External login failed for {EmailAddress}: {Message}", userInfo.Email, ex.Message); - return BadRequest("Account Creation is currently disabled."); - } catch (Exception ex) { - _logger.LogCritical(ex, "External login failed for {EmailAddress}: {Message}", userInfo.Email, ex.Message); - return BadRequest("An error occurred while processing user info."); - } + var client = createClient(new RequestFactory(), new OAuth2.Configuration.ClientConfiguration { + ClientId = appId, + ClientSecret = appSecret, + RedirectUri = authInfo.RedirectUri + }); - if (user == null) { - _logger.LogCritical("External login failed for {EmailAddress}: Unable to process user info.", userInfo.Email); - return BadRequest("Unable to process user info."); - } + UserInfo userInfo; + try { + userInfo = await client.GetUserInfoAsync(authInfo.Code, authInfo.RedirectUri); + } + catch (Exception ex) { + _logger.LogCritical(ex, "External login failed: {Message}", ex.Message); + return BadRequest("Unable to get user info."); + } - if (!String.IsNullOrWhiteSpace(authInfo.InviteToken)) - await AddInvitedUserToOrganizationAsync(authInfo.InviteToken, user); + User user; + try { + user = await FromExternalLoginAsync(userInfo); + } + catch (ApplicationException ex) { + _logger.LogCritical(ex, "External login failed for {EmailAddress}: {Message}", userInfo.Email, ex.Message); + return BadRequest("Account Creation is currently disabled."); + } + catch (Exception ex) { + _logger.LogCritical(ex, "External login failed for {EmailAddress}: {Message}", userInfo.Email, ex.Message); + return BadRequest("An error occurred while processing user info."); + } - _logger.UserLoggedIn(user.EmailAddress); - return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(user) }); + if (user == null) { + _logger.LogCritical("External login failed for {EmailAddress}: Unable to process user info.", userInfo.Email); + return BadRequest("Unable to process user info."); } - } - private async Task FromExternalLoginAsync(UserInfo userInfo) { - var existingUser = await _userRepository.GetUserByOAuthProviderAsync(userInfo.ProviderName, userInfo.Id); - - // Link user accounts. - if (CurrentUser != null) { - if (existingUser != null) { - if (existingUser.Id != CurrentUser.Id) { - // Existing user account is not the current user. Remove it and we'll add it to the current user below. - if (!existingUser.RemoveOAuthAccount(userInfo.ProviderName, userInfo.Id)) { - using (_logger.BeginScope(new ExceptionlessState().Tag("External Login").Identity(CurrentUser.EmailAddress).Property("User Info", userInfo).Property("User", CurrentUser).Property("ExistingUser", existingUser).SetHttpContext(HttpContext))) - _logger.LogError("Unable to remove existing oauth account for existing user {EmailAddress}", existingUser.EmailAddress); - - return null; - } - - await _userRepository.SaveAsync(existingUser, o => o.Cache()); - } else { - // User is already logged in. - return CurrentUser; - } - } + if (!String.IsNullOrWhiteSpace(authInfo.InviteToken)) + await AddInvitedUserToOrganizationAsync(authInfo.InviteToken, user); - // Add it to the current user if it doesn't already exist and save it. - CurrentUser.AddOAuthAccount(userInfo.ProviderName, userInfo.Id, userInfo.Email); - await _userRepository.SaveAsync(CurrentUser, o => o.Cache()); - return CurrentUser; - } + _logger.UserLoggedIn(user.EmailAddress); + return Ok(new TokenResult { Token = await GetOrCreateAccessTokenAsync(user) }); + } + } - // Create a new user account or return an existing one. + private async Task FromExternalLoginAsync(UserInfo userInfo) { + var existingUser = await _userRepository.GetUserByOAuthProviderAsync(userInfo.ProviderName, userInfo.Id); + + // Link user accounts. + if (CurrentUser != null) { if (existingUser != null) { - if (!existingUser.IsEmailAddressVerified) { - existingUser.MarkEmailAddressVerified(); + if (existingUser.Id != CurrentUser.Id) { + // Existing user account is not the current user. Remove it and we'll add it to the current user below. + if (!existingUser.RemoveOAuthAccount(userInfo.ProviderName, userInfo.Id)) { + using (_logger.BeginScope(new ExceptionlessState().Tag("External Login").Identity(CurrentUser.EmailAddress).Property("User Info", userInfo).Property("User", CurrentUser).Property("ExistingUser", existingUser).SetHttpContext(HttpContext))) + _logger.LogError("Unable to remove existing oauth account for existing user {EmailAddress}", existingUser.EmailAddress); + + return null; + } + await _userRepository.SaveAsync(existingUser, o => o.Cache()); } - - return existingUser; + else { + // User is already logged in. + return CurrentUser; + } } - // Check to see if a user already exists with this email address. - var user = !String.IsNullOrEmpty(userInfo.Email) ? await _userRepository.GetByEmailAddressAsync(userInfo.Email) : null; - if (user == null) { - if (!_authOptions.EnableAccountCreation) - throw new ApplicationException("Account Creation is currently disabled."); + // Add it to the current user if it doesn't already exist and save it. + CurrentUser.AddOAuthAccount(userInfo.ProviderName, userInfo.Id, userInfo.Email); + await _userRepository.SaveAsync(CurrentUser, o => o.Cache()); + return CurrentUser; + } - user = new User { FullName = userInfo.GetFullName(), EmailAddress = userInfo.Email }; - user.Roles.Add(AuthorizationRoles.Client); - user.Roles.Add(AuthorizationRoles.User); - await AddGlobalAdminRoleIfFirstUserAsync(user); + // Create a new user account or return an existing one. + if (existingUser != null) { + if (!existingUser.IsEmailAddressVerified) { + existingUser.MarkEmailAddressVerified(); + await _userRepository.SaveAsync(existingUser, o => o.Cache()); } - user.MarkEmailAddressVerified(); - user.AddOAuthAccount(userInfo.ProviderName, userInfo.Id, userInfo.Email); + return existingUser; + } - if (String.IsNullOrEmpty(user.Id)) - await _userRepository.AddAsync(user, o => o.Cache()); - else - await _userRepository.SaveAsync(user, o => o.Cache()); + // Check to see if a user already exists with this email address. + var user = !String.IsNullOrEmpty(userInfo.Email) ? await _userRepository.GetByEmailAddressAsync(userInfo.Email) : null; + if (user == null) { + if (!_authOptions.EnableAccountCreation) + throw new ApplicationException("Account Creation is currently disabled."); - return user; + user = new User { FullName = userInfo.GetFullName(), EmailAddress = userInfo.Email }; + user.Roles.Add(AuthorizationRoles.Client); + user.Roles.Add(AuthorizationRoles.User); + await AddGlobalAdminRoleIfFirstUserAsync(user); } - private async Task IsAccountCreationEnabledAsync(string token) { - if (_authOptions.EnableAccountCreation) - return true; + user.MarkEmailAddressVerified(); + user.AddOAuthAccount(userInfo.ProviderName, userInfo.Id, userInfo.Email); - if (String.IsNullOrEmpty(token)) - return false; + if (String.IsNullOrEmpty(user.Id)) + await _userRepository.AddAsync(user, o => o.Cache()); + else + await _userRepository.SaveAsync(user, o => o.Cache()); - var organization = await _organizationRepository.GetByInviteTokenAsync(token); - return organization != null; - } + return user; + } - private async Task AddInvitedUserToOrganizationAsync(string token, User user) { - if (String.IsNullOrWhiteSpace(token) || user == null) - return; + private async Task IsAccountCreationEnabledAsync(string token) { + if (_authOptions.EnableAccountCreation) + return true; - using (_logger.BeginScope(new ExceptionlessState().Tag("Invite").Identity(user.EmailAddress).Property("User", user).SetHttpContext(HttpContext))) { - var organization = await _organizationRepository.GetByInviteTokenAsync(token); - var invite = organization?.GetInvite(token); - if (organization == null || invite == null) { - _logger.UnableToAddInvitedUserInvalidToken(user.EmailAddress, token); - return; - } + if (String.IsNullOrEmpty(token)) + return false; - if (!user.IsEmailAddressVerified && String.Equals(user.EmailAddress, invite.EmailAddress, StringComparison.OrdinalIgnoreCase)) { - _logger.MarkedInvitedUserAsVerified(user.EmailAddress); - user.MarkEmailAddressVerified(); - await _userRepository.SaveAsync(user, o => o.Cache()); - } + var organization = await _organizationRepository.GetByInviteTokenAsync(token); + return organization != null; + } - if (!user.OrganizationIds.Contains(organization.Id)) { - _logger.UserJoinedFromInvite(user.EmailAddress); - user.OrganizationIds.Add(organization.Id); - await _userRepository.SaveAsync(user, o => o.Cache()); - } + private async Task AddInvitedUserToOrganizationAsync(string token, User user) { + if (String.IsNullOrWhiteSpace(token) || user == null) + return; + + using (_logger.BeginScope(new ExceptionlessState().Tag("Invite").Identity(user.EmailAddress).Property("User", user).SetHttpContext(HttpContext))) { + var organization = await _organizationRepository.GetByInviteTokenAsync(token); + var invite = organization?.GetInvite(token); + if (organization == null || invite == null) { + _logger.UnableToAddInvitedUserInvalidToken(user.EmailAddress, token); + return; + } + + if (!user.IsEmailAddressVerified && String.Equals(user.EmailAddress, invite.EmailAddress, StringComparison.OrdinalIgnoreCase)) { + _logger.MarkedInvitedUserAsVerified(user.EmailAddress); + user.MarkEmailAddressVerified(); + await _userRepository.SaveAsync(user, o => o.Cache()); + } - organization.Invites.Remove(invite); - await _organizationRepository.SaveAsync(organization, o => o.Cache()); + if (!user.OrganizationIds.Contains(organization.Id)) { + _logger.UserJoinedFromInvite(user.EmailAddress); + user.OrganizationIds.Add(organization.Id); + await _userRepository.SaveAsync(user, o => o.Cache()); } + + organization.Invites.Remove(invite); + await _organizationRepository.SaveAsync(organization, o => o.Cache()); } + } - private async Task ChangePasswordAsync(User user, string password, string tag) { - using (_logger.BeginScope(new ExceptionlessState().Tag(tag).Identity(user.EmailAddress).SetHttpContext(HttpContext))) { - if (String.IsNullOrEmpty(user.Salt)) - user.Salt = Core.Extensions.StringExtensions.GetNewToken(); + private async Task ChangePasswordAsync(User user, string password, string tag) { + using (_logger.BeginScope(new ExceptionlessState().Tag(tag).Identity(user.EmailAddress).SetHttpContext(HttpContext))) { + if (String.IsNullOrEmpty(user.Salt)) + user.Salt = Core.Extensions.StringExtensions.GetNewToken(); - user.Password = password.ToSaltedHash(user.Salt); - user.ResetPasswordResetToken(); + user.Password = password.ToSaltedHash(user.Salt); + user.ResetPasswordResetToken(); - try { - await _userRepository.SaveAsync(user, o => o.Cache()); - _logger.ChangedUserPassword(user.EmailAddress); - } catch (Exception ex) { - _logger.LogCritical(ex, "Error changing password for {EmailAddress}: {Message}", user.EmailAddress, ex.Message); - throw; - } + try { + await _userRepository.SaveAsync(user, o => o.Cache()); + _logger.ChangedUserPassword(user.EmailAddress); + } + catch (Exception ex) { + _logger.LogCritical(ex, "Error changing password for {EmailAddress}: {Message}", user.EmailAddress, ex.Message); + throw; } } + } - private async Task ResetUserTokensAsync(User user, string tag) { - using (_logger.BeginScope(new ExceptionlessState().Tag(tag).Identity(user.EmailAddress).SetHttpContext(HttpContext))) { - try { - long total = await _tokenRepository.RemoveAllByUserIdAsync(user.Id, o => o.ImmediateConsistency(true)); - _logger.RemovedUserTokens(total, user.EmailAddress); - } catch (Exception ex) { - _logger.LogCritical(ex, "Error removing user tokens for {EmailAddress}: {Message}", user.EmailAddress, ex.Message); - } + private async Task ResetUserTokensAsync(User user, string tag) { + using (_logger.BeginScope(new ExceptionlessState().Tag(tag).Identity(user.EmailAddress).SetHttpContext(HttpContext))) { + try { + long total = await _tokenRepository.RemoveAllByUserIdAsync(user.Id, o => o.ImmediateConsistency(true)); + _logger.RemovedUserTokens(total, user.EmailAddress); + } + catch (Exception ex) { + _logger.LogCritical(ex, "Error removing user tokens for {EmailAddress}: {Message}", user.EmailAddress, ex.Message); } } + } - private async Task GetOrCreateAccessTokenAsync(User user) { - var userTokens = await _tokenRepository.GetByTypeAndUserIdAsync(TokenType.Access, user.Id); - var validAccessToken = userTokens.Documents.FirstOrDefault(t => (!t.ExpiresUtc.HasValue || t.ExpiresUtc > SystemClock.UtcNow)); - if (validAccessToken != null) - return validAccessToken.Id; - - var token = await _tokenRepository.AddAsync(new Token { - Id = Core.Extensions.StringExtensions.GetNewToken(), - UserId = user.Id, - CreatedUtc = SystemClock.UtcNow, - UpdatedUtc = SystemClock.UtcNow, - ExpiresUtc = SystemClock.UtcNow.AddMonths(3), - CreatedBy = user.Id, - Type = TokenType.Access - }, o => o.ImmediateConsistency(true).Cache()); - - return token.Id; - } + private async Task GetOrCreateAccessTokenAsync(User user) { + var userTokens = await _tokenRepository.GetByTypeAndUserIdAsync(TokenType.Access, user.Id); + var validAccessToken = userTokens.Documents.FirstOrDefault(t => (!t.ExpiresUtc.HasValue || t.ExpiresUtc > SystemClock.UtcNow)); + if (validAccessToken != null) + return validAccessToken.Id; + + var token = await _tokenRepository.AddAsync(new Token { + Id = Core.Extensions.StringExtensions.GetNewToken(), + UserId = user.Id, + CreatedUtc = SystemClock.UtcNow, + UpdatedUtc = SystemClock.UtcNow, + ExpiresUtc = SystemClock.UtcNow.AddMonths(3), + CreatedBy = user.Id, + Type = TokenType.Access + }, o => o.ImmediateConsistency(true).Cache()); + + return token.Id; + } - private bool IsValidActiveDirectoryLogin(string email, string password) { - string domainUsername = _domainLoginProvider.GetUsernameFromEmailAddress(email); - return domainUsername != null && _domainLoginProvider.Login(domainUsername, password); - } + private bool IsValidActiveDirectoryLogin(string email, string password) { + string domainUsername = _domainLoginProvider.GetUsernameFromEmailAddress(email); + return domainUsername != null && _domainLoginProvider.Login(domainUsername, password); + } - private static bool PasswordMeetsRequirements(string password) { - if (String.IsNullOrWhiteSpace(password)) - return false; + private static bool PasswordMeetsRequirements(string password) { + if (String.IsNullOrWhiteSpace(password)) + return false; - password = password.Trim(); - return password.Length >= 6 && password.Length <= 100; - } + password = password.Trim(); + return password.Length >= 6 && password.Length <= 100; } } diff --git a/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs b/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs index 7ab76b9c30..dee0e3e48e 100644 --- a/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs +++ b/src/Exceptionless.Web/Controllers/Base/ExceptionlessApiController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Web.Extensions; +using Exceptionless.Web.Extensions; using Exceptionless.Core.Repositories; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories.Queries; @@ -10,204 +6,205 @@ using Exceptionless.Web.Utility; using Exceptionless.Web.Utility.Results; using Foundatio.Repositories; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Exceptionless.Web.Controllers { - [Produces("application/json")] - [ApiController] - public abstract class ExceptionlessApiController : Controller { - public const string API_PREFIX = "api/v2"; - protected const int DEFAULT_LIMIT = 10; - protected const int MAXIMUM_LIMIT = 100; - protected const int MAXIMUM_SKIP = 1000; - - public ExceptionlessApiController() { - AllowedDateFields = new List(); - } +namespace Exceptionless.Web.Controllers; + +[Produces("application/json")] +[ApiController] +public abstract class ExceptionlessApiController : Controller { + public const string API_PREFIX = "api/v2"; + protected const int DEFAULT_LIMIT = 10; + protected const int MAXIMUM_LIMIT = 100; + protected const int MAXIMUM_SKIP = 1000; + + public ExceptionlessApiController() { + AllowedDateFields = new List(); + } - protected TimeSpan GetOffset(string offset) { - if (!String.IsNullOrEmpty(offset) && TimeUnit.TryParse(offset, out var value) && value.HasValue) - return value.Value; + protected TimeSpan GetOffset(string offset) { + if (!String.IsNullOrEmpty(offset) && TimeUnit.TryParse(offset, out var value) && value.HasValue) + return value.Value; - return TimeSpan.Zero; + return TimeSpan.Zero; + } + + protected ICollection AllowedDateFields { get; private set; } + protected string DefaultDateField { get; set; } = "created_utc"; + + protected virtual TimeInfo GetTimeInfo(string time, string offset, DateTime? minimumUtcStartDate = null) { + string field = DefaultDateField; + if (!String.IsNullOrEmpty(time) && time.Contains("|")) { + string[] parts = time.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + field = parts.Length > 0 && AllowedDateFields.Contains(parts[0]) ? parts[0] : DefaultDateField; + time = parts.Length > 1 ? parts[1] : null; } - protected ICollection AllowedDateFields { get; private set; } - protected string DefaultDateField { get; set; } = "created_utc"; + var utcOffset = GetOffset(offset); - protected virtual TimeInfo GetTimeInfo(string time, string offset, DateTime? minimumUtcStartDate = null) { - string field = DefaultDateField; - if (!String.IsNullOrEmpty(time) && time.Contains("|")) { - string[] parts = time.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); - field = parts.Length > 0 && AllowedDateFields.Contains(parts[0]) ? parts[0] : DefaultDateField; - time = parts.Length > 1 ? parts[1] : null; - } + // range parsing needs to be based on the user's local time. + var range = DateTimeRange.Parse(time, Foundatio.Utility.SystemClock.OffsetUtcNow.ToOffset(utcOffset)); + var timeInfo = new TimeInfo { Field = field, Offset = utcOffset, Range = range }; + if (minimumUtcStartDate.HasValue) + timeInfo.ApplyMinimumUtcStartDate(minimumUtcStartDate.Value); - var utcOffset = GetOffset(offset); + timeInfo.AdjustEndTimeIfMaxValue(); + return timeInfo; + } - // range parsing needs to be based on the user's local time. - var range = DateTimeRange.Parse(time, Foundatio.Utility.SystemClock.OffsetUtcNow.ToOffset(utcOffset)); - var timeInfo = new TimeInfo { Field = field, Offset = utcOffset, Range = range }; - if (minimumUtcStartDate.HasValue) - timeInfo.ApplyMinimumUtcStartDate(minimumUtcStartDate.Value); + protected int GetLimit(int limit, int maximumLimit = MAXIMUM_LIMIT) { + if (maximumLimit < MAXIMUM_LIMIT) + throw new ArgumentOutOfRangeException(nameof(maximumLimit)); - timeInfo.AdjustEndTimeIfMaxValue(); - return timeInfo; - } + if (limit < 1) + limit = DEFAULT_LIMIT; + else if (limit > maximumLimit) + limit = maximumLimit; - protected int GetLimit(int limit, int maximumLimit = MAXIMUM_LIMIT) { - if (maximumLimit < MAXIMUM_LIMIT) - throw new ArgumentOutOfRangeException(nameof(maximumLimit)); - - if (limit < 1) - limit = DEFAULT_LIMIT; - else if (limit > maximumLimit) - limit = maximumLimit; + return limit; + } - return limit; - } + protected int GetPage(int page) { + if (page < 1) + page = 1; - protected int GetPage(int page) { - if (page < 1) - page = 1; + return page; + } - return page; - } + protected int GetSkip(int currentPage, int limit) { + if (currentPage < 1) + currentPage = 1; - protected int GetSkip(int currentPage, int limit) { - if (currentPage < 1) - currentPage = 1; + int skip = (currentPage - 1) * limit; + if (skip < 0) + skip = 0; - int skip = (currentPage - 1) * limit; - if (skip < 0) - skip = 0; + return skip; + } - return skip; - } + protected User CurrentUser => Request.GetUser(); - protected User CurrentUser => Request.GetUser(); + protected bool CanAccessOrganization(string organizationId) { + return Request.CanAccessOrganization(organizationId); + } - protected bool CanAccessOrganization(string organizationId) { - return Request.CanAccessOrganization(organizationId); - } + protected bool IsInOrganization(string organizationId) { + if (String.IsNullOrEmpty(organizationId)) + return false; - protected bool IsInOrganization(string organizationId) { - if (String.IsNullOrEmpty(organizationId)) - return false; + return Request.IsInOrganization(organizationId); + } - return Request.IsInOrganization(organizationId); - } + protected ICollection GetAssociatedOrganizationIds() { + return Request.GetAssociatedOrganizationIds(); + } - protected ICollection GetAssociatedOrganizationIds() { - return Request.GetAssociatedOrganizationIds(); - } + private static readonly IReadOnlyCollection EmptyOrganizations = new List(0).AsReadOnly(); + protected async Task> GetSelectedOrganizationsAsync(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IStackRepository stackRepository, string filter = null) { + var associatedOrganizationIds = GetAssociatedOrganizationIds(); + if (associatedOrganizationIds.Count == 0) + return EmptyOrganizations; - private static readonly IReadOnlyCollection EmptyOrganizations = new List(0).AsReadOnly(); - protected async Task> GetSelectedOrganizationsAsync(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IStackRepository stackRepository, string filter = null) { - var associatedOrganizationIds = GetAssociatedOrganizationIds(); - if (associatedOrganizationIds.Count == 0) - return EmptyOrganizations; - - if (!String.IsNullOrEmpty(filter)) { - var scope = GetFilterScopeVisitor.Run(filter); - if (scope.IsScopable) { - Organization organization = null; - if (scope.OrganizationId != null) { - organization = await organizationRepository.GetByIdAsync(scope.OrganizationId, o => o.Cache()); - } else if (scope.ProjectId != null) { - var project = await projectRepository.GetByIdAsync(scope.ProjectId, o => o.Cache()); - if (project != null) - organization = await organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()); - } else if (scope.StackId != null) { - var stack = await stackRepository.GetByIdAsync(scope.StackId, o => o.Cache()); - if (stack != null) - organization = await organizationRepository.GetByIdAsync(stack.OrganizationId, o => o.Cache()); - } - - if (organization != null) { - if (associatedOrganizationIds.Contains(organization.Id) || Request.IsGlobalAdmin()) - return new[] { organization }.ToList().AsReadOnly(); - - return EmptyOrganizations; - } + if (!String.IsNullOrEmpty(filter)) { + var scope = GetFilterScopeVisitor.Run(filter); + if (scope.IsScopable) { + Organization organization = null; + if (scope.OrganizationId != null) { + organization = await organizationRepository.GetByIdAsync(scope.OrganizationId, o => o.Cache()); + } + else if (scope.ProjectId != null) { + var project = await projectRepository.GetByIdAsync(scope.ProjectId, o => o.Cache()); + if (project != null) + organization = await organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()); + } + else if (scope.StackId != null) { + var stack = await stackRepository.GetByIdAsync(scope.StackId, o => o.Cache()); + if (stack != null) + organization = await organizationRepository.GetByIdAsync(stack.OrganizationId, o => o.Cache()); } - } - var organizations = await organizationRepository.GetByIdsAsync(associatedOrganizationIds.ToArray(), o => o.Cache()); - return organizations.ToList().AsReadOnly(); + if (organization != null) { + if (associatedOrganizationIds.Contains(organization.Id) || Request.IsGlobalAdmin()) + return new[] { organization }.ToList().AsReadOnly(); + + return EmptyOrganizations; + } + } } - protected bool ShouldApplySystemFilter(AppFilter sf, string filter) { - // Apply filter to non admin user. - if (!Request.IsGlobalAdmin()) - return true; + var organizations = await organizationRepository.GetByIdsAsync(associatedOrganizationIds.ToArray(), o => o.Cache()); + return organizations.ToList().AsReadOnly(); + } - // Apply filter as it's scoped via a controller action. - if (!sf.IsUserOrganizationsFilter) - return true; + protected bool ShouldApplySystemFilter(AppFilter sf, string filter) { + // Apply filter to non admin user. + if (!Request.IsGlobalAdmin()) + return true; - // Empty user filter - if (String.IsNullOrEmpty(filter)) - return true; - - // Used for impersonating a user. Only skip the filter if it contains an org, project or stack. - var scope = GetFilterScopeVisitor.Run(filter); - bool hasOrganizationOrProjectOrStackFilter = !String.IsNullOrEmpty(scope.OrganizationId) || !String.IsNullOrEmpty(scope.ProjectId) || !String.IsNullOrEmpty(scope.StackId); - return !hasOrganizationOrProjectOrStackFilter; - } + // Apply filter as it's scoped via a controller action. + if (!sf.IsUserOrganizationsFilter) + return true; - protected ObjectResult Permission(PermissionResult permission) { - return StatusCode(permission.StatusCode, new MessageContent(permission.Id, permission.Message)); - } + // Empty user filter + if (String.IsNullOrEmpty(filter)) + return true; - protected ObjectResult StatusCodeWithMessage(int statusCode, string message, string reason = null) { - return StatusCode(statusCode, new MessageContent(message, reason)); - } + // Used for impersonating a user. Only skip the filter if it contains an org, project or stack. + var scope = GetFilterScopeVisitor.Run(filter); + bool hasOrganizationOrProjectOrStackFilter = !String.IsNullOrEmpty(scope.OrganizationId) || !String.IsNullOrEmpty(scope.ProjectId) || !String.IsNullOrEmpty(scope.StackId); + return !hasOrganizationOrProjectOrStackFilter; + } - protected ActionResult WorkInProgress(IEnumerable workers) { - return StatusCode(StatusCodes.Status202Accepted, new WorkInProgressResult(workers)); - } + protected ObjectResult Permission(PermissionResult permission) { + return StatusCode(permission.StatusCode, new MessageContent(permission.Id, permission.Message)); + } - protected ObjectResult BadRequest(ModelActionResults results) { - return StatusCode(StatusCodes.Status400BadRequest, results); - } + protected ObjectResult StatusCodeWithMessage(int statusCode, string message, string reason = null) { + return StatusCode(statusCode, new MessageContent(message, reason)); + } - protected ObjectResult PlanLimitReached(string message) { - return StatusCode(StatusCodes.Status426UpgradeRequired, new MessageContent(message)); - } + protected ActionResult WorkInProgress(IEnumerable workers) { + return StatusCode(StatusCodes.Status202Accepted, new WorkInProgressResult(workers)); + } - protected ObjectResult NotImplemented(string message) { - return StatusCode(StatusCodes.Status501NotImplemented, new MessageContent(message)); - } + protected ObjectResult BadRequest(ModelActionResults results) { + return StatusCode(StatusCodes.Status400BadRequest, results); + } - protected OkWithHeadersContentResult OkWithLinks(T content, string link) { - return OkWithLinks(content, new[] { link }); - } + protected ObjectResult PlanLimitReached(string message) { + return StatusCode(StatusCodes.Status426UpgradeRequired, new MessageContent(message)); + } - protected OkWithHeadersContentResult OkWithLinks(T content, string[] links) { - var headers = new HeaderDictionary(); - string[] linksToAdd = links.Where(l => l != null).ToArray(); - if (linksToAdd.Length > 0) - headers.Add("Link", linksToAdd); + protected ObjectResult NotImplemented(string message) { + return StatusCode(StatusCodes.Status501NotImplemented, new MessageContent(message)); + } - return new OkWithHeadersContentResult(content, headers); - } + protected OkWithHeadersContentResult OkWithLinks(T content, string link) { + return OkWithLinks(content, new[] { link }); + } - protected OkWithResourceLinks OkWithResourceLinks(IEnumerable content, bool hasMore, Func pagePropertyAccessor = null, IHeaderDictionary headers = null, bool isDescending = false) where TEntity : class { - return new OkWithResourceLinks(content, hasMore, null, pagePropertyAccessor, headers, isDescending); - } + protected OkWithHeadersContentResult OkWithLinks(T content, string[] links) { + var headers = new HeaderDictionary(); + string[] linksToAdd = links.Where(l => l != null).ToArray(); + if (linksToAdd.Length > 0) + headers.Add("Link", linksToAdd); - protected OkWithResourceLinks OkWithResourceLinks(IEnumerable content, bool hasMore, int page, long? total = null, IHeaderDictionary headers = null) where TEntity : class { - return new OkWithResourceLinks(content, hasMore, page, total, headers: headers); - } + return new OkWithHeadersContentResult(content, headers); + } - protected string GetResourceLink(string url, string type) { - return url != null ? $"<{url}>; rel=\"{type}\"" : null; - } + protected OkWithResourceLinks OkWithResourceLinks(IEnumerable content, bool hasMore, Func pagePropertyAccessor = null, IHeaderDictionary headers = null, bool isDescending = false) where TEntity : class { + return new OkWithResourceLinks(content, hasMore, null, pagePropertyAccessor, headers, isDescending); + } - protected bool NextPageExceedsSkipLimit(int page, int limit) { - return (page + 1) * limit >= MAXIMUM_SKIP; - } + protected OkWithResourceLinks OkWithResourceLinks(IEnumerable content, bool hasMore, int page, long? total = null, IHeaderDictionary headers = null) where TEntity : class { + return new OkWithResourceLinks(content, hasMore, page, total, headers: headers); + } + + protected string GetResourceLink(string url, string type) { + return url != null ? $"<{url}>; rel=\"{type}\"" : null; + } + + protected bool NextPageExceedsSkipLimit(int page, int limit) { + return (page + 1) * limit >= MAXIMUM_SKIP; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Controllers/Base/ModelActionResults.cs b/src/Exceptionless.Web/Controllers/Base/ModelActionResults.cs index 582be85d3e..c37f4edcf5 100644 --- a/src/Exceptionless.Web/Controllers/Base/ModelActionResults.cs +++ b/src/Exceptionless.Web/Controllers/Base/ModelActionResults.cs @@ -1,18 +1,15 @@ -using System.Collections.Generic; -using System.Linq; +namespace Exceptionless.Web.Controllers; -namespace Exceptionless.Web.Controllers { - public class ModelActionResults: WorkInProgressResult { - public ModelActionResults() { - Success = new List(); - Failure = new List(); - } +public class ModelActionResults : WorkInProgressResult { + public ModelActionResults() { + Success = new List(); + Failure = new List(); + } - public List Success { get; set; } - public List Failure { get; set; } + public List Success { get; set; } + public List Failure { get; set; } - public void AddNotFound(IEnumerable ids) { - Failure.AddRange(ids.Select(PermissionResult.DenyWithNotFound)); - } + public void AddNotFound(IEnumerable ids) { + Failure.AddRange(ids.Select(PermissionResult.DenyWithNotFound)); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Controllers/Base/PermissionResult.cs b/src/Exceptionless.Web/Controllers/Base/PermissionResult.cs index 49bd94aa64..1a1bbf7187 100644 --- a/src/Exceptionless.Web/Controllers/Base/PermissionResult.cs +++ b/src/Exceptionless.Web/Controllers/Base/PermissionResult.cs @@ -1,52 +1,50 @@ -using Microsoft.AspNetCore.Http; - -namespace Exceptionless.Web.Controllers { - public class PermissionResult { - public bool Allowed { get; set; } - - public string Id { get; set; } - - public string Message { get; set; } - - public int StatusCode { get; set; } - - public static PermissionResult Allow = new PermissionResult { Allowed = true, StatusCode = StatusCodes.Status200OK }; - - public static PermissionResult Deny = new PermissionResult { Allowed = false, StatusCode = StatusCodes.Status400BadRequest }; - - public static PermissionResult DenyWithNotFound(string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - StatusCode = StatusCodes.Status404NotFound - }; - } - - public static PermissionResult DenyWithMessage(string message, string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - Message = message, - StatusCode = StatusCodes.Status400BadRequest - }; - } - - public static PermissionResult DenyWithStatus(int statusCode, string message = null, string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - Message = message, - StatusCode = statusCode - }; - } - - public static PermissionResult DenyWithPlanLimitReached(string message, string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - Message = message, - StatusCode = StatusCodes.Status426UpgradeRequired - }; - } +namespace Exceptionless.Web.Controllers; + +public class PermissionResult { + public bool Allowed { get; set; } + + public string Id { get; set; } + + public string Message { get; set; } + + public int StatusCode { get; set; } + + public static PermissionResult Allow = new PermissionResult { Allowed = true, StatusCode = StatusCodes.Status200OK }; + + public static PermissionResult Deny = new PermissionResult { Allowed = false, StatusCode = StatusCodes.Status400BadRequest }; + + public static PermissionResult DenyWithNotFound(string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + StatusCode = StatusCodes.Status404NotFound + }; + } + + public static PermissionResult DenyWithMessage(string message, string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + Message = message, + StatusCode = StatusCodes.Status400BadRequest + }; + } + + public static PermissionResult DenyWithStatus(int statusCode, string message = null, string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + Message = message, + StatusCode = statusCode + }; + } + + public static PermissionResult DenyWithPlanLimitReached(string message, string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + Message = message, + StatusCode = StatusCodes.Status426UpgradeRequired + }; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Controllers/Base/ReadOnlyRepositoryApiController.cs b/src/Exceptionless.Web/Controllers/Base/ReadOnlyRepositoryApiController.cs index 7466d28e38..ac720909b2 100644 --- a/src/Exceptionless.Web/Controllers/Base/ReadOnlyRepositoryApiController.cs +++ b/src/Exceptionless.Web/Controllers/Base/ReadOnlyRepositoryApiController.cs @@ -1,99 +1,92 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Queries.Validation; -using Exceptionless.Core.Repositories; -using Exceptionless.Core.Repositories.Queries; using Foundatio.Repositories; using Foundatio.Repositories.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -namespace Exceptionless.Web.Controllers { - public abstract class ReadOnlyRepositoryApiController : ExceptionlessApiController where TRepository : ISearchableReadOnlyRepository where TModel : class, IIdentity, new() where TViewModel : class, IIdentity, new() { - protected readonly TRepository _repository; - protected static readonly bool _isOwnedByOrganization = typeof(IOwnedByOrganization).IsAssignableFrom(typeof(TModel)); - protected static readonly bool _isOrganization = typeof(TModel) == typeof(Organization); - protected static readonly bool _supportsSoftDeletes = typeof(ISupportSoftDeletes).IsAssignableFrom(typeof(TModel)); - protected static readonly IReadOnlyCollection EmptyModels = new List(0).AsReadOnly(); - protected readonly IMapper _mapper; - protected readonly IAppQueryValidator _validator; - protected readonly ILogger _logger; - - public ReadOnlyRepositoryApiController(TRepository repository, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) { - _repository = repository; - _mapper = mapper; - _validator = validator; - _logger = loggerFactory.CreateLogger(GetType()); - } - - protected async Task> GetByIdImplAsync(string id) { - var model = await GetModelAsync(id); - if (model == null) - return NotFound(); +namespace Exceptionless.Web.Controllers; + +public abstract class ReadOnlyRepositoryApiController : ExceptionlessApiController where TRepository : ISearchableReadOnlyRepository where TModel : class, IIdentity, new() where TViewModel : class, IIdentity, new() { + protected readonly TRepository _repository; + protected static readonly bool _isOwnedByOrganization = typeof(IOwnedByOrganization).IsAssignableFrom(typeof(TModel)); + protected static readonly bool _isOrganization = typeof(TModel) == typeof(Organization); + protected static readonly bool _supportsSoftDeletes = typeof(ISupportSoftDeletes).IsAssignableFrom(typeof(TModel)); + protected static readonly IReadOnlyCollection EmptyModels = new List(0).AsReadOnly(); + protected readonly IMapper _mapper; + protected readonly IAppQueryValidator _validator; + protected readonly ILogger _logger; + + public ReadOnlyRepositoryApiController(TRepository repository, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) { + _repository = repository; + _mapper = mapper; + _validator = validator; + _logger = loggerFactory.CreateLogger(GetType()); + } - return await OkModelAsync(model); - } + protected async Task> GetByIdImplAsync(string id) { + var model = await GetModelAsync(id); + if (model == null) + return NotFound(); - protected async Task> OkModelAsync(TModel model) { - return Ok(await MapAsync(model, true)); - } + return await OkModelAsync(model); + } - protected virtual async Task GetModelAsync(string id, bool useCache = true) { - if (String.IsNullOrEmpty(id)) - return null; + protected async Task> OkModelAsync(TModel model) { + return Ok(await MapAsync(model, true)); + } - var model = await _repository.GetByIdAsync(id, o => o.Cache(useCache)); - if (model == null) - return null; + protected virtual async Task GetModelAsync(string id, bool useCache = true) { + if (String.IsNullOrEmpty(id)) + return null; - if (_isOwnedByOrganization && !CanAccessOrganization(((IOwnedByOrganization)model).OrganizationId)) - return null; + var model = await _repository.GetByIdAsync(id, o => o.Cache(useCache)); + if (model == null) + return null; - return model; - } + if (_isOwnedByOrganization && !CanAccessOrganization(((IOwnedByOrganization)model).OrganizationId)) + return null; - protected virtual async Task> GetModelsAsync(string[] ids, bool useCache = true) { - if (ids == null || ids.Length == 0) - return EmptyModels; + return model; + } - var models = await _repository.GetByIdsAsync(ids, o => o.Cache(useCache)); + protected virtual async Task> GetModelsAsync(string[] ids, bool useCache = true) { + if (ids == null || ids.Length == 0) + return EmptyModels; - if (_isOwnedByOrganization) - models = models.Where(m => CanAccessOrganization(((IOwnedByOrganization)m).OrganizationId)).ToList(); + var models = await _repository.GetByIdsAsync(ids, o => o.Cache(useCache)); - return models; - } + if (_isOwnedByOrganization) + models = models.Where(m => CanAccessOrganization(((IOwnedByOrganization)m).OrganizationId)).ToList(); - #region Mapping + return models; + } - protected async Task MapAsync(object source, bool isResult = false) { - var destination = _mapper.Map(source); - if (isResult) - await AfterResultMapAsync(new List(new[] { destination })); + #region Mapping - return destination; - } + protected async Task MapAsync(object source, bool isResult = false) { + var destination = _mapper.Map(source); + if (isResult) + await AfterResultMapAsync(new List(new[] { destination })); - protected async Task> MapCollectionAsync(object source, bool isResult = false) { - var destination = _mapper.Map>(source); - if (isResult) - await AfterResultMapAsync(destination); + return destination; + } - return destination; - } + protected async Task> MapCollectionAsync(object source, bool isResult = false) { + var destination = _mapper.Map>(source); + if (isResult) + await AfterResultMapAsync(destination); - protected virtual Task AfterResultMapAsync(ICollection models) { - foreach (var model in models.OfType()) - model.Data.RemoveSensitiveData(); + return destination; + } - return Task.CompletedTask; - } + protected virtual Task AfterResultMapAsync(ICollection models) { + foreach (var model in models.OfType()) + model.Data.RemoveSensitiveData(); - #endregion + return Task.CompletedTask; } + + #endregion } diff --git a/src/Exceptionless.Web/Controllers/Base/RepositoryApiController.cs b/src/Exceptionless.Web/Controllers/Base/RepositoryApiController.cs index d446e39857..98af98d0dd 100644 --- a/src/Exceptionless.Web/Controllers/Base/RepositoryApiController.cs +++ b/src/Exceptionless.Web/Controllers/Base/RepositoryApiController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Web.Extensions; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; @@ -11,218 +7,220 @@ using FluentValidation; using Foundatio.Repositories; using Foundatio.Repositories.Models; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; #pragma warning disable 1998 -namespace Exceptionless.Web.Controllers { - public abstract class RepositoryApiController : ReadOnlyRepositoryApiController where TRepository : ISearchableRepository where TModel : class, IIdentity, new() where TViewModel : class, IIdentity, new() where TNewModel : class, new() where TUpdateModel : class, new() { - public RepositoryApiController(TRepository repository, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) : base(repository, mapper, validator, loggerFactory) {} +namespace Exceptionless.Web.Controllers; - protected async Task> PostImplAsync(TNewModel value) { - if (value == null) - return BadRequest(); +public abstract class RepositoryApiController : ReadOnlyRepositoryApiController where TRepository : ISearchableRepository where TModel : class, IIdentity, new() where TViewModel : class, IIdentity, new() where TNewModel : class, new() where TUpdateModel : class, new() { + public RepositoryApiController(TRepository repository, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) : base(repository, mapper, validator, loggerFactory) { } - var mapped = await MapAsync(value); - // if no organization id is specified, default to the user's 1st associated org. - if (!_isOrganization && mapped is IOwnedByOrganization orgModel && String.IsNullOrEmpty(orgModel.OrganizationId) && GetAssociatedOrganizationIds().Any()) - orgModel.OrganizationId = Request.GetDefaultOrganizationId(); + protected async Task> PostImplAsync(TNewModel value) { + if (value == null) + return BadRequest(); - var permission = await CanAddAsync(mapped); - if (!permission.Allowed) - return Permission(permission); + var mapped = await MapAsync(value); + // if no organization id is specified, default to the user's 1st associated org. + if (!_isOrganization && mapped is IOwnedByOrganization orgModel && String.IsNullOrEmpty(orgModel.OrganizationId) && GetAssociatedOrganizationIds().Any()) + orgModel.OrganizationId = Request.GetDefaultOrganizationId(); - TModel model; - try { - model = await AddModelAsync(mapped); - await AfterAddAsync(model); - } catch (ValidationException ex) { - return BadRequest(ex.Errors.ToErrorMessage()); - } + var permission = await CanAddAsync(mapped); + if (!permission.Allowed) + return Permission(permission); - return Created(new Uri(GetEntityLink(model.Id)), await MapAsync(model, true)); + TModel model; + try { + model = await AddModelAsync(mapped); + await AfterAddAsync(model); + } + catch (ValidationException ex) { + return BadRequest(ex.Errors.ToErrorMessage()); } - protected async Task> UpdateModelAsync(string id, Func> modelUpdateFunc) { - var model = await GetModelAsync(id); - if (model == null) - return NotFound(); + return Created(new Uri(GetEntityLink(model.Id)), await MapAsync(model, true)); + } - if (modelUpdateFunc != null) - model = await modelUpdateFunc(model); + protected async Task> UpdateModelAsync(string id, Func> modelUpdateFunc) { + var model = await GetModelAsync(id); + if (model == null) + return NotFound(); - await _repository.SaveAsync(model, o => o.Cache()); - await AfterUpdateAsync(model); + if (modelUpdateFunc != null) + model = await modelUpdateFunc(model); - if (typeof(TViewModel) == typeof(TModel)) - return Ok(model); + await _repository.SaveAsync(model, o => o.Cache()); + await AfterUpdateAsync(model); - return Ok(await MapAsync(model, true)); - } + if (typeof(TViewModel) == typeof(TModel)) + return Ok(model); - protected async Task> UpdateModelsAsync(string[] ids, Func> modelUpdateFunc) { - var models = await GetModelsAsync(ids, false); - if (models == null || models.Count == 0) - return NotFound(); + return Ok(await MapAsync(model, true)); + } - if (modelUpdateFunc != null) - foreach (var model in models) - await modelUpdateFunc(model); + protected async Task> UpdateModelsAsync(string[] ids, Func> modelUpdateFunc) { + var models = await GetModelsAsync(ids, false); + if (models == null || models.Count == 0) + return NotFound(); - await _repository.SaveAsync(models, o => o.Cache()); + if (modelUpdateFunc != null) foreach (var model in models) - await AfterUpdateAsync(model); - - if (typeof(TViewModel) == typeof(TModel)) - return Ok(models); + await modelUpdateFunc(model); - return Ok(await MapAsync(models, true)); - } + await _repository.SaveAsync(models, o => o.Cache()); + foreach (var model in models) + await AfterUpdateAsync(model); - protected virtual string GetEntityLink(string id) { - return Url.Link($"Get{typeof(TModel).Name}ById", new { - id - }); - } + if (typeof(TViewModel) == typeof(TModel)) + return Ok(models); - protected virtual string GetEntityResourceLink(string id, string type) { - return GetResourceLink(Url.Link($"Get{typeof(TModel).Name}ById", new { - id - }), type); - } + return Ok(await MapAsync(models, true)); + } - protected virtual string GetEntityLink(string id) { - return Url.Link($"Get{typeof(TEntityType).Name}ById", new { - id - }); - } + protected virtual string GetEntityLink(string id) { + return Url.Link($"Get{typeof(TModel).Name}ById", new { + id + }); + } - protected virtual string GetEntityResourceLink(string id, string type) { - return GetResourceLink(Url.Link($"Get{typeof(TEntityType).Name}ById", new { - id - }), type); - } + protected virtual string GetEntityResourceLink(string id, string type) { + return GetResourceLink(Url.Link($"Get{typeof(TModel).Name}ById", new { + id + }), type); + } - protected virtual async Task CanAddAsync(TModel value) { - if (_isOrganization || !(value is IOwnedByOrganization orgModel)) - return PermissionResult.Allow; + protected virtual string GetEntityLink(string id) { + return Url.Link($"Get{typeof(TEntityType).Name}ById", new { + id + }); + } - if (!CanAccessOrganization(orgModel.OrganizationId)) - return PermissionResult.DenyWithMessage("Invalid organization id specified."); + protected virtual string GetEntityResourceLink(string id, string type) { + return GetResourceLink(Url.Link($"Get{typeof(TEntityType).Name}ById", new { + id + }), type); + } + protected virtual async Task CanAddAsync(TModel value) { + if (_isOrganization || !(value is IOwnedByOrganization orgModel)) return PermissionResult.Allow; - } - - protected virtual Task AddModelAsync(TModel value) { - return _repository.AddAsync(value, o => o.Cache()); - } - protected virtual Task AfterAddAsync(TModel value) { - return Task.FromResult(value); - } + if (!CanAccessOrganization(orgModel.OrganizationId)) + return PermissionResult.DenyWithMessage("Invalid organization id specified."); - protected virtual Task AfterUpdateAsync(TModel value) { - return Task.FromResult(value); - } + return PermissionResult.Allow; + } - protected async Task> PatchImplAsync(string id, Delta changes) { - var original = await GetModelAsync(id, false); - if (original == null) - return NotFound(); + protected virtual Task AddModelAsync(TModel value) { + return _repository.AddAsync(value, o => o.Cache()); + } - // if there are no changes in the delta, then ignore the request - if (changes == null || !changes.GetChangedPropertyNames().Any()) - return await OkModelAsync(original); + protected virtual Task AfterAddAsync(TModel value) { + return Task.FromResult(value); + } - var permission = await CanUpdateAsync(original, changes); - if (!permission.Allowed) - return Permission(permission); + protected virtual Task AfterUpdateAsync(TModel value) { + return Task.FromResult(value); + } - try { - await UpdateModelAsync(original, changes); - await AfterPatchAsync(original); - } catch (ValidationException ex) { - return BadRequest(ex.Errors.ToErrorMessage()); - } + protected async Task> PatchImplAsync(string id, Delta changes) { + var original = await GetModelAsync(id, false); + if (original == null) + return NotFound(); + // if there are no changes in the delta, then ignore the request + if (changes == null || !changes.GetChangedPropertyNames().Any()) return await OkModelAsync(original); + + var permission = await CanUpdateAsync(original, changes); + if (!permission.Allowed) + return Permission(permission); + + try { + await UpdateModelAsync(original, changes); + await AfterPatchAsync(original); + } + catch (ValidationException ex) { + return BadRequest(ex.Errors.ToErrorMessage()); } - protected virtual async Task CanUpdateAsync(TModel original, Delta changes) { - if (original is IOwnedByOrganization orgModel && !CanAccessOrganization(orgModel.OrganizationId)) - return PermissionResult.DenyWithMessage("Invalid organization id specified."); + return await OkModelAsync(original); + } - if (changes.GetChangedPropertyNames().Contains("OrganizationId")) - return PermissionResult.DenyWithMessage("OrganizationId cannot be modified."); + protected virtual async Task CanUpdateAsync(TModel original, Delta changes) { + if (original is IOwnedByOrganization orgModel && !CanAccessOrganization(orgModel.OrganizationId)) + return PermissionResult.DenyWithMessage("Invalid organization id specified."); - return PermissionResult.Allow; - } + if (changes.GetChangedPropertyNames().Contains("OrganizationId")) + return PermissionResult.DenyWithMessage("OrganizationId cannot be modified."); - protected virtual Task UpdateModelAsync(TModel original, Delta changes) { - changes.Patch(original); - return _repository.SaveAsync(original, o => o.Cache()); - } + return PermissionResult.Allow; + } - protected virtual Task AfterPatchAsync(TModel value) { - return Task.FromResult(value); - } + protected virtual Task UpdateModelAsync(TModel original, Delta changes) { + changes.Patch(original); + return _repository.SaveAsync(original, o => o.Cache()); + } + + protected virtual Task AfterPatchAsync(TModel value) { + return Task.FromResult(value); + } - protected async Task> DeleteImplAsync(string[] ids) { - var items = await GetModelsAsync(ids, false); - if (items.Count == 0) - return NotFound(); - - var results = new ModelActionResults(); - results.AddNotFound(ids.Except(items.Select(i => i.Id))); - - var list = items.ToList(); - foreach (var model in items) { - var permission = await CanDeleteAsync(model); - if (permission.Allowed) - continue; - - list.Remove(model); - results.Failure.Add(permission); - } - - if (list.Count == 0) - return results.Failure.Count == 1 ? Permission(results.Failure.First()) : BadRequest(results); - - IEnumerable workIds; - try { - workIds = await DeleteModelsAsync(list) ?? new List(); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, ex.Message); - return StatusCode(StatusCodes.Status500InternalServerError); - } - - if (results.Failure.Count == 0) - return WorkInProgress(workIds); - - results.Workers.AddRange(workIds); - results.Success.AddRange(list.Select(i => i.Id)); - return BadRequest(results); + protected async Task> DeleteImplAsync(string[] ids) { + var items = await GetModelsAsync(ids, false); + if (items.Count == 0) + return NotFound(); + + var results = new ModelActionResults(); + results.AddNotFound(ids.Except(items.Select(i => i.Id))); + + var list = items.ToList(); + foreach (var model in items) { + var permission = await CanDeleteAsync(model); + if (permission.Allowed) + continue; + + list.Remove(model); + results.Failure.Add(permission); } - protected virtual async Task CanDeleteAsync(TModel value) { - if (value is IOwnedByOrganization orgModel && !CanAccessOrganization(orgModel.OrganizationId)) - return PermissionResult.DenyWithNotFound(value.Id); + if (list.Count == 0) + return results.Failure.Count == 1 ? Permission(results.Failure.First()) : BadRequest(results); - return PermissionResult.Allow; + IEnumerable workIds; + try { + workIds = await DeleteModelsAsync(list) ?? new List(); + } + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError); } - protected virtual async Task> DeleteModelsAsync(ICollection values) { - if (_supportsSoftDeletes) { - values.Cast().ForEach(v => v.IsDeleted = true); - await _repository.SaveAsync(values); - } else { - await _repository.RemoveAsync(values); - } + if (results.Failure.Count == 0) + return WorkInProgress(workIds); + + results.Workers.AddRange(workIds); + results.Success.AddRange(list.Select(i => i.Id)); + return BadRequest(results); + } + + protected virtual async Task CanDeleteAsync(TModel value) { + if (value is IOwnedByOrganization orgModel && !CanAccessOrganization(orgModel.OrganizationId)) + return PermissionResult.DenyWithNotFound(value.Id); + + return PermissionResult.Allow; + } - return Enumerable.Empty(); + protected virtual async Task> DeleteModelsAsync(ICollection values) { + if (_supportsSoftDeletes) { + values.Cast().ForEach(v => v.IsDeleted = true); + await _repository.SaveAsync(values); + } + else { + await _repository.RemoveAsync(values); } + + return Enumerable.Empty(); } } diff --git a/src/Exceptionless.Web/Controllers/Base/TimeInfo.cs b/src/Exceptionless.Web/Controllers/Base/TimeInfo.cs index 3dbc8f7fbb..9eddcc89a3 100644 --- a/src/Exceptionless.Web/Controllers/Base/TimeInfo.cs +++ b/src/Exceptionless.Web/Controllers/Base/TimeInfo.cs @@ -1,38 +1,37 @@ -using System; using System.Diagnostics; using Exceptionless.DateTimeExtensions; using Foundatio.Utility; -namespace Exceptionless.Web.Controllers { - [DebuggerDisplay("Range: {Range} Offset: {Offset} Field: {Field}")] - public class TimeInfo { - public string Field { get; set; } - public DateTimeRange Range { get; set; } - public TimeSpan Offset { get; set; } - } +namespace Exceptionless.Web.Controllers; + +[DebuggerDisplay("Range: {Range} Offset: {Offset} Field: {Field}")] +public class TimeInfo { + public string Field { get; set; } + public DateTimeRange Range { get; set; } + public TimeSpan Offset { get; set; } +} - public static class TimeInfoExtensions { - public static void ApplyMinimumUtcStartDate(this TimeInfo ti, DateTime minimumUtcStartDate) { - if (ti.Range.UtcStart >= minimumUtcStartDate) - return; +public static class TimeInfoExtensions { + public static void ApplyMinimumUtcStartDate(this TimeInfo ti, DateTime minimumUtcStartDate) { + if (ti.Range.UtcStart >= minimumUtcStartDate) + return; - long startTicks = minimumUtcStartDate.Ticks + ti.Offset.Ticks; - var start = startTicks > DateTime.MinValue.Ticks ? new DateTimeOffset(startTicks, ti.Offset) : new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero); + long startTicks = minimumUtcStartDate.Ticks + ti.Offset.Ticks; + var start = startTicks > DateTime.MinValue.Ticks ? new DateTimeOffset(startTicks, ti.Offset) : new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero); - long endTicks = ti.Range.UtcEnd.Ticks + ti.Offset.Ticks; - var end = ti.Range.UtcEnd < DateTime.MaxValue && endTicks < DateTime.MaxValue.Ticks ? new DateTimeOffset(endTicks, ti.Offset) : new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero); - ti.Range = new DateTimeRange(start, end); - } + long endTicks = ti.Range.UtcEnd.Ticks + ti.Offset.Ticks; + var end = ti.Range.UtcEnd < DateTime.MaxValue && endTicks < DateTime.MaxValue.Ticks ? new DateTimeOffset(endTicks, ti.Offset) : new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero); + ti.Range = new DateTimeRange(start, end); + } - public static void AdjustEndTimeIfMaxValue(this TimeInfo ti) { - if (ti.Range.UtcEnd != DateTime.MaxValue) - return; + public static void AdjustEndTimeIfMaxValue(this TimeInfo ti) { + if (ti.Range.UtcEnd != DateTime.MaxValue) + return; - long startTicks = ti.Range.UtcStart.Ticks + ti.Offset.Ticks; - var start = startTicks > DateTime.MinValue.Ticks ? new DateTimeOffset(startTicks, ti.Offset) : new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero); + long startTicks = ti.Range.UtcStart.Ticks + ti.Offset.Ticks; + var start = startTicks > DateTime.MinValue.Ticks ? new DateTimeOffset(startTicks, ti.Offset) : new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero); - var end = new DateTimeOffset(SystemClock.UtcNow.Ticks + ti.Offset.Ticks, ti.Offset); - ti.Range = new DateTimeRange(start, end); - } + var end = new DateTimeOffset(SystemClock.UtcNow.Ticks + ti.Offset.Ticks, ti.Offset); + ti.Range = new DateTimeRange(start, end); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Controllers/Base/WorkInProgressResult.cs b/src/Exceptionless.Web/Controllers/Base/WorkInProgressResult.cs index 822b55dade..337a490f29 100644 --- a/src/Exceptionless.Web/Controllers/Base/WorkInProgressResult.cs +++ b/src/Exceptionless.Web/Controllers/Base/WorkInProgressResult.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; +namespace Exceptionless.Web.Controllers; -namespace Exceptionless.Web.Controllers { - public class WorkInProgressResult { - public WorkInProgressResult() { - Workers = new List(); - } - - public WorkInProgressResult(IEnumerable workers) : this() { - Workers.AddRange(workers ?? new List()); - } +public class WorkInProgressResult { + public WorkInProgressResult() { + Workers = new List(); + } - public List Workers { get; set; } + public WorkInProgressResult(IEnumerable workers) : this() { + Workers.AddRange(workers ?? new List()); } -} \ No newline at end of file + + public List Workers { get; set; } +} diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index ef7481901b..683a821f74 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using AutoMapper; using Exceptionless.Web.Extensions; using Exceptionless.Core; @@ -29,1263 +24,1266 @@ using Foundatio.Repositories.Models; using Foundatio.Utility; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using System.Text; using Foundatio.Repositories.Extensions; -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/events")] - [Authorize(Policy = AuthorizationRoles.ClientPolicy)] - public class EventController : RepositoryApiController { - private static readonly HashSet _ignoredKeys = new HashSet(StringComparer.OrdinalIgnoreCase) { "access_token", "api_key", "apikey" }; - - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IStackRepository _stackRepository; - private readonly EventPostService _eventPostService; - private readonly IQueue _eventUserDescriptionQueue; - private readonly IValidator _userDescriptionValidator; - private readonly FormattingPluginManager _formattingPluginManager; - private readonly ICacheClient _cache; - private readonly JsonSerializerSettings _jsonSerializerSettings; - private readonly AppOptions _appOptions; - - public EventController(IEventRepository repository, - IOrganizationRepository organizationRepository, - IProjectRepository projectRepository, - IStackRepository stackRepository, - EventPostService eventPostService, - IQueue eventUserDescriptionQueue, - IValidator userDescriptionValidator, - FormattingPluginManager formattingPluginManager, - ICacheClient cacheClient, - JsonSerializerSettings jsonSerializerSettings, - IMapper mapper, - PersistentEventQueryValidator validator, - AppOptions appOptions, - ILoggerFactory loggerFactory - ) : base(repository, mapper, validator, loggerFactory) { - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _stackRepository = stackRepository; - _eventPostService = eventPostService; - _eventUserDescriptionQueue = eventUserDescriptionQueue; - _userDescriptionValidator = userDescriptionValidator; - _formattingPluginManager = formattingPluginManager; - _cache = cacheClient; - _jsonSerializerSettings = jsonSerializerSettings; - _appOptions = appOptions; - - AllowedDateFields.Add(EventIndex.Alias.Date); - DefaultDateField = EventIndex.Alias.Date; - } - - /// - /// Count - /// - /// A filter that controls what data is returned from the server. - /// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// Invalid filter. - [HttpGet("count")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetCountAsync(string filter = null, string aggregations = null, string time = null, string offset = null, string mode = null) { - var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); - if (organizations.All(o => o.IsSuspended)) - return Ok(CountResult.Empty); - - var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; - return await CountInternalAsync(sf, ti, filter, aggregations, mode); - } +namespace Exceptionless.Web.Controllers; + +[Route(API_PREFIX + "/events")] +[Authorize(Policy = AuthorizationRoles.ClientPolicy)] +public class EventController : RepositoryApiController { + private static readonly HashSet _ignoredKeys = new HashSet(StringComparer.OrdinalIgnoreCase) { "access_token", "api_key", "apikey" }; + + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IStackRepository _stackRepository; + private readonly EventPostService _eventPostService; + private readonly IQueue _eventUserDescriptionQueue; + private readonly IValidator _userDescriptionValidator; + private readonly FormattingPluginManager _formattingPluginManager; + private readonly ICacheClient _cache; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly AppOptions _appOptions; + + public EventController(IEventRepository repository, + IOrganizationRepository organizationRepository, + IProjectRepository projectRepository, + IStackRepository stackRepository, + EventPostService eventPostService, + IQueue eventUserDescriptionQueue, + IValidator userDescriptionValidator, + FormattingPluginManager formattingPluginManager, + ICacheClient cacheClient, + JsonSerializerSettings jsonSerializerSettings, + IMapper mapper, + PersistentEventQueryValidator validator, + AppOptions appOptions, + ILoggerFactory loggerFactory + ) : base(repository, mapper, validator, loggerFactory) { + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _stackRepository = stackRepository; + _eventPostService = eventPostService; + _eventUserDescriptionQueue = eventUserDescriptionQueue; + _userDescriptionValidator = userDescriptionValidator; + _formattingPluginManager = formattingPluginManager; + _cache = cacheClient; + _jsonSerializerSettings = jsonSerializerSettings; + _appOptions = appOptions; + + AllowedDateFields.Add(EventIndex.Alias.Date); + DefaultDateField = EventIndex.Alias.Date; + } - /// - /// Count by organization - /// - /// The identifier of the organization. - /// A filter that controls what data is returned from the server. - /// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// Invalid filter. - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/events/count")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetCountByOrganizationAsync(string organizationId, string filter = null, string aggregations = null, string time = null, string offset = null, string mode = null) { - var organization = await GetOrganizationAsync(organizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organization); - return await CountInternalAsync(sf, ti, filter, aggregations, mode); - } + /// + /// Count + /// + /// A filter that controls what data is returned from the server. + /// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// Invalid filter. + [HttpGet("count")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetCountAsync(string filter = null, string aggregations = null, string time = null, string offset = null, string mode = null) { + var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); + if (organizations.All(o => o.IsSuspended)) + return Ok(CountResult.Empty); + + var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; + return await CountInternalAsync(sf, ti, filter, aggregations, mode); + } - /// - /// Count by project - /// - /// The identifier of the project. - /// A filter that controls what data is returned from the server. - /// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// Invalid filter. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/count")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetCountByProjectAsync(string projectId, string filter = null, string aggregations = null, string time = null, string offset = null, string mode = null) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); - - var organization = await GetOrganizationAsync(project.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); - var sf = new AppFilter(project, organization); - return await CountInternalAsync(sf, ti, filter, aggregations, mode); - } + /// + /// Count by organization + /// + /// The identifier of the organization. + /// A filter that controls what data is returned from the server. + /// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// Invalid filter. + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/events/count")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetCountByOrganizationAsync(string organizationId, string filter = null, string aggregations = null, string time = null, string offset = null, string mode = null) { + var organization = await GetOrganizationAsync(organizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organization); + return await CountInternalAsync(sf, ti, filter, aggregations, mode); + } - /// - /// Get by id - /// - /// The identifier of the event. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// The event occurrence could not be found. - /// Unable to view event occurrence due to plan limits. - [HttpGet("{id:objectid}", Name = "GetPersistentEventById")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetAsync(string id, string time = null, string offset = null) { - var model = await GetModelAsync(id, false); - if (model == null) - return NotFound(); - - var organization = await GetOrganizationAsync(model.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended || organization.RetentionDays > 0 && model.Date.UtcDateTime < SystemClock.UtcNow.SubtractDays(organization.RetentionDays)) - return PlanLimitReached("Unable to view event occurrence due to plan limits."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organization); - var result = await _repository.GetPreviousAndNextEventIdsAsync(model, sf, ti.Range.UtcStart, ti.Range.UtcEnd); - return OkWithLinks(model, new [] { GetEntityResourceLink(result.Previous, "previous"), GetEntityResourceLink(result.Next, "next"), GetEntityResourceLink(model.StackId, "parent") }); - } + /// + /// Count by project + /// + /// The identifier of the project. + /// A filter that controls what data is returned from the server. + /// A list of values you want returned. Example: avg:value cardinality:value sum:users max:value min:value + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// Invalid filter. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/count")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetCountByProjectAsync(string projectId, string filter = null, string aggregations = null, string time = null, string offset = null, string mode = null) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + var organization = await GetOrganizationAsync(project.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); + var sf = new AppFilter(project, organization); + return await CountInternalAsync(sf, ti, filter, aggregations, mode); + } - /// - /// Get all - /// - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - [HttpGet] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetAsync(string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); - if (organizations.All(o => o.IsSuspended)) - return Ok(EmptyModels); - - var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; - return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); - } + /// + /// Get by id + /// + /// The identifier of the event. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// The event occurrence could not be found. + /// Unable to view event occurrence due to plan limits. + [HttpGet("{id:objectid}", Name = "GetPersistentEventById")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetAsync(string id, string time = null, string offset = null) { + var model = await GetModelAsync(id, false); + if (model == null) + return NotFound(); + + var organization = await GetOrganizationAsync(model.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended || organization.RetentionDays > 0 && model.Date.UtcDateTime < SystemClock.UtcNow.SubtractDays(organization.RetentionDays)) + return PlanLimitReached("Unable to view event occurrence due to plan limits."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organization); + var result = await _repository.GetPreviousAndNextEventIdsAsync(model, sf, ti.Range.UtcStart, ti.Range.UtcEnd); + return OkWithLinks(model, new[] { GetEntityResourceLink(result.Previous, "previous"), GetEntityResourceLink(result.Next, "next"), GetEntityResourceLink(model.StackId, "parent") }); + } - private async Task> CountInternalAsync(AppFilter sf, TimeInfo ti, string filter = null, string aggregations = null, string mode = null) { - var pr = await _validator.ValidateQueryAsync(filter); - if (!pr.IsValid) - return BadRequest(pr.Message); + /// + /// Get all + /// + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + [HttpGet] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetAsync(string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); + if (organizations.All(o => o.IsSuspended)) + return Ok(EmptyModels); + + var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; + return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); + } - var far = await _validator.ValidateAggregationsAsync(aggregations); - if (!far.IsValid) - return BadRequest(far.Message); + private async Task> CountInternalAsync(AppFilter sf, TimeInfo ti, string filter = null, string aggregations = null, string mode = null) { + var pr = await _validator.ValidateQueryAsync(filter); + if (!pr.IsValid) + return BadRequest(pr.Message); - sf.UsesPremiumFeatures = pr.UsesPremiumFeatures || far.UsesPremiumFeatures; + var far = await _validator.ValidateAggregationsAsync(aggregations); + if (!far.IsValid) + return BadRequest(far.Message); - if (mode == "stack_new") - filter = AddFirstOccurrenceFilter(ti.Range, filter); + sf.UsesPremiumFeatures = pr.UsesPremiumFeatures || far.UsesPremiumFeatures; - var query = new RepositoryQuery() - .AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null) - .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field) - .Index(ti.Range.UtcStart, ti.Range.UtcEnd); + if (mode == "stack_new") + filter = AddFirstOccurrenceFilter(ti.Range, filter); - CountResult result; - try { - result = await _repository.CountAsync(q => q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression(aggregations)); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Property("Search Filter", new { SystemFilter = sf, UserFilter = filter, Time = ti, Aggregations = aggregations }).Tag("Search").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, "An error has occurred. Please check your filter or aggregations."); + var query = new RepositoryQuery() + .AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null) + .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field) + .Index(ti.Range.UtcStart, ti.Range.UtcEnd); - return BadRequest("An error has occurred. Please check your search filter."); - } + CountResult result; + try { + result = await _repository.CountAsync(q => q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression(aggregations)); + } + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Property("Search Filter", new { SystemFilter = sf, UserFilter = filter, Time = ti, Aggregations = aggregations }).Tag("Search").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, "An error has occurred. Please check your filter or aggregations."); - return Ok(result); + return BadRequest("An error has occurred. Please check your search filter."); } - private async Task>> GetInternalAsync(AppFilter sf, TimeInfo ti, string filter = null, string sort = null, string mode = null, int page = 1, int limit = 10, string after = null, bool usesPremiumFeatures = false) { - page = GetPage(page); - limit = GetLimit(limit); - int skip = GetSkip(page, limit); - if (skip > MAXIMUM_SKIP) - return Ok(EmptyModels); - - var pr = await _validator.ValidateQueryAsync(filter); - if (!pr.IsValid) - return BadRequest(pr.Message); - - sf.UsesPremiumFeatures = pr.UsesPremiumFeatures || usesPremiumFeatures; - bool useSearchAfter = !String.IsNullOrEmpty(after); - - try { - FindResults events; - switch (mode) { - case "summary": - events = await GetEventsInternalAsync(sf, ti, filter, sort, page, limit, after, useSearchAfter); - return OkWithResourceLinks(events.Documents.Select(e => { - var summaryData = _formattingPluginManager.GetEventSummaryData(e); - return new EventSummaryModel { - TemplateKey = summaryData.TemplateKey, - Id = e.Id, - Date = e.Date, - Data = summaryData.Data - }; - }).ToList(), events.HasMore && !NextPageExceedsSkipLimit(page, limit), page, events.Total); - case "stack_recent": - case "stack_frequent": - case "stack_new": - case "stack_users": - if (!String.IsNullOrEmpty(sort)) - return BadRequest("Sort is not supported in stack mode."); - - var systemFilter = new RepositoryQuery() - .AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null) - .EnforceEventStackFilter() - .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, (PersistentEvent e) => e.Date) - .Index(ti.Range.UtcStart, ti.Range.UtcEnd); - - string stackAggregations = mode switch { - "stack_recent" => "cardinality:user sum:count~1 min:date -max:date", - "stack_frequent" => "cardinality:user -sum:count~1 min:date max:date", - "stack_new" => "cardinality:user sum:count~1 -min:date max:date", - "stack_users" => "-cardinality:user sum:count~1 min:date max:date", - _ => null - }; + return Ok(result); + } - if (mode == "stack_new") - filter = AddFirstOccurrenceFilter(ti.Range, filter); - - var countResponse = await _repository.CountAsync(q => q - .SystemFilter(systemFilter) - .FilterExpression(filter) - .EnforceEventStackFilter() - .AggregationsExpression($"terms:(stack_id~{GetSkip(page + 1, limit) + 1} {stackAggregations})")); - - var stackTerms = countResponse.Aggregations.Terms("terms_stack_id"); - if (stackTerms == null || stackTerms.Buckets.Count == 0) - return Ok(EmptyModels); - - string[] stackIds = stackTerms.Buckets.Skip(skip).Take(limit + 1).Select(t => t.Key).ToArray(); - var stacks = (await _stackRepository.GetByIdsAsync(stackIds)).Select(s => s.ApplyOffset(ti.Offset)).ToList(); - - var summaries = await GetStackSummariesAsync(stacks, stackTerms.Buckets, sf, ti); - return OkWithResourceLinks(summaries.Take(limit).ToList(), summaries.Count > limit, page); - default: - events = await GetEventsInternalAsync(sf, ti, filter, sort, page, limit, after, useSearchAfter); - return OkWithResourceLinks(events.Documents, events.HasMore && !NextPageExceedsSkipLimit(page, limit), page, events.Total); - } - } catch (ApplicationException ex) { - string message = "An error has occurred. Please check your search filter."; - if (ex is DocumentLimitExceededException) - message = $"An error has occurred. {ex.Message ?? "Please limit your search criteria."}"; - - using (_logger.BeginScope(new ExceptionlessState().Property("Search Filter", new { SystemFilter = sf, UserFilter = filter, Time = ti, Page = page, Limit = limit }).Tag("Search").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, message); - - return BadRequest(message); + private async Task>> GetInternalAsync(AppFilter sf, TimeInfo ti, string filter = null, string sort = null, string mode = null, int page = 1, int limit = 10, string after = null, bool usesPremiumFeatures = false) { + page = GetPage(page); + limit = GetLimit(limit); + int skip = GetSkip(page, limit); + if (skip > MAXIMUM_SKIP) + return Ok(EmptyModels); + + var pr = await _validator.ValidateQueryAsync(filter); + if (!pr.IsValid) + return BadRequest(pr.Message); + + sf.UsesPremiumFeatures = pr.UsesPremiumFeatures || usesPremiumFeatures; + bool useSearchAfter = !String.IsNullOrEmpty(after); + + try { + FindResults events; + switch (mode) { + case "summary": + events = await GetEventsInternalAsync(sf, ti, filter, sort, page, limit, after, useSearchAfter); + return OkWithResourceLinks(events.Documents.Select(e => { + var summaryData = _formattingPluginManager.GetEventSummaryData(e); + return new EventSummaryModel { + TemplateKey = summaryData.TemplateKey, + Id = e.Id, + Date = e.Date, + Data = summaryData.Data + }; + }).ToList(), events.HasMore && !NextPageExceedsSkipLimit(page, limit), page, events.Total); + case "stack_recent": + case "stack_frequent": + case "stack_new": + case "stack_users": + if (!String.IsNullOrEmpty(sort)) + return BadRequest("Sort is not supported in stack mode."); + + var systemFilter = new RepositoryQuery() + .AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null) + .EnforceEventStackFilter() + .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, (PersistentEvent e) => e.Date) + .Index(ti.Range.UtcStart, ti.Range.UtcEnd); + + string stackAggregations = mode switch { + "stack_recent" => "cardinality:user sum:count~1 min:date -max:date", + "stack_frequent" => "cardinality:user -sum:count~1 min:date max:date", + "stack_new" => "cardinality:user sum:count~1 -min:date max:date", + "stack_users" => "-cardinality:user sum:count~1 min:date max:date", + _ => null + }; + + if (mode == "stack_new") + filter = AddFirstOccurrenceFilter(ti.Range, filter); + + var countResponse = await _repository.CountAsync(q => q + .SystemFilter(systemFilter) + .FilterExpression(filter) + .EnforceEventStackFilter() + .AggregationsExpression($"terms:(stack_id~{GetSkip(page + 1, limit) + 1} {stackAggregations})")); + + var stackTerms = countResponse.Aggregations.Terms("terms_stack_id"); + if (stackTerms == null || stackTerms.Buckets.Count == 0) + return Ok(EmptyModels); + + string[] stackIds = stackTerms.Buckets.Skip(skip).Take(limit + 1).Select(t => t.Key).ToArray(); + var stacks = (await _stackRepository.GetByIdsAsync(stackIds)).Select(s => s.ApplyOffset(ti.Offset)).ToList(); + + var summaries = await GetStackSummariesAsync(stacks, stackTerms.Buckets, sf, ti); + return OkWithResourceLinks(summaries.Take(limit).ToList(), summaries.Count > limit, page); + default: + events = await GetEventsInternalAsync(sf, ti, filter, sort, page, limit, after, useSearchAfter); + return OkWithResourceLinks(events.Documents, events.HasMore && !NextPageExceedsSkipLimit(page, limit), page, events.Total); } } + catch (ApplicationException ex) { + string message = "An error has occurred. Please check your search filter."; + if (ex is DocumentLimitExceededException) + message = $"An error has occurred. {ex.Message ?? "Please limit your search criteria."}"; - private string AddFirstOccurrenceFilter(DateTimeRange timeRange, string filter) { - bool inverted = false; - if (filter != null && filter.StartsWith("@!")) { - inverted = true; - filter = filter.Substring(2); - } - - var sb = new StringBuilder(); - if (inverted) - sb.Append("@!"); - - sb.Append("first_occurrence:["); - sb.Append((long)timeRange.UtcStart.Subtract(DateTime.UnixEpoch).TotalMilliseconds); - sb.Append(" TO "); - sb.Append((long)timeRange.UtcEnd.Subtract(DateTime.UnixEpoch).TotalMilliseconds); - sb.Append(']'); + using (_logger.BeginScope(new ExceptionlessState().Property("Search Filter", new { SystemFilter = sf, UserFilter = filter, Time = ti, Page = page, Limit = limit }).Tag("Search").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, message); - if (String.IsNullOrEmpty(filter)) - return sb.ToString(); + return BadRequest(message); + } + } - sb.Append(' '); + private string AddFirstOccurrenceFilter(DateTimeRange timeRange, string filter) { + bool inverted = false; + if (filter != null && filter.StartsWith("@!")) { + inverted = true; + filter = filter.Substring(2); + } - bool isGrouped = filter.StartsWith('(') && filter.EndsWith(')'); + var sb = new StringBuilder(); + if (inverted) + sb.Append("@!"); - if (isGrouped) - sb.Append(filter); - else - sb.Append('(').Append(filter).Append(')'); + sb.Append("first_occurrence:["); + sb.Append((long)timeRange.UtcStart.Subtract(DateTime.UnixEpoch).TotalMilliseconds); + sb.Append(" TO "); + sb.Append((long)timeRange.UtcEnd.Subtract(DateTime.UnixEpoch).TotalMilliseconds); + sb.Append(']'); + if (String.IsNullOrEmpty(filter)) return sb.ToString(); - } - private Task> GetEventsInternalAsync(AppFilter sf, TimeInfo ti, string filter, string sort, int page, int limit, string after, bool useSearchAfter) { - if (String.IsNullOrEmpty(sort)) - sort = "-date"; + sb.Append(' '); - return _repository.FindAsync(q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null).FilterExpression(filter).EnforceEventStackFilter().SortExpression(sort).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field), - o => useSearchAfter - ? o.SearchAfterPaging().SearchAfter(after).PageLimit(limit) - : o.PageNumber(page).PageLimit(limit)); - } + bool isGrouped = filter.StartsWith('(') && filter.EndsWith(')'); - /// - /// Get by organization - /// - /// The identifier of the organization. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - /// The organization could not be found. - /// Unable to view event occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/events")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByOrganizationAsync(string organizationId = null, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var organization = await GetOrganizationAsync(organizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organization); - return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); - } + if (isGrouped) + sb.Append(filter); + else + sb.Append('(').Append(filter).Append(')'); - /// - /// Get by project - /// - /// The identifier of the project. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - /// The project could not be found. - /// Unable to view event occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByProjectAsync(string projectId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); - - var organization = await GetOrganizationAsync(project.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); - var sf = new AppFilter(project, organization); - return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); - } + return sb.ToString(); + } - /// - /// Get by stack - /// - /// The identifier of the stack. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - /// The stack could not be found. - /// Unable to view event occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/stacks/{stackId:objectid}/events")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByStackAsync(string stackId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var stack = await GetStackAsync(stackId); - if (stack == null) - return NotFound(); - - var organization = await GetOrganizationAsync(stack.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(stack, _appOptions.MaximumRetentionDays)); - var sf = new AppFilter(stack, organization); - return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); - } + private Task> GetEventsInternalAsync(AppFilter sf, TimeInfo ti, string filter, string sort, int page, int limit, string after, bool useSearchAfter) { + if (String.IsNullOrEmpty(sort)) + sort = "-date"; - /// - /// Get by reference id - /// - /// An identifier used that references an event instance. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - [HttpGet("by-ref/{referenceId:identifier}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByReferenceIdAsync(string referenceId, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository); - if (organizations.All(o => o.IsSuspended)) - return Ok(EmptyModels); - - var ti = GetTimeInfo(null, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; - return await GetInternalAsync(sf, ti, String.Concat("reference:", referenceId), null, mode, page, limit, after); - } + return _repository.FindAsync(q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null).FilterExpression(filter).EnforceEventStackFilter().SortExpression(sort).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field), + o => useSearchAfter + ? o.SearchAfterPaging().SearchAfter(after).PageLimit(limit) + : o.PageNumber(page).PageLimit(limit)); + } - /// - /// Get by reference id - /// - /// An identifier used that references an event instance. - /// The identifier of the project. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - /// The project could not be found. - /// Unable to view event occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/by-ref/{referenceId:identifier}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByReferenceIdAsync(string referenceId, string projectId, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); - - var organization = await GetOrganizationAsync(project.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(null, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); - var sf = new AppFilter(project, organization); - return await GetInternalAsync(sf, ti, String.Concat("reference:", referenceId), null, mode, page, limit, after); - } + /// + /// Get by organization + /// + /// The identifier of the organization. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + /// The organization could not be found. + /// Unable to view event occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/events")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByOrganizationAsync(string organizationId = null, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var organization = await GetOrganizationAsync(organizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organization); + return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); + } - /// - /// Get a list of all sessions or events by a session id - /// - /// An identifier that represents a session of events. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - [HttpGet("sessions/{sessionId:identifier}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetBySessionIdAsync(string sessionId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); - if (organizations.All(o => o.IsSuspended)) - return Ok(EmptyModels); - - var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; - return await GetInternalAsync(sf, ti, $"(reference:{sessionId} OR ref.session:{sessionId}) {filter}", sort, mode, page, limit, after, true); - } + /// + /// Get by project + /// + /// The identifier of the project. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + /// The project could not be found. + /// Unable to view event occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByProjectAsync(string projectId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + var organization = await GetOrganizationAsync(project.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); + var sf = new AppFilter(project, organization); + return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); + } - /// - /// Get a list of by a session id - /// - /// An identifier that represents a session of events. - /// The identifier of the project. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - /// The project could not be found. - /// Unable to view event occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/sessions/{sessionId:identifier}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetBySessionIdAndProjectAsync(string sessionId, string projectId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); - - var organization = await GetOrganizationAsync(project.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); - var sf = new AppFilter(project, organization); - return await GetInternalAsync(sf, ti, $"(reference:{sessionId} OR ref.session:{sessionId}) {filter}", sort, mode, page, limit, after, true); - } + /// + /// Get by stack + /// + /// The identifier of the stack. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + /// The stack could not be found. + /// Unable to view event occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/stacks/{stackId:objectid}/events")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByStackAsync(string stackId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var stack = await GetStackAsync(stackId); + if (stack == null) + return NotFound(); + + var organization = await GetOrganizationAsync(stack.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(stack, _appOptions.MaximumRetentionDays)); + var sf = new AppFilter(stack, organization); + return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit, after); + } - /// - /// Get a list of all sessions - /// - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - [HttpGet("sessions")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetSessionsAsync(string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); - if (organizations.All(o => o.IsSuspended)) - return Ok(EmptyModels); - - var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; - return await GetInternalAsync(sf, ti, $"type:{Event.KnownTypes.Session} {filter}", sort, mode, page, limit, after, true); - } + /// + /// Get by reference id + /// + /// An identifier used that references an event instance. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + [HttpGet("by-ref/{referenceId:identifier}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByReferenceIdAsync(string referenceId, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository); + if (organizations.All(o => o.IsSuspended)) + return Ok(EmptyModels); + + var ti = GetTimeInfo(null, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; + return await GetInternalAsync(sf, ti, String.Concat("reference:", referenceId), null, mode, page, limit, after); + } - /// - /// Get a list of all sessions - /// - /// The identifier of the organization. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - /// The project could not be found. - /// Unable to view event occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/events/sessions")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetSessionByOrganizationAsync(string organizationId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var organization = await GetOrganizationAsync(organizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); - var sf = new AppFilter(organization); - return await GetInternalAsync(sf, ti, $"type:{Event.KnownTypes.Session} {filter}", sort, mode, page, limit, after, true); - } + /// + /// Get by reference id + /// + /// An identifier used that references an event instance. + /// The identifier of the project. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + /// The project could not be found. + /// Unable to view event occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/by-ref/{referenceId:identifier}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByReferenceIdAsync(string referenceId, string projectId, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + var organization = await GetOrganizationAsync(project.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(null, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); + var sf = new AppFilter(project, organization); + return await GetInternalAsync(sf, ti, String.Concat("reference:", referenceId), null, mode, page, limit, after); + } - /// - /// Get a list of all sessions - /// - /// The identifier of the project. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. - /// Invalid filter. - /// The project could not be found. - /// Unable to view event occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/sessions")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetSessionByProjectAsync(string projectId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); - - var organization = await GetOrganizationAsync(project.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view event occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); - var sf = new AppFilter(project, organization); - return await GetInternalAsync(sf, ti, $"type:{Event.KnownTypes.Session} {filter}", sort, mode, page, limit, after, true); - } + /// + /// Get a list of all sessions or events by a session id + /// + /// An identifier that represents a session of events. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + [HttpGet("sessions/{sessionId:identifier}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetBySessionIdAsync(string sessionId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); + if (organizations.All(o => o.IsSuspended)) + return Ok(EmptyModels); + + var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; + return await GetInternalAsync(sf, ti, $"(reference:{sessionId} OR ref.session:{sessionId}) {filter}", sort, mode, page, limit, after, true); + } - /// - /// Set user description - /// - /// You can also save an end users contact information and a description of the event. This is really useful for error events as a user can specify reproduction steps in the description. - /// An identifier used that references an event instance. - /// The user description. - /// The identifier of the project. - /// Description must be specified. - /// The event occurrence with the specified reference id could not be found. - [HttpPost("by-ref/{referenceId:identifier}/user-description")] - [HttpPost("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/by-ref/{referenceId:identifier}/user-description")] - [Consumes("application/json")] - [ConfigurationResponseFilter] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public async Task SetUserDescriptionAsync(string referenceId, UserDescription description, string projectId = null) { - if (String.IsNullOrEmpty(referenceId)) - return NotFound(); - - if (description == null) - return BadRequest("Description must be specified."); - - if (projectId == null) - projectId = Request.GetDefaultProjectId(); - - // must have a project id - if (String.IsNullOrEmpty(projectId)) - return BadRequest("No project id specified and no default project was found."); - - var result = await _userDescriptionValidator.ValidateAsync(description); - if (!result.IsValid) - return BadRequest(result.Errors.ToErrorMessage()); - - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); + /// + /// Get a list of by a session id + /// + /// An identifier that represents a session of events. + /// The identifier of the project. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + /// The project could not be found. + /// Unable to view event occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/sessions/{sessionId:identifier}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetBySessionIdAndProjectAsync(string sessionId, string projectId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + var organization = await GetOrganizationAsync(project.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); + var sf = new AppFilter(project, organization); + return await GetInternalAsync(sf, ti, $"(reference:{sessionId} OR ref.session:{sessionId}) {filter}", sort, mode, page, limit, after, true); + } - // Set the project for the configuration response filter. - Request.SetProject(project); + /// + /// Get a list of all sessions + /// + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + [HttpGet("sessions")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetSessionsAsync(string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); + if (organizations.All(o => o.IsSuspended)) + return Ok(EmptyModels); + + var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; + return await GetInternalAsync(sf, ti, $"type:{Event.KnownTypes.Session} {filter}", sort, mode, page, limit, after, true); + } - var eventUserDescription = await MapAsync(description); - eventUserDescription.ProjectId = projectId; - eventUserDescription.ReferenceId = referenceId; + /// + /// Get a list of all sessions + /// + /// The identifier of the organization. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + /// The project could not be found. + /// Unable to view event occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/events/sessions")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetSessionByOrganizationAsync(string organizationId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var organization = await GetOrganizationAsync(organizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_appOptions.MaximumRetentionDays)); + var sf = new AppFilter(organization); + return await GetInternalAsync(sf, ti, $"type:{Event.KnownTypes.Session} {filter}", sort, mode, page, limit, after, true); + } - await _eventUserDescriptionQueue.EnqueueAsync(eventUserDescription); - return StatusCode(StatusCodes.Status202Accepted); - } + /// + /// Get a list of all sessions + /// + /// The identifier of the project. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole event object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The after parameter is a cursor used for pagination and defines your place in the list of results. Pass in the last event id in the previous call to fetch the next page of the list. + /// Invalid filter. + /// The project could not be found. + /// Unable to view event occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/sessions")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetSessionByProjectAsync(string projectId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10, string after = null) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + var organization = await GetOrganizationAsync(project.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view event occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _appOptions.MaximumRetentionDays)); + var sf = new AppFilter(project, organization); + return await GetInternalAsync(sf, ti, $"type:{Event.KnownTypes.Session} {filter}", sort, mode, page, limit, after, true); + } - [Obsolete] - [HttpPatch("~/api/v1/error/{id:objectid}")] - [Consumes("application/json")] - [ConfigurationResponseFilter] - public async Task LegacyPatchAsync(string id, Delta changes) { - if (changes == null) - return Ok(); + /// + /// Set user description + /// + /// You can also save an end users contact information and a description of the event. This is really useful for error events as a user can specify reproduction steps in the description. + /// An identifier used that references an event instance. + /// The user description. + /// The identifier of the project. + /// Description must be specified. + /// The event occurrence with the specified reference id could not be found. + [HttpPost("by-ref/{referenceId:identifier}/user-description")] + [HttpPost("~/" + API_PREFIX + "/projects/{projectId:objectid}/events/by-ref/{referenceId:identifier}/user-description")] + [Consumes("application/json")] + [ConfigurationResponseFilter] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public async Task SetUserDescriptionAsync(string referenceId, UserDescription description, string projectId = null) { + if (String.IsNullOrEmpty(referenceId)) + return NotFound(); + + if (description == null) + return BadRequest("Description must be specified."); + + if (projectId == null) + projectId = Request.GetDefaultProjectId(); + + // must have a project id + if (String.IsNullOrEmpty(projectId)) + return BadRequest("No project id specified and no default project was found."); + + var result = await _userDescriptionValidator.ValidateAsync(description); + if (!result.IsValid) + return BadRequest(result.Errors.ToErrorMessage()); + + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + // Set the project for the configuration response filter. + Request.SetProject(project); + + var eventUserDescription = await MapAsync(description); + eventUserDescription.ProjectId = projectId; + eventUserDescription.ReferenceId = referenceId; + + await _eventUserDescriptionQueue.EnqueueAsync(eventUserDescription); + return StatusCode(StatusCodes.Status202Accepted); + } - if (changes.UnknownProperties.TryGetValue("UserEmail", out object value)) - changes.TrySetPropertyValue("EmailAddress", value); - if (changes.UnknownProperties.TryGetValue("UserDescription", out value)) - changes.TrySetPropertyValue("Description", value); + [Obsolete] + [HttpPatch("~/api/v1/error/{id:objectid}")] + [Consumes("application/json")] + [ConfigurationResponseFilter] + public async Task LegacyPatchAsync(string id, Delta changes) { + if (changes == null) + return Ok(); - var userDescription = new UserDescription(); - changes.Patch(userDescription); + if (changes.UnknownProperties.TryGetValue("UserEmail", out object value)) + changes.TrySetPropertyValue("EmailAddress", value); + if (changes.UnknownProperties.TryGetValue("UserDescription", out value)) + changes.TrySetPropertyValue("Description", value); - return await SetUserDescriptionAsync(id, userDescription); - } + var userDescription = new UserDescription(); + changes.Patch(userDescription); - /// - /// Submit heartbeat - /// - /// The session id or user id. - /// If true, the session will be closed. - /// OK - /// No project id specified and no default project was found. - /// No project was found. - [HttpGet("session/heartbeat")] - public async Task RecordHeartbeatAsync(string id = null, bool close = false) { - if (_appOptions.EventSubmissionDisabled || String.IsNullOrEmpty(id)) - return Ok(); - - string projectId = Request.GetDefaultProjectId(); - if (String.IsNullOrEmpty(projectId)) - return BadRequest("No project id specified and no default project was found."); - - string identityHash = id.ToSHA1(); - string heartbeatCacheKey = String.Concat("Project:", projectId, ":heartbeat:", identityHash); - try { - await Task.WhenAll( - _cache.SetAsync(heartbeatCacheKey, SystemClock.UtcNow, TimeSpan.FromHours(2)), - close ? _cache.SetAsync(String.Concat(heartbeatCacheKey, "-close"), true, TimeSpan.FromHours(2)) : Task.CompletedTask - ); - } catch (Exception ex) { - if (projectId != _appOptions.InternalProjectId) { - using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Identity(CurrentUser?.EmailAddress).Property("User", CurrentUser).Property("Id", id).Property("Close", close).SetHttpContext(HttpContext))) - _logger.LogError(ex, "Error enqueuing session heartbeat."); - } - - return StatusCode(StatusCodes.Status500InternalServerError); - } + return await SetUserDescriptionAsync(id, userDescription); + } + /// + /// Submit heartbeat + /// + /// The session id or user id. + /// If true, the session will be closed. + /// OK + /// No project id specified and no default project was found. + /// No project was found. + [HttpGet("session/heartbeat")] + public async Task RecordHeartbeatAsync(string id = null, bool close = false) { + if (_appOptions.EventSubmissionDisabled || String.IsNullOrEmpty(id)) return Ok(); - } - [Obsolete] - [HttpGet("~/api/v1/events/submit")] - [HttpGet("~/api/v1/events/submit/{type:minlength(1)}")] - [HttpGet("~/api/v1/projects/{projectId:objectid}/events/submit")] - [HttpGet("~/api/v1/projects/{projectId:objectid}/events/submit/{type:minlength(1)}")] - [ConfigurationResponseFilter] - public Task GetSubmitEventV1Async(string projectId = null, string type = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { - return GetSubmitEventAsync(projectId, 1, type, userAgent, parameters); + string projectId = Request.GetDefaultProjectId(); + if (String.IsNullOrEmpty(projectId)) + return BadRequest("No project id specified and no default project was found."); + + string identityHash = id.ToSHA1(); + string heartbeatCacheKey = String.Concat("Project:", projectId, ":heartbeat:", identityHash); + try { + await Task.WhenAll( + _cache.SetAsync(heartbeatCacheKey, SystemClock.UtcNow, TimeSpan.FromHours(2)), + close ? _cache.SetAsync(String.Concat(heartbeatCacheKey, "-close"), true, TimeSpan.FromHours(2)) : Task.CompletedTask + ); } + catch (Exception ex) { + if (projectId != _appOptions.InternalProjectId) { + using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Identity(CurrentUser?.EmailAddress).Property("User", CurrentUser).Property("Id", id).Property("Close", close).SetHttpContext(HttpContext))) + _logger.LogError(ex, "Error enqueuing session heartbeat."); + } - /// - /// Submit event by GET - /// - /// - /// You can submit an event using an HTTP GET and query string parameters. Any unknown query string parameters will be added to the extended data of the event. - /// - /// Feature usage named build with a duration of 10: - /// - /// - /// Log with message, geo and extended data - /// - /// - /// The event type (ie. error, log message, feature usage). - /// The event source (ie. machine name, log name, feature name). - /// The event message. - /// An optional identifier to be used for referencing this event instance at a later time. - /// The date that the event occurred on. - /// The number of duplicated events. - /// The value of the event if any. - /// The geo coordinates where the event happened. - /// A list of tags used to categorize this event (comma separated). - /// The user's identity that the event happened to. - /// The user's friendly name that the event happened to. - /// The user agent that submitted the event. - /// Query string parameters that control what properties are set on the event - /// OK - /// No project id specified and no default project was found. - /// No project was found. - [HttpGet("submit")] - [ConfigurationResponseFilter] - public Task GetSubmitEventV2Async(string type = null, string source = null, string message = null, string reference = null, - string date = null, int? count = null, decimal? value = null, string geo = null, string tags = null, string identity = null, - string identityname = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { - return GetSubmitEventAsync(null, 2, null, userAgent, parameters); + return StatusCode(StatusCodes.Status500InternalServerError); } - /// - /// Submit event type by GET - /// - /// - /// You can submit an event using an HTTP GET and query string parameters. - /// - /// Feature usage event named build with a value of 10: - /// - /// - /// Log event with message, geo and extended data - /// - /// - /// The event type (ie. error, log message, feature usage). - /// The event source (ie. machine name, log name, feature name). - /// The event message. - /// An optional identifier to be used for referencing this event instance at a later time. - /// The date that the event occurred on. - /// The number of duplicated events. - /// The value of the event if any. - /// The geo coordinates where the event happened. - /// A list of tags used to categorize this event (comma separated). - /// The user's identity that the event happened to. - /// The user's friendly name that the event happened to. - /// The user agent that submitted the event. - /// Query string parameters that control what properties are set on the event - /// OK - /// No project id specified and no default project was found. - /// No project was found. - [HttpGet("submit/{type:minlength(1)}")] - [ConfigurationResponseFilter] - public Task GetSubmitEventByTypeV2Async(string type, string source = null, string message = null, string reference = null, - string date = null, int? count = null, decimal? value = null, string geo = null, string tags = null, string identity = null, - string identityname = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { - return GetSubmitEventAsync(null, 2, type, userAgent, parameters); - } + return Ok(); + } - /// - /// Submit event type by GET for a specific project - /// - /// - /// You can submit an event using an HTTP GET and query string parameters. - /// - /// Feature usage named build with a duration of 10: - /// - /// - /// Log with message, geo and extended data - /// - /// - /// The identifier of the project. - /// The event type (ie. error, log message, feature usage). - /// The event source (ie. machine name, log name, feature name). - /// The event message. - /// An optional identifier to be used for referencing this event instance at a later time. - /// The date that the event occurred on. - /// The number of duplicated events. - /// The value of the event if any. - /// The geo coordinates where the event happened. - /// A list of tags used to categorize this event (comma separated). - /// The user's identity that the event happened to. - /// The user's friendly name that the event happened to. - /// The user agent that submitted the event. - /// Query String parameters that control what properties are set on the event - /// OK - /// No project id specified and no default project was found. - /// No project was found. - [HttpGet("~/api/v2/projects/{projectId:objectid}/events/submit")] - [HttpGet("~/api/v2/projects/{projectId:objectid}/events/submit/{type:minlength(1)}")] - [ConfigurationResponseFilter] - public Task GetSubmitEventByProjectV2Async(string projectId, string type = null, string source = null, string message = null, string reference = null, - string date = null, int? count = null, decimal? value = null, string geo = null, string tags = null, string identity = null, - string identityname = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { - return GetSubmitEventAsync(projectId, 2, type, userAgent, parameters); - } + [Obsolete] + [HttpGet("~/api/v1/events/submit")] + [HttpGet("~/api/v1/events/submit/{type:minlength(1)}")] + [HttpGet("~/api/v1/projects/{projectId:objectid}/events/submit")] + [HttpGet("~/api/v1/projects/{projectId:objectid}/events/submit/{type:minlength(1)}")] + [ConfigurationResponseFilter] + public Task GetSubmitEventV1Async(string projectId = null, string type = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { + return GetSubmitEventAsync(projectId, 1, type, userAgent, parameters); + } - private async Task GetSubmitEventAsync(string projectId = null, int apiVersion = 2, string type = null, string userAgent = null, IQueryCollection parameters = null) { - var filteredParameters = parameters?.Where(p => !String.IsNullOrEmpty(p.Key) && !p.Value.All(String.IsNullOrEmpty) && !_ignoredKeys.Contains(p.Key)).ToList(); - if (filteredParameters == null || filteredParameters.Count == 0) - return Ok(); + /// + /// Submit event by GET + /// + /// + /// You can submit an event using an HTTP GET and query string parameters. Any unknown query string parameters will be added to the extended data of the event. + /// + /// Feature usage named build with a duration of 10: + /// + /// + /// Log with message, geo and extended data + /// + /// + /// The event type (ie. error, log message, feature usage). + /// The event source (ie. machine name, log name, feature name). + /// The event message. + /// An optional identifier to be used for referencing this event instance at a later time. + /// The date that the event occurred on. + /// The number of duplicated events. + /// The value of the event if any. + /// The geo coordinates where the event happened. + /// A list of tags used to categorize this event (comma separated). + /// The user's identity that the event happened to. + /// The user's friendly name that the event happened to. + /// The user agent that submitted the event. + /// Query string parameters that control what properties are set on the event + /// OK + /// No project id specified and no default project was found. + /// No project was found. + [HttpGet("submit")] + [ConfigurationResponseFilter] + public Task GetSubmitEventV2Async(string type = null, string source = null, string message = null, string reference = null, + string date = null, int? count = null, decimal? value = null, string geo = null, string tags = null, string identity = null, + string identityname = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { + return GetSubmitEventAsync(null, 2, null, userAgent, parameters); + } - if (projectId == null) - projectId = Request.GetDefaultProjectId(); + /// + /// Submit event type by GET + /// + /// + /// You can submit an event using an HTTP GET and query string parameters. + /// + /// Feature usage event named build with a value of 10: + /// + /// + /// Log event with message, geo and extended data + /// + /// + /// The event type (ie. error, log message, feature usage). + /// The event source (ie. machine name, log name, feature name). + /// The event message. + /// An optional identifier to be used for referencing this event instance at a later time. + /// The date that the event occurred on. + /// The number of duplicated events. + /// The value of the event if any. + /// The geo coordinates where the event happened. + /// A list of tags used to categorize this event (comma separated). + /// The user's identity that the event happened to. + /// The user's friendly name that the event happened to. + /// The user agent that submitted the event. + /// Query string parameters that control what properties are set on the event + /// OK + /// No project id specified and no default project was found. + /// No project was found. + [HttpGet("submit/{type:minlength(1)}")] + [ConfigurationResponseFilter] + public Task GetSubmitEventByTypeV2Async(string type, string source = null, string message = null, string reference = null, + string date = null, int? count = null, decimal? value = null, string geo = null, string tags = null, string identity = null, + string identityname = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { + return GetSubmitEventAsync(null, 2, type, userAgent, parameters); + } - // must have a project id - if (String.IsNullOrEmpty(projectId)) - return BadRequest("No project id specified and no default project was found."); + /// + /// Submit event type by GET for a specific project + /// + /// + /// You can submit an event using an HTTP GET and query string parameters. + /// + /// Feature usage named build with a duration of 10: + /// + /// + /// Log with message, geo and extended data + /// + /// + /// The identifier of the project. + /// The event type (ie. error, log message, feature usage). + /// The event source (ie. machine name, log name, feature name). + /// The event message. + /// An optional identifier to be used for referencing this event instance at a later time. + /// The date that the event occurred on. + /// The number of duplicated events. + /// The value of the event if any. + /// The geo coordinates where the event happened. + /// A list of tags used to categorize this event (comma separated). + /// The user's identity that the event happened to. + /// The user's friendly name that the event happened to. + /// The user agent that submitted the event. + /// Query String parameters that control what properties are set on the event + /// OK + /// No project id specified and no default project was found. + /// No project was found. + [HttpGet("~/api/v2/projects/{projectId:objectid}/events/submit")] + [HttpGet("~/api/v2/projects/{projectId:objectid}/events/submit/{type:minlength(1)}")] + [ConfigurationResponseFilter] + public Task GetSubmitEventByProjectV2Async(string projectId, string type = null, string source = null, string message = null, string reference = null, + string date = null, int? count = null, decimal? value = null, string geo = null, string tags = null, string identity = null, + string identityname = null, [FromHeader][UserAgent] string userAgent = null, [FromQuery][QueryStringParameters] IQueryCollection parameters = null) { + return GetSubmitEventAsync(projectId, 2, type, userAgent, parameters); + } - var project = Request.GetProject(); - if (!String.Equals(project?.Id, projectId)) { - _logger.ProjectRouteDoesNotMatch(project?.Id, projectId); + private async Task GetSubmitEventAsync(string projectId = null, int apiVersion = 2, string type = null, string userAgent = null, IQueryCollection parameters = null) { + var filteredParameters = parameters?.Where(p => !String.IsNullOrEmpty(p.Key) && !p.Value.All(String.IsNullOrEmpty) && !_ignoredKeys.Contains(p.Key)).ToList(); + if (filteredParameters == null || filteredParameters.Count == 0) + return Ok(); - project = await GetProjectAsync(projectId); + if (projectId == null) + projectId = Request.GetDefaultProjectId(); - // Set the project for the configuration response filter. - Request.SetProject(project); - } + // must have a project id + if (String.IsNullOrEmpty(projectId)) + return BadRequest("No project id specified and no default project was found."); - if (project == null) - return NotFound(); - - string contentEncoding = Request.Headers.TryGetAndReturn(Headers.ContentEncoding); - var ev = new Event { Type = !String.IsNullOrEmpty(type) ? type : Event.KnownTypes.Log }; - - string identity = null; - string identityName = null; - - var exclusions = project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.DataExclusions).ToList(); - foreach (var kvp in filteredParameters) { - switch (kvp.Key.ToLowerInvariant()) { - case "type": - ev.Type = kvp.Value.FirstOrDefault(); - break; - case "source": - ev.Source = kvp.Value.FirstOrDefault(); - break; - case "message": - ev.Message = kvp.Value.FirstOrDefault(); - break; - case "reference": - ev.ReferenceId = kvp.Value.FirstOrDefault(); - break; - case "date": - if (DateTimeOffset.TryParse(kvp.Value.FirstOrDefault(), out var dtValue)) - ev.Date = dtValue; - break; - case "count": - if (Int32.TryParse(kvp.Value.FirstOrDefault(), out int intValue)) - ev.Count = intValue; - break; - case "value": - if (Decimal.TryParse(kvp.Value.FirstOrDefault(), out decimal decValue)) - ev.Value = decValue; - break; - case "geo": - if (GeoResult.TryParse(kvp.Value.FirstOrDefault(), out var geo)) - ev.Geo = geo.ToString(); - break; - case "tags": - ev.Tags.AddRange(kvp.Value.SelectMany(t => t.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)).Distinct()); - break; - case "identity": - identity = kvp.Value.FirstOrDefault(); - break; - case "identity.name": - identityName = kvp.Value.FirstOrDefault(); - break; - default: - if (kvp.Key.AnyWildcardMatches(exclusions, true)) - continue; - - if (kvp.Value.Count > 1) - ev.Data[kvp.Key] = kvp.Value; - else - ev.Data[kvp.Key] = kvp.Value.FirstOrDefault(); - - break; - } - } + var project = Request.GetProject(); + if (!String.Equals(project?.Id, projectId)) { + _logger.ProjectRouteDoesNotMatch(project?.Id, projectId); - ev.SetUserIdentity(identity, identityName); - - try { - string mediaType = String.Empty; - string charSet = String.Empty; - if (Request.ContentType != null && MediaTypeHeaderValue.TryParse(Request.ContentType, out var contentTypeHeader)) { - mediaType = contentTypeHeader.MediaType.ToString(); - charSet = contentTypeHeader.Charset.ToString(); - } - - var stream = new MemoryStream(ev.GetBytes(_jsonSerializerSettings)); - await _eventPostService.EnqueueAsync(new EventPost(_appOptions.EnableArchive) { - ApiVersion = apiVersion, - CharSet = charSet, - ContentEncoding = contentEncoding, - IpAddress = Request.GetClientIpAddress(), - MediaType = mediaType, - OrganizationId = project.OrganizationId, - ProjectId = project.Id, - UserAgent = userAgent - }, stream); - } catch (Exception ex) { - if (projectId != _appOptions.InternalProjectId) { - using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Identity(CurrentUser?.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, "Error enqueuing event post."); - } - - return StatusCode(StatusCodes.Status500InternalServerError); - } + project = await GetProjectAsync(projectId); - return Ok(); + // Set the project for the configuration response filter. + Request.SetProject(project); } - [Obsolete] - [HttpPost("~/api/v1/error")] - [Consumes("application/json", "text/plain")] - [RequestBodyContentAttribute] - [ConfigurationResponseFilter] - public Task LegacyPostAsync([FromHeader][UserAgent] string userAgent = null) { - return PostAsync(null, 1, userAgent); + if (project == null) + return NotFound(); + + string contentEncoding = Request.Headers.TryGetAndReturn(Headers.ContentEncoding); + var ev = new Event { Type = !String.IsNullOrEmpty(type) ? type : Event.KnownTypes.Log }; + + string identity = null; + string identityName = null; + + var exclusions = project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.DataExclusions).ToList(); + foreach (var kvp in filteredParameters) { + switch (kvp.Key.ToLowerInvariant()) { + case "type": + ev.Type = kvp.Value.FirstOrDefault(); + break; + case "source": + ev.Source = kvp.Value.FirstOrDefault(); + break; + case "message": + ev.Message = kvp.Value.FirstOrDefault(); + break; + case "reference": + ev.ReferenceId = kvp.Value.FirstOrDefault(); + break; + case "date": + if (DateTimeOffset.TryParse(kvp.Value.FirstOrDefault(), out var dtValue)) + ev.Date = dtValue; + break; + case "count": + if (Int32.TryParse(kvp.Value.FirstOrDefault(), out int intValue)) + ev.Count = intValue; + break; + case "value": + if (Decimal.TryParse(kvp.Value.FirstOrDefault(), out decimal decValue)) + ev.Value = decValue; + break; + case "geo": + if (GeoResult.TryParse(kvp.Value.FirstOrDefault(), out var geo)) + ev.Geo = geo.ToString(); + break; + case "tags": + ev.Tags.AddRange(kvp.Value.SelectMany(t => t.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)).Distinct()); + break; + case "identity": + identity = kvp.Value.FirstOrDefault(); + break; + case "identity.name": + identityName = kvp.Value.FirstOrDefault(); + break; + default: + if (kvp.Key.AnyWildcardMatches(exclusions, true)) + continue; + + if (kvp.Value.Count > 1) + ev.Data[kvp.Key] = kvp.Value; + else + ev.Data[kvp.Key] = kvp.Value.FirstOrDefault(); + + break; + } } - [Obsolete] - [HttpPost("~/api/v1/events")] - [HttpPost("~/api/v1/projects/{projectId:objectid}/events")] - [Consumes("application/json", "text/plain")] - [RequestBodyContentAttribute] - [ConfigurationResponseFilter] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task PostV1Async(string projectId = null, [FromHeader][UserAgent]string userAgent = null) { - return PostAsync(projectId, 1, userAgent); - } + ev.SetUserIdentity(identity, identityName); + + try { + string mediaType = String.Empty; + string charSet = String.Empty; + if (Request.ContentType != null && MediaTypeHeaderValue.TryParse(Request.ContentType, out var contentTypeHeader)) { + mediaType = contentTypeHeader.MediaType.ToString(); + charSet = contentTypeHeader.Charset.ToString(); + } - /// - /// Submit event by POST - /// - /// - /// You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it - /// we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON - /// object into the events data collection. - /// - /// You can also post a multi-line string. We automatically split strings by the \n character and create a new log event for every line. - /// - /// Simple event: - /// - /// { "message": "Exceptionless is amazing!" } - /// - /// - /// Simple log event with user identity: - /// - /// { - /// "type": "log", - /// "message": "Exceptionless is amazing!", - /// "date":"2030-01-01T12:00:00.0000000-05:00", - /// "@user":{ "identity":"123456789", "name": "Test User" } - /// } - /// - /// - /// Multiple events from string content: - /// - /// Exceptionless is amazing! - /// Exceptionless is really amazing! - /// - /// - /// Simple error: - /// - /// { - /// "type": "error", - /// "date":"2030-01-01T12:00:00.0000000-05:00", - /// "@simple_error": { - /// "message": "Simple Exception", - /// "type": "System.Exception", - /// "stack_trace": " at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77" - /// } - /// } - /// - /// - /// The user agent that submitted the event. - /// Accepted - /// No project id specified and no default project was found. - /// No project was found. - [HttpPost] - [Consumes("application/json", "text/plain")] - [RequestBodyContentAttribute] - [ConfigurationResponseFilter] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task PostV2Async([FromHeader][UserAgent]string userAgent = null) { - return PostAsync(null, 2, userAgent); + var stream = new MemoryStream(ev.GetBytes(_jsonSerializerSettings)); + await _eventPostService.EnqueueAsync(new EventPost(_appOptions.EnableArchive) { + ApiVersion = apiVersion, + CharSet = charSet, + ContentEncoding = contentEncoding, + IpAddress = Request.GetClientIpAddress(), + MediaType = mediaType, + OrganizationId = project.OrganizationId, + ProjectId = project.Id, + UserAgent = userAgent + }, stream); } + catch (Exception ex) { + if (projectId != _appOptions.InternalProjectId) { + using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Identity(CurrentUser?.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, "Error enqueuing event post."); + } - /// - /// Submit event by POST for a specific project - /// - /// - /// You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it - /// we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON - /// object into the events data collection. - /// - /// You can also post a multi-line string. We automatically split strings by the \n character and create a new log event for every line. - /// - /// Simple event: - /// - /// { "message": "Exceptionless is amazing!" } - /// - /// - /// Simple log event with user identity: - /// - /// { - /// "type": "log", - /// "message": "Exceptionless is amazing!", - /// "date":"2030-01-01T12:00:00.0000000-05:00", - /// "@user":{ "identity":"123456789", "name": "Test User" } - /// } - /// - /// - /// Multiple events from string content: - /// - /// Exceptionless is amazing! - /// Exceptionless is really amazing! - /// - /// - /// Simple error: - /// - /// { - /// "type": "error", - /// "date":"2030-01-01T12:00:00.0000000-05:00", - /// "@simple_error": { - /// "message": "Simple Exception", - /// "type": "System.Exception", - /// "stack_trace": " at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77" - /// } - /// } - /// - /// - /// The identifier of the project. - /// The user agent that submitted the event. - /// Accepted - /// No project id specified and no default project was found. - /// No project was found. - [HttpPost("~/api/v2/projects/{projectId:objectid}/events")] - [Consumes("application/json", "text/plain")] - [RequestBodyContentAttribute] - [ConfigurationResponseFilter] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task PostByProjectV2Async(string projectId = null, [FromHeader][UserAgent]string userAgent = null) { - return PostAsync(projectId, 2, userAgent); + return StatusCode(StatusCodes.Status500InternalServerError); } - private async Task PostAsync(string projectId = null, int apiVersion = 2, [FromHeader][UserAgent]string userAgent = null) { - if (Request.ContentLength.HasValue && Request.ContentLength.Value <= 0) - return StatusCode(StatusCodes.Status202Accepted); + return Ok(); + } - if (projectId == null) - projectId = Request.GetDefaultProjectId(); + [Obsolete] + [HttpPost("~/api/v1/error")] + [Consumes("application/json", "text/plain")] + [RequestBodyContentAttribute] + [ConfigurationResponseFilter] + public Task LegacyPostAsync([FromHeader][UserAgent] string userAgent = null) { + return PostAsync(null, 1, userAgent); + } - // must have a project id - if (String.IsNullOrEmpty(projectId)) - return BadRequest("No project id specified and no default project was found."); + [Obsolete] + [HttpPost("~/api/v1/events")] + [HttpPost("~/api/v1/projects/{projectId:objectid}/events")] + [Consumes("application/json", "text/plain")] + [RequestBodyContentAttribute] + [ConfigurationResponseFilter] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task PostV1Async(string projectId = null, [FromHeader][UserAgent] string userAgent = null) { + return PostAsync(projectId, 1, userAgent); + } - var project = Request.GetProject(); - if (!String.Equals(project?.Id, projectId)) { - _logger.ProjectRouteDoesNotMatch(project?.Id, projectId); + /// + /// Submit event by POST + /// + /// + /// You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it + /// we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON + /// object into the events data collection. + /// + /// You can also post a multi-line string. We automatically split strings by the \n character and create a new log event for every line. + /// + /// Simple event: + /// + /// { "message": "Exceptionless is amazing!" } + /// + /// + /// Simple log event with user identity: + /// + /// { + /// "type": "log", + /// "message": "Exceptionless is amazing!", + /// "date":"2030-01-01T12:00:00.0000000-05:00", + /// "@user":{ "identity":"123456789", "name": "Test User" } + /// } + /// + /// + /// Multiple events from string content: + /// + /// Exceptionless is amazing! + /// Exceptionless is really amazing! + /// + /// + /// Simple error: + /// + /// { + /// "type": "error", + /// "date":"2030-01-01T12:00:00.0000000-05:00", + /// "@simple_error": { + /// "message": "Simple Exception", + /// "type": "System.Exception", + /// "stack_trace": " at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77" + /// } + /// } + /// + /// + /// The user agent that submitted the event. + /// Accepted + /// No project id specified and no default project was found. + /// No project was found. + [HttpPost] + [Consumes("application/json", "text/plain")] + [RequestBodyContentAttribute] + [ConfigurationResponseFilter] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task PostV2Async([FromHeader][UserAgent] string userAgent = null) { + return PostAsync(null, 2, userAgent); + } - project = await GetProjectAsync(projectId); + /// + /// Submit event by POST for a specific project + /// + /// + /// You can create an event by posting any uncompressed or compressed (gzip or deflate) string or json object. If we know how to handle it + /// we will create a new event. If none of the JSON properties match the event object then we will create a new event and place your JSON + /// object into the events data collection. + /// + /// You can also post a multi-line string. We automatically split strings by the \n character and create a new log event for every line. + /// + /// Simple event: + /// + /// { "message": "Exceptionless is amazing!" } + /// + /// + /// Simple log event with user identity: + /// + /// { + /// "type": "log", + /// "message": "Exceptionless is amazing!", + /// "date":"2030-01-01T12:00:00.0000000-05:00", + /// "@user":{ "identity":"123456789", "name": "Test User" } + /// } + /// + /// + /// Multiple events from string content: + /// + /// Exceptionless is amazing! + /// Exceptionless is really amazing! + /// + /// + /// Simple error: + /// + /// { + /// "type": "error", + /// "date":"2030-01-01T12:00:00.0000000-05:00", + /// "@simple_error": { + /// "message": "Simple Exception", + /// "type": "System.Exception", + /// "stack_trace": " at Client.Tests.ExceptionlessClientTests.CanSubmitSimpleException() in ExceptionlessClientTests.cs:line 77" + /// } + /// } + /// + /// + /// The identifier of the project. + /// The user agent that submitted the event. + /// Accepted + /// No project id specified and no default project was found. + /// No project was found. + [HttpPost("~/api/v2/projects/{projectId:objectid}/events")] + [Consumes("application/json", "text/plain")] + [RequestBodyContentAttribute] + [ConfigurationResponseFilter] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task PostByProjectV2Async(string projectId = null, [FromHeader][UserAgent] string userAgent = null) { + return PostAsync(projectId, 2, userAgent); + } - // Set the project for the configuration response filter. - Request.SetProject(project); - } + private async Task PostAsync(string projectId = null, int apiVersion = 2, [FromHeader][UserAgent] string userAgent = null) { + if (Request.ContentLength.HasValue && Request.ContentLength.Value <= 0) + return StatusCode(StatusCodes.Status202Accepted); - if (project == null) - return NotFound(); - - try { - string mediaType = String.Empty; - string charSet = String.Empty; - if (Request.ContentType != null) { - var contentType = MediaTypeHeaderValue.Parse(Request.ContentType); - mediaType = contentType.MediaType.ToString(); - charSet = contentType.Charset.ToString(); - } - - await _eventPostService.EnqueueAsync(new EventPost(_appOptions.EnableArchive) { - ApiVersion = apiVersion, - CharSet = charSet, - ContentEncoding = Request.Headers.TryGetAndReturn(Headers.ContentEncoding), - IpAddress = Request.GetClientIpAddress(), - MediaType = mediaType, - OrganizationId = project.OrganizationId, - ProjectId = project.Id, - UserAgent = userAgent, - }, Request.Body); - } catch (Exception ex) { - if (projectId != _appOptions.InternalProjectId) { - using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Identity(CurrentUser?.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, "Error enqueuing event post."); - } - - return StatusCode(StatusCodes.Status500InternalServerError); - } + if (projectId == null) + projectId = Request.GetDefaultProjectId(); - return StatusCode(StatusCodes.Status202Accepted); - } + // must have a project id + if (String.IsNullOrEmpty(projectId)) + return BadRequest("No project id specified and no default project was found."); - /// - /// Remove - /// - /// A comma delimited list of event identifiers. - /// No Content. - /// One or more validation errors occurred. - /// One or more event occurrences were not found. - /// An error occurred while deleting one or more event occurrences. - [HttpDelete("{ids:objectids}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteAsync(string ids) { - return DeleteImplAsync(ids.FromDelimitedString()); - } + var project = Request.GetProject(); + if (!String.Equals(project?.Id, projectId)) { + _logger.ProjectRouteDoesNotMatch(project?.Id, projectId); - private Task GetOrganizationAsync(string organizationId, bool useCache = true) { - if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) - return Task.FromResult(null); + project = await GetProjectAsync(projectId); - return _organizationRepository.GetByIdAsync(organizationId, o => o.Cache(useCache)); + // Set the project for the configuration response filter. + Request.SetProject(project); } - private async Task GetProjectAsync(string projectId, bool useCache = true) { - if (String.IsNullOrEmpty(projectId)) - return null; + if (project == null) + return NotFound(); - var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); - if (project == null || !CanAccessOrganization(project.OrganizationId)) - return null; + try { + string mediaType = String.Empty; + string charSet = String.Empty; + if (Request.ContentType != null) { + var contentType = MediaTypeHeaderValue.Parse(Request.ContentType); + mediaType = contentType.MediaType.ToString(); + charSet = contentType.Charset.ToString(); + } - return project; + await _eventPostService.EnqueueAsync(new EventPost(_appOptions.EnableArchive) { + ApiVersion = apiVersion, + CharSet = charSet, + ContentEncoding = Request.Headers.TryGetAndReturn(Headers.ContentEncoding), + IpAddress = Request.GetClientIpAddress(), + MediaType = mediaType, + OrganizationId = project.OrganizationId, + ProjectId = project.Id, + UserAgent = userAgent, + }, Request.Body); } + catch (Exception ex) { + if (projectId != _appOptions.InternalProjectId) { + using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Identity(CurrentUser?.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, "Error enqueuing event post."); + } - private async Task GetStackAsync(string stackId, bool useCache = true) { - if (String.IsNullOrEmpty(stackId)) - return null; + return StatusCode(StatusCodes.Status500InternalServerError); + } - var stack = await _stackRepository.GetByIdAsync(stackId, o => o.Cache(useCache)); - if (stack == null || !CanAccessOrganization(stack.OrganizationId)) - return null; + return StatusCode(StatusCodes.Status202Accepted); + } - return stack; - } + /// + /// Remove + /// + /// A comma delimited list of event identifiers. + /// No Content. + /// One or more validation errors occurred. + /// One or more event occurrences were not found. + /// An error occurred while deleting one or more event occurrences. + [HttpDelete("{ids:objectids}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteAsync(string ids) { + return DeleteImplAsync(ids.FromDelimitedString()); + } - private async Task> GetStackSummariesAsync(List stacks, IReadOnlyCollection> stackTerms, AppFilter sf, TimeInfo ti) { - if (stacks.Count == 0) - return new List(0); - - var totalUsers = await GetUserCountByProjectIdsAsync(stacks, sf, ti.Range.UtcStart, ti.Range.UtcEnd); - return stacks.Join(stackTerms, s => s.Id, tk => tk.Key, (stack, term) => { - var data = _formattingPluginManager.GetStackSummaryData(stack); - var summary = new StackSummaryModel { - TemplateKey = data.TemplateKey, - Data = data.Data, - Id = stack.Id, - Title = stack.Title, - Status = stack.Status, - FirstOccurrence = term.Aggregations.Min("min_date").Value, - LastOccurrence = term.Aggregations.Max("max_date").Value, - Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), - - Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), - TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) - }; - - return summary; - }).ToList(); - } + private Task GetOrganizationAsync(string organizationId, bool useCache = true) { + if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) + return Task.FromResult(null); - private async Task> GetUserCountByProjectIdsAsync(ICollection stacks, AppFilter sf, DateTime utcStart, DateTime utcEnd) { - var scopedCacheClient = new ScopedCacheClient(_cache, $"Project:user-count:{utcStart.Floor(TimeSpan.FromMinutes(15)).Ticks}-{utcEnd.Floor(TimeSpan.FromMinutes(15)).Ticks}"); - var projectIds = stacks.Select(s => s.ProjectId).Distinct().ToList(); - var cachedTotals = await scopedCacheClient.GetAllAsync(projectIds); + return _organizationRepository.GetByIdAsync(organizationId, o => o.Cache(useCache)); + } + + private async Task GetProjectAsync(string projectId, bool useCache = true) { + if (String.IsNullOrEmpty(projectId)) + return null; - var totals = cachedTotals.Where(kvp => kvp.Value.HasValue).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value); - if (totals.Count == projectIds.Count) - return totals; + var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); + if (project == null || !CanAccessOrganization(project.OrganizationId)) + return null; + + return project; + } - var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(utcStart, utcEnd, (PersistentEvent e) => e.Date).Index(utcStart, utcEnd); - var projects = cachedTotals.Where(kvp => !kvp.Value.HasValue).Select(kvp => new Project { Id = kvp.Key, OrganizationId = stacks.FirstOrDefault(s => s.ProjectId == kvp.Key)?.OrganizationId }).ToList(); - var countResult = await _repository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).EnforceEventStackFilter().AggregationsExpression("terms:(project_id cardinality:user)")); + private async Task GetStackAsync(string stackId, bool useCache = true) { + if (String.IsNullOrEmpty(stackId)) + return null; - // Cache all projects that have more than 10 users for 5 minutes. - var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; - var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); - await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); - totals.AddRange(aggregations); + var stack = await _stackRepository.GetByIdAsync(stackId, o => o.Cache(useCache)); + if (stack == null || !CanAccessOrganization(stack.OrganizationId)) + return null; + return stack; + } + + private async Task> GetStackSummariesAsync(List stacks, IReadOnlyCollection> stackTerms, AppFilter sf, TimeInfo ti) { + if (stacks.Count == 0) + return new List(0); + + var totalUsers = await GetUserCountByProjectIdsAsync(stacks, sf, ti.Range.UtcStart, ti.Range.UtcEnd); + return stacks.Join(stackTerms, s => s.Id, tk => tk.Key, (stack, term) => { + var data = _formattingPluginManager.GetStackSummaryData(stack); + var summary = new StackSummaryModel { + TemplateKey = data.TemplateKey, + Data = data.Data, + Id = stack.Id, + Title = stack.Title, + Status = stack.Status, + FirstOccurrence = term.Aggregations.Min("min_date").Value, + LastOccurrence = term.Aggregations.Max("max_date").Value, + Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), + + Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), + TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) + }; + + return summary; + }).ToList(); + } + + private async Task> GetUserCountByProjectIdsAsync(ICollection stacks, AppFilter sf, DateTime utcStart, DateTime utcEnd) { + var scopedCacheClient = new ScopedCacheClient(_cache, $"Project:user-count:{utcStart.Floor(TimeSpan.FromMinutes(15)).Ticks}-{utcEnd.Floor(TimeSpan.FromMinutes(15)).Ticks}"); + var projectIds = stacks.Select(s => s.ProjectId).Distinct().ToList(); + var cachedTotals = await scopedCacheClient.GetAllAsync(projectIds); + + var totals = cachedTotals.Where(kvp => kvp.Value.HasValue).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value); + if (totals.Count == projectIds.Count) return totals; - } + + var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(utcStart, utcEnd, (PersistentEvent e) => e.Date).Index(utcStart, utcEnd); + var projects = cachedTotals.Where(kvp => !kvp.Value.HasValue).Select(kvp => new Project { Id = kvp.Key, OrganizationId = stacks.FirstOrDefault(s => s.ProjectId == kvp.Key)?.OrganizationId }).ToList(); + var countResult = await _repository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).EnforceEventStackFilter().AggregationsExpression("terms:(project_id cardinality:user)")); + + // Cache all projects that have more than 10 users for 5 minutes. + var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; + var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); + await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); + totals.AddRange(aggregations); + + return totals; } } diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs index 8e923150b0..436ee309ee 100644 --- a/src/Exceptionless.Web/Controllers/OrganizationController.cs +++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Web.Extensions; using Exceptionless.Core; using Exceptionless.Core.Authorization; @@ -25,732 +21,739 @@ using Foundatio.Repositories.Models; using Foundatio.Utility; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Stripe; using Invoice = Exceptionless.Web.Models.Invoice; using InvoiceLineItem = Exceptionless.Web.Models.InvoiceLineItem; #pragma warning disable 1998 -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/organizations")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public class OrganizationController : RepositoryApiController { - private readonly OrganizationService _organizationService; - private readonly ICacheClient _cacheClient; - private readonly IEventRepository _eventRepository; - private readonly IUserRepository _userRepository; - private readonly IProjectRepository _projectRepository; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - private readonly IMailer _mailer; - private readonly IMessagePublisher _messagePublisher; - private readonly AppOptions _options; - - public OrganizationController( - OrganizationService organizationService, - IOrganizationRepository organizationRepository, - ICacheClient cacheClient, - IEventRepository eventRepository, - IUserRepository userRepository, - IProjectRepository projectRepository, - BillingManager billingManager, - IMailer mailer, - IMessagePublisher messagePublisher, - IMapper mapper, - IAppQueryValidator validator, - AppOptions options, - ILoggerFactory loggerFactory, - BillingPlans plans) : base(organizationRepository, mapper, validator, loggerFactory) { - _organizationService = organizationService; - _cacheClient = cacheClient; - _eventRepository = eventRepository; - _userRepository = userRepository; - _projectRepository = projectRepository; - _billingManager = billingManager; - _mailer = mailer; - _messagePublisher = messagePublisher; - _options = options; - _plans = plans; - } +namespace Exceptionless.Web.Controllers; + +[Route(API_PREFIX + "/organizations")] +[Authorize(Policy = AuthorizationRoles.UserPolicy)] +public class OrganizationController : RepositoryApiController { + private readonly OrganizationService _organizationService; + private readonly ICacheClient _cacheClient; + private readonly IEventRepository _eventRepository; + private readonly IUserRepository _userRepository; + private readonly IProjectRepository _projectRepository; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + private readonly IMailer _mailer; + private readonly IMessagePublisher _messagePublisher; + private readonly AppOptions _options; + + public OrganizationController( + OrganizationService organizationService, + IOrganizationRepository organizationRepository, + ICacheClient cacheClient, + IEventRepository eventRepository, + IUserRepository userRepository, + IProjectRepository projectRepository, + BillingManager billingManager, + IMailer mailer, + IMessagePublisher messagePublisher, + IMapper mapper, + IAppQueryValidator validator, + AppOptions options, + ILoggerFactory loggerFactory, + BillingPlans plans) : base(organizationRepository, mapper, validator, loggerFactory) { + _organizationService = organizationService; + _cacheClient = cacheClient; + _eventRepository = eventRepository; + _userRepository = userRepository; + _projectRepository = projectRepository; + _billingManager = billingManager; + _mailer = mailer; + _messagePublisher = messagePublisher; + _options = options; + _plans = plans; + } - #region CRUD + #region CRUD - /// - /// Get all - /// - /// If no mode is set then the a light weight organization object will be returned. If the mode is set to stats than the fully populated object will be returned. - [HttpGet] - public async Task> GetAsync(string mode = null) { - var organizations = await GetModelsAsync(GetAssociatedOrganizationIds().ToArray()); - var viewOrganizations = await MapCollectionAsync(organizations, true); + /// + /// Get all + /// + /// If no mode is set then the a light weight organization object will be returned. If the mode is set to stats than the fully populated object will be returned. + [HttpGet] + public async Task> GetAsync(string mode = null) { + var organizations = await GetModelsAsync(GetAssociatedOrganizationIds().ToArray()); + var viewOrganizations = await MapCollectionAsync(organizations, true); - if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) - return Ok(await PopulateOrganizationStatsAsync(viewOrganizations.ToList())); + if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) + return Ok(await PopulateOrganizationStatsAsync(viewOrganizations.ToList())); - return Ok(viewOrganizations); - } + return Ok(viewOrganizations); + } - [HttpGet("~/" + API_PREFIX + "/admin/organizations")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task>> GetForAdminsAsync(string criteria = null, bool? paid = null, bool? suspended = null, string mode = null, int page = 1, int limit = 10, OrganizationSortBy sort = OrganizationSortBy.Newest) { - page = GetPage(page); - limit = GetLimit(limit); - var organizations = await _repository.GetByCriteriaAsync(criteria, o => o.PageNumber(page).PageLimit(limit), sort, paid, suspended); - var viewOrganizations = (await MapCollectionAsync(organizations.Documents, true)).ToList(); + [HttpGet("~/" + API_PREFIX + "/admin/organizations")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task>> GetForAdminsAsync(string criteria = null, bool? paid = null, bool? suspended = null, string mode = null, int page = 1, int limit = 10, OrganizationSortBy sort = OrganizationSortBy.Newest) { + page = GetPage(page); + limit = GetLimit(limit); + var organizations = await _repository.GetByCriteriaAsync(criteria, o => o.PageNumber(page).PageLimit(limit), sort, paid, suspended); + var viewOrganizations = (await MapCollectionAsync(organizations.Documents, true)).ToList(); - if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) - return OkWithResourceLinks(await PopulateOrganizationStatsAsync(viewOrganizations), organizations.HasMore, page, organizations.Total); + if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) + return OkWithResourceLinks(await PopulateOrganizationStatsAsync(viewOrganizations), organizations.HasMore, page, organizations.Total); - return OkWithResourceLinks(viewOrganizations, organizations.HasMore, page, organizations.Total); - } + return OkWithResourceLinks(viewOrganizations, organizations.HasMore, page, organizations.Total); + } - [HttpGet("~/" + API_PREFIX + "/admin/organizations/stats")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task> PlanStatsAsync() { - return Ok(await _repository.GetBillingPlanStatsAsync()); - } + [HttpGet("~/" + API_PREFIX + "/admin/organizations/stats")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task> PlanStatsAsync() { + return Ok(await _repository.GetBillingPlanStatsAsync()); + } - /// - /// Get by id - /// - /// The identifier of the organization. - /// If no mode is set then the a light weight organization object will be returned. If the mode is set to stats than the fully populated object will be returned. - /// The organization could not be found. - [HttpGet("{id:objectid}", Name = "GetOrganizationById")] - public async Task> GetAsync(string id, string mode = null) { - var organization = await GetModelAsync(id); - if (organization == null) - return NotFound(); - - var viewOrganization = await MapAsync(organization, true); - if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) - return Ok(await PopulateOrganizationStatsAsync(viewOrganization)); - - return Ok(viewOrganization); - } + /// + /// Get by id + /// + /// The identifier of the organization. + /// If no mode is set then the a light weight organization object will be returned. If the mode is set to stats than the fully populated object will be returned. + /// The organization could not be found. + [HttpGet("{id:objectid}", Name = "GetOrganizationById")] + public async Task> GetAsync(string id, string mode = null) { + var organization = await GetModelAsync(id); + if (organization == null) + return NotFound(); + + var viewOrganization = await MapAsync(organization, true); + if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) + return Ok(await PopulateOrganizationStatsAsync(viewOrganization)); + + return Ok(viewOrganization); + } - /// - /// Create - /// - /// The organization. - /// - /// An error occurred while creating the organization. - /// The organization already exists. - [HttpPost] - [Consumes("application/json")] - public Task> PostAsync(NewOrganization organization) { - return PostImplAsync(organization); - } + /// + /// Create + /// + /// The organization. + /// + /// An error occurred while creating the organization. + /// The organization already exists. + [HttpPost] + [Consumes("application/json")] + public Task> PostAsync(NewOrganization organization) { + return PostImplAsync(organization); + } + + /// + /// Update + /// + /// The identifier of the organization. + /// The changes + /// An error occurred while updating the organization. + /// The organization could not be found. + [HttpPatch] + [HttpPut] + [Consumes("application/json")] + [Route("{id:objectid}")] + public Task> PatchAsync(string id, Delta changes) { + return PatchImplAsync(id, changes); + } - /// - /// Update - /// - /// The identifier of the organization. - /// The changes - /// An error occurred while updating the organization. - /// The organization could not be found. - [HttpPatch] - [HttpPut] - [Consumes("application/json")] - [Route("{id:objectid}")] - public Task> PatchAsync(string id, Delta changes) { - return PatchImplAsync(id, changes); + /// + /// Remove + /// + /// A comma delimited list of organization identifiers. + /// No Content. + /// One or more validation errors occurred. + /// One or more organizations were not found. + /// An error occurred while deleting one or more organizations. + [HttpDelete] + [Route("{ids:objectids}")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteAsync(string ids) { + return DeleteImplAsync(ids.FromDelimitedString()); + } + + protected override async Task> DeleteModelsAsync(ICollection organizations) { + foreach (var organization in organizations) { + using (_logger.BeginScope(new ExceptionlessState().Organization(organization.Id).Tag("Delete").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) { + _logger.UserDeletingOrganization(CurrentUser.Id, organization.Name, organization.Id); + await _organizationService.SoftDeleteOrganizationAsync(organization, CurrentUser.Id); + } } - /// - /// Remove - /// - /// A comma delimited list of organization identifiers. - /// No Content. - /// One or more validation errors occurred. - /// One or more organizations were not found. - /// An error occurred while deleting one or more organizations. - [HttpDelete] - [Route("{ids:objectids}")] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteAsync(string ids) { - return DeleteImplAsync(ids.FromDelimitedString()); + return Enumerable.Empty(); + } + + #endregion + + /// + /// Get invoice + /// + /// The identifier of the invoice. + /// The invoice was not found. + [HttpGet] + [Route("invoice/{id:minlength(10)}")] + public async Task> GetInvoiceAsync(string id) { + if (!_options.StripeOptions.EnableBilling) + return NotFound(); + + if (!id.StartsWith("in_")) + id = "in_" + id; + + Stripe.Invoice stripeInvoice = null; + try { + var client = new StripeClient(_options.StripeOptions.StripeApiKey); + var invoiceService = new InvoiceService(client); + stripeInvoice = await invoiceService.GetAsync(id); } - - protected override async Task> DeleteModelsAsync(ICollection organizations) { - foreach (var organization in organizations) { - using (_logger.BeginScope(new ExceptionlessState().Organization(organization.Id).Tag("Delete").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) { - _logger.UserDeletingOrganization(CurrentUser.Id, organization.Name, organization.Id); - await _organizationService.SoftDeleteOrganizationAsync(organization, CurrentUser.Id); - } - } - - return Enumerable.Empty(); + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Tag("Invoice").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}", id); } - #endregion - - /// - /// Get invoice - /// - /// The identifier of the invoice. - /// The invoice was not found. - [HttpGet] - [Route("invoice/{id:minlength(10)}")] - public async Task> GetInvoiceAsync(string id) { - if (!_options.StripeOptions.EnableBilling) - return NotFound(); - - if (!id.StartsWith("in_")) - id = "in_" + id; - - Stripe.Invoice stripeInvoice = null; - try { - var client = new StripeClient(_options.StripeOptions.StripeApiKey); - var invoiceService = new InvoiceService(client); - stripeInvoice = await invoiceService.GetAsync(id); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Tag("Invoice").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}", id); - } + if (String.IsNullOrEmpty(stripeInvoice?.CustomerId)) + return NotFound(); - if (String.IsNullOrEmpty(stripeInvoice?.CustomerId)) - return NotFound(); - - var organization = await _repository.GetByStripeCustomerIdAsync(stripeInvoice.CustomerId); - if (organization == null || !CanAccessOrganization(organization.Id)) - return NotFound(); - - var invoice = new Invoice { - Id = stripeInvoice.Id.Substring(3), - OrganizationId = organization.Id, - OrganizationName = organization.Name, - Date = stripeInvoice.Created, - Paid = stripeInvoice.Paid, - Total = stripeInvoice.Total / 100.0m - }; - - - var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - foreach (var line in stripeInvoice.Lines.Data) { - var item = new InvoiceLineItem { Amount = line.Amount / 100.0m }; - - if (line.Plan != null) { - string planName = line.Plan.Nickname ?? _billingManager.GetBillingPlan(line.Plan.Id)?.Name; - item.Description = $"Exceptionless - {planName} Plan ({(line.Plan.Amount / 100.0):c}/{line.Plan.Interval})"; - } else { - item.Description = line.Description; - } + var organization = await _repository.GetByStripeCustomerIdAsync(stripeInvoice.CustomerId); + if (organization == null || !CanAccessOrganization(organization.Id)) + return NotFound(); - var periodStart = line.Period.Start >= 0 ? unixEpoch.AddSeconds(line.Period.Start) : stripeInvoice.PeriodStart; - var periodEnd = line.Period.End >= 0 ? unixEpoch.AddSeconds(line.Period.End) : stripeInvoice.PeriodEnd; - item.Date = $"{periodStart.ToShortDateString()} - {periodEnd.ToShortDateString()}"; - invoice.Items.Add(item); - } + var invoice = new Invoice { + Id = stripeInvoice.Id.Substring(3), + OrganizationId = organization.Id, + OrganizationName = organization.Name, + Date = stripeInvoice.Created, + Paid = stripeInvoice.Paid, + Total = stripeInvoice.Total / 100.0m + }; - var coupon = stripeInvoice.Discount?.Coupon; - if (coupon != null) { - if (coupon.AmountOff.HasValue) { - decimal discountAmount = coupon.AmountOff.GetValueOrDefault() / 100.0m; - string description = $"{coupon.Id} ({discountAmount.ToString("C")} off)"; - invoice.Items.Add(new InvoiceLineItem { Description = description, Amount = discountAmount }); - } else { - decimal discountAmount = (stripeInvoice.Subtotal / 100.0m) * (coupon.PercentOff.GetValueOrDefault() / 100.0m); - string description = $"{coupon.Id} ({coupon.PercentOff.GetValueOrDefault()}% off)"; - invoice.Items.Add(new InvoiceLineItem { Description = description, Amount = discountAmount }); - } + + var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + foreach (var line in stripeInvoice.Lines.Data) { + var item = new InvoiceLineItem { Amount = line.Amount / 100.0m }; + + if (line.Plan != null) { + string planName = line.Plan.Nickname ?? _billingManager.GetBillingPlan(line.Plan.Id)?.Name; + item.Description = $"Exceptionless - {planName} Plan ({(line.Plan.Amount / 100.0):c}/{line.Plan.Interval})"; + } + else { + item.Description = line.Description; } - return Ok(invoice); + var periodStart = line.Period.Start >= 0 ? unixEpoch.AddSeconds(line.Period.Start) : stripeInvoice.PeriodStart; + var periodEnd = line.Period.End >= 0 ? unixEpoch.AddSeconds(line.Period.End) : stripeInvoice.PeriodEnd; + item.Date = $"{periodStart.ToShortDateString()} - {periodEnd.ToShortDateString()}"; + invoice.Items.Add(item); } - /// - /// Get invoices - /// - /// The identifier of the organization. - /// A cursor for use in pagination. before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_bar, your subsequent call can include before=obj_bar in order to fetch the previous page of the list. - /// A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The organization was not found. - [HttpGet] - [Route("{id:objectid}/invoices")] - public async Task>> GetInvoicesAsync(string id, string before = null, string after = null, int limit = 12) { - if (!_options.StripeOptions.EnableBilling) - return NotFound(); - - var organization = await GetModelAsync(id); - if (organization == null) - return NotFound(); - - if (String.IsNullOrWhiteSpace(organization.StripeCustomerId)) - return Ok(new List()); - - if (!String.IsNullOrEmpty(before) && !before.StartsWith("in_")) - before = "in_" + before; - - if (!String.IsNullOrEmpty(after) && !after.StartsWith("in_")) - after = "in_" + after; - - var client = new StripeClient(_options.StripeOptions.StripeApiKey); - var invoiceService = new InvoiceService(client); - var invoiceOptions = new InvoiceListOptions { Customer = organization.StripeCustomerId, Limit = limit + 1, EndingBefore = before, StartingAfter = after }; - var invoices = (await MapCollectionAsync(await invoiceService.ListAsync(invoiceOptions), true)).ToList(); - return OkWithResourceLinks(invoices.Take(limit).ToList(), invoices.Count > limit, i => i.Id); + var coupon = stripeInvoice.Discount?.Coupon; + if (coupon != null) { + if (coupon.AmountOff.HasValue) { + decimal discountAmount = coupon.AmountOff.GetValueOrDefault() / 100.0m; + string description = $"{coupon.Id} ({discountAmount.ToString("C")} off)"; + invoice.Items.Add(new InvoiceLineItem { Description = description, Amount = discountAmount }); + } + else { + decimal discountAmount = (stripeInvoice.Subtotal / 100.0m) * (coupon.PercentOff.GetValueOrDefault() / 100.0m); + string description = $"{coupon.Id} ({coupon.PercentOff.GetValueOrDefault()}% off)"; + invoice.Items.Add(new InvoiceLineItem { Description = description, Amount = discountAmount }); + } } - /// - /// Get plans - /// - /// - /// Gets available plans for a specific organization. - /// - /// The identifier of the organization. - /// The organization was not found. - [HttpGet] - [Route("{id:objectid}/plans")] - public async Task>> GetPlansAsync(string id) { - var organization = await GetModelAsync(id); - if (organization == null) - return NotFound(); - - var plans = _plans.Plans; - if (!Request.IsGlobalAdmin()) - plans = plans.Where(p => !p.IsHidden || p.Id == organization.PlanId).ToList(); - - var currentPlan = new BillingPlan { - Id = organization.PlanId, - Name = organization.PlanName, - Description = organization.PlanDescription, - IsHidden = false, - Price = organization.BillingPrice, - MaxProjects = organization.MaxProjects, - MaxUsers = organization.MaxUsers, - RetentionDays = organization.RetentionDays, - MaxEventsPerMonth = organization.MaxEventsPerMonth, - HasPremiumFeatures = organization.HasPremiumFeatures - }; - - if (plans.All(p => p.Id != organization.PlanId)) - plans.Add(currentPlan); - else - plans[plans.FindIndex(p => p.Id == organization.PlanId)] = currentPlan; - - return Ok(plans); + return Ok(invoice); + } + + /// + /// Get invoices + /// + /// The identifier of the organization. + /// A cursor for use in pagination. before is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_bar, your subsequent call can include before=obj_bar in order to fetch the previous page of the list. + /// A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The organization was not found. + [HttpGet] + [Route("{id:objectid}/invoices")] + public async Task>> GetInvoicesAsync(string id, string before = null, string after = null, int limit = 12) { + if (!_options.StripeOptions.EnableBilling) + return NotFound(); + + var organization = await GetModelAsync(id); + if (organization == null) + return NotFound(); + + if (String.IsNullOrWhiteSpace(organization.StripeCustomerId)) + return Ok(new List()); + + if (!String.IsNullOrEmpty(before) && !before.StartsWith("in_")) + before = "in_" + before; + + if (!String.IsNullOrEmpty(after) && !after.StartsWith("in_")) + after = "in_" + after; + + var client = new StripeClient(_options.StripeOptions.StripeApiKey); + var invoiceService = new InvoiceService(client); + var invoiceOptions = new InvoiceListOptions { Customer = organization.StripeCustomerId, Limit = limit + 1, EndingBefore = before, StartingAfter = after }; + var invoices = (await MapCollectionAsync(await invoiceService.ListAsync(invoiceOptions), true)).ToList(); + return OkWithResourceLinks(invoices.Take(limit).ToList(), invoices.Count > limit, i => i.Id); + } + + /// + /// Get plans + /// + /// + /// Gets available plans for a specific organization. + /// + /// The identifier of the organization. + /// The organization was not found. + [HttpGet] + [Route("{id:objectid}/plans")] + public async Task>> GetPlansAsync(string id) { + var organization = await GetModelAsync(id); + if (organization == null) + return NotFound(); + + var plans = _plans.Plans; + if (!Request.IsGlobalAdmin()) + plans = plans.Where(p => !p.IsHidden || p.Id == organization.PlanId).ToList(); + + var currentPlan = new BillingPlan { + Id = organization.PlanId, + Name = organization.PlanName, + Description = organization.PlanDescription, + IsHidden = false, + Price = organization.BillingPrice, + MaxProjects = organization.MaxProjects, + MaxUsers = organization.MaxUsers, + RetentionDays = organization.RetentionDays, + MaxEventsPerMonth = organization.MaxEventsPerMonth, + HasPremiumFeatures = organization.HasPremiumFeatures + }; + + if (plans.All(p => p.Id != organization.PlanId)) + plans.Add(currentPlan); + else + plans[plans.FindIndex(p => p.Id == organization.PlanId)] = currentPlan; + + return Ok(plans); + } + + /// + /// Change plan + /// + /// + /// Upgrades or downgrades the organizations plan. + /// + /// The identifier of the organization. + /// The identifier of the plan. + /// The token returned from the stripe service. + /// The last four numbers of the card. + /// The coupon id. + /// The organization was not found. + [HttpPost] + [Consumes("application/json")] + [Route("{id:objectid}/change-plan")] + public async Task> ChangePlanAsync(string id, string planId, string stripeToken = null, string last4 = null, string couponId = null) { + if (String.IsNullOrEmpty(id) || !CanAccessOrganization(id)) + return NotFound(); + + if (!_options.StripeOptions.EnableBilling) + return Ok(ChangePlanResult.FailWithMessage("Plans cannot be changed while billing is disabled.")); + + var organization = await GetModelAsync(id, false); + if (organization == null) + return Ok(ChangePlanResult.FailWithMessage("Invalid OrganizationId.")); + + var plan = _billingManager.GetBillingPlan(planId); + if (plan == null) + return Ok(ChangePlanResult.FailWithMessage("Invalid PlanId.")); + + if (String.Equals(organization.PlanId, plan.Id) && String.Equals(_plans.FreePlan.Id, plan.Id)) + return Ok(ChangePlanResult.SuccessWithMessage("Your plan was not changed as you were already on the free plan.")); + + // Only see if they can downgrade a plan if the plans are different. + if (!String.Equals(organization.PlanId, plan.Id)) { + var result = await _billingManager.CanDownGradeAsync(organization, plan, CurrentUser); + if (!result.Success) + return Ok(result); } - /// - /// Change plan - /// - /// - /// Upgrades or downgrades the organizations plan. - /// - /// The identifier of the organization. - /// The identifier of the plan. - /// The token returned from the stripe service. - /// The last four numbers of the card. - /// The coupon id. - /// The organization was not found. - [HttpPost] - [Consumes("application/json")] - [Route("{id:objectid}/change-plan")] - public async Task> ChangePlanAsync(string id, string planId, string stripeToken = null, string last4 = null, string couponId = null) { - if (String.IsNullOrEmpty(id) || !CanAccessOrganization(id)) - return NotFound(); - - if (!_options.StripeOptions.EnableBilling) - return Ok(ChangePlanResult.FailWithMessage("Plans cannot be changed while billing is disabled.")); - - var organization = await GetModelAsync(id, false); - if (organization == null) - return Ok(ChangePlanResult.FailWithMessage("Invalid OrganizationId.")); - - var plan = _billingManager.GetBillingPlan(planId); - if (plan == null) - return Ok(ChangePlanResult.FailWithMessage("Invalid PlanId.")); - - if (String.Equals(organization.PlanId, plan.Id) && String.Equals(_plans.FreePlan.Id, plan.Id)) - return Ok(ChangePlanResult.SuccessWithMessage("Your plan was not changed as you were already on the free plan.")); - - // Only see if they can downgrade a plan if the plans are different. - if (!String.Equals(organization.PlanId, plan.Id)) { - var result = await _billingManager.CanDownGradeAsync(organization, plan, CurrentUser); - if (!result.Success) - return Ok(result); + var client = new StripeClient(_options.StripeOptions.StripeApiKey); + var customerService = new CustomerService(client); + var subscriptionService = new SubscriptionService(client); + + try { + // If they are on a paid plan and then downgrade to a free plan then cancel their stripe subscription. + if (!String.Equals(organization.PlanId, _plans.FreePlan.Id) && String.Equals(plan.Id, _plans.FreePlan.Id)) { + if (!String.IsNullOrEmpty(organization.StripeCustomerId)) { + var subs = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId }); + foreach (var sub in subs.Where(s => !s.CanceledAt.HasValue)) + await subscriptionService.CancelAsync(sub.Id, new SubscriptionCancelOptions()); + } + + organization.BillingStatus = BillingStatus.Trialing; + organization.RemoveSuspension(); } + else if (String.IsNullOrEmpty(organization.StripeCustomerId)) { + if (String.IsNullOrEmpty(stripeToken)) + return Ok(ChangePlanResult.FailWithMessage("Billing information was not set.")); - var client = new StripeClient(_options.StripeOptions.StripeApiKey); - var customerService = new CustomerService(client); - var subscriptionService = new SubscriptionService(client); - - try { - // If they are on a paid plan and then downgrade to a free plan then cancel their stripe subscription. - if (!String.Equals(organization.PlanId, _plans.FreePlan.Id) && String.Equals(plan.Id, _plans.FreePlan.Id)) { - if (!String.IsNullOrEmpty(organization.StripeCustomerId)) { - var subs = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId }); - foreach (var sub in subs.Where(s => !s.CanceledAt.HasValue)) - await subscriptionService.CancelAsync(sub.Id, new SubscriptionCancelOptions()); - } - - organization.BillingStatus = BillingStatus.Trialing; - organization.RemoveSuspension(); - } else if (String.IsNullOrEmpty(organization.StripeCustomerId)) { - if (String.IsNullOrEmpty(stripeToken)) - return Ok(ChangePlanResult.FailWithMessage("Billing information was not set.")); - - organization.SubscribeDate = SystemClock.UtcNow; - - var createCustomer = new CustomerCreateOptions { - Source = stripeToken, - Plan = planId, - Description = organization.Name, - Email = CurrentUser.EmailAddress - }; - - if (!String.IsNullOrWhiteSpace(couponId)) - createCustomer.Coupon = couponId; - - var customer = await customerService.CreateAsync(createCustomer); - - organization.BillingStatus = BillingStatus.Active; - organization.RemoveSuspension(); - organization.StripeCustomerId = customer.Id; - organization.CardLast4 = last4; - } else { - var update = new SubscriptionUpdateOptions { Items = new List() }; - var create = new SubscriptionCreateOptions { Customer = organization.StripeCustomerId, Items = new List() }; - bool cardUpdated = false; - - var customerUpdateOptions = new CustomerUpdateOptions { Description = organization.Name }; - if (!Request.IsGlobalAdmin()) - customerUpdateOptions.Email = CurrentUser.EmailAddress; - - if (!String.IsNullOrEmpty(stripeToken)) { - customerUpdateOptions.Source = stripeToken; - cardUpdated = true; - } - - await customerService.UpdateAsync(organization.StripeCustomerId, customerUpdateOptions); - - var subscriptionList = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId }); - var subscription = subscriptionList.FirstOrDefault(s => !s.CanceledAt.HasValue); - if (subscription != null) { - update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Plan = planId }); - await subscriptionService.UpdateAsync(subscription.Id, update); - } else { - create.Items.Add(new SubscriptionItemOptions { Plan = planId }); - await subscriptionService.CreateAsync(create); - } - - if (cardUpdated) - organization.CardLast4 = last4; - - organization.BillingStatus = BillingStatus.Active; - organization.RemoveSuspension(); - } + organization.SubscribeDate = SystemClock.UtcNow; - _billingManager.ApplyBillingPlan(organization, plan, CurrentUser); - await _repository.SaveAsync(organization, o => o.Cache()); - await _messagePublisher.PublishAsync(new PlanChanged { OrganizationId = organization.Id }); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Tag("Change Plan").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogCritical(ex, "An error occurred while trying to update your billing plan: {Message}", ex.Message); + var createCustomer = new CustomerCreateOptions { + Source = stripeToken, + Plan = planId, + Description = organization.Name, + Email = CurrentUser.EmailAddress + }; - return Ok(ChangePlanResult.FailWithMessage(ex.Message)); - } + if (!String.IsNullOrWhiteSpace(couponId)) + createCustomer.Coupon = couponId; - return Ok(new ChangePlanResult { Success = true }); - } + var customer = await customerService.CreateAsync(createCustomer); - /// - /// Add user - /// - /// The identifier of the organization. - /// The email address of the user you wish to add to your organization. - /// The organization was not found. - /// Please upgrade your plan to add an additional user. - [HttpPost] - [Route("{id:objectid}/users/{email:minlength(1)}")] - public async Task> AddUserAsync(string id, string email) { - if (String.IsNullOrEmpty(id) || !CanAccessOrganization(id) || String.IsNullOrEmpty(email)) - return NotFound(); - - var organization = await GetModelAsync(id); - if (organization == null) - return NotFound(); - - if (!await _billingManager.CanAddUserAsync(organization)) - return PlanLimitReached("Please upgrade your plan to add an additional user."); - - var user = await _userRepository.GetByEmailAddressAsync(email); - if (user != null) { - if (!user.OrganizationIds.Contains(organization.Id)) { - user.OrganizationIds.Add(organization.Id); - await _userRepository.SaveAsync(user, o => o.Cache()); - await _messagePublisher.PublishAsync(new UserMembershipChanged { - ChangeType = ChangeType.Added, - UserId = user.Id, - OrganizationId = organization.Id - }); + organization.BillingStatus = BillingStatus.Active; + organization.RemoveSuspension(); + organization.StripeCustomerId = customer.Id; + organization.CardLast4 = last4; + } + else { + var update = new SubscriptionUpdateOptions { Items = new List() }; + var create = new SubscriptionCreateOptions { Customer = organization.StripeCustomerId, Items = new List() }; + bool cardUpdated = false; + + var customerUpdateOptions = new CustomerUpdateOptions { Description = organization.Name }; + if (!Request.IsGlobalAdmin()) + customerUpdateOptions.Email = CurrentUser.EmailAddress; + + if (!String.IsNullOrEmpty(stripeToken)) { + customerUpdateOptions.Source = stripeToken; + cardUpdated = true; } - await _mailer.SendOrganizationAddedAsync(CurrentUser, organization, user); - } else { - var invite = organization.Invites.FirstOrDefault(i => String.Equals(i.EmailAddress, email, StringComparison.OrdinalIgnoreCase)); - if (invite == null) { - invite = new Invite { - Token = StringExtensions.GetNewToken(), - EmailAddress = email.ToLowerInvariant(), - DateAdded = SystemClock.UtcNow - }; - organization.Invites.Add(invite); - await _repository.SaveAsync(organization, o => o.Cache()); + await customerService.UpdateAsync(organization.StripeCustomerId, customerUpdateOptions); + + var subscriptionList = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId }); + var subscription = subscriptionList.FirstOrDefault(s => !s.CanceledAt.HasValue); + if (subscription != null) { + update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Plan = planId }); + await subscriptionService.UpdateAsync(subscription.Id, update); + } + else { + create.Items.Add(new SubscriptionItemOptions { Plan = planId }); + await subscriptionService.CreateAsync(create); } - await _mailer.SendOrganizationInviteAsync(CurrentUser, organization, invite); + if (cardUpdated) + organization.CardLast4 = last4; + + organization.BillingStatus = BillingStatus.Active; + organization.RemoveSuspension(); } - return Ok(new User { EmailAddress = email }); + _billingManager.ApplyBillingPlan(organization, plan, CurrentUser); + await _repository.SaveAsync(organization, o => o.Cache()); + await _messagePublisher.PublishAsync(new PlanChanged { OrganizationId = organization.Id }); } + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Tag("Change Plan").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogCritical(ex, "An error occurred while trying to update your billing plan: {Message}", ex.Message); - /// - /// Remove user - /// - /// The identifier of the organization. - /// The email address of the user you wish to remove from your organization. - /// The error occurred while removing the user from your organization - /// The organization was not found. - [HttpDelete] - [Route("{id:objectid}/users/{email:minlength(1)}")] - public async Task RemoveUserAsync(string id, string email) { - var organization = await GetModelAsync(id, false); - if (organization == null) - return NotFound(); - - var user = await _userRepository.GetByEmailAddressAsync(email); - if (user == null || !user.OrganizationIds.Contains(id)) { - var invite = organization.Invites.FirstOrDefault(i => String.Equals(i.EmailAddress, email, StringComparison.OrdinalIgnoreCase)); - if (invite == null) - return Ok(); - - organization.Invites.Remove(invite); - await _repository.SaveAsync(organization, o => o.Cache()); - } else { - if (!user.OrganizationIds.Contains(organization.Id)) - return BadRequest(); - - if ((await _userRepository.GetByOrganizationIdAsync(organization.Id)).Total == 1) - return BadRequest("An organization must contain at least one user."); - - var projects = (await _projectRepository.GetByOrganizationIdAsync(organization.Id)).Documents.Where(p => p.NotificationSettings.ContainsKey(user.Id)).ToList(); - if (projects.Count > 0) { - foreach (var project in projects) - project.NotificationSettings.Remove(user.Id); + return Ok(ChangePlanResult.FailWithMessage(ex.Message)); + } - await _projectRepository.SaveAsync(projects); - } + return Ok(new ChangePlanResult { Success = true }); + } - user.OrganizationIds.Remove(organization.Id); + /// + /// Add user + /// + /// The identifier of the organization. + /// The email address of the user you wish to add to your organization. + /// The organization was not found. + /// Please upgrade your plan to add an additional user. + [HttpPost] + [Route("{id:objectid}/users/{email:minlength(1)}")] + public async Task> AddUserAsync(string id, string email) { + if (String.IsNullOrEmpty(id) || !CanAccessOrganization(id) || String.IsNullOrEmpty(email)) + return NotFound(); + + var organization = await GetModelAsync(id); + if (organization == null) + return NotFound(); + + if (!await _billingManager.CanAddUserAsync(organization)) + return PlanLimitReached("Please upgrade your plan to add an additional user."); + + var user = await _userRepository.GetByEmailAddressAsync(email); + if (user != null) { + if (!user.OrganizationIds.Contains(organization.Id)) { + user.OrganizationIds.Add(organization.Id); await _userRepository.SaveAsync(user, o => o.Cache()); await _messagePublisher.PublishAsync(new UserMembershipChanged { - ChangeType = ChangeType.Removed, + ChangeType = ChangeType.Added, UserId = user.Id, OrganizationId = organization.Id }); } - return Ok(); + await _mailer.SendOrganizationAddedAsync(CurrentUser, organization, user); } + else { + var invite = organization.Invites.FirstOrDefault(i => String.Equals(i.EmailAddress, email, StringComparison.OrdinalIgnoreCase)); + if (invite == null) { + invite = new Invite { + Token = StringExtensions.GetNewToken(), + EmailAddress = email.ToLowerInvariant(), + DateAdded = SystemClock.UtcNow + }; + organization.Invites.Add(invite); + await _repository.SaveAsync(organization, o => o.Cache()); + } - [HttpPost] - [Route("{id:objectid}/suspend")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task SuspendAsync(string id, SuspensionCode code, string notes = null) { - var organization = await GetModelAsync(id, false); - if (organization == null) - return NotFound(); - - organization.IsSuspended = true; - organization.SuspensionDate = SystemClock.UtcNow; - organization.SuspendedByUserId = CurrentUser.Id; - organization.SuspensionCode = code; - organization.SuspensionNotes = notes; - await _repository.SaveAsync(organization, o => o.Cache()); - - return Ok(); + await _mailer.SendOrganizationInviteAsync(CurrentUser, organization, invite); } - [HttpDelete] - [Route("{id:objectid}/suspend")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task UnsuspendAsync(string id) { - var organization = await GetModelAsync(id, false); - if (organization == null) - return NotFound(); - - organization.IsSuspended = false; - organization.SuspensionDate = null; - organization.SuspendedByUserId = null; - organization.SuspensionCode = null; - organization.SuspensionNotes = null; - await _repository.SaveAsync(organization, o => o.Cache()); + return Ok(new User { EmailAddress = email }); + } - return Ok(); + /// + /// Remove user + /// + /// The identifier of the organization. + /// The email address of the user you wish to remove from your organization. + /// The error occurred while removing the user from your organization + /// The organization was not found. + [HttpDelete] + [Route("{id:objectid}/users/{email:minlength(1)}")] + public async Task RemoveUserAsync(string id, string email) { + var organization = await GetModelAsync(id, false); + if (organization == null) + return NotFound(); + + var user = await _userRepository.GetByEmailAddressAsync(email); + if (user == null || !user.OrganizationIds.Contains(id)) { + var invite = organization.Invites.FirstOrDefault(i => String.Equals(i.EmailAddress, email, StringComparison.OrdinalIgnoreCase)); + if (invite == null) + return Ok(); + + organization.Invites.Remove(invite); + await _repository.SaveAsync(organization, o => o.Cache()); } - - /// - /// Add custom data - /// - /// The identifier of the organization. - /// The key name of the data object. - /// Any string value. - /// The organization was not found. - [HttpPost] - [Consumes("application/json")] - [Route("{id:objectid}/data/{key:minlength(1)}")] - public async Task PostDataAsync(string id, string key, ValueFromBody value) { - if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value) || key.StartsWith("-")) + else { + if (!user.OrganizationIds.Contains(organization.Id)) return BadRequest(); - var organization = await GetModelAsync(id, false); - if (organization == null) - return NotFound(); + if ((await _userRepository.GetByOrganizationIdAsync(organization.Id)).Total == 1) + return BadRequest("An organization must contain at least one user."); - organization.Data[key.Trim()] = value.Value.Trim(); - await _repository.SaveAsync(organization, o => o.Cache()); + var projects = (await _projectRepository.GetByOrganizationIdAsync(organization.Id)).Documents.Where(p => p.NotificationSettings.ContainsKey(user.Id)).ToList(); + if (projects.Count > 0) { + foreach (var project in projects) + project.NotificationSettings.Remove(user.Id); - return Ok(); + await _projectRepository.SaveAsync(projects); + } + + user.OrganizationIds.Remove(organization.Id); + await _userRepository.SaveAsync(user, o => o.Cache()); + await _messagePublisher.PublishAsync(new UserMembershipChanged { + ChangeType = ChangeType.Removed, + UserId = user.Id, + OrganizationId = organization.Id + }); } - /// - /// Remove custom data - /// - /// The identifier of the organization. - /// The key name of the data object. - /// The organization was not found. - [HttpDelete] - [Route("{id:objectid}/data/{key:minlength(1)}")] - public async Task DeleteDataAsync(string id, string key) { - var organization = await GetModelAsync(id, false); - if (organization == null) - return NotFound(); - - if (organization.Data.Remove(key)) - await _repository.SaveAsync(organization, o => o.Cache()); + return Ok(); + } - return Ok(); - } + [HttpPost] + [Route("{id:objectid}/suspend")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task SuspendAsync(string id, SuspensionCode code, string notes = null) { + var organization = await GetModelAsync(id, false); + if (organization == null) + return NotFound(); + + organization.IsSuspended = true; + organization.SuspensionDate = SystemClock.UtcNow; + organization.SuspendedByUserId = CurrentUser.Id; + organization.SuspensionCode = code; + organization.SuspensionNotes = notes; + await _repository.SaveAsync(organization, o => o.Cache()); + + return Ok(); + } - /// - /// Check for unique name - /// - /// The organization name to check. - /// The organization name is available. - /// The organization name is not available. - [HttpGet] - [Route("check-name")] - public async Task IsNameAvailableAsync(string name) { - if (await IsOrganizationNameAvailableInternalAsync(name)) - return StatusCode(StatusCodes.Status204NoContent); - - return StatusCode(StatusCodes.Status201Created); - } + [HttpDelete] + [Route("{id:objectid}/suspend")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task UnsuspendAsync(string id) { + var organization = await GetModelAsync(id, false); + if (organization == null) + return NotFound(); + + organization.IsSuspended = false; + organization.SuspensionDate = null; + organization.SuspendedByUserId = null; + organization.SuspensionCode = null; + organization.SuspensionNotes = null; + await _repository.SaveAsync(organization, o => o.Cache()); + + return Ok(); + } - private async Task IsOrganizationNameAvailableInternalAsync(string name) { - if (String.IsNullOrWhiteSpace(name)) - return false; + /// + /// Add custom data + /// + /// The identifier of the organization. + /// The key name of the data object. + /// Any string value. + /// The organization was not found. + [HttpPost] + [Consumes("application/json")] + [Route("{id:objectid}/data/{key:minlength(1)}")] + public async Task PostDataAsync(string id, string key, ValueFromBody value) { + if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value) || key.StartsWith("-")) + return BadRequest(); + + var organization = await GetModelAsync(id, false); + if (organization == null) + return NotFound(); + + organization.Data[key.Trim()] = value.Value.Trim(); + await _repository.SaveAsync(organization, o => o.Cache()); + + return Ok(); + } - string decodedName = Uri.UnescapeDataString(name).Trim().ToLowerInvariant(); - var results = await _repository.GetByIdsAsync(GetAssociatedOrganizationIds().ToArray(), o => o.Cache()); - return !results.Any(o => String.Equals(o.Name.Trim().ToLowerInvariant(), decodedName, StringComparison.OrdinalIgnoreCase)); - } + /// + /// Remove custom data + /// + /// The identifier of the organization. + /// The key name of the data object. + /// The organization was not found. + [HttpDelete] + [Route("{id:objectid}/data/{key:minlength(1)}")] + public async Task DeleteDataAsync(string id, string key) { + var organization = await GetModelAsync(id, false); + if (organization == null) + return NotFound(); + + if (organization.Data.Remove(key)) + await _repository.SaveAsync(organization, o => o.Cache()); - protected override async Task CanAddAsync(Organization value) { - if (String.IsNullOrEmpty(value.Name)) - return PermissionResult.DenyWithMessage("Organization name is required."); + return Ok(); + } - if (!await IsOrganizationNameAvailableInternalAsync(value.Name)) - return PermissionResult.DenyWithMessage("A organization with this name already exists."); + /// + /// Check for unique name + /// + /// The organization name to check. + /// The organization name is available. + /// The organization name is not available. + [HttpGet] + [Route("check-name")] + public async Task IsNameAvailableAsync(string name) { + if (await IsOrganizationNameAvailableInternalAsync(name)) + return StatusCode(StatusCodes.Status204NoContent); + + return StatusCode(StatusCodes.Status201Created); + } - if (!await _billingManager.CanAddOrganizationAsync(CurrentUser)) - return PermissionResult.DenyWithPlanLimitReached("Please upgrade your plan to add an additional organization."); + private async Task IsOrganizationNameAvailableInternalAsync(string name) { + if (String.IsNullOrWhiteSpace(name)) + return false; - return await base.CanAddAsync(value); - } + string decodedName = Uri.UnescapeDataString(name).Trim().ToLowerInvariant(); + var results = await _repository.GetByIdsAsync(GetAssociatedOrganizationIds().ToArray(), o => o.Cache()); + return !results.Any(o => String.Equals(o.Name.Trim().ToLowerInvariant(), decodedName, StringComparison.OrdinalIgnoreCase)); + } - protected override async Task AddModelAsync(Organization value) { - _billingManager.ApplyBillingPlan(value, _options.StripeOptions.EnableBilling ? _plans.FreePlan : _plans.UnlimitedPlan, CurrentUser); + protected override async Task CanAddAsync(Organization value) { + if (String.IsNullOrEmpty(value.Name)) + return PermissionResult.DenyWithMessage("Organization name is required."); - var organization = await base.AddModelAsync(value); + if (!await IsOrganizationNameAvailableInternalAsync(value.Name)) + return PermissionResult.DenyWithMessage("A organization with this name already exists."); - CurrentUser.OrganizationIds.Add(organization.Id); - await _userRepository.SaveAsync(CurrentUser, o => o.Cache()); - await _messagePublisher.PublishAsync(new UserMembershipChanged { - UserId = CurrentUser.Id, - OrganizationId = organization.Id, - ChangeType = ChangeType.Added - }); + if (!await _billingManager.CanAddOrganizationAsync(CurrentUser)) + return PermissionResult.DenyWithPlanLimitReached("Please upgrade your plan to add an additional organization."); - return organization; - } + return await base.CanAddAsync(value); + } - protected override async Task CanUpdateAsync(Organization original, Delta changes) { - var changed = changes.GetEntity(); - if (!await IsOrganizationNameAvailableInternalAsync(changed.Name)) - return PermissionResult.DenyWithMessage("A organization with this name already exists."); + protected override async Task AddModelAsync(Organization value) { + _billingManager.ApplyBillingPlan(value, _options.StripeOptions.EnableBilling ? _plans.FreePlan : _plans.UnlimitedPlan, CurrentUser); - return await base.CanUpdateAsync(original, changes); - } + var organization = await base.AddModelAsync(value); - protected override async Task CanDeleteAsync(Organization value) { - if (!String.IsNullOrEmpty(value.StripeCustomerId) && !User.IsInRole(AuthorizationRoles.GlobalAdmin)) - return PermissionResult.DenyWithMessage("An organization cannot be deleted if it has a subscription.", value.Id); + CurrentUser.OrganizationIds.Add(organization.Id); + await _userRepository.SaveAsync(CurrentUser, o => o.Cache()); + await _messagePublisher.PublishAsync(new UserMembershipChanged { + UserId = CurrentUser.Id, + OrganizationId = organization.Id, + ChangeType = ChangeType.Added + }); - var projects = (await _projectRepository.GetByOrganizationIdAsync(value.Id)).Documents.ToList(); - if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && projects.Any()) - return PermissionResult.DenyWithMessage("An organization cannot be deleted if it contains any projects.", value.Id); + return organization; + } - return await base.CanDeleteAsync(value); - } + protected override async Task CanUpdateAsync(Organization original, Delta changes) { + var changed = changes.GetEntity(); + if (!await IsOrganizationNameAvailableInternalAsync(changed.Name)) + return PermissionResult.DenyWithMessage("A organization with this name already exists."); - protected override async Task AfterResultMapAsync(ICollection models) { - await base.AfterResultMapAsync(models); + return await base.CanUpdateAsync(original, changes); + } - var viewOrganizations = models.OfType().ToList(); - foreach (var viewOrganization in viewOrganizations) { - var usageRetention = SystemClock.UtcNow.SubtractYears(1).StartOfMonth(); - viewOrganization.Usage = viewOrganization.Usage.Where(u => u.Date > usageRetention).ToList(); - viewOrganization.OverageHours = viewOrganization.OverageHours.Where(u => u.Date > usageRetention).ToList(); - viewOrganization.IsOverRequestLimit = await OrganizationExtensions.IsOverRequestLimitAsync(viewOrganization.Id, _cacheClient, _options.ApiThrottleLimit); - } - } + protected override async Task CanDeleteAsync(Organization value) { + if (!String.IsNullOrEmpty(value.StripeCustomerId) && !User.IsInRole(AuthorizationRoles.GlobalAdmin)) + return PermissionResult.DenyWithMessage("An organization cannot be deleted if it has a subscription.", value.Id); + + var projects = (await _projectRepository.GetByOrganizationIdAsync(value.Id)).Documents.ToList(); + if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && projects.Any()) + return PermissionResult.DenyWithMessage("An organization cannot be deleted if it contains any projects.", value.Id); + + return await base.CanDeleteAsync(value); + } - private async Task PopulateOrganizationStatsAsync(ViewOrganization organization) { - return (await PopulateOrganizationStatsAsync(new List { organization })).FirstOrDefault(); + protected override async Task AfterResultMapAsync(ICollection models) { + await base.AfterResultMapAsync(models); + + var viewOrganizations = models.OfType().ToList(); + foreach (var viewOrganization in viewOrganizations) { + var usageRetention = SystemClock.UtcNow.SubtractYears(1).StartOfMonth(); + viewOrganization.Usage = viewOrganization.Usage.Where(u => u.Date > usageRetention).ToList(); + viewOrganization.OverageHours = viewOrganization.OverageHours.Where(u => u.Date > usageRetention).ToList(); + viewOrganization.IsOverRequestLimit = await OrganizationExtensions.IsOverRequestLimitAsync(viewOrganization.Id, _cacheClient, _options.ApiThrottleLimit); } + } - private async Task> PopulateOrganizationStatsAsync(List viewOrganizations) { - if (viewOrganizations.Count <= 0) - return viewOrganizations; - - int maximumRetentionDays = _options.MaximumRetentionDays; - var organizations = viewOrganizations.Select(o => new Organization { Id = o.Id, CreatedUtc = o.CreatedUtc, RetentionDays = o.RetentionDays }).ToList(); - var sf = new AppFilter(organizations); - var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow, (PersistentEvent e) => e.Date).Index(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow); - var result = await _eventRepository.CountAsync(q => q - .SystemFilter(systemFilter) - .AggregationsExpression($"terms:(organization_id~{viewOrganizations.Count} cardinality:stack_id)") - .EnforceEventStackFilter(false)); - foreach (var organization in viewOrganizations) { - var organizationStats = result.Aggregations.Terms("terms_organization_id")?.Buckets.FirstOrDefault(t => t.Key == organization.Id); - organization.EventCount = organizationStats?.Total ?? 0; - organization.StackCount = (long?)organizationStats?.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0; - organization.ProjectCount = await _projectRepository.GetCountByOrganizationIdAsync(organization.Id); - } + private async Task PopulateOrganizationStatsAsync(ViewOrganization organization) { + return (await PopulateOrganizationStatsAsync(new List { organization })).FirstOrDefault(); + } + private async Task> PopulateOrganizationStatsAsync(List viewOrganizations) { + if (viewOrganizations.Count <= 0) return viewOrganizations; + + int maximumRetentionDays = _options.MaximumRetentionDays; + var organizations = viewOrganizations.Select(o => new Organization { Id = o.Id, CreatedUtc = o.CreatedUtc, RetentionDays = o.RetentionDays }).ToList(); + var sf = new AppFilter(organizations); + var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow, (PersistentEvent e) => e.Date).Index(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow); + var result = await _eventRepository.CountAsync(q => q + .SystemFilter(systemFilter) + .AggregationsExpression($"terms:(organization_id~{viewOrganizations.Count} cardinality:stack_id)") + .EnforceEventStackFilter(false)); + foreach (var organization in viewOrganizations) { + var organizationStats = result.Aggregations.Terms("terms_organization_id")?.Buckets.FirstOrDefault(t => t.Key == organization.Id); + organization.EventCount = organizationStats?.Total ?? 0; + organization.StackCount = (long?)organizationStats?.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0; + organization.ProjectCount = await _projectRepository.GetCountByOrganizationIdAsync(organization.Id); } + + return viewOrganizations; } } diff --git a/src/Exceptionless.Web/Controllers/ProjectController.cs b/src/Exceptionless.Web/Controllers/ProjectController.cs index 23885a27f9..18b83b6581 100644 --- a/src/Exceptionless.Web/Controllers/ProjectController.cs +++ b/src/Exceptionless.Web/Controllers/ProjectController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Core; using Exceptionless.Web.Extensions; using Exceptionless.Core.Authorization; @@ -22,719 +18,718 @@ using Foundatio.Repositories.Models; using Foundatio.Utility; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Exceptionless.Core.Repositories.Options; - -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/projects")] - [Authorize(Policy = AuthorizationRoles.ClientPolicy)] - public class ProjectController : RepositoryApiController { - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly ITokenRepository _tokenRepository; - private readonly IQueue _workItemQueue; - private readonly BillingManager _billingManager; - private readonly SlackService _slackService; - private readonly AppOptions _options; - - public ProjectController( - IOrganizationRepository organizationRepository, - IProjectRepository projectRepository, - IStackRepository stackRepository, - IEventRepository eventRepository, - ITokenRepository tokenRepository, - IQueue workItemQueue, - BillingManager billingManager, - SlackService slackService, - IMapper mapper, - IAppQueryValidator validator, - AppOptions options, - ILoggerFactory loggerFactory - ) : base(projectRepository, mapper, validator, loggerFactory) { - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _stackRepository = stackRepository; - _eventRepository = eventRepository; - _tokenRepository = tokenRepository; - _workItemQueue = workItemQueue; - _billingManager = billingManager; - _slackService = slackService; - _options = options; - } - #region CRUD - - /// - /// Get all - /// - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned. - [HttpGet] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetAsync(string filter = null, string sort = null, int page = 1, int limit = 10, string mode = null) { - var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); - if (organizations.Count == 0) - return Ok(EmptyModels); - - page = GetPage(page); - limit = GetLimit(limit, 1000); - - var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; - var projects = await _repository.GetByFilterAsync(sf, filter, sort, o => o.PageNumber(page).PageLimit(limit)); - var viewProjects = await MapCollectionAsync(projects.Documents, true); - - if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) - return OkWithResourceLinks(await PopulateProjectStatsAsync(viewProjects.ToList()), projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); - - return OkWithResourceLinks(viewProjects, projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); - } +namespace Exceptionless.Web.Controllers; + +[Route(API_PREFIX + "/projects")] +[Authorize(Policy = AuthorizationRoles.ClientPolicy)] +public class ProjectController : RepositoryApiController { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly ITokenRepository _tokenRepository; + private readonly IQueue _workItemQueue; + private readonly BillingManager _billingManager; + private readonly SlackService _slackService; + private readonly AppOptions _options; + + public ProjectController( + IOrganizationRepository organizationRepository, + IProjectRepository projectRepository, + IStackRepository stackRepository, + IEventRepository eventRepository, + ITokenRepository tokenRepository, + IQueue workItemQueue, + BillingManager billingManager, + SlackService slackService, + IMapper mapper, + IAppQueryValidator validator, + AppOptions options, + ILoggerFactory loggerFactory + ) : base(projectRepository, mapper, validator, loggerFactory) { + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _stackRepository = stackRepository; + _eventRepository = eventRepository; + _tokenRepository = tokenRepository; + _workItemQueue = workItemQueue; + _billingManager = billingManager; + _slackService = slackService; + _options = options; + } - /// - /// Get all - /// - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date. - /// The identifier of the organization. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned. - /// The organization could not be found. - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/projects")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByOrganizationAsync(string organizationId, string filter = null, string sort = null, int page = 1, int limit = 10, string mode = null) { - var organization = await GetOrganizationAsync(organizationId); - if (organization == null) - return NotFound(); - - page = GetPage(page); - limit = GetLimit(limit, 1000); - var sf = new AppFilter(organization); - var projects = await _repository.GetByFilterAsync(sf, filter, sort, o => o.PageNumber(page).PageLimit(limit)); - var viewProjects = (await MapCollectionAsync(projects.Documents, true)).ToList(); - - if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) - return OkWithResourceLinks(await PopulateProjectStatsAsync(viewProjects), projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); - - return OkWithResourceLinks(viewProjects, projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); - } + #region CRUD + + /// + /// Get all + /// + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned. + [HttpGet] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetAsync(string filter = null, string sort = null, int page = 1, int limit = 10, string mode = null) { + var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); + if (organizations.Count == 0) + return Ok(EmptyModels); + + page = GetPage(page); + limit = GetLimit(limit, 1000); + + var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; + var projects = await _repository.GetByFilterAsync(sf, filter, sort, o => o.PageNumber(page).PageLimit(limit)); + var viewProjects = await MapCollectionAsync(projects.Documents, true); + + if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) + return OkWithResourceLinks(await PopulateProjectStatsAsync(viewProjects.ToList()), projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); + + return OkWithResourceLinks(viewProjects, projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); + } - /// - /// Get by id - /// - /// The identifier of the project. - /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned. - /// The project could not be found. - [HttpGet("{id:objectid}", Name = "GetProjectById")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetAsync(string id, string mode = null) { - var project = await GetModelAsync(id); - if (project == null) - return NotFound(); - - var viewProject = await MapAsync(project, true); - if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) - return Ok(await PopulateProjectStatsAsync(viewProject)); - - return Ok(viewProject); - } + /// + /// Get all + /// + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -created returns the results descending by the created date. + /// The identifier of the organization. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned. + /// The organization could not be found. + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/projects")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByOrganizationAsync(string organizationId, string filter = null, string sort = null, int page = 1, int limit = 10, string mode = null) { + var organization = await GetOrganizationAsync(organizationId); + if (organization == null) + return NotFound(); + + page = GetPage(page); + limit = GetLimit(limit, 1000); + var sf = new AppFilter(organization); + var projects = await _repository.GetByFilterAsync(sf, filter, sort, o => o.PageNumber(page).PageLimit(limit)); + var viewProjects = (await MapCollectionAsync(projects.Documents, true)).ToList(); + + if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) + return OkWithResourceLinks(await PopulateProjectStatsAsync(viewProjects), projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); + + return OkWithResourceLinks(viewProjects, projects.HasMore && !NextPageExceedsSkipLimit(page, limit), page, projects.Total); + } - /// - /// Create - /// - /// The project. - /// - /// An error occurred while creating the project. - /// The project already exists. - [HttpPost] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status201Created)] - public Task> PostAsync(NewProject project) { - return PostImplAsync(project); - } + /// + /// Get by id + /// + /// The identifier of the project. + /// If no mode is set then the a light weight project object will be returned. If the mode is set to stats than the fully populated object will be returned. + /// The project could not be found. + [HttpGet("{id:objectid}", Name = "GetProjectById")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetAsync(string id, string mode = null) { + var project = await GetModelAsync(id); + if (project == null) + return NotFound(); + + var viewProject = await MapAsync(project, true); + if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "stats", StringComparison.OrdinalIgnoreCase)) + return Ok(await PopulateProjectStatsAsync(viewProject)); + + return Ok(viewProject); + } - /// - /// Update - /// - /// The identifier of the project. - /// The changes - /// An error occurred while updating the project. - /// The project could not be found. - [HttpPatch("{id:objectid}")] - [HttpPut("{id:objectid}")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public Task> PatchAsync(string id, Delta changes) { - return PatchImplAsync(id, changes); - } + /// + /// Create + /// + /// The project. + /// + /// An error occurred while creating the project. + /// The project already exists. + [HttpPost] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status201Created)] + public Task> PostAsync(NewProject project) { + return PostImplAsync(project); + } - /// - /// Remove - /// - /// A comma delimited list of project identifiers. - /// No Content. - /// One or more validation errors occurred. - /// One or more projects were not found. - /// An error occurred while deleting one or more projects. - [HttpDelete("{ids:objectids}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteAsync(string ids) { - return DeleteImplAsync(ids.FromDelimitedString()); - } + /// + /// Update + /// + /// The identifier of the project. + /// The changes + /// An error occurred while updating the project. + /// The project could not be found. + [HttpPatch("{id:objectid}")] + [HttpPut("{id:objectid}")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public Task> PatchAsync(string id, Delta changes) { + return PatchImplAsync(id, changes); + } - protected override async Task> DeleteModelsAsync(ICollection projects) { - foreach (var project in projects) { - using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id).Tag("Delete").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.UserDeletingProject(CurrentUser.Id, project.Name); + /// + /// Remove + /// + /// A comma delimited list of project identifiers. + /// No Content. + /// One or more validation errors occurred. + /// One or more projects were not found. + /// An error occurred while deleting one or more projects. + [HttpDelete("{ids:objectids}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteAsync(string ids) { + return DeleteImplAsync(ids.FromDelimitedString()); + } - await _tokenRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id); - } + protected override async Task> DeleteModelsAsync(ICollection projects) { + foreach (var project in projects) { + using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id).Tag("Delete").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.UserDeletingProject(CurrentUser.Id, project.Name); - return await base.DeleteModelsAsync(projects); + await _tokenRepository.RemoveAllByProjectIdAsync(project.OrganizationId, project.Id); } - #endregion + return await base.DeleteModelsAsync(projects); + } - [Obsolete] - [HttpGet("~/api/v1/project/config")] - public Task> GetV1ConfigAsync(int? v = null) { - return GetConfigAsync(null, v); - } + #endregion - /// - /// Get configuration settings - /// - /// The client configuration version. - /// The client configuration version is the current version. - /// The project could not be found. - [HttpGet("config")] - public Task> GetV2ConfigAsync(int? v = null) { - return GetConfigAsync(null, v); - } + [Obsolete] + [HttpGet("~/api/v1/project/config")] + public Task> GetV1ConfigAsync(int? v = null) { + return GetConfigAsync(null, v); + } - /// - /// Get configuration settings - /// - /// The identifier of the project. - /// The client configuration version. - /// The client configuration version is the current version. - /// The project could not be found. - [HttpGet("{id:objectid}/config")] - public async Task> GetConfigAsync(string id = null, int? v = null) { - if (String.IsNullOrEmpty(id)) - id = User.GetProjectId(); - - var project = await GetModelAsync(id); - if (project == null) - return NotFound(); - - if (v.HasValue && v == project.Configuration.Version) - return StatusCode(StatusCodes.Status304NotModified); - - return Ok(project.Configuration); - } + /// + /// Get configuration settings + /// + /// The client configuration version. + /// The client configuration version is the current version. + /// The project could not be found. + [HttpGet("config")] + public Task> GetV2ConfigAsync(int? v = null) { + return GetConfigAsync(null, v); + } - /// - /// Add configuration value - /// - /// The identifier of the project. - /// The key name of the configuration object. - /// The configuration value. - /// Invalid configuration value. - /// The project could not be found. - [HttpPost("{id:objectid}/config")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task SetConfigAsync(string id, string key, ValueFromBody value) { - if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value)) - return BadRequest(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - project.Configuration.Settings[key.Trim()] = value.Value.Trim(); + /// + /// Get configuration settings + /// + /// The identifier of the project. + /// The client configuration version. + /// The client configuration version is the current version. + /// The project could not be found. + [HttpGet("{id:objectid}/config")] + public async Task> GetConfigAsync(string id = null, int? v = null) { + if (String.IsNullOrEmpty(id)) + id = User.GetProjectId(); + + var project = await GetModelAsync(id); + if (project == null) + return NotFound(); + + if (v.HasValue && v == project.Configuration.Version) + return StatusCode(StatusCodes.Status304NotModified); + + return Ok(project.Configuration); + } + + /// + /// Add configuration value + /// + /// The identifier of the project. + /// The key name of the configuration object. + /// The configuration value. + /// Invalid configuration value. + /// The project could not be found. + [HttpPost("{id:objectid}/config")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task SetConfigAsync(string id, string key, ValueFromBody value) { + if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value)) + return BadRequest(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + project.Configuration.Settings[key.Trim()] = value.Value.Trim(); + project.Configuration.IncrementVersion(); + await _repository.SaveAsync(project, o => o.Cache()); + + return Ok(); + } + + /// + /// Remove configuration value + /// + /// The identifier of the project. + /// The key name of the configuration object. + /// Invalid key value. + /// The project could not be found. + [HttpDelete("{id:objectid}/config")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task DeleteConfigAsync(string id, string key) { + if (String.IsNullOrWhiteSpace(key)) + return BadRequest(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + if (project.Configuration.Settings.Remove(key.Trim())) { project.Configuration.IncrementVersion(); await _repository.SaveAsync(project, o => o.Cache()); - - return Ok(); } - /// - /// Remove configuration value - /// - /// The identifier of the project. - /// The key name of the configuration object. - /// Invalid key value. - /// The project could not be found. - [HttpDelete("{id:objectid}/config")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task DeleteConfigAsync(string id, string key) { - if (String.IsNullOrWhiteSpace(key)) - return BadRequest(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - if (project.Configuration.Settings.Remove(key.Trim())) { - project.Configuration.IncrementVersion(); - await _repository.SaveAsync(project, o => o.Cache()); - } + return Ok(); + } - return Ok(); - } + /// + /// Reset project data + /// + /// The identifier of the project. + /// The project could not be found. + [HttpGet("{id:objectid}/reset-data")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public async Task> ResetDataAsync(string id) { + var project = await GetModelAsync(id); + if (project == null) + return NotFound(); + + string workItemId = await _workItemQueue.EnqueueAsync(new RemoveStacksWorkItem { + OrganizationId = project.OrganizationId, + ProjectId = project.Id + }); + + return WorkInProgress(new[] { workItemId }); + } - /// - /// Reset project data - /// - /// The identifier of the project. - /// The project could not be found. - [HttpGet("{id:objectid}/reset-data")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public async Task> ResetDataAsync(string id) { - var project = await GetModelAsync(id); - if (project == null) - return NotFound(); - - string workItemId = await _workItemQueue.EnqueueAsync(new RemoveStacksWorkItem { - OrganizationId = project.OrganizationId, - ProjectId = project.Id - }); - - return WorkInProgress(new [] { workItemId }); - } + [HttpGet("{id:objectid}/notifications")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task>> GetNotificationSettingsAsync(string id) { + var project = await GetModelAsync(id); + if (project == null) + return NotFound(); - [HttpGet("{id:objectid}/notifications")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task>> GetNotificationSettingsAsync(string id) { - var project = await GetModelAsync(id); - if (project == null) - return NotFound(); + return Ok(project.NotificationSettings); + } - return Ok(project.NotificationSettings); - } + /// + /// Get user notification settings + /// + /// The identifier of the project. + /// The identifier of the user. + /// The project could not be found. + [HttpGet("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetNotificationSettingsAsync(string id, string userId) { + var project = await GetModelAsync(id); + if (project == null) + return NotFound(); + + if (!Request.IsGlobalAdmin() && !String.Equals(CurrentUser.Id, userId)) + return NotFound(); + + return Ok(project.NotificationSettings.TryGetValue(userId, out var settings) ? settings : new NotificationSettings()); + } - /// - /// Get user notification settings - /// - /// The identifier of the project. - /// The identifier of the user. - /// The project could not be found. - [HttpGet("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetNotificationSettingsAsync(string id, string userId) { - var project = await GetModelAsync(id); - if (project == null) - return NotFound(); - - if (!Request.IsGlobalAdmin() && !String.Equals(CurrentUser.Id, userId)) - return NotFound(); - - return Ok(project.NotificationSettings.TryGetValue(userId, out var settings) ? settings : new NotificationSettings()); - } + /// + /// Get an integrations notification settings + /// + /// The identifier of the project. + /// The identifier of the integration. + /// The project or integration could not be found. + [ApiExplorerSettings(IgnoreApi = true)] + [HttpGet("{id:objectid}/{integration:minlength(1)}/notifications")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetIntegrationNotificationSettingsAsync(string id, string integration) { + var project = await GetModelAsync(id); + if (project == null) + return NotFound(); + + if (!String.Equals(Project.NotificationIntegrations.Slack, integration)) + return NotFound(); + + return Ok(project.NotificationSettings.TryGetValue(Project.NotificationIntegrations.Slack, out var settings) ? settings : new NotificationSettings()); + } - /// - /// Get an integrations notification settings - /// - /// The identifier of the project. - /// The identifier of the integration. - /// The project or integration could not be found. - [ApiExplorerSettings(IgnoreApi = true)] - [HttpGet("{id:objectid}/{integration:minlength(1)}/notifications")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetIntegrationNotificationSettingsAsync(string id, string integration) { - var project = await GetModelAsync(id); - if (project == null) - return NotFound(); - - if (!String.Equals(Project.NotificationIntegrations.Slack, integration)) - return NotFound(); - - return Ok(project.NotificationSettings.TryGetValue(Project.NotificationIntegrations.Slack, out var settings) ? settings : new NotificationSettings()); - } + /// + /// Set user notification settings + /// + /// The identifier of the project. + /// The identifier of the user. + /// The notification settings. + /// The project could not be found. + [HttpPut("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] + [HttpPost("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task SetNotificationSettingsAsync(string id, string userId, NotificationSettings settings) { + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + if (!Request.IsGlobalAdmin() && !String.Equals(CurrentUser.Id, userId)) + return NotFound(); + + if (settings == null) + project.NotificationSettings.Remove(userId); + else + project.NotificationSettings[userId] = settings; + + await _repository.SaveAsync(project, o => o.Cache()); + return Ok(); + } - /// - /// Set user notification settings - /// - /// The identifier of the project. - /// The identifier of the user. - /// The notification settings. - /// The project could not be found. - [HttpPut("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] - [HttpPost("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task SetNotificationSettingsAsync(string id, string userId, NotificationSettings settings) { - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - if (!Request.IsGlobalAdmin() && !String.Equals(CurrentUser.Id, userId)) - return NotFound(); - - if (settings == null) - project.NotificationSettings.Remove(userId); - else - project.NotificationSettings[userId] = settings; + /// + /// Set an integrations notification settings + /// + /// The identifier of the project. + /// The identifier of the user. + /// The notification settings. + /// The project or integration could not be found. + /// Please upgrade your plan to enable integrations. + [HttpPut("{id:objectid}/{integration:minlength(1)}/notifications")] + [HttpPost("{id:objectid}/{integration:minlength(1)}/notifications")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task SetIntegrationNotificationSettingsAsync(string id, string integration, NotificationSettings settings) { + if (!String.Equals(Project.NotificationIntegrations.Slack, integration)) + return NotFound(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()); + if (organization == null) + return NotFound(); + + if (!organization.HasPremiumFeatures) + return PlanLimitReached($"Please upgrade your plan to enable {integration.TrimStart('@')} integration."); + + if (settings == null) + project.NotificationSettings.Remove(integration); + else + project.NotificationSettings[integration] = settings; + + await _repository.SaveAsync(project, o => o.Cache()); + return Ok(); + } + /// + /// Remove user notification settings + /// + /// The identifier of the project. + /// The identifier of the user. + /// The project could not be found. + [HttpDelete("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task DeleteNotificationSettingsAsync(string id, string userId) { + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + if (!Request.IsGlobalAdmin() && !String.Equals(CurrentUser.Id, userId)) + return NotFound(); + + if (project.NotificationSettings.ContainsKey(userId)) { + project.NotificationSettings.Remove(userId); await _repository.SaveAsync(project, o => o.Cache()); - return Ok(); } - /// - /// Set an integrations notification settings - /// - /// The identifier of the project. - /// The identifier of the user. - /// The notification settings. - /// The project or integration could not be found. - /// Please upgrade your plan to enable integrations. - [HttpPut("{id:objectid}/{integration:minlength(1)}/notifications")] - [HttpPost("{id:objectid}/{integration:minlength(1)}/notifications")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task SetIntegrationNotificationSettingsAsync(string id, string integration, NotificationSettings settings) { - if (!String.Equals(Project.NotificationIntegrations.Slack, integration)) - return NotFound(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()); - if (organization == null) - return NotFound(); - - if (!organization.HasPremiumFeatures) - return PlanLimitReached($"Please upgrade your plan to enable {integration.TrimStart('@')} integration."); - - if (settings == null) - project.NotificationSettings.Remove(integration); - else - project.NotificationSettings[integration] = settings; + return Ok(); + } + /// + /// Promote tab + /// + /// The identifier of the project. + /// The tab name. + /// Invalid tab name. + /// The project could not be found. + [HttpPut("{id:objectid}/promotedtabs")] + [HttpPost("{id:objectid}/promotedtabs")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task PromoteTabAsync(string id, string name) { + if (String.IsNullOrWhiteSpace(name)) + return BadRequest(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + if (!project.PromotedTabs.Contains(name.Trim())) { + project.PromotedTabs.Add(name.Trim()); await _repository.SaveAsync(project, o => o.Cache()); - return Ok(); - } - - /// - /// Remove user notification settings - /// - /// The identifier of the project. - /// The identifier of the user. - /// The project could not be found. - [HttpDelete("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task DeleteNotificationSettingsAsync(string id, string userId) { - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - if (!Request.IsGlobalAdmin() && !String.Equals(CurrentUser.Id, userId)) - return NotFound(); - - if (project.NotificationSettings.ContainsKey(userId)) { - project.NotificationSettings.Remove(userId); - await _repository.SaveAsync(project, o => o.Cache()); - } - - return Ok(); } - /// - /// Promote tab - /// - /// The identifier of the project. - /// The tab name. - /// Invalid tab name. - /// The project could not be found. - [HttpPut("{id:objectid}/promotedtabs")] - [HttpPost("{id:objectid}/promotedtabs")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task PromoteTabAsync(string id, string name) { - if (String.IsNullOrWhiteSpace(name)) - return BadRequest(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - if (!project.PromotedTabs.Contains(name.Trim())) { - project.PromotedTabs.Add(name.Trim()); - await _repository.SaveAsync(project, o => o.Cache()); - } + return Ok(); + } - return Ok(); + /// + /// Demote tab + /// + /// The identifier of the project. + /// The tab name. + /// Invalid tab name. + /// The project could not be found. + [HttpDelete("{id:objectid}/promotedtabs")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task DemoteTabAsync(string id, string name) { + if (String.IsNullOrWhiteSpace(name)) + return BadRequest(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + if (project.PromotedTabs.Contains(name.Trim())) { + project.PromotedTabs.Remove(name.Trim()); + await _repository.SaveAsync(project, o => o.Cache()); } - /// - /// Demote tab - /// - /// The identifier of the project. - /// The tab name. - /// Invalid tab name. - /// The project could not be found. - [HttpDelete("{id:objectid}/promotedtabs")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task DemoteTabAsync(string id, string name) { - if (String.IsNullOrWhiteSpace(name)) - return BadRequest(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - if (project.PromotedTabs.Contains(name.Trim())) { - project.PromotedTabs.Remove(name.Trim()); - await _repository.SaveAsync(project, o => o.Cache()); - } + return Ok(); + } - return Ok(); - } + /// + /// Check for unique name + /// + /// The project name to check. + /// If set the check name will be scoped to a specific organization. + /// The project name is available. + /// The project name is not available. + [HttpGet("check-name")] + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/projects/check-name")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task IsNameAvailableAsync(string name, string organizationId = null) { + if (await IsProjectNameAvailableInternalAsync(organizationId, name)) + return StatusCode(StatusCodes.Status204NoContent); + + return StatusCode(StatusCodes.Status201Created); + } - /// - /// Check for unique name - /// - /// The project name to check. - /// If set the check name will be scoped to a specific organization. - /// The project name is available. - /// The project name is not available. - [HttpGet("check-name")] - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/projects/check-name")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status201Created)] - public async Task IsNameAvailableAsync(string name, string organizationId = null) { - if (await IsProjectNameAvailableInternalAsync(organizationId, name)) - return StatusCode(StatusCodes.Status204NoContent); - - return StatusCode(StatusCodes.Status201Created); - } + private async Task IsProjectNameAvailableInternalAsync(string organizationId, string name) { + if (String.IsNullOrWhiteSpace(name)) + return false; - private async Task IsProjectNameAvailableInternalAsync(string organizationId, string name) { - if (String.IsNullOrWhiteSpace(name)) - return false; + var organizationIds = IsInOrganization(organizationId) ? new List { organizationId } : GetAssociatedOrganizationIds(); + var projects = await _repository.GetByOrganizationIdsAsync(organizationIds); - var organizationIds = IsInOrganization(organizationId) ? new List { organizationId } : GetAssociatedOrganizationIds(); - var projects = await _repository.GetByOrganizationIdsAsync(organizationIds); + string decodedName = Uri.UnescapeDataString(name).Trim().ToLowerInvariant(); + return !projects.Documents.Any(p => String.Equals(p.Name.Trim().ToLowerInvariant(), decodedName, StringComparison.OrdinalIgnoreCase)); + } - string decodedName = Uri.UnescapeDataString(name).Trim().ToLowerInvariant(); - return !projects.Documents.Any(p => String.Equals(p.Name.Trim().ToLowerInvariant(), decodedName, StringComparison.OrdinalIgnoreCase)); - } + /// + /// Add custom data + /// + /// The identifier of the project. + /// The key name of the data object. + /// Any string value. + /// Invalid key or value. + /// The project could not be found. + [HttpPost("{id:objectid}/data")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task PostDataAsync(string id, string key, ValueFromBody value) { + if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value) || key.StartsWith("-")) + return BadRequest(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + project.Data[key.Trim()] = value.Value.Trim(); + await _repository.SaveAsync(project, o => o.Cache()); + + return Ok(); + } - /// - /// Add custom data - /// - /// The identifier of the project. - /// The key name of the data object. - /// Any string value. - /// Invalid key or value. - /// The project could not be found. - [HttpPost("{id:objectid}/data")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task PostDataAsync(string id, string key, ValueFromBody value) { - if (String.IsNullOrWhiteSpace(key) || String.IsNullOrWhiteSpace(value?.Value) || key.StartsWith("-")) - return BadRequest(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - project.Data[key.Trim()] = value.Value.Trim(); + /// + /// Remove custom data + /// + /// The identifier of the project. + /// The key name of the data object. + /// Invalid key or value. + /// The project could not be found. + [HttpDelete("{id:objectid}/data")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task DeleteDataAsync(string id, string key) { + if (String.IsNullOrWhiteSpace(key) || key.StartsWith("-")) + return BadRequest(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + if (project.Data.Remove(key.Trim())) await _repository.SaveAsync(project, o => o.Cache()); - return Ok(); - } + return Ok(); + } - /// - /// Remove custom data - /// - /// The identifier of the project. - /// The key name of the data object. - /// Invalid key or value. - /// The project could not be found. - [HttpDelete("{id:objectid}/data")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task DeleteDataAsync(string id, string key) { - if (String.IsNullOrWhiteSpace(key) || key.StartsWith("-")) - return BadRequest(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - if (project.Data.Remove(key.Trim())) - await _repository.SaveAsync(project, o => o.Cache()); - - return Ok(); - } + /// + /// Adds slack integration to the project + /// + /// The identifier of the project. + /// The oauth code that must be exchanged for an auth token.D + /// Invalid code or error contacting slack. + /// The project could not be found. + [ApiExplorerSettings(IgnoreApi = true)] + [HttpPost("{id:objectid}/slack")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task AddSlackAsync(string id, string code) { + if (String.IsNullOrWhiteSpace(code)) + return BadRequest(); + + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + if (project.Data.ContainsKey(Project.KnownDataKeys.SlackToken)) + return StatusCode(StatusCodes.Status304NotModified); + + SlackToken token = null; + try { + token = await _slackService.GetAccessTokenAsync(code); + } + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id).Property("Code", code).Tag("Slack").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, "Error getting slack access token: {Message}", ex.Message); + } + + if (token == null) + return BadRequest(); + + project.AddDefaultNotificationSettings(Project.NotificationIntegrations.Slack); + project.Data[Project.KnownDataKeys.SlackToken] = token; + await _repository.SaveAsync(project, o => o.Cache()); + + return Ok(); + } - /// - /// Adds slack integration to the project - /// - /// The identifier of the project. - /// The oauth code that must be exchanged for an auth token.D - /// Invalid code or error contacting slack. - /// The project could not be found. - [ApiExplorerSettings(IgnoreApi = true)] - [HttpPost("{id:objectid}/slack")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task AddSlackAsync(string id, string code) { - if (String.IsNullOrWhiteSpace(code)) - return BadRequest(); - - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - if (project.Data.ContainsKey(Project.KnownDataKeys.SlackToken)) - return StatusCode(StatusCodes.Status304NotModified); - - SlackToken token = null; + /// + /// Remove custom data + /// + /// The identifier of the project. + /// The project could not be found. + [HttpDelete("{id:objectid}/slack")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task RemoveSlackAsync(string id) { + var project = await GetModelAsync(id, false); + if (project == null) + return NotFound(); + + var token = project.GetSlackToken(); + if (token != null) { try { - token = await _slackService.GetAccessTokenAsync(code); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Organization(project.OrganizationId).Project(project.Id).Property("Code", code).Tag("Slack").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, "Error getting slack access token: {Message}", ex.Message); + await _slackService.RevokeAccessTokenAsync(token.AccessToken); } + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Property("Token", token).Tag("Slack").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, "Error revoking slack access token: {Message}", ex.Message); + } + } - if (token == null) - return BadRequest(); - - project.AddDefaultNotificationSettings(Project.NotificationIntegrations.Slack); - project.Data[Project.KnownDataKeys.SlackToken] = token; + if (project.NotificationSettings.Remove(Project.NotificationIntegrations.Slack) | project.Data.Remove(Project.KnownDataKeys.SlackToken)) await _repository.SaveAsync(project, o => o.Cache()); - return Ok(); - } + return Ok(); + } - /// - /// Remove custom data - /// - /// The identifier of the project. - /// The project could not be found. - [HttpDelete("{id:objectid}/slack")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task RemoveSlackAsync(string id) { - var project = await GetModelAsync(id, false); - if (project == null) - return NotFound(); - - var token = project.GetSlackToken(); - if (token != null) { - try { - await _slackService.RevokeAccessTokenAsync(token.AccessToken); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Property("Token", token).Tag("Slack").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, "Error revoking slack access token: {Message}", ex.Message); - } + protected override async Task AfterResultMapAsync(ICollection models) { + await base.AfterResultMapAsync(models); + + // TODO: We can optimize this by normalizing the project model to include the organization name. + var viewProjects = models.OfType().ToList(); + var organizations = await _organizationRepository.GetByIdsAsync(viewProjects.Select(p => p.OrganizationId).ToArray(), o => o.Cache()); + foreach (var viewProject in viewProjects) { + var organization = organizations.FirstOrDefault(o => o.Id == viewProject.OrganizationId); + if (organization != null) { + viewProject.OrganizationName = organization.Name; + viewProject.HasPremiumFeatures = organization.HasPremiumFeatures; } - if (project.NotificationSettings.Remove(Project.NotificationIntegrations.Slack) | project.Data.Remove(Project.KnownDataKeys.SlackToken)) - await _repository.SaveAsync(project, o => o.Cache()); - - return Ok(); - } - - protected override async Task AfterResultMapAsync(ICollection models) { - await base.AfterResultMapAsync(models); - - // TODO: We can optimize this by normalizing the project model to include the organization name. - var viewProjects = models.OfType().ToList(); - var organizations = await _organizationRepository.GetByIdsAsync(viewProjects.Select(p => p.OrganizationId).ToArray(), o => o.Cache()); - foreach (var viewProject in viewProjects) { - var organization = organizations.FirstOrDefault(o => o.Id == viewProject.OrganizationId); - if (organization != null) { - viewProject.OrganizationName = organization.Name; - viewProject.HasPremiumFeatures = organization.HasPremiumFeatures; - } - - if (!viewProject.IsConfigured.HasValue) { - viewProject.IsConfigured = true; - await _workItemQueue.EnqueueAsync(new SetProjectIsConfiguredWorkItem { - ProjectId = viewProject.Id - }); - } + if (!viewProject.IsConfigured.HasValue) { + viewProject.IsConfigured = true; + await _workItemQueue.EnqueueAsync(new SetProjectIsConfiguredWorkItem { + ProjectId = viewProject.Id + }); } } + } - protected override async Task CanAddAsync(Project value) { - if (String.IsNullOrEmpty(value.Name)) - return PermissionResult.DenyWithMessage("Project name is required."); - - if (!await IsProjectNameAvailableInternalAsync(value.OrganizationId, value.Name)) - return PermissionResult.DenyWithMessage("A project with this name already exists."); + protected override async Task CanAddAsync(Project value) { + if (String.IsNullOrEmpty(value.Name)) + return PermissionResult.DenyWithMessage("Project name is required."); - if (!await _billingManager.CanAddProjectAsync(value)) - return PermissionResult.DenyWithPlanLimitReached("Please upgrade your plan to add additional projects."); + if (!await IsProjectNameAvailableInternalAsync(value.OrganizationId, value.Name)) + return PermissionResult.DenyWithMessage("A project with this name already exists."); - return await base.CanAddAsync(value); - } + if (!await _billingManager.CanAddProjectAsync(value)) + return PermissionResult.DenyWithPlanLimitReached("Please upgrade your plan to add additional projects."); - protected override Task AddModelAsync(Project value) { - value.IsConfigured = false; - value.NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks; - value.AddDefaultNotificationSettings(CurrentUser.Id); - value.SetDefaultUserAgentBotPatterns(); - value.Configuration.IncrementVersion(); + return await base.CanAddAsync(value); + } - return base.AddModelAsync(value); - } + protected override Task AddModelAsync(Project value) { + value.IsConfigured = false; + value.NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks; + value.AddDefaultNotificationSettings(CurrentUser.Id); + value.SetDefaultUserAgentBotPatterns(); + value.Configuration.IncrementVersion(); - protected override async Task CanUpdateAsync(Project original, Delta changes) { - var changed = changes.GetEntity(); - if (changes.ContainsChangedProperty(p => p.Name) && !await IsProjectNameAvailableInternalAsync(original.OrganizationId, changed.Name)) - return PermissionResult.DenyWithMessage("A project with this name already exists."); + return base.AddModelAsync(value); + } - return await base.CanUpdateAsync(original, changes); - } + protected override async Task CanUpdateAsync(Project original, Delta changes) { + var changed = changes.GetEntity(); + if (changes.ContainsChangedProperty(p => p.Name) && !await IsProjectNameAvailableInternalAsync(original.OrganizationId, changed.Name)) + return PermissionResult.DenyWithMessage("A project with this name already exists."); - private Task GetOrganizationAsync(string organizationId, bool useCache = true) { - if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) - return Task.FromResult(null); + return await base.CanUpdateAsync(original, changes); + } - return _organizationRepository.GetByIdAsync(organizationId, o => o.Cache(useCache)); - } + private Task GetOrganizationAsync(string organizationId, bool useCache = true) { + if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) + return Task.FromResult(null); - private async Task PopulateProjectStatsAsync(ViewProject project) { - return (await PopulateProjectStatsAsync(new List { project })).FirstOrDefault(); - } + return _organizationRepository.GetByIdAsync(organizationId, o => o.Cache(useCache)); + } - private async Task> PopulateProjectStatsAsync(List viewProjects) { - if (viewProjects.Count <= 0) - return viewProjects; - - int maximumRetentionDays = _options.MaximumRetentionDays; - var organizations = await _organizationRepository.GetByIdsAsync(viewProjects.Select(p => p.OrganizationId).ToArray(), o => o.Cache()); - var projects = viewProjects.Select(p => new Project { Id = p.Id, CreatedUtc = p.CreatedUtc, OrganizationId = p.OrganizationId }).ToList(); - var sf = new AppFilter(projects, organizations); - var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow, (PersistentEvent e) => e.Date).Index(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow); - var result = await _eventRepository.CountAsync(q => q - .SystemFilter(systemFilter) - .AggregationsExpression($"terms:(project_id~{viewProjects.Count} cardinality:stack_id)") - .EnforceEventStackFilter(false)); - foreach (var project in viewProjects) { - var term = result.Aggregations.Terms("terms_project_id")?.Buckets.FirstOrDefault(t => t.Key == project.Id); - project.EventCount = term?.Total ?? 0; - project.StackCount = (long)(term?.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0); - } + private async Task PopulateProjectStatsAsync(ViewProject project) { + return (await PopulateProjectStatsAsync(new List { project })).FirstOrDefault(); + } + private async Task> PopulateProjectStatsAsync(List viewProjects) { + if (viewProjects.Count <= 0) return viewProjects; - } + + int maximumRetentionDays = _options.MaximumRetentionDays; + var organizations = await _organizationRepository.GetByIdsAsync(viewProjects.Select(p => p.OrganizationId).ToArray(), o => o.Cache()); + var projects = viewProjects.Select(p => new Project { Id = p.Id, CreatedUtc = p.CreatedUtc, OrganizationId = p.OrganizationId }).ToList(); + var sf = new AppFilter(projects, organizations); + var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow, (PersistentEvent e) => e.Date).Index(organizations.GetRetentionUtcCutoff(maximumRetentionDays), SystemClock.UtcNow); + var result = await _eventRepository.CountAsync(q => q + .SystemFilter(systemFilter) + .AggregationsExpression($"terms:(project_id~{viewProjects.Count} cardinality:stack_id)") + .EnforceEventStackFilter(false)); + foreach (var project in viewProjects) { + var term = result.Aggregations.Terms("terms_project_id")?.Buckets.FirstOrDefault(t => t.Key == project.Id); + project.EventCount = term?.Total ?? 0; + project.StackCount = (long)(term?.Aggregations.Cardinality("cardinality_stack_id")?.Value ?? 0); + } + + return viewProjects; } } diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index 3483b792c1..7486b6a701 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Core; using Exceptionless.Core.Authorization; using Exceptionless.Core.Billing; @@ -25,571 +21,571 @@ using Foundatio.Repositories.Models; using McSherry.SemanticVersioning; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/stacks")] - [Authorize(Policy = AuthorizationRoles.ClientPolicy)] - public class StackController : RepositoryApiController { - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly IWebHookRepository _webHookRepository; - private readonly SemanticVersionParser _semanticVersionParser; - private readonly WebHookDataPluginManager _webHookDataPluginManager; - private readonly ICacheClient _cache; - private readonly IQueue _webHookNotificationQueue; - private readonly BillingManager _billingManager; - private readonly FormattingPluginManager _formattingPluginManager; - private readonly AppOptions _options; - - public StackController( - IStackRepository stackRepository, - IOrganizationRepository organizationRepository, - IProjectRepository projectRepository, - IEventRepository eventRepository, - IWebHookRepository webHookRepository, - WebHookDataPluginManager webHookDataPluginManager, - IQueue webHookNotificationQueue, - ICacheClient cacheClient, - BillingManager billingManager, - FormattingPluginManager formattingPluginManager, - SemanticVersionParser semanticVersionParser, - IMapper mapper, - StackQueryValidator validator, - AppOptions options, - ILoggerFactory loggerFactory - ) : base(stackRepository, mapper, validator, loggerFactory) { - _stackRepository = stackRepository; - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _eventRepository = eventRepository; - _webHookRepository = webHookRepository; - _webHookDataPluginManager = webHookDataPluginManager; - _webHookNotificationQueue = webHookNotificationQueue; - _cache = cacheClient; - _billingManager = billingManager; - _formattingPluginManager = formattingPluginManager; - _semanticVersionParser = semanticVersionParser; - _options = options; - - AllowedDateFields.AddRange(new[] { StackIndex.Alias.FirstOccurrence, StackIndex.Alias.LastOccurrence }); - DefaultDateField = StackIndex.Alias.LastOccurrence; - } - - /// - /// Get by id - /// - /// The identifier of the stack. - /// The time offset in minutes that controls what data is returned based on the `time` filter. This is used for time zone support. - /// The stack could not be found. - [HttpGet("{id:objectid}", Name = "GetStackById")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task> GetAsync(string id, string offset = null) { - var stack = await GetModelAsync(id); - if (stack == null) - return NotFound(); - - return Ok(stack.ApplyOffset(GetOffset(offset))); - } +namespace Exceptionless.Web.Controllers; + +[Route(API_PREFIX + "/stacks")] +[Authorize(Policy = AuthorizationRoles.ClientPolicy)] +public class StackController : RepositoryApiController { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly IWebHookRepository _webHookRepository; + private readonly SemanticVersionParser _semanticVersionParser; + private readonly WebHookDataPluginManager _webHookDataPluginManager; + private readonly ICacheClient _cache; + private readonly IQueue _webHookNotificationQueue; + private readonly BillingManager _billingManager; + private readonly FormattingPluginManager _formattingPluginManager; + private readonly AppOptions _options; + + public StackController( + IStackRepository stackRepository, + IOrganizationRepository organizationRepository, + IProjectRepository projectRepository, + IEventRepository eventRepository, + IWebHookRepository webHookRepository, + WebHookDataPluginManager webHookDataPluginManager, + IQueue webHookNotificationQueue, + ICacheClient cacheClient, + BillingManager billingManager, + FormattingPluginManager formattingPluginManager, + SemanticVersionParser semanticVersionParser, + IMapper mapper, + StackQueryValidator validator, + AppOptions options, + ILoggerFactory loggerFactory + ) : base(stackRepository, mapper, validator, loggerFactory) { + _stackRepository = stackRepository; + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _eventRepository = eventRepository; + _webHookRepository = webHookRepository; + _webHookDataPluginManager = webHookDataPluginManager; + _webHookNotificationQueue = webHookNotificationQueue; + _cache = cacheClient; + _billingManager = billingManager; + _formattingPluginManager = formattingPluginManager; + _semanticVersionParser = semanticVersionParser; + _options = options; + + AllowedDateFields.AddRange(new[] { StackIndex.Alias.FirstOccurrence, StackIndex.Alias.LastOccurrence }); + DefaultDateField = StackIndex.Alias.LastOccurrence; + } - /// - /// Mark fixed - /// - /// A comma delimited list of stack identifiers. - /// A version number that the stack was fixed in. - /// One or more stacks could not be found. - [HttpPost("{ids:objectids}/mark-fixed")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public async Task MarkFixedAsync(string ids, string version = null) { - SemanticVersion semanticVersion = null; - - if (!String.IsNullOrEmpty(version)) { - semanticVersion = await _semanticVersionParser.ParseAsync(version); - if (semanticVersion == null) - return BadRequest("Invalid semantic version"); - } + /// + /// Get by id + /// + /// The identifier of the stack. + /// The time offset in minutes that controls what data is returned based on the `time` filter. This is used for time zone support. + /// The stack could not be found. + [HttpGet("{id:objectid}", Name = "GetStackById")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task> GetAsync(string id, string offset = null) { + var stack = await GetModelAsync(id); + if (stack == null) + return NotFound(); + + return Ok(stack.ApplyOffset(GetOffset(offset))); + } - var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); - if (!stacks.Any()) - return NotFound(); + /// + /// Mark fixed + /// + /// A comma delimited list of stack identifiers. + /// A version number that the stack was fixed in. + /// One or more stacks could not be found. + [HttpPost("{ids:objectids}/mark-fixed")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public async Task MarkFixedAsync(string ids, string version = null) { + SemanticVersion semanticVersion = null; + + if (!String.IsNullOrEmpty(version)) { + semanticVersion = await _semanticVersionParser.ParseAsync(version); + if (semanticVersion == null) + return BadRequest("Invalid semantic version"); + } - if (stacks.Count > 0) { - foreach (var stack in stacks) - stack.MarkFixed(semanticVersion); + var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); + if (!stacks.Any()) + return NotFound(); - await _stackRepository.SaveAsync(stacks); - } + if (stacks.Count > 0) { + foreach (var stack in stacks) + stack.MarkFixed(semanticVersion); - return Ok(); + await _stackRepository.SaveAsync(stacks); } - /// - /// This controller action is called by zapier to mark the stack as fixed. - /// - [HttpPost("~/api/v1/stack/markfixed")] - [HttpPost("mark-fixed")] - [Consumes("application/json")] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task MarkFixedAsync(JObject data) { - string id = null; - if (data.TryGetValue("ErrorStack", out var value)) - id = value.Value(); + return Ok(); + } - if (data.TryGetValue("Stack", out value)) - id = value.Value(); + /// + /// This controller action is called by zapier to mark the stack as fixed. + /// + [HttpPost("~/api/v1/stack/markfixed")] + [HttpPost("mark-fixed")] + [Consumes("application/json")] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task MarkFixedAsync(JObject data) { + string id = null; + if (data.TryGetValue("ErrorStack", out var value)) + id = value.Value(); - if (String.IsNullOrEmpty(id)) - return NotFound(); + if (data.TryGetValue("Stack", out value)) + id = value.Value(); - if (id.StartsWith("http")) - id = id.Substring(id.LastIndexOf('/') + 1); + if (String.IsNullOrEmpty(id)) + return NotFound(); - return await MarkFixedAsync(id); - } + if (id.StartsWith("http")) + id = id.Substring(id.LastIndexOf('/') + 1); - /// - /// Mark the selected stacks as snoozed - /// - /// A comma delimited list of stack identifiers. - /// A time that the stack should be snoozed until. - /// One or more stacks could not be found. - [HttpPost("{ids:objectids}/mark-snoozed")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public async Task> SnoozeAsync(string ids, DateTime snoozeUntilUtc) { - if (snoozeUntilUtc < DateTime.UtcNow.AddMinutes(5)) - return BadRequest("Must snooze for at least 5 minutes."); - - var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); - if (!stacks.Any()) - return NotFound(); - - if (stacks.Count > 0) { - foreach (var stack in stacks) { - stack.Status = StackStatus.Snoozed; - stack.SnoozeUntilUtc = snoozeUntilUtc; - stack.FixedInVersion = null; - stack.DateFixed = null; - } + return await MarkFixedAsync(id); + } - await _stackRepository.SaveAsync(stacks); + /// + /// Mark the selected stacks as snoozed + /// + /// A comma delimited list of stack identifiers. + /// A time that the stack should be snoozed until. + /// One or more stacks could not be found. + [HttpPost("{ids:objectids}/mark-snoozed")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public async Task> SnoozeAsync(string ids, DateTime snoozeUntilUtc) { + if (snoozeUntilUtc < DateTime.UtcNow.AddMinutes(5)) + return BadRequest("Must snooze for at least 5 minutes."); + + var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); + if (!stacks.Any()) + return NotFound(); + + if (stacks.Count > 0) { + foreach (var stack in stacks) { + stack.Status = StackStatus.Snoozed; + stack.SnoozeUntilUtc = snoozeUntilUtc; + stack.FixedInVersion = null; + stack.DateFixed = null; } - return Ok(); + await _stackRepository.SaveAsync(stacks); } - /// - /// Add reference link - /// - /// The identifier of the stack. - /// The reference link. - /// Invalid reference link. - /// The stack could not be found. - [HttpPost("{id:objectid}/add-link")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task AddLinkAsync(string id, ValueFromBody url) { - if (String.IsNullOrWhiteSpace(url?.Value)) - return BadRequest(); - - var stack = await GetModelAsync(id, false); - if (stack == null) - return NotFound(); - - if (!stack.References.Contains(url.Value.Trim())) { - stack.References.Add(url.Value.Trim()); - await _stackRepository.SaveAsync(stack); - } + return Ok(); + } - return Ok(); + /// + /// Add reference link + /// + /// The identifier of the stack. + /// The reference link. + /// Invalid reference link. + /// The stack could not be found. + [HttpPost("{id:objectid}/add-link")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task AddLinkAsync(string id, ValueFromBody url) { + if (String.IsNullOrWhiteSpace(url?.Value)) + return BadRequest(); + + var stack = await GetModelAsync(id, false); + if (stack == null) + return NotFound(); + + if (!stack.References.Contains(url.Value.Trim())) { + stack.References.Add(url.Value.Trim()); + await _stackRepository.SaveAsync(stack); } - /// - /// This controller action is called by zapier to add a reference link to a stack. - /// - [HttpPost("~/api/v1/stack/addlink")] - [HttpPost("add-link")] - [Consumes("application/json")] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task AddLinkAsync(JObject data) { - string id = null; - if (data.TryGetValue("ErrorStack", out var value)) - id = value.Value(); - - if (data.TryGetValue("Stack", out value)) - id = value.Value(); - - if (String.IsNullOrEmpty(id)) - return NotFound(); - - if (id.StartsWith("http")) - id = id.Substring(id.LastIndexOf('/') + 1); - - string url = data.GetValue("Link").Value(); - return await AddLinkAsync(id, new ValueFromBody(url)); - } + return Ok(); + } - /// - /// Remove reference link - /// - /// The identifier of the stack. - /// The reference link. - /// The reference link was removed. - /// Invalid reference link. - /// The stack could not be found. - [HttpPost("{id:objectid}/remove-link")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveLinkAsync(string id, ValueFromBody url) { - if (String.IsNullOrWhiteSpace(url?.Value)) - return BadRequest(); - - var stack = await GetModelAsync(id, false); - if (stack == null) - return NotFound(); - - if (stack.References.Contains(url.Value.Trim())) { - stack.References.Remove(url.Value.Trim()); - await _stackRepository.SaveAsync(stack); - } + /// + /// This controller action is called by zapier to add a reference link to a stack. + /// + [HttpPost("~/api/v1/stack/addlink")] + [HttpPost("add-link")] + [Consumes("application/json")] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task AddLinkAsync(JObject data) { + string id = null; + if (data.TryGetValue("ErrorStack", out var value)) + id = value.Value(); + + if (data.TryGetValue("Stack", out value)) + id = value.Value(); + + if (String.IsNullOrEmpty(id)) + return NotFound(); + + if (id.StartsWith("http")) + id = id.Substring(id.LastIndexOf('/') + 1); + + string url = data.GetValue("Link").Value(); + return await AddLinkAsync(id, new ValueFromBody(url)); + } - return StatusCode(StatusCodes.Status204NoContent); + /// + /// Remove reference link + /// + /// The identifier of the stack. + /// The reference link. + /// The reference link was removed. + /// Invalid reference link. + /// The stack could not be found. + [HttpPost("{id:objectid}/remove-link")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RemoveLinkAsync(string id, ValueFromBody url) { + if (String.IsNullOrWhiteSpace(url?.Value)) + return BadRequest(); + + var stack = await GetModelAsync(id, false); + if (stack == null) + return NotFound(); + + if (stack.References.Contains(url.Value.Trim())) { + stack.References.Remove(url.Value.Trim()); + await _stackRepository.SaveAsync(stack); } - /// - /// Mark future occurrences as critical - /// - /// A comma delimited list of stack identifiers. - /// One or more stacks could not be found. - [HttpPost("{ids:objectids}/mark-critical")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task MarkCriticalAsync(string ids) { - var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); - if (!stacks.Any()) - return NotFound(); - - stacks = stacks.Where(s => !s.OccurrencesAreCritical).ToList(); - if (stacks.Count > 0) { - foreach (var stack in stacks) - stack.OccurrencesAreCritical = true; - - await _stackRepository.SaveAsync(stacks); - } + return StatusCode(StatusCodes.Status204NoContent); + } - return Ok(); + /// + /// Mark future occurrences as critical + /// + /// A comma delimited list of stack identifiers. + /// One or more stacks could not be found. + [HttpPost("{ids:objectids}/mark-critical")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task MarkCriticalAsync(string ids) { + var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); + if (!stacks.Any()) + return NotFound(); + + stacks = stacks.Where(s => !s.OccurrencesAreCritical).ToList(); + if (stacks.Count > 0) { + foreach (var stack in stacks) + stack.OccurrencesAreCritical = true; + + await _stackRepository.SaveAsync(stacks); } - /// - /// Mark future occurrences as not critical - /// - /// A comma delimited list of stack identifiers. - /// The stacks were marked as not critical. - /// One or more stacks could not be found. - [HttpDelete("{ids:objectids}/mark-critical")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task MarkNotCriticalAsync(string ids) { - var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); - if (!stacks.Any()) - return NotFound(); - - stacks = stacks.Where(s => s.OccurrencesAreCritical).ToList(); - if (stacks.Count > 0) { - foreach (var stack in stacks) - stack.OccurrencesAreCritical = false; - - await _stackRepository.SaveAsync(stacks); - } + return Ok(); + } - return StatusCode(StatusCodes.Status204NoContent); + /// + /// Mark future occurrences as not critical + /// + /// A comma delimited list of stack identifiers. + /// The stacks were marked as not critical. + /// One or more stacks could not be found. + [HttpDelete("{ids:objectids}/mark-critical")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task MarkNotCriticalAsync(string ids) { + var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); + if (!stacks.Any()) + return NotFound(); + + stacks = stacks.Where(s => s.OccurrencesAreCritical).ToList(); + if (stacks.Count > 0) { + foreach (var stack in stacks) + stack.OccurrencesAreCritical = false; + + await _stackRepository.SaveAsync(stacks); } - /// - /// Change stack status - /// - /// A comma delimited list of stack identifiers. - /// The status that the stack should be changed to. - /// One or more stacks could not be found. - [HttpPost("{ids:objectids}/change-status")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task ChangeStatusAsync(string ids, StackStatus status) { - if (status == StackStatus.Regressed || status == StackStatus.Snoozed) - return BadRequest("Can't set stack status to regressed or snoozed."); - - var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); - if (!stacks.Any()) - return NotFound(); - - stacks = stacks.Where(s => s.Status != status).ToList(); - if (stacks.Count > 0) { - foreach (var stack in stacks) { - stack.Status = status; - if (status == StackStatus.Fixed) { - stack.DateFixed = DateTime.UtcNow; - } else { - stack.DateFixed = null; - stack.FixedInVersion = null; - } - - if (status != StackStatus.Snoozed) - stack.SnoozeUntilUtc = null; + return StatusCode(StatusCodes.Status204NoContent); + } + + /// + /// Change stack status + /// + /// A comma delimited list of stack identifiers. + /// The status that the stack should be changed to. + /// One or more stacks could not be found. + [HttpPost("{ids:objectids}/change-status")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task ChangeStatusAsync(string ids, StackStatus status) { + if (status == StackStatus.Regressed || status == StackStatus.Snoozed) + return BadRequest("Can't set stack status to regressed or snoozed."); + + var stacks = await GetModelsAsync(ids.FromDelimitedString(), false); + if (!stacks.Any()) + return NotFound(); + + stacks = stacks.Where(s => s.Status != status).ToList(); + if (stacks.Count > 0) { + foreach (var stack in stacks) { + stack.Status = status; + if (status == StackStatus.Fixed) { + stack.DateFixed = DateTime.UtcNow; + } + else { + stack.DateFixed = null; + stack.FixedInVersion = null; } - await _stackRepository.SaveAsync(stacks); + if (status != StackStatus.Snoozed) + stack.SnoozeUntilUtc = null; } - return Ok(); + await _stackRepository.SaveAsync(stacks); } - /// - /// Promote to external service - /// - /// The identifier of the stack. - /// The stack could not be found. - /// Promote to External is a premium feature used to promote an error stack to an external system. - /// "No promoted web hooks are configured for this project. - [HttpPost("{id:objectid}/promote")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task PromoteAsync(string id) { - if (String.IsNullOrEmpty(id)) - return NotFound(); - - var stack = await _stackRepository.GetByIdAsync(id); - if (stack == null || !CanAccessOrganization(stack.OrganizationId)) - return NotFound(); - - if (!await _billingManager.HasPremiumFeaturesAsync(stack.OrganizationId)) - return PlanLimitReached("Promote to External is a premium feature used to promote an error stack to an external system. Please upgrade your plan to enable this feature."); - - var promotedProjectHooks = (await _webHookRepository.GetByProjectIdAsync(stack.ProjectId)).Documents.Where(p => p.EventTypes.Contains(WebHookRepository.EventTypes.StackPromoted)).ToList(); - if (!promotedProjectHooks.Any()) - return NotImplemented("No promoted web hooks are configured for this project. Please add a promoted web hook to use this feature."); - - foreach (var hook in promotedProjectHooks) { - var context = new WebHookDataContext(hook.Version, stack, isNew: stack.TotalOccurrences == 1, isRegression: stack.Status == StackStatus.Regressed); - await _webHookNotificationQueue.EnqueueAsync(new WebHookNotification { - OrganizationId = stack.OrganizationId, - ProjectId = stack.ProjectId, - WebHookId = hook.Id, - Url = hook.Url, - Type = WebHookType.General, - Data = await _webHookDataPluginManager.CreateFromStackAsync(context) - }); - } - - return Ok(); - } + return Ok(); + } - /// - /// Remove - /// - /// A comma delimited list of stack identifiers. - /// No Content. - /// One or more validation errors occurred. - /// One or more stacks were not found. - /// An error occurred while deleting one or more stacks. - [HttpDelete("{ids:objectids}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteAsync(string ids) { - return DeleteImplAsync(ids.FromDelimitedString()); + /// + /// Promote to external service + /// + /// The identifier of the stack. + /// The stack could not be found. + /// Promote to External is a premium feature used to promote an error stack to an external system. + /// "No promoted web hooks are configured for this project. + [HttpPost("{id:objectid}/promote")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task PromoteAsync(string id) { + if (String.IsNullOrEmpty(id)) + return NotFound(); + + var stack = await _stackRepository.GetByIdAsync(id); + if (stack == null || !CanAccessOrganization(stack.OrganizationId)) + return NotFound(); + + if (!await _billingManager.HasPremiumFeaturesAsync(stack.OrganizationId)) + return PlanLimitReached("Promote to External is a premium feature used to promote an error stack to an external system. Please upgrade your plan to enable this feature."); + + var promotedProjectHooks = (await _webHookRepository.GetByProjectIdAsync(stack.ProjectId)).Documents.Where(p => p.EventTypes.Contains(WebHookRepository.EventTypes.StackPromoted)).ToList(); + if (!promotedProjectHooks.Any()) + return NotImplemented("No promoted web hooks are configured for this project. Please add a promoted web hook to use this feature."); + + foreach (var hook in promotedProjectHooks) { + var context = new WebHookDataContext(hook.Version, stack, isNew: stack.TotalOccurrences == 1, isRegression: stack.Status == StackStatus.Regressed); + await _webHookNotificationQueue.EnqueueAsync(new WebHookNotification { + OrganizationId = stack.OrganizationId, + ProjectId = stack.ProjectId, + WebHookId = hook.Id, + Url = hook.Url, + Type = WebHookType.General, + Data = await _webHookDataPluginManager.CreateFromStackAsync(context) + }); } - /// - /// Get all - /// - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// Invalid filter. - [HttpGet] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetAsync(string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10) { - var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); - if (organizations.Count(o => !o.IsSuspended) == 0) - return Ok(EmptyModels); - - var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_options.MaximumRetentionDays)); - var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; - return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit); - } + return Ok(); + } - private async Task>> GetInternalAsync(AppFilter sf, TimeInfo ti, string filter = null, string sort = null, string mode = null, int page = 1, int limit = 10) { - page = GetPage(page); - limit = GetLimit(limit); - int skip = GetSkip(page, limit); - if (skip > MAXIMUM_SKIP) - return Ok(EmptyModels); + /// + /// Remove + /// + /// A comma delimited list of stack identifiers. + /// No Content. + /// One or more validation errors occurred. + /// One or more stacks were not found. + /// An error occurred while deleting one or more stacks. + [HttpDelete("{ids:objectids}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteAsync(string ids) { + return DeleteImplAsync(ids.FromDelimitedString()); + } - var pr = await _validator.ValidateQueryAsync(filter); - if (!pr.IsValid) - return BadRequest(pr.Message); + /// + /// Get all + /// + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// Invalid filter. + [HttpGet] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetAsync(string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10) { + var organizations = await GetSelectedOrganizationsAsync(_organizationRepository, _projectRepository, _stackRepository, filter); + if (organizations.Count(o => !o.IsSuspended) == 0) + return Ok(EmptyModels); + + var ti = GetTimeInfo(time, offset, organizations.GetRetentionUtcCutoff(_options.MaximumRetentionDays)); + var sf = new AppFilter(organizations) { IsUserOrganizationsFilter = true }; + return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit); + } - sf.UsesPremiumFeatures = pr.UsesPremiumFeatures; + private async Task>> GetInternalAsync(AppFilter sf, TimeInfo ti, string filter = null, string sort = null, string mode = null, int page = 1, int limit = 10) { + page = GetPage(page); + limit = GetLimit(limit); + int skip = GetSkip(page, limit); + if (skip > MAXIMUM_SKIP) + return Ok(EmptyModels); - try { - var results = await _repository.FindAsync(q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null).FilterExpression(filter).SortExpression(sort).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field), o => o.PageNumber(page).PageLimit(limit)); + var pr = await _validator.ValidateQueryAsync(filter); + if (!pr.IsValid) + return BadRequest(pr.Message); - var stacks = results.Documents.Select(s => s.ApplyOffset(ti.Offset)).ToList(); - if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "summary", StringComparison.OrdinalIgnoreCase)) - return OkWithResourceLinks(await GetStackSummariesAsync(stacks, sf, ti), results.HasMore && !NextPageExceedsSkipLimit(page, limit), page); + sf.UsesPremiumFeatures = pr.UsesPremiumFeatures; - return OkWithResourceLinks(stacks, results.HasMore && !NextPageExceedsSkipLimit(page, limit), page); - } catch (ApplicationException ex) { - using (_logger.BeginScope(new ExceptionlessState().Property("Search Filter", new { SystemFilter = sf, UserFilter = filter, Time = ti, Page = page, Limit = limit }).Tag("Search").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) - _logger.LogError(ex, "An error has occurred. Please check your search filter."); + try { + var results = await _repository.FindAsync(q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null).FilterExpression(filter).SortExpression(sort).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field), o => o.PageNumber(page).PageLimit(limit)); - return BadRequest("An error has occurred. Please check your search filter."); - } - } + var stacks = results.Documents.Select(s => s.ApplyOffset(ti.Offset)).ToList(); + if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "summary", StringComparison.OrdinalIgnoreCase)) + return OkWithResourceLinks(await GetStackSummariesAsync(stacks, sf, ti), results.HasMore && !NextPageExceedsSkipLimit(page, limit), page); - /// - /// Get by organization - /// - /// The identifier of the organization. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// Invalid filter. - /// The organization could not be found. - /// Unable to view stack occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/stacks")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByOrganizationAsync(string organizationId = null, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10) { - var organization = await GetOrganizationAsync(organizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view stack occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_options.MaximumRetentionDays)); - var sf = new AppFilter(organization); - return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit); + return OkWithResourceLinks(stacks, results.HasMore && !NextPageExceedsSkipLimit(page, limit), page); } + catch (ApplicationException ex) { + using (_logger.BeginScope(new ExceptionlessState().Property("Search Filter", new { SystemFilter = sf, UserFilter = filter, Time = ti, Page = page, Limit = limit }).Tag("Search").Identity(CurrentUser.EmailAddress).Property("User", CurrentUser).SetHttpContext(HttpContext))) + _logger.LogError(ex, "An error has occurred. Please check your search filter."); - /// - /// Get by project - /// - /// The identifier of the project. - /// A filter that controls what data is returned from the server. - /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. - /// The time filter that limits the data being returned to a specific date range. - /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. - /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// Invalid filter. - /// The organization could not be found. - /// Unable to view stack occurrences for the suspended organization. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/stacks")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByProjectAsync(string projectId = null, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); - - var organization = await GetOrganizationAsync(project.OrganizationId); - if (organization == null) - return NotFound(); - - if (organization.IsSuspended) - return PlanLimitReached("Unable to view stack occurrences for the suspended organization."); - - var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _options.MaximumRetentionDays)); - var sf = new AppFilter(project, organization); - return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit); + return BadRequest("An error has occurred. Please check your search filter."); } - - private Task GetOrganizationAsync(string organizationId, bool useCache = true) { - if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) - return null; + } - return _organizationRepository.GetByIdAsync(organizationId, o => o.Cache(useCache)); - } + /// + /// Get by organization + /// + /// The identifier of the organization. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// Invalid filter. + /// The organization could not be found. + /// Unable to view stack occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/stacks")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByOrganizationAsync(string organizationId = null, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10) { + var organization = await GetOrganizationAsync(organizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view stack occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(_options.MaximumRetentionDays)); + var sf = new AppFilter(organization); + return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit); + } - private async Task GetProjectAsync(string projectId, bool useCache = true) { - if (String.IsNullOrEmpty(projectId)) - return null; + /// + /// Get by project + /// + /// The identifier of the project. + /// A filter that controls what data is returned from the server. + /// Controls the sort order that the data is returned in. In this example -date returns the results descending by date. + /// The time filter that limits the data being returned to a specific date range. + /// The time offset in minutes that controls what data is returned based on the time filter. This is used for time zone support. + /// If no mode is set then the whole stack object will be returned. If the mode is set to summary than a light weight object will be returned. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// Invalid filter. + /// The organization could not be found. + /// Unable to view stack occurrences for the suspended organization. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/stacks")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByProjectAsync(string projectId = null, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + var organization = await GetOrganizationAsync(project.OrganizationId); + if (organization == null) + return NotFound(); + + if (organization.IsSuspended) + return PlanLimitReached("Unable to view stack occurrences for the suspended organization."); + + var ti = GetTimeInfo(time, offset, organization.GetRetentionUtcCutoff(project, _options.MaximumRetentionDays)); + var sf = new AppFilter(project, organization); + return await GetInternalAsync(sf, ti, filter, sort, mode, page, limit); + } - var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); - if (project == null || !CanAccessOrganization(project.OrganizationId)) - return null; + private Task GetOrganizationAsync(string organizationId, bool useCache = true) { + if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) + return null; - return project; - } + return _organizationRepository.GetByIdAsync(organizationId, o => o.Cache(useCache)); + } - private async Task> GetStackSummariesAsync(ICollection stacks, AppFilter eventSystemFilter, TimeInfo ti) { - if (stacks.Count == 0) - return new List(); + private async Task GetProjectAsync(string projectId, bool useCache = true) { + if (String.IsNullOrEmpty(projectId)) + return null; - var systemFilter = new RepositoryQuery().AppFilter(eventSystemFilter).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, (PersistentEvent e) => e.Date).Index(ti.Range.UtcStart, ti.Range.UtcEnd); - var stackTerms = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(String.Join(" OR ", stacks.Select(r => $"stack:{r.Id}"))).AggregationsExpression($"terms:(stack_id~{stacks.Count} cardinality:user sum:count~1 min:date max:date)")); - return await GetStackSummariesAsync(stacks, stackTerms.Aggregations.Terms("terms_stack_id").Buckets, eventSystemFilter, ti); - } + var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); + if (project == null || !CanAccessOrganization(project.OrganizationId)) + return null; - private async Task> GetStackSummariesAsync(ICollection stacks, IReadOnlyCollection> stackTerms, AppFilter sf, TimeInfo ti) { - if (stacks.Count == 0) - return new List(0); - - var totalUsers = await GetUserCountByProjectIdsAsync(stacks, sf, ti.Range.UtcStart, ti.Range.UtcEnd); - return stacks.Join(stackTerms, s => s.Id, tk => tk.Key, (stack, term) => { - var data = _formattingPluginManager.GetStackSummaryData(stack); - var summary = new StackSummaryModel { - TemplateKey = data.TemplateKey, - Data = data.Data, - Id = stack.Id, - Title = stack.Title, - Status = stack.Status, - FirstOccurrence = term.Aggregations.Min("min_date").Value, - LastOccurrence = term.Aggregations.Max("max_date").Value, - Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), - - Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), - TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) - }; - - return summary; - }).ToList(); - } + return project; + } - private async Task> GetUserCountByProjectIdsAsync(ICollection stacks, AppFilter sf, DateTime utcStart, DateTime utcEnd) { - var scopedCacheClient = new ScopedCacheClient(_cache, $"Project:user-count:{utcStart.Floor(TimeSpan.FromMinutes(15)).Ticks}-{utcEnd.Floor(TimeSpan.FromMinutes(15)).Ticks}"); - var projectIds = stacks.Select(s => s.ProjectId).Distinct().ToList(); - var cachedTotals = await scopedCacheClient.GetAllAsync(projectIds); + private async Task> GetStackSummariesAsync(ICollection stacks, AppFilter eventSystemFilter, TimeInfo ti) { + if (stacks.Count == 0) + return new List(); - var totals = cachedTotals.Where(kvp => kvp.Value.HasValue).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value); - if (totals.Count == projectIds.Count) - return totals; + var systemFilter = new RepositoryQuery().AppFilter(eventSystemFilter).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, (PersistentEvent e) => e.Date).Index(ti.Range.UtcStart, ti.Range.UtcEnd); + var stackTerms = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(String.Join(" OR ", stacks.Select(r => $"stack:{r.Id}"))).AggregationsExpression($"terms:(stack_id~{stacks.Count} cardinality:user sum:count~1 min:date max:date)")); + return await GetStackSummariesAsync(stacks, stackTerms.Aggregations.Terms("terms_stack_id").Buckets, eventSystemFilter, ti); + } - var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(utcStart, utcEnd, (PersistentEvent e) => e.Date).Index(utcStart, utcEnd); - var projects = cachedTotals.Where(kvp => !kvp.Value.HasValue).Select(kvp => new Project { Id = kvp.Key, OrganizationId = stacks.FirstOrDefault(s => s.ProjectId == kvp.Key)?.OrganizationId }).ToList(); - var countResult = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).AggregationsExpression("terms:(project_id cardinality:user)")); + private async Task> GetStackSummariesAsync(ICollection stacks, IReadOnlyCollection> stackTerms, AppFilter sf, TimeInfo ti) { + if (stacks.Count == 0) + return new List(0); + + var totalUsers = await GetUserCountByProjectIdsAsync(stacks, sf, ti.Range.UtcStart, ti.Range.UtcEnd); + return stacks.Join(stackTerms, s => s.Id, tk => tk.Key, (stack, term) => { + var data = _formattingPluginManager.GetStackSummaryData(stack); + var summary = new StackSummaryModel { + TemplateKey = data.TemplateKey, + Data = data.Data, + Id = stack.Id, + Title = stack.Title, + Status = stack.Status, + FirstOccurrence = term.Aggregations.Min("min_date").Value, + LastOccurrence = term.Aggregations.Max("max_date").Value, + Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), + + Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), + TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) + }; + + return summary; + }).ToList(); + } - // Cache all projects that have more than 10 users for 5 minutes. - var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; - var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); - await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); - totals.AddRange(aggregations); + private async Task> GetUserCountByProjectIdsAsync(ICollection stacks, AppFilter sf, DateTime utcStart, DateTime utcEnd) { + var scopedCacheClient = new ScopedCacheClient(_cache, $"Project:user-count:{utcStart.Floor(TimeSpan.FromMinutes(15)).Ticks}-{utcEnd.Floor(TimeSpan.FromMinutes(15)).Ticks}"); + var projectIds = stacks.Select(s => s.ProjectId).Distinct().ToList(); + var cachedTotals = await scopedCacheClient.GetAllAsync(projectIds); + var totals = cachedTotals.Where(kvp => kvp.Value.HasValue).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value); + if (totals.Count == projectIds.Count) return totals; - } + + var systemFilter = new RepositoryQuery().AppFilter(sf).DateRange(utcStart, utcEnd, (PersistentEvent e) => e.Date).Index(utcStart, utcEnd); + var projects = cachedTotals.Where(kvp => !kvp.Value.HasValue).Select(kvp => new Project { Id = kvp.Key, OrganizationId = stacks.FirstOrDefault(s => s.ProjectId == kvp.Key)?.OrganizationId }).ToList(); + var countResult = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).AggregationsExpression("terms:(project_id cardinality:user)")); + + // Cache all projects that have more than 10 users for 5 minutes. + var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; + var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); + await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); + totals.AddRange(aggregations); + + return totals; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Controllers/StatusController.cs b/src/Exceptionless.Web/Controllers/StatusController.cs index 6daeb2952c..00091f3475 100644 --- a/src/Exceptionless.Web/Controllers/StatusController.cs +++ b/src/Exceptionless.Web/Controllers/StatusController.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core; +using Exceptionless.Core; using Exceptionless.Core.Authorization; using Exceptionless.Core.Messaging.Models; using Exceptionless.Core.Queues.Models; @@ -12,131 +10,131 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX)] - [ApiExplorerSettings(IgnoreApi = true)] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public class StatusController : ExceptionlessApiController { - private readonly ICacheClient _cacheClient; - private readonly IMessagePublisher _messagePublisher; - private readonly IQueue _eventQueue; - private readonly IQueue _mailQueue; - private readonly IQueue _notificationQueue; - private readonly IQueue _webHooksQueue; - private readonly IQueue _userDescriptionQueue; - private readonly AppOptions _appOptions; +namespace Exceptionless.Web.Controllers; - public StatusController( - ICacheClient cacheClient, - IMessagePublisher messagePublisher, - IQueue eventQueue, - IQueue mailQueue, - IQueue notificationQueue, - IQueue webHooksQueue, - IQueue userDescriptionQueue, - AppOptions appOptions) { - _cacheClient = cacheClient; - _messagePublisher = messagePublisher; - _eventQueue = eventQueue; - _mailQueue = mailQueue; - _notificationQueue = notificationQueue; - _webHooksQueue = webHooksQueue; - _userDescriptionQueue = userDescriptionQueue; - _appOptions = appOptions; - } +[Route(API_PREFIX)] +[ApiExplorerSettings(IgnoreApi = true)] +[Authorize(Policy = AuthorizationRoles.UserPolicy)] +public class StatusController : ExceptionlessApiController { + private readonly ICacheClient _cacheClient; + private readonly IMessagePublisher _messagePublisher; + private readonly IQueue _eventQueue; + private readonly IQueue _mailQueue; + private readonly IQueue _notificationQueue; + private readonly IQueue _webHooksQueue; + private readonly IQueue _userDescriptionQueue; + private readonly AppOptions _appOptions; - /// - /// Get the info of the API - /// - [AllowAnonymous] - [HttpGet("about")] - public IActionResult IndexAsync() { - return Ok(new { - _appOptions.InformationalVersion, - AppMode = _appOptions.AppMode.ToString(), - Environment.MachineName - }); - } + public StatusController( + ICacheClient cacheClient, + IMessagePublisher messagePublisher, + IQueue eventQueue, + IQueue mailQueue, + IQueue notificationQueue, + IQueue webHooksQueue, + IQueue userDescriptionQueue, + AppOptions appOptions) { + _cacheClient = cacheClient; + _messagePublisher = messagePublisher; + _eventQueue = eventQueue; + _mailQueue = mailQueue; + _notificationQueue = notificationQueue; + _webHooksQueue = webHooksQueue; + _userDescriptionQueue = userDescriptionQueue; + _appOptions = appOptions; + } - [HttpGet("queue-stats")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - public async Task QueueStatsAsync() { - var eventQueueStats = await _eventQueue.GetQueueStatsAsync(); - var mailQueueStats = await _mailQueue.GetQueueStatsAsync(); - var userDescriptionQueueStats = await _userDescriptionQueue.GetQueueStatsAsync(); - var notificationQueueStats = await _notificationQueue.GetQueueStatsAsync(); - var webHooksQueueStats = await _webHooksQueue.GetQueueStatsAsync(); + /// + /// Get the info of the API + /// + [AllowAnonymous] + [HttpGet("about")] + public IActionResult IndexAsync() { + return Ok(new { + _appOptions.InformationalVersion, + AppMode = _appOptions.AppMode.ToString(), + Environment.MachineName + }); + } - return Ok(new { - EventPosts = new { - Active = eventQueueStats.Enqueued, - eventQueueStats.Deadletter, - eventQueueStats.Working - }, - MailMessages = new { - Active = mailQueueStats.Enqueued, - mailQueueStats.Deadletter, - mailQueueStats.Working - }, - UserDescriptions = new { - Active = userDescriptionQueueStats.Enqueued, - userDescriptionQueueStats.Deadletter, - userDescriptionQueueStats.Working - }, - Notifications = new { - Active = notificationQueueStats.Enqueued, - notificationQueueStats.Deadletter, - notificationQueueStats.Working - }, - WebHooks = new { - Active = webHooksQueueStats.Enqueued, - webHooksQueueStats.Deadletter, - webHooksQueueStats.Working - } - }); - } + [HttpGet("queue-stats")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + public async Task QueueStatsAsync() { + var eventQueueStats = await _eventQueue.GetQueueStatsAsync(); + var mailQueueStats = await _mailQueue.GetQueueStatsAsync(); + var userDescriptionQueueStats = await _userDescriptionQueue.GetQueueStatsAsync(); + var notificationQueueStats = await _notificationQueue.GetQueueStatsAsync(); + var webHooksQueueStats = await _webHooksQueue.GetQueueStatsAsync(); - [HttpPost("notifications/release")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - public async Task> PostReleaseNotificationAsync(ValueFromBody message, bool critical = false) { - var notification = new ReleaseNotification { Critical = critical, Date = SystemClock.UtcNow, Message = message?.Value }; - await _messagePublisher.PublishAsync(notification); - return Ok(notification); - } + return Ok(new { + EventPosts = new { + Active = eventQueueStats.Enqueued, + eventQueueStats.Deadletter, + eventQueueStats.Working + }, + MailMessages = new { + Active = mailQueueStats.Enqueued, + mailQueueStats.Deadletter, + mailQueueStats.Working + }, + UserDescriptions = new { + Active = userDescriptionQueueStats.Enqueued, + userDescriptionQueueStats.Deadletter, + userDescriptionQueueStats.Working + }, + Notifications = new { + Active = notificationQueueStats.Enqueued, + notificationQueueStats.Deadletter, + notificationQueueStats.Working + }, + WebHooks = new { + Active = webHooksQueueStats.Enqueued, + webHooksQueueStats.Deadletter, + webHooksQueueStats.Working + } + }); + } - /// - /// Returns the current system notification messages. - /// - [HttpGet("notifications/system")] - public async Task> GetSystemNotificationAsync() { - var notification = await _cacheClient.GetAsync("system-notification"); - if (!notification.HasValue) - return Ok(); + [HttpPost("notifications/release")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + public async Task> PostReleaseNotificationAsync(ValueFromBody message, bool critical = false) { + var notification = new ReleaseNotification { Critical = critical, Date = SystemClock.UtcNow, Message = message?.Value }; + await _messagePublisher.PublishAsync(notification); + return Ok(notification); + } - return Ok(notification.Value); - } + /// + /// Returns the current system notification messages. + /// + [HttpGet("notifications/system")] + public async Task> GetSystemNotificationAsync() { + var notification = await _cacheClient.GetAsync("system-notification"); + if (!notification.HasValue) + return Ok(); - [HttpPost("notifications/system")] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - public async Task> PostSystemNotificationAsync(ValueFromBody message) { - if (String.IsNullOrWhiteSpace(message?.Value)) - return NotFound(); + return Ok(notification.Value); + } - var notification = new SystemNotification { Date = SystemClock.UtcNow, Message = message.Value }; - await _cacheClient.SetAsync("system-notification", notification); - await _messagePublisher.PublishAsync(notification); + [HttpPost("notifications/system")] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + public async Task> PostSystemNotificationAsync(ValueFromBody message) { + if (String.IsNullOrWhiteSpace(message?.Value)) + return NotFound(); - return Ok(notification); - } + var notification = new SystemNotification { Date = SystemClock.UtcNow, Message = message.Value }; + await _cacheClient.SetAsync("system-notification", notification); + await _messagePublisher.PublishAsync(notification); - [HttpDelete("notifications/system")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - public async Task RemoveSystemNotificationAsync() { - await _cacheClient.RemoveAsync("system-notification"); - await _messagePublisher.PublishAsync(new SystemNotification { Date = SystemClock.UtcNow }); - return Ok(); - } + return Ok(notification); + } + + [HttpDelete("notifications/system")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + public async Task RemoveSystemNotificationAsync() { + await _cacheClient.RemoveAsync("system-notification"); + await _messagePublisher.PublishAsync(new SystemNotification { Date = SystemClock.UtcNow }); + return Ok(); } } diff --git a/src/Exceptionless.Web/Controllers/StripeController.cs b/src/Exceptionless.Web/Controllers/StripeController.cs index c665e8d8fe..0637beb65e 100644 --- a/src/Exceptionless.Web/Controllers/StripeController.cs +++ b/src/Exceptionless.Web/Controllers/StripeController.cs @@ -1,55 +1,52 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Core.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Stripe; -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/stripe")] - [ApiExplorerSettings(IgnoreApi = true)] - [Authorize] - public class StripeController : ExceptionlessApiController { - private readonly StripeEventHandler _stripeEventHandler; - private readonly StripeOptions _stripeOptions; - private readonly ILogger _logger; +namespace Exceptionless.Web.Controllers; - public StripeController(StripeEventHandler stripeEventHandler, StripeOptions stripeOptions, ILogger logger) { - _stripeEventHandler = stripeEventHandler; - _stripeOptions = stripeOptions; - _logger = logger; - } +[Route(API_PREFIX + "/stripe")] +[ApiExplorerSettings(IgnoreApi = true)] +[Authorize] +public class StripeController : ExceptionlessApiController { + private readonly StripeEventHandler _stripeEventHandler; + private readonly StripeOptions _stripeOptions; + private readonly ILogger _logger; - [AllowAnonymous] - [HttpPost] - [Consumes("application/json")] - public async Task PostAsync() { - string json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); - using (_logger.BeginScope(new ExceptionlessState().SetHttpContext(HttpContext).Property("event", json))) { - if (String.IsNullOrEmpty(json)) { - _logger.LogWarning("Unable to get json of incoming event."); - return BadRequest(); - } + public StripeController(StripeEventHandler stripeEventHandler, StripeOptions stripeOptions, ILogger logger) { + _stripeEventHandler = stripeEventHandler; + _stripeOptions = stripeOptions; + _logger = logger; + } - Stripe.Event stripeEvent; - try { - stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _stripeOptions.StripeWebHookSigningSecret, throwOnApiVersionMismatch: false); - } catch (Exception ex) { - _logger.LogError(ex, "Unable to parse incoming event with {Signature}: {Message}", Request.Headers["Stripe-Signature"], ex.Message); - return BadRequest(); - } + [AllowAnonymous] + [HttpPost] + [Consumes("application/json")] + public async Task PostAsync() { + string json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); + using (_logger.BeginScope(new ExceptionlessState().SetHttpContext(HttpContext).Property("event", json))) { + if (String.IsNullOrEmpty(json)) { + _logger.LogWarning("Unable to get json of incoming event."); + return BadRequest(); + } - if (stripeEvent == null) { - _logger.LogWarning("Null stripe event."); - return BadRequest(); - } + Stripe.Event stripeEvent; + try { + stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _stripeOptions.StripeWebHookSigningSecret, throwOnApiVersionMismatch: false); + } + catch (Exception ex) { + _logger.LogError(ex, "Unable to parse incoming event with {Signature}: {Message}", Request.Headers["Stripe-Signature"], ex.Message); + return BadRequest(); + } - await _stripeEventHandler.HandleEventAsync(stripeEvent); - return Ok(); + if (stripeEvent == null) { + _logger.LogWarning("Null stripe event."); + return BadRequest(); } + + await _stripeEventHandler.HandleEventAsync(stripeEvent); + return Ok(); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Controllers/TokenController.cs b/src/Exceptionless.Web/Controllers/TokenController.cs index 5f561fff32..e983d6cf46 100644 --- a/src/Exceptionless.Web/Controllers/TokenController.cs +++ b/src/Exceptionless.Web/Controllers/TokenController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Web.Controllers; using Exceptionless.Web.Extensions; using Exceptionless.Web.Models; @@ -15,302 +11,300 @@ using Foundatio.Repositories; using Foundatio.Utility; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -namespace Exceptionless.App.Controllers.API { - [Route(API_PREFIX + "/tokens")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public class TokenController : RepositoryApiController { - private readonly IProjectRepository _projectRepository; +namespace Exceptionless.App.Controllers.API; - public TokenController(ITokenRepository repository, IProjectRepository projectRepository, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) : base(repository, mapper, validator, loggerFactory) { - _projectRepository = projectRepository; - } - - #region CRUD - - /// - /// Get by organization - /// - /// The identifier of the organization. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The organization could not be found. - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/tokens")] - public async Task>> GetByOrganizationAsync(string organizationId, int page = 1, int limit = 10) { - if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) - return NotFound(); - - page = GetPage(page); - limit = GetLimit(limit); - var tokens = await _repository.GetByTypeAndOrganizationIdAsync(TokenType.Access, organizationId, o => o.PageNumber(page).PageLimit(limit)); - var viewTokens = (await MapCollectionAsync(tokens.Documents, true)).ToList(); - return OkWithResourceLinks(viewTokens, tokens.HasMore && !NextPageExceedsSkipLimit(page, limit), page, tokens.Total); - } +[Route(API_PREFIX + "/tokens")] +[Authorize(Policy = AuthorizationRoles.UserPolicy)] +public class TokenController : RepositoryApiController { + private readonly IProjectRepository _projectRepository; - /// - /// Get by project - /// - /// The identifier of the project. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The project could not be found. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/tokens")] - public async Task>> GetByProjectAsync(string projectId, int page = 1, int limit = 10) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); + public TokenController(ITokenRepository repository, IProjectRepository projectRepository, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) : base(repository, mapper, validator, loggerFactory) { + _projectRepository = projectRepository; + } - page = GetPage(page); - limit = GetLimit(limit); - var tokens = await _repository.GetByTypeAndProjectIdAsync(TokenType.Access, projectId, o => o.PageNumber(page).PageLimit(limit)); - var viewTokens = (await MapCollectionAsync(tokens.Documents, true)).ToList(); - return OkWithResourceLinks(viewTokens, tokens.HasMore && !NextPageExceedsSkipLimit(page, limit), page, tokens.Total); - } + #region CRUD + + /// + /// Get by organization + /// + /// The identifier of the organization. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The organization could not be found. + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/tokens")] + public async Task>> GetByOrganizationAsync(string organizationId, int page = 1, int limit = 10) { + if (String.IsNullOrEmpty(organizationId) || !CanAccessOrganization(organizationId)) + return NotFound(); + + page = GetPage(page); + limit = GetLimit(limit); + var tokens = await _repository.GetByTypeAndOrganizationIdAsync(TokenType.Access, organizationId, o => o.PageNumber(page).PageLimit(limit)); + var viewTokens = (await MapCollectionAsync(tokens.Documents, true)).ToList(); + return OkWithResourceLinks(viewTokens, tokens.HasMore && !NextPageExceedsSkipLimit(page, limit), page, tokens.Total); + } - /// - /// Get a projects default token - /// - /// The identifier of the project. - /// The project could not be found. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/tokens/default")] - public async Task> GetDefaultTokenAsync(string projectId) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); + /// + /// Get by project + /// + /// The identifier of the project. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The project could not be found. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/tokens")] + public async Task>> GetByProjectAsync(string projectId, int page = 1, int limit = 10) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + page = GetPage(page); + limit = GetLimit(limit); + var tokens = await _repository.GetByTypeAndProjectIdAsync(TokenType.Access, projectId, o => o.PageNumber(page).PageLimit(limit)); + var viewTokens = (await MapCollectionAsync(tokens.Documents, true)).ToList(); + return OkWithResourceLinks(viewTokens, tokens.HasMore && !NextPageExceedsSkipLimit(page, limit), page, tokens.Total); + } - var token = (await _repository.GetByTypeAndProjectIdAsync(TokenType.Access, projectId, o => o.PageLimit(1))).Documents.FirstOrDefault(); - if (token != null) - return await OkModelAsync(token); + /// + /// Get a projects default token + /// + /// The identifier of the project. + /// The project could not be found. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/tokens/default")] + public async Task> GetDefaultTokenAsync(string projectId) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + var token = (await _repository.GetByTypeAndProjectIdAsync(TokenType.Access, projectId, o => o.PageLimit(1))).Documents.FirstOrDefault(); + if (token != null) + return await OkModelAsync(token); + + return await PostImplAsync(new NewToken { OrganizationId = project.OrganizationId, ProjectId = projectId }); + } - return await PostImplAsync(new NewToken { OrganizationId = project.OrganizationId, ProjectId = projectId}); - } + /// + /// Get by id + /// + /// The identifier of the token. + /// The token could not be found. + [HttpGet("{id:token}", Name = "GetTokenById")] + public Task> GetAsync(string id) { + return GetByIdImplAsync(id); + } - /// - /// Get by id - /// - /// The identifier of the token. - /// The token could not be found. - [HttpGet("{id:token}", Name = "GetTokenById")] - public Task> GetAsync(string id) { - return GetByIdImplAsync(id); - } + /// + /// Create + /// + /// + /// To create a new token, you must specify an organization_id. There are three valid scopes: client, user and admin. + /// + /// The token. + /// An error occurred while creating the token. + /// The token already exists. + [HttpPost] + [Consumes("application/json")] + public Task> PostAsync(NewToken token) { + return PostImplAsync(token); + } - /// - /// Create - /// - /// - /// To create a new token, you must specify an organization_id. There are three valid scopes: client, user and admin. - /// - /// The token. - /// An error occurred while creating the token. - /// The token already exists. - [HttpPost] - [Consumes("application/json")] - public Task> PostAsync(NewToken token) { - return PostImplAsync(token); - } + /// + /// Create for project + /// + /// + /// This is a helper action that makes it easier to create a token for a specific project. + /// You may also specify a scope when creating a token. There are three valid scopes: client, user and admin. + /// + /// The identifier of the project. + /// The token. + /// An error occurred while creating the token. + /// The project could not be found. + /// The token already exists. + [HttpPost("~/" + API_PREFIX + "/projects/{projectId:objectid}/tokens")] + [Consumes("application/json")] + public async Task> PostByProjectAsync(string projectId, NewToken token) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + if (token == null) + token = new NewToken(); + + token.OrganizationId = project.OrganizationId; + token.ProjectId = projectId; + return await PostImplAsync(token); + } - /// - /// Create for project - /// - /// - /// This is a helper action that makes it easier to create a token for a specific project. - /// You may also specify a scope when creating a token. There are three valid scopes: client, user and admin. - /// - /// The identifier of the project. - /// The token. - /// An error occurred while creating the token. - /// The project could not be found. - /// The token already exists. - [HttpPost("~/" + API_PREFIX + "/projects/{projectId:objectid}/tokens")] - [Consumes("application/json")] - public async Task> PostByProjectAsync(string projectId, NewToken token) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); + /// + /// Create for organization + /// + /// + /// This is a helper action that makes it easier to create a token for a specific organization. + /// You may also specify a scope when creating a token. There are three valid scopes: client, user and admin. + /// + /// The identifier of the organization. + /// The token. + /// An error occurred while creating the token. + /// The token already exists. + [HttpPost("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/tokens")] + [Consumes("application/json")] + public async Task> PostByOrganizationAsync(string organizationId, NewToken token) { + if (token == null) + token = new NewToken(); + + if (!IsInOrganization(organizationId)) + return BadRequest(); + + token.OrganizationId = organizationId; + return await PostImplAsync(token); + } - if (token == null) - token = new NewToken(); + /// + /// Update + /// + /// The identifier of the token. + /// The changes + /// An error occurred while updating the token. + /// The token could not be found. + [HttpPatch("{id:tokens}")] + [HttpPut("{id:tokens}")] + [Consumes("application/json")] + public Task> PatchAsync(string id, Delta changes) { + return PatchImplAsync(id, changes); + } - token.OrganizationId = project.OrganizationId; - token.ProjectId = projectId; - return await PostImplAsync(token); - } + /// + /// Remove + /// + /// A comma delimited list of token identifiers. + /// No Content. + /// One or more validation errors occurred. + /// One or more tokens were not found. + /// An error occurred while deleting one or more tokens. + [HttpDelete("{ids:tokens}")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteAsync(string ids) { + return DeleteImplAsync(ids.FromDelimitedString()); + } - /// - /// Create for organization - /// - /// - /// This is a helper action that makes it easier to create a token for a specific organization. - /// You may also specify a scope when creating a token. There are three valid scopes: client, user and admin. - /// - /// The identifier of the organization. - /// The token. - /// An error occurred while creating the token. - /// The token already exists. - [HttpPost("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/tokens")] - [Consumes("application/json")] - public async Task> PostByOrganizationAsync(string organizationId, NewToken token) { - if (token == null) - token = new NewToken(); - - if (!IsInOrganization(organizationId)) - return BadRequest(); - - token.OrganizationId = organizationId; - return await PostImplAsync(token); - } + #endregion - /// - /// Update - /// - /// The identifier of the token. - /// The changes - /// An error occurred while updating the token. - /// The token could not be found. - [HttpPatch("{id:tokens}")] - [HttpPut("{id:tokens}")] - [Consumes("application/json")] - public Task> PatchAsync(string id, Delta changes) { - return PatchImplAsync(id, changes); - } + protected override async Task GetModelAsync(string id, bool useCache = true) { + if (String.IsNullOrEmpty(id)) + return null; - /// - /// Remove - /// - /// A comma delimited list of token identifiers. - /// No Content. - /// One or more validation errors occurred. - /// One or more tokens were not found. - /// An error occurred while deleting one or more tokens. - [HttpDelete("{ids:tokens}")] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteAsync(string ids) { - return DeleteImplAsync(ids.FromDelimitedString()); - } + var model = await _repository.GetByIdAsync(id, o => o.Cache(useCache)); + if (model == null) + return null; - #endregion + if (!String.IsNullOrEmpty(model.OrganizationId) && !IsInOrganization(model.OrganizationId)) + return null; - protected override async Task GetModelAsync(string id, bool useCache = true) { - if (String.IsNullOrEmpty(id)) - return null; + if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && !String.IsNullOrEmpty(model.UserId) && model.UserId != CurrentUser.Id) + return null; - var model = await _repository.GetByIdAsync(id, o => o.Cache(useCache)); - if (model == null) - return null; + if (model.Type != TokenType.Access) + return null; - if (!String.IsNullOrEmpty(model.OrganizationId) && !IsInOrganization(model.OrganizationId)) - return null; + if (!String.IsNullOrEmpty(model.ProjectId) && !await IsInProjectAsync(model.ProjectId)) + return null; - if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && !String.IsNullOrEmpty(model.UserId) && model.UserId != CurrentUser.Id) - return null; + return model; + } - if (model.Type != TokenType.Access) - return null; + protected override async Task CanAddAsync(Token value) { + // We only allow users to create organization scoped tokens. + if (String.IsNullOrEmpty(value.OrganizationId)) + return PermissionResult.Deny; - if (!String.IsNullOrEmpty(model.ProjectId) && !await IsInProjectAsync(model.ProjectId)) - return null; + bool hasUserRole = User.IsInRole(AuthorizationRoles.User); + bool hasGlobalAdminRole = User.IsInRole(AuthorizationRoles.GlobalAdmin); + if (!hasGlobalAdminRole && !String.IsNullOrEmpty(value.UserId) && value.UserId != CurrentUser.Id) + return PermissionResult.Deny; - return model; - } + if (!String.IsNullOrEmpty(value.ProjectId) && !String.IsNullOrEmpty(value.UserId)) + return PermissionResult.DenyWithMessage("Token can't be associated to both user and project."); - protected override async Task CanAddAsync(Token value) { - // We only allow users to create organization scoped tokens. - if (String.IsNullOrEmpty(value.OrganizationId)) - return PermissionResult.Deny; + foreach (string scope in value.Scopes.ToList()) { + string lowerCaseScoped = scope.ToLowerInvariant(); + if (!String.Equals(scope, lowerCaseScoped)) { + value.Scopes.Remove(scope); + value.Scopes.Add(lowerCaseScoped); + } - bool hasUserRole = User.IsInRole(AuthorizationRoles.User); - bool hasGlobalAdminRole = User.IsInRole(AuthorizationRoles.GlobalAdmin); - if (!hasGlobalAdminRole && !String.IsNullOrEmpty(value.UserId) && value.UserId != CurrentUser.Id) - return PermissionResult.Deny; + if (!AuthorizationRoles.AllScopes.Contains(lowerCaseScoped)) + return PermissionResult.DenyWithMessage("Invalid token scope requested."); + } - if (!String.IsNullOrEmpty(value.ProjectId) && !String.IsNullOrEmpty(value.UserId)) - return PermissionResult.DenyWithMessage("Token can't be associated to both user and project."); + if (value.Scopes.Count == 0) + value.Scopes.Add(AuthorizationRoles.Client); - foreach (string scope in value.Scopes.ToList()) { - string lowerCaseScoped = scope.ToLowerInvariant(); - if (!String.Equals(scope, lowerCaseScoped)) { - value.Scopes.Remove(scope); - value.Scopes.Add(lowerCaseScoped); - } + if (value.Scopes.Contains(AuthorizationRoles.Client) && !hasUserRole) + return PermissionResult.Deny; - if (!AuthorizationRoles.AllScopes.Contains(lowerCaseScoped)) - return PermissionResult.DenyWithMessage("Invalid token scope requested."); - } + if (value.Scopes.Contains(AuthorizationRoles.User) && !hasUserRole) + return PermissionResult.Deny; - if (value.Scopes.Count == 0) - value.Scopes.Add(AuthorizationRoles.Client); + if (value.Scopes.Contains(AuthorizationRoles.GlobalAdmin) && !hasGlobalAdminRole) + return PermissionResult.Deny; - if (value.Scopes.Contains(AuthorizationRoles.Client) && !hasUserRole) + if (!String.IsNullOrEmpty(value.ProjectId)) { + var project = await GetProjectAsync(value.ProjectId); + if (project == null) return PermissionResult.Deny; - if (value.Scopes.Contains(AuthorizationRoles.User) && !hasUserRole) - return PermissionResult.Deny; + value.OrganizationId = project.OrganizationId; + value.DefaultProjectId = null; + } - if (value.Scopes.Contains(AuthorizationRoles.GlobalAdmin) && !hasGlobalAdminRole) + if (!String.IsNullOrEmpty(value.DefaultProjectId)) { + var project = await GetProjectAsync(value.DefaultProjectId); + if (project == null) return PermissionResult.Deny; - - if (!String.IsNullOrEmpty(value.ProjectId)) { - var project = await GetProjectAsync(value.ProjectId); - if (project == null) - return PermissionResult.Deny; - - value.OrganizationId = project.OrganizationId; - value.DefaultProjectId = null; - } - - if (!String.IsNullOrEmpty(value.DefaultProjectId)) { - var project = await GetProjectAsync(value.DefaultProjectId); - if (project == null) - return PermissionResult.Deny; - } - - return await base.CanAddAsync(value); } - protected override Task AddModelAsync(Token value) { - value.Id = StringExtensions.GetNewToken(); - value.CreatedUtc = value.UpdatedUtc = SystemClock.UtcNow; - value.Type = TokenType.Access; - value.CreatedBy = Request.GetUser().Id; + return await base.CanAddAsync(value); + } - // add implied scopes - if (value.Scopes.Contains(AuthorizationRoles.GlobalAdmin) && !value.Scopes.Contains(AuthorizationRoles.User)) - value.Scopes.Add(AuthorizationRoles.User); + protected override Task AddModelAsync(Token value) { + value.Id = StringExtensions.GetNewToken(); + value.CreatedUtc = value.UpdatedUtc = SystemClock.UtcNow; + value.Type = TokenType.Access; + value.CreatedBy = Request.GetUser().Id; - if (value.Scopes.Contains(AuthorizationRoles.User) && !value.Scopes.Contains(AuthorizationRoles.Client)) - value.Scopes.Add(AuthorizationRoles.Client); + // add implied scopes + if (value.Scopes.Contains(AuthorizationRoles.GlobalAdmin) && !value.Scopes.Contains(AuthorizationRoles.User)) + value.Scopes.Add(AuthorizationRoles.User); - return base.AddModelAsync(value); - } + if (value.Scopes.Contains(AuthorizationRoles.User) && !value.Scopes.Contains(AuthorizationRoles.Client)) + value.Scopes.Add(AuthorizationRoles.Client); + + return base.AddModelAsync(value); + } - protected override async Task CanDeleteAsync(Token value) { - if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && !String.IsNullOrEmpty(value.UserId) && value.UserId != CurrentUser.Id) - return PermissionResult.DenyWithMessage("Can only delete tokens created by you."); + protected override async Task CanDeleteAsync(Token value) { + if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && !String.IsNullOrEmpty(value.UserId) && value.UserId != CurrentUser.Id) + return PermissionResult.DenyWithMessage("Can only delete tokens created by you."); - if (!String.IsNullOrEmpty(value.ProjectId) && !await IsInProjectAsync(value.ProjectId)) - return PermissionResult.DenyWithNotFound(value.Id); + if (!String.IsNullOrEmpty(value.ProjectId) && !await IsInProjectAsync(value.ProjectId)) + return PermissionResult.DenyWithNotFound(value.Id); - return await base.CanDeleteAsync(value); - } + return await base.CanDeleteAsync(value); + } - private async Task GetProjectAsync(string projectId, bool useCache = true) { - if (String.IsNullOrEmpty(projectId)) - return null; + private async Task GetProjectAsync(string projectId, bool useCache = true) { + if (String.IsNullOrEmpty(projectId)) + return null; - var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); - if (project == null || !CanAccessOrganization(project.OrganizationId)) - return null; + var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); + if (project == null || !CanAccessOrganization(project.OrganizationId)) + return null; - return project; - } + return project; + } - private async Task IsInProjectAsync(string projectId) { - var project = await GetProjectAsync(projectId); - return project != null; - } + private async Task IsInProjectAsync(string projectId) { + var project = await GetProjectAsync(projectId); + return project != null; } } diff --git a/src/Exceptionless.Web/Controllers/UserController.cs b/src/Exceptionless.Web/Controllers/UserController.cs index dfa25e4f34..8f94de80b7 100644 --- a/src/Exceptionless.Web/Controllers/UserController.cs +++ b/src/Exceptionless.Web/Controllers/UserController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Web.Extensions; using Exceptionless.Core.Authorization; using Exceptionless.Core.Configuration; @@ -19,300 +15,300 @@ using Foundatio.Repositories; using Foundatio.Utility; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace Exceptionless.Web.Controllers { - [Route(API_PREFIX + "/users")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public class UserController : RepositoryApiController { - private readonly IOrganizationRepository _organizationRepository; - private readonly ITokenRepository _tokenRepository; - private readonly ICacheClient _cache; - private readonly IMailer _mailer; - private readonly IntercomOptions _intercomOptions; - - public UserController(IUserRepository userRepository, IOrganizationRepository organizationRepository, ITokenRepository tokenRepository, ICacheClient cacheClient, IMailer mailer, IMapper mapper, IAppQueryValidator validator, IntercomOptions intercomOptions, ILoggerFactory loggerFactory) : base(userRepository, mapper, validator, loggerFactory) { - _organizationRepository = organizationRepository; - _tokenRepository = tokenRepository; - _cache = new ScopedCacheClient(cacheClient, "User"); - _mailer = mailer; - _intercomOptions = intercomOptions; - } - /// - /// Get current user - /// - /// The current user could not be found. - [HttpGet("me")] - public async Task> GetCurrentUserAsync() { - var currentUser = await GetModelAsync(CurrentUser.Id); - if (currentUser == null) - return NotFound(); - - return Ok(new ViewCurrentUser(currentUser, _intercomOptions)); - } +namespace Exceptionless.Web.Controllers; + +[Route(API_PREFIX + "/users")] +[Authorize(Policy = AuthorizationRoles.UserPolicy)] +public class UserController : RepositoryApiController { + private readonly IOrganizationRepository _organizationRepository; + private readonly ITokenRepository _tokenRepository; + private readonly ICacheClient _cache; + private readonly IMailer _mailer; + private readonly IntercomOptions _intercomOptions; + + public UserController(IUserRepository userRepository, IOrganizationRepository organizationRepository, ITokenRepository tokenRepository, ICacheClient cacheClient, IMailer mailer, IMapper mapper, IAppQueryValidator validator, IntercomOptions intercomOptions, ILoggerFactory loggerFactory) : base(userRepository, mapper, validator, loggerFactory) { + _organizationRepository = organizationRepository; + _tokenRepository = tokenRepository; + _cache = new ScopedCacheClient(cacheClient, "User"); + _mailer = mailer; + _intercomOptions = intercomOptions; + } - /// - /// Get by id - /// - /// The identifier of the user. - /// The user could not be found. - [HttpGet("{id:objectid}", Name = "GetUserById")] - public Task> GetAsync(string id) { - return GetByIdImplAsync(id); - } + /// + /// Get current user + /// + /// The current user could not be found. + [HttpGet("me")] + public async Task> GetCurrentUserAsync() { + var currentUser = await GetModelAsync(CurrentUser.Id); + if (currentUser == null) + return NotFound(); + + return Ok(new ViewCurrentUser(currentUser, _intercomOptions)); + } - /// - /// Get by organization - /// - /// The identifier of the organization. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The organization could not be found. - [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/users")] - public async Task>> GetByOrganizationAsync(string organizationId, int page = 1, int limit = 10) { - if (!CanAccessOrganization(organizationId)) - return NotFound(); - - var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); - if (organization == null) - return NotFound(); - - page = GetPage(page); - limit = GetLimit(limit); - int skip = GetSkip(page, limit); - if (skip > MAXIMUM_SKIP) - return Ok(Enumerable.Empty()); - - var results = await _repository.GetByOrganizationIdAsync(organizationId, o => o.PageLimit(MAXIMUM_SKIP)); - var users = (await MapCollectionAsync(results.Documents, true)).ToList(); - if (!Request.IsGlobalAdmin()) - users.ForEach(u => u.Roles.Remove(AuthorizationRoles.GlobalAdmin)); - - if (organization.Invites.Any()) { - users.AddRange(organization.Invites.Select(i => new ViewUser { - EmailAddress = i.EmailAddress, - IsInvite = true - })); - } - - var pagedUsers = users.Skip(skip).Take(limit).ToList(); - return OkWithResourceLinks(pagedUsers, users.Count > GetSkip(page + 1, limit), page); - } + /// + /// Get by id + /// + /// The identifier of the user. + /// The user could not be found. + [HttpGet("{id:objectid}", Name = "GetUserById")] + public Task> GetAsync(string id) { + return GetByIdImplAsync(id); + } - /// - /// Update - /// - /// The identifier of the user. - /// The changes - /// An error occurred while updating the user. - /// The user could not be found. - [HttpPatch("{id:objectid}")] - [HttpPut("{id:objectid}")] - [Consumes("application/json")] - public Task> PatchAsync(string id, Delta changes) { - return PatchImplAsync(id, changes); + /// + /// Get by organization + /// + /// The identifier of the organization. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The organization could not be found. + [HttpGet("~/" + API_PREFIX + "/organizations/{organizationId:objectid}/users")] + public async Task>> GetByOrganizationAsync(string organizationId, int page = 1, int limit = 10) { + if (!CanAccessOrganization(organizationId)) + return NotFound(); + + var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); + if (organization == null) + return NotFound(); + + page = GetPage(page); + limit = GetLimit(limit); + int skip = GetSkip(page, limit); + if (skip > MAXIMUM_SKIP) + return Ok(Enumerable.Empty()); + + var results = await _repository.GetByOrganizationIdAsync(organizationId, o => o.PageLimit(MAXIMUM_SKIP)); + var users = (await MapCollectionAsync(results.Documents, true)).ToList(); + if (!Request.IsGlobalAdmin()) + users.ForEach(u => u.Roles.Remove(AuthorizationRoles.GlobalAdmin)); + + if (organization.Invites.Any()) { + users.AddRange(organization.Invites.Select(i => new ViewUser { + EmailAddress = i.EmailAddress, + IsInvite = true + })); } - /// - /// Delete current user - /// - /// The current user could not be found. - [HttpDelete("me")] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteCurrentUserAsync() { - return DeleteImplAsync(new [] { CurrentUser.Id }); - } + var pagedUsers = users.Skip(skip).Take(limit).ToList(); + return OkWithResourceLinks(pagedUsers, users.Count > GetSkip(page + 1, limit), page); + } - /// - /// Remove - /// - /// A comma delimited list of user identifiers. - /// No Content. - /// One or more validation errors occurred. - /// One or more users were not found. - /// An error occurred while deleting one or more users. - [HttpDelete("{ids:objectids}")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteAsync(string ids) { - return DeleteImplAsync(ids.FromDelimitedString()); - } + /// + /// Update + /// + /// The identifier of the user. + /// The changes + /// An error occurred while updating the user. + /// The user could not be found. + [HttpPatch("{id:objectid}")] + [HttpPut("{id:objectid}")] + [Consumes("application/json")] + public Task> PatchAsync(string id, Delta changes) { + return PatchImplAsync(id, changes); + } - /// - /// Update email address - /// - /// The identifier of the user. - /// The new email address. - /// An error occurred while updating the users email address. - /// The user could not be found. - [HttpPost("{id:objectid}/email-address/{email:minlength(1)}")] - public async Task> UpdateEmailAddressAsync(string id, string email) { - var user = await GetModelAsync(id, false); - if (user == null) - return NotFound(); - - email = email.Trim().ToLowerInvariant(); - if (String.Equals(CurrentUser.EmailAddress, email, StringComparison.InvariantCultureIgnoreCase)) - return Ok(new UpdateEmailAddressResult { IsVerified = user.IsEmailAddressVerified }); - - // Only allow 3 email address updates per hour period by a single user. - string updateEmailAddressAttemptsCacheKey = $"{CurrentUser.Id}:attempts"; - long attempts = await _cache.IncrementAsync(updateEmailAddressAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); - if (attempts > 3) - return BadRequest("Update email address rate limit reached. Please try updating later."); - - if (!await IsEmailAddressAvailableInternalAsync(email)) - return BadRequest("A user with this email address already exists."); - - user.ResetPasswordResetToken(); - user.EmailAddress = email; - user.IsEmailAddressVerified = user.OAuthAccounts.Count(oa => String.Equals(oa.EmailAddress(), email, StringComparison.InvariantCultureIgnoreCase)) > 0; - if (!user.IsEmailAddressVerified) - user.CreateVerifyEmailAddressToken(); - else - user.ResetVerifyEmailAddressToken(); - - try { - await _repository.SaveAsync(user, o => o.Cache()); - } catch (ValidationException ex) { - return BadRequest(String.Join(", ", ex.Errors)); - } catch (Exception ex) { - using (_logger.BeginScope(new ExceptionlessState().Property("User", user).SetHttpContext(HttpContext))) - _logger.LogError(ex, ex.Message); - return BadRequest("An error occurred."); - } - - if (!user.IsEmailAddressVerified) - await ResendVerificationEmailAsync(id); - - // TODO: We may want to send an email to old email addresses as well. + /// + /// Delete current user + /// + /// The current user could not be found. + [HttpDelete("me")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteCurrentUserAsync() { + return DeleteImplAsync(new[] { CurrentUser.Id }); + } + + /// + /// Remove + /// + /// A comma delimited list of user identifiers. + /// No Content. + /// One or more validation errors occurred. + /// One or more users were not found. + /// An error occurred while deleting one or more users. + [HttpDelete("{ids:objectids}")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteAsync(string ids) { + return DeleteImplAsync(ids.FromDelimitedString()); + } + /// + /// Update email address + /// + /// The identifier of the user. + /// The new email address. + /// An error occurred while updating the users email address. + /// The user could not be found. + [HttpPost("{id:objectid}/email-address/{email:minlength(1)}")] + public async Task> UpdateEmailAddressAsync(string id, string email) { + var user = await GetModelAsync(id, false); + if (user == null) + return NotFound(); + + email = email.Trim().ToLowerInvariant(); + if (String.Equals(CurrentUser.EmailAddress, email, StringComparison.InvariantCultureIgnoreCase)) return Ok(new UpdateEmailAddressResult { IsVerified = user.IsEmailAddressVerified }); - } - /// - /// Verify email address - /// - /// The token identifier. - /// Verify Email Address Token has expired. - /// The user could not be found. - [HttpGet("verify-email-address/{token:token}")] - public async Task VerifyAsync(string token) { - var user = await _repository.GetByVerifyEmailAddressTokenAsync(token); - if (user == null) { - // The user may already be logged in and verified. - if (CurrentUser != null && CurrentUser.IsEmailAddressVerified) - return Ok(); - - return NotFound(); - } - - if (!user.HasValidVerifyEmailAddressTokenExpiration()) - return BadRequest("Verify Email Address Token has expired."); - - user.MarkEmailAddressVerified(); - await _repository.SaveAsync(user, o => o.Cache()); + // Only allow 3 email address updates per hour period by a single user. + string updateEmailAddressAttemptsCacheKey = $"{CurrentUser.Id}:attempts"; + long attempts = await _cache.IncrementAsync(updateEmailAddressAttemptsCacheKey, 1, SystemClock.UtcNow.Ceiling(TimeSpan.FromHours(1))); + if (attempts > 3) + return BadRequest("Update email address rate limit reached. Please try updating later."); - return Ok(); - } + if (!await IsEmailAddressAvailableInternalAsync(email)) + return BadRequest("A user with this email address already exists."); - /// - /// Resend verification email - /// - /// The identifier of the user. - /// The user could not be found. - [HttpGet("{id:objectid}/resend-verification-email")] - public async Task ResendVerificationEmailAsync(string id) { - var user = await GetModelAsync(id, false); - if (user == null) - return NotFound(); - - if (!user.IsEmailAddressVerified) { - user.CreateVerifyEmailAddressToken(); - await _repository.SaveAsync(user, o => o.Cache()); - await _mailer.SendUserEmailVerifyAsync(user); - } - - return Ok(); + user.ResetPasswordResetToken(); + user.EmailAddress = email; + user.IsEmailAddressVerified = user.OAuthAccounts.Count(oa => String.Equals(oa.EmailAddress(), email, StringComparison.InvariantCultureIgnoreCase)) > 0; + if (!user.IsEmailAddressVerified) + user.CreateVerifyEmailAddressToken(); + else + user.ResetVerifyEmailAddressToken(); + + try { + await _repository.SaveAsync(user, o => o.Cache()); + } + catch (ValidationException ex) { + return BadRequest(String.Join(", ", ex.Errors)); } + catch (Exception ex) { + using (_logger.BeginScope(new ExceptionlessState().Property("User", user).SetHttpContext(HttpContext))) + _logger.LogError(ex, ex.Message); + return BadRequest("An error occurred."); + } + + if (!user.IsEmailAddressVerified) + await ResendVerificationEmailAsync(id); - [HttpPost("{id:objectid}/admin-role")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task AddAdminRoleAsync(string id) { - var user = await GetModelAsync(id, false); - if (user == null) - return NotFound(); + // TODO: We may want to send an email to old email addresses as well. - if (!user.Roles.Contains(AuthorizationRoles.GlobalAdmin)) { - user.Roles.Add(AuthorizationRoles.GlobalAdmin); - await _repository.SaveAsync(user, o => o.Cache()); - } + return Ok(new UpdateEmailAddressResult { IsVerified = user.IsEmailAddressVerified }); + } - return Ok(); + /// + /// Verify email address + /// + /// The token identifier. + /// Verify Email Address Token has expired. + /// The user could not be found. + [HttpGet("verify-email-address/{token:token}")] + public async Task VerifyAsync(string token) { + var user = await _repository.GetByVerifyEmailAddressTokenAsync(token); + if (user == null) { + // The user may already be logged in and verified. + if (CurrentUser != null && CurrentUser.IsEmailAddressVerified) + return Ok(); + + return NotFound(); } - [HttpDelete("{id:objectid}/admin-role")] - [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task DeleteAdminRoleAsync(string id) { - var user = await GetModelAsync(id, false); - if (user == null) - return NotFound(); + if (!user.HasValidVerifyEmailAddressTokenExpiration()) + return BadRequest("Verify Email Address Token has expired."); + + user.MarkEmailAddressVerified(); + await _repository.SaveAsync(user, o => o.Cache()); - if (user.Roles.Contains(AuthorizationRoles.GlobalAdmin)) { - user.Roles.Remove(AuthorizationRoles.GlobalAdmin); - await _repository.SaveAsync(user, o => o.Cache()); - } + return Ok(); + } - return StatusCode(StatusCodes.Status204NoContent); + /// + /// Resend verification email + /// + /// The identifier of the user. + /// The user could not be found. + [HttpGet("{id:objectid}/resend-verification-email")] + public async Task ResendVerificationEmailAsync(string id) { + var user = await GetModelAsync(id, false); + if (user == null) + return NotFound(); + + if (!user.IsEmailAddressVerified) { + user.CreateVerifyEmailAddressToken(); + await _repository.SaveAsync(user, o => o.Cache()); + await _mailer.SendUserEmailVerifyAsync(user); } - private async Task IsEmailAddressAvailableInternalAsync(string email) { - if (String.IsNullOrWhiteSpace(email)) - return false; + return Ok(); + } - email = email.Trim().ToLowerInvariant(); - if (CurrentUser != null && String.Equals(CurrentUser.EmailAddress, email, StringComparison.InvariantCultureIgnoreCase)) - return true; + [HttpPost("{id:objectid}/admin-role")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task AddAdminRoleAsync(string id) { + var user = await GetModelAsync(id, false); + if (user == null) + return NotFound(); - return await _repository.GetByEmailAddressAsync(email) == null; + if (!user.Roles.Contains(AuthorizationRoles.GlobalAdmin)) { + user.Roles.Add(AuthorizationRoles.GlobalAdmin); + await _repository.SaveAsync(user, o => o.Cache()); } - protected override async Task GetModelAsync(string id, bool useCache = true) { - if (Request.IsGlobalAdmin() || String.Equals(CurrentUser.Id, id)) - return await base.GetModelAsync(id, useCache); + return Ok(); + } + + [HttpDelete("{id:objectid}/admin-role")] + [Authorize(Policy = AuthorizationRoles.GlobalAdminPolicy)] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task DeleteAdminRoleAsync(string id) { + var user = await GetModelAsync(id, false); + if (user == null) + return NotFound(); - return null; + if (user.Roles.Contains(AuthorizationRoles.GlobalAdmin)) { + user.Roles.Remove(AuthorizationRoles.GlobalAdmin); + await _repository.SaveAsync(user, o => o.Cache()); } - protected override Task> GetModelsAsync(string[] ids, bool useCache = true) { - if (Request.IsGlobalAdmin()) - return base.GetModelsAsync(ids, useCache); + return StatusCode(StatusCodes.Status204NoContent); + } - return base.GetModelsAsync(ids.Where(id => String.Equals(CurrentUser.Id, id)).ToArray(), useCache); - } + private async Task IsEmailAddressAvailableInternalAsync(string email) { + if (String.IsNullOrWhiteSpace(email)) + return false; - protected override async Task CanDeleteAsync(User value) { - if (value.OrganizationIds.Count > 0) - return PermissionResult.DenyWithMessage("Please delete or leave any organizations before deleting your account."); + email = email.Trim().ToLowerInvariant(); + if (CurrentUser != null && String.Equals(CurrentUser.EmailAddress, email, StringComparison.InvariantCultureIgnoreCase)) + return true; - if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && value.Id != CurrentUser.Id) - return PermissionResult.Deny; + return await _repository.GetByEmailAddressAsync(email) == null; + } - return await base.CanDeleteAsync(value); - } + protected override async Task GetModelAsync(string id, bool useCache = true) { + if (Request.IsGlobalAdmin() || String.Equals(CurrentUser.Id, id)) + return await base.GetModelAsync(id, useCache); + + return null; + } + + protected override Task> GetModelsAsync(string[] ids, bool useCache = true) { + if (Request.IsGlobalAdmin()) + return base.GetModelsAsync(ids, useCache); - protected override async Task> DeleteModelsAsync(ICollection values) { - foreach (var user in values) { - long removed = await _tokenRepository.RemoveAllByUserIdAsync(user.Id); - _logger.RemovedTokens(removed, user.Id); - } + return base.GetModelsAsync(ids.Where(id => String.Equals(CurrentUser.Id, id)).ToArray(), useCache); + } - return await base.DeleteModelsAsync(values); + protected override async Task CanDeleteAsync(User value) { + if (value.OrganizationIds.Count > 0) + return PermissionResult.DenyWithMessage("Please delete or leave any organizations before deleting your account."); + + if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && value.Id != CurrentUser.Id) + return PermissionResult.Deny; + + return await base.CanDeleteAsync(value); + } + + protected override async Task> DeleteModelsAsync(ICollection values) { + foreach (var user in values) { + long removed = await _tokenRepository.RemoveAllByUserIdAsync(user.Id); + _logger.RemovedTokens(removed, user.Id); } + + return await base.DeleteModelsAsync(values); } } diff --git a/src/Exceptionless.Web/Controllers/UtilityController.cs b/src/Exceptionless.Web/Controllers/UtilityController.cs index 0ea3678524..2a280599be 100644 --- a/src/Exceptionless.Web/Controllers/UtilityController.cs +++ b/src/Exceptionless.Web/Controllers/UtilityController.cs @@ -1,38 +1,37 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Authorization; +using Exceptionless.Core.Authorization; using Exceptionless.Core.Queries.Validation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Exceptionless.Web.Controllers { - [ApiExplorerSettings(IgnoreApi = true)] - [Route(API_PREFIX)] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public class UtilityController : ExceptionlessApiController { - private readonly PersistentEventQueryValidator _eventQueryValidator; - private readonly StackQueryValidator _stackQueryValidator; +namespace Exceptionless.Web.Controllers; - public UtilityController(PersistentEventQueryValidator eventQueryValidator, StackQueryValidator stackQueryValidator) { - _eventQueryValidator = eventQueryValidator; - _stackQueryValidator = stackQueryValidator; - } +[ApiExplorerSettings(IgnoreApi = true)] +[Route(API_PREFIX)] +[Authorize(Policy = AuthorizationRoles.UserPolicy)] +public class UtilityController : ExceptionlessApiController { + private readonly PersistentEventQueryValidator _eventQueryValidator; + private readonly StackQueryValidator _stackQueryValidator; - /// - /// Validate search query - /// - /// - /// Validate a search query to ensure that it can successfully be searched by the api - /// - /// The query you wish to validate. - [HttpGet("search/validate")] - public async Task> ValidateAsync(string query) { - var eventResults = await _eventQueryValidator.ValidateQueryAsync(query); - var stackResults = await _stackQueryValidator.ValidateQueryAsync(query); - return Ok(new AppQueryValidator.QueryProcessResult { - IsValid = eventResults.IsValid || stackResults.IsValid, - UsesPremiumFeatures = eventResults.UsesPremiumFeatures && stackResults.UsesPremiumFeatures, - Message = eventResults.Message ?? stackResults.Message - }); - } + public UtilityController(PersistentEventQueryValidator eventQueryValidator, StackQueryValidator stackQueryValidator) { + _eventQueryValidator = eventQueryValidator; + _stackQueryValidator = stackQueryValidator; } -} \ No newline at end of file + + /// + /// Validate search query + /// + /// + /// Validate a search query to ensure that it can successfully be searched by the api + /// + /// The query you wish to validate. + [HttpGet("search/validate")] + public async Task> ValidateAsync(string query) { + var eventResults = await _eventQueryValidator.ValidateQueryAsync(query); + var stackResults = await _stackQueryValidator.ValidateQueryAsync(query); + return Ok(new AppQueryValidator.QueryProcessResult { + IsValid = eventResults.IsValid || stackResults.IsValid, + UsesPremiumFeatures = eventResults.UsesPremiumFeatures && stackResults.UsesPremiumFeatures, + Message = eventResults.Message ?? stackResults.Message + }); + } +} diff --git a/src/Exceptionless.Web/Controllers/WebHookController.cs b/src/Exceptionless.Web/Controllers/WebHookController.cs index 29f743997c..03632aa6fa 100644 --- a/src/Exceptionless.Web/Controllers/WebHookController.cs +++ b/src/Exceptionless.Web/Controllers/WebHookController.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Exceptionless.Web.Controllers; using Exceptionless.Web.Extensions; using Exceptionless.Web.Models; @@ -14,252 +10,250 @@ using Exceptionless.Core.Queries.Validation; using Foundatio.Repositories; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; -namespace Exceptionless.App.Controllers.API { - [Route(API_PREFIX + "/webhooks")] - [Authorize(Policy = AuthorizationRoles.ClientPolicy)] - public class WebHookController : RepositoryApiController { - private readonly IProjectRepository _projectRepository; - private readonly BillingManager _billingManager; +namespace Exceptionless.App.Controllers.API; - public WebHookController(IWebHookRepository repository, IProjectRepository projectRepository, BillingManager billingManager, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) : base(repository, mapper, validator, loggerFactory) { - _projectRepository = projectRepository; - _billingManager = billingManager; - } +[Route(API_PREFIX + "/webhooks")] +[Authorize(Policy = AuthorizationRoles.ClientPolicy)] +public class WebHookController : RepositoryApiController { + private readonly IProjectRepository _projectRepository; + private readonly BillingManager _billingManager; - #region CRUD - - /// - /// Get by project - /// - /// The identifier of the project. - /// The page parameter is used for pagination. This value must be greater than 0. - /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. - /// The project could not be found. - [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/webhooks")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task>> GetByProjectAsync(string projectId, int page = 1, int limit = 10) { - var project = await GetProjectAsync(projectId); - if (project == null) - return NotFound(); + public WebHookController(IWebHookRepository repository, IProjectRepository projectRepository, BillingManager billingManager, IMapper mapper, IAppQueryValidator validator, ILoggerFactory loggerFactory) : base(repository, mapper, validator, loggerFactory) { + _projectRepository = projectRepository; + _billingManager = billingManager; + } - page = GetPage(page); - limit = GetLimit(limit); - var results = await _repository.GetByProjectIdAsync(projectId, o => o.PageNumber(page).PageLimit(limit)); - return OkWithResourceLinks(results.Documents, results.HasMore && !NextPageExceedsSkipLimit(page, limit), page); - } + #region CRUD + + /// + /// Get by project + /// + /// The identifier of the project. + /// The page parameter is used for pagination. This value must be greater than 0. + /// A limit on the number of objects to be returned. Limit can range between 1 and 100 items. + /// The project could not be found. + [HttpGet("~/" + API_PREFIX + "/projects/{projectId:objectid}/webhooks")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public async Task>> GetByProjectAsync(string projectId, int page = 1, int limit = 10) { + var project = await GetProjectAsync(projectId); + if (project == null) + return NotFound(); + + page = GetPage(page); + limit = GetLimit(limit); + var results = await _repository.GetByProjectIdAsync(projectId, o => o.PageNumber(page).PageLimit(limit)); + return OkWithResourceLinks(results.Documents, results.HasMore && !NextPageExceedsSkipLimit(page, limit), page); + } - /// - /// Get by id - /// - /// The identifier of the web hook. - /// The web hook could not be found. - [HttpGet("{id:objectid}", Name = "GetWebHookById")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public Task> GetAsync(string id) { - return GetByIdImplAsync(id); - } + /// + /// Get by id + /// + /// The identifier of the web hook. + /// The web hook could not be found. + [HttpGet("{id:objectid}", Name = "GetWebHookById")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public Task> GetAsync(string id) { + return GetByIdImplAsync(id); + } - /// - /// Create - /// - /// The web hook. - /// - /// An error occurred while creating the web hook. - /// The web hook already exists. - [HttpPost] - [Consumes("application/json")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public Task> PostAsync(NewWebHook webhook) { - return PostImplAsync(webhook); - } + /// + /// Create + /// + /// The web hook. + /// + /// An error occurred while creating the web hook. + /// The web hook already exists. + [HttpPost] + [Consumes("application/json")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + public Task> PostAsync(NewWebHook webhook) { + return PostImplAsync(webhook); + } - /// - /// Remove - /// - /// A comma delimited list of web hook identifiers. - /// No Content. - /// One or more validation errors occurred. - /// One or more web hooks were not found. - /// An error occurred while deleting one or more web hooks. - [HttpDelete("{ids:objectids}")] - [Authorize(Policy = AuthorizationRoles.UserPolicy)] - [ProducesResponseType(StatusCodes.Status202Accepted)] - public Task> DeleteAsync(string ids) { - return DeleteImplAsync(ids.FromDelimitedString()); - } + /// + /// Remove + /// + /// A comma delimited list of web hook identifiers. + /// No Content. + /// One or more validation errors occurred. + /// One or more web hooks were not found. + /// An error occurred while deleting one or more web hooks. + [HttpDelete("{ids:objectids}")] + [Authorize(Policy = AuthorizationRoles.UserPolicy)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public Task> DeleteAsync(string ids) { + return DeleteImplAsync(ids.FromDelimitedString()); + } - #endregion - - /// - /// This controller action is called by zapier to create a hook subscription. - /// - [HttpPost("subscribe")] - [HttpPost("~/api/v{apiVersion:int=2}/webhooks/subscribe")] - [HttpPost("~/api/v1/projecthook/subscribe")] - [Consumes("application/json")] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task> SubscribeAsync(JObject data, int apiVersion = 1) { - var webHook = new NewWebHook { - EventTypes = new[] { data.GetValue("event").Value() }, - Url = data.GetValue("target_url").Value(), - Version = new Version(apiVersion >= 0 ? apiVersion : 0, 0) - }; - - if (!webHook.Url.StartsWith("https://hooks.zapier.com")) - return NotFound(); - - string projectId = User.GetProjectId(); - if (projectId != null) - webHook.ProjectId = projectId; - else - webHook.OrganizationId = Request.GetDefaultOrganizationId(); - - return await PostImplAsync(webHook); - } + #endregion + + /// + /// This controller action is called by zapier to create a hook subscription. + /// + [HttpPost("subscribe")] + [HttpPost("~/api/v{apiVersion:int=2}/webhooks/subscribe")] + [HttpPost("~/api/v1/projecthook/subscribe")] + [Consumes("application/json")] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task> SubscribeAsync(JObject data, int apiVersion = 1) { + var webHook = new NewWebHook { + EventTypes = new[] { data.GetValue("event").Value() }, + Url = data.GetValue("target_url").Value(), + Version = new Version(apiVersion >= 0 ? apiVersion : 0, 0) + }; + + if (!webHook.Url.StartsWith("https://hooks.zapier.com")) + return NotFound(); + + string projectId = User.GetProjectId(); + if (projectId != null) + webHook.ProjectId = projectId; + else + webHook.OrganizationId = Request.GetDefaultOrganizationId(); + + return await PostImplAsync(webHook); + } - /// - /// This controller action is called by zapier to remove a hook subscription. - /// - [AllowAnonymous] - [HttpPost("unsubscribe")] - [HttpPost("~/api/v1/projecthook/unsubscribe")] - [Consumes("application/json")] - [ApiExplorerSettings(IgnoreApi = true)] - public async Task UnsubscribeAsync(JObject data) { - string targetUrl = data.GetValue("target_url").Value(); - - // don't let this anon method delete non-zapier hooks - if (!targetUrl.StartsWith("https://hooks.zapier.com")) - return NotFound(); - - var results = await _repository.GetByUrlAsync(targetUrl); - if (results.Documents.Count > 0) { - string organizationId = results.Documents.First().OrganizationId; - if (results.Documents.Any(h => h.OrganizationId != organizationId)) - throw new ArgumentException("All OrganizationIds must be the same."); - - _logger.RemovingZapierUrls(results.Documents.Count, targetUrl); - await _repository.RemoveAsync(results.Documents); - } - - return Ok(); + /// + /// This controller action is called by zapier to remove a hook subscription. + /// + [AllowAnonymous] + [HttpPost("unsubscribe")] + [HttpPost("~/api/v1/projecthook/unsubscribe")] + [Consumes("application/json")] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task UnsubscribeAsync(JObject data) { + string targetUrl = data.GetValue("target_url").Value(); + + // don't let this anon method delete non-zapier hooks + if (!targetUrl.StartsWith("https://hooks.zapier.com")) + return NotFound(); + + var results = await _repository.GetByUrlAsync(targetUrl); + if (results.Documents.Count > 0) { + string organizationId = results.Documents.First().OrganizationId; + if (results.Documents.Any(h => h.OrganizationId != organizationId)) + throw new ArgumentException("All OrganizationIds must be the same."); + + _logger.RemovingZapierUrls(results.Documents.Count, targetUrl); + await _repository.RemoveAsync(results.Documents); } - /// - /// This controller action is called by zapier to test auth. - /// - [HttpGet("test")] - [HttpPost("test")] - [HttpGet("~/api/v1/projecthook/test")] - [HttpPost("~/api/v1/projecthook/test")] - [ApiExplorerSettings(IgnoreApi = true)] - public IActionResult Test() { - return Ok(new[] { + return Ok(); + } + + /// + /// This controller action is called by zapier to test auth. + /// + [HttpGet("test")] + [HttpPost("test")] + [HttpGet("~/api/v1/projecthook/test")] + [HttpPost("~/api/v1/projecthook/test")] + [ApiExplorerSettings(IgnoreApi = true)] + public IActionResult Test() { + return Ok(new[] { new { id = 1, Message = "Test message 1." }, new { id = 2, Message = "Test message 2." } }); - } + } - protected override async Task GetModelAsync(string id, bool useCache = true) { - if (String.IsNullOrEmpty(id)) - return null; + protected override async Task GetModelAsync(string id, bool useCache = true) { + if (String.IsNullOrEmpty(id)) + return null; - var webHook = await _repository.GetByIdAsync(id, o => o.Cache(useCache)); - if (webHook == null) - return null; + var webHook = await _repository.GetByIdAsync(id, o => o.Cache(useCache)); + if (webHook == null) + return null; - if (!String.IsNullOrEmpty(webHook.OrganizationId) && !IsInOrganization(webHook.OrganizationId)) - return null; + if (!String.IsNullOrEmpty(webHook.OrganizationId) && !IsInOrganization(webHook.OrganizationId)) + return null; - if (!String.IsNullOrEmpty(webHook.ProjectId) && !await IsInProjectAsync(webHook.ProjectId)) - return null; + if (!String.IsNullOrEmpty(webHook.ProjectId) && !await IsInProjectAsync(webHook.ProjectId)) + return null; - return webHook; - } - - protected override async Task> GetModelsAsync(string[] ids, bool useCache = true) { - if (ids == null || ids.Length == 0) - return EmptyModels; + return webHook; + } - var webHooks = await _repository.GetByIdsAsync(ids, o => o.Cache(useCache)); - if (webHooks.Count == 0) - return EmptyModels; + protected override async Task> GetModelsAsync(string[] ids, bool useCache = true) { + if (ids == null || ids.Length == 0) + return EmptyModels; - var results = new List(); - foreach (var webHook in webHooks) { - if ((!String.IsNullOrEmpty(webHook.OrganizationId) && IsInOrganization(webHook.OrganizationId)) - || (!String.IsNullOrEmpty(webHook.ProjectId) && (await IsInProjectAsync(webHook.ProjectId)))) - results.Add(webHook); - } + var webHooks = await _repository.GetByIdsAsync(ids, o => o.Cache(useCache)); + if (webHooks.Count == 0) + return EmptyModels; - return results; + var results = new List(); + foreach (var webHook in webHooks) { + if ((!String.IsNullOrEmpty(webHook.OrganizationId) && IsInOrganization(webHook.OrganizationId)) + || (!String.IsNullOrEmpty(webHook.ProjectId) && (await IsInProjectAsync(webHook.ProjectId)))) + results.Add(webHook); } - protected override async Task CanAddAsync(WebHook value) { - if (String.IsNullOrEmpty(value.Url) || value.EventTypes.Length == 0) - return PermissionResult.Deny; - - if (String.IsNullOrEmpty(value.ProjectId) && String.IsNullOrEmpty(value.OrganizationId)) - return PermissionResult.Deny; + return results; + } - if (!String.IsNullOrEmpty(value.OrganizationId) && !IsInOrganization(value.OrganizationId)) - return PermissionResult.DenyWithMessage("Invalid organization id specified."); + protected override async Task CanAddAsync(WebHook value) { + if (String.IsNullOrEmpty(value.Url) || value.EventTypes.Length == 0) + return PermissionResult.Deny; - Project project = null; - if (!String.IsNullOrEmpty(value.ProjectId)) { - project = await GetProjectAsync(value.ProjectId); - if (project == null) - return PermissionResult.DenyWithMessage("Invalid project id specified."); + if (String.IsNullOrEmpty(value.ProjectId) && String.IsNullOrEmpty(value.OrganizationId)) + return PermissionResult.Deny; - value.OrganizationId = project.OrganizationId; - } + if (!String.IsNullOrEmpty(value.OrganizationId) && !IsInOrganization(value.OrganizationId)) + return PermissionResult.DenyWithMessage("Invalid organization id specified."); - if (!await _billingManager.HasPremiumFeaturesAsync(project != null ? project.OrganizationId : value.OrganizationId)) - return PermissionResult.DenyWithPlanLimitReached("Please upgrade your plan to add integrations."); + Project project = null; + if (!String.IsNullOrEmpty(value.ProjectId)) { + project = await GetProjectAsync(value.ProjectId); + if (project == null) + return PermissionResult.DenyWithMessage("Invalid project id specified."); - return PermissionResult.Allow; + value.OrganizationId = project.OrganizationId; } - protected override Task AddModelAsync(WebHook value) { - if (!IsValidWebHookVersion(value.Version)) - value.Version = WebHook.KnownVersions.Version2; + if (!await _billingManager.HasPremiumFeaturesAsync(project != null ? project.OrganizationId : value.OrganizationId)) + return PermissionResult.DenyWithPlanLimitReached("Please upgrade your plan to add integrations."); - return base.AddModelAsync(value); - } + return PermissionResult.Allow; + } - protected override async Task CanDeleteAsync(WebHook value) { - if (!String.IsNullOrEmpty(value.ProjectId) && !await IsInProjectAsync(value.ProjectId)) - return PermissionResult.DenyWithNotFound(value.Id); + protected override Task AddModelAsync(WebHook value) { + if (!IsValidWebHookVersion(value.Version)) + value.Version = WebHook.KnownVersions.Version2; - if (!String.IsNullOrEmpty(value.OrganizationId) && !IsInOrganization(value.OrganizationId)) - return PermissionResult.DenyWithNotFound(value.Id); + return base.AddModelAsync(value); + } - return PermissionResult.Allow; - } + protected override async Task CanDeleteAsync(WebHook value) { + if (!String.IsNullOrEmpty(value.ProjectId) && !await IsInProjectAsync(value.ProjectId)) + return PermissionResult.DenyWithNotFound(value.Id); - private async Task GetProjectAsync(string projectId, bool useCache = true) { - if (String.IsNullOrEmpty(projectId)) - return null; + if (!String.IsNullOrEmpty(value.OrganizationId) && !IsInOrganization(value.OrganizationId)) + return PermissionResult.DenyWithNotFound(value.Id); - var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); - if (project == null || !CanAccessOrganization(project.OrganizationId)) - return null; + return PermissionResult.Allow; + } - return project; - } + private async Task GetProjectAsync(string projectId, bool useCache = true) { + if (String.IsNullOrEmpty(projectId)) + return null; - private async Task IsInProjectAsync(string projectId) { - var project = await GetProjectAsync(projectId); - return project != null; - } + var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache(useCache)); + if (project == null || !CanAccessOrganization(project.OrganizationId)) + return null; - private bool IsValidWebHookVersion(string version) { - return String.Equals(version, WebHook.KnownVersions.Version1) || String.Equals(version, WebHook.KnownVersions.Version2); - } + return project; + } + + private async Task IsInProjectAsync(string projectId) { + var project = await GetProjectAsync(projectId); + return project != null; + } + + private bool IsValidWebHookVersion(string version) { + return String.Equals(version, WebHook.KnownVersions.Version1) || String.Equals(version, WebHook.KnownVersions.Version2); } } diff --git a/src/Exceptionless.Web/Extensions/DeltaExtensions.cs b/src/Exceptionless.Web/Extensions/DeltaExtensions.cs index b6dd130900..a2cf18a50c 100644 --- a/src/Exceptionless.Web/Extensions/DeltaExtensions.cs +++ b/src/Exceptionless.Web/Extensions/DeltaExtensions.cs @@ -1,16 +1,14 @@ -using System; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; using Exceptionless.Web.Utility; -namespace Exceptionless.Web.Extensions { - public static class DeltaExtensions { - public static bool ContainsChangedProperty(this Delta value, Expression> action) where T : class, new() { - if (!value.GetChangedPropertyNames().Any()) - return false; +namespace Exceptionless.Web.Extensions; - var expression = action.Body as MemberExpression ?? ((UnaryExpression)action.Body).Operand as MemberExpression; - return expression != null && value.GetChangedPropertyNames().Contains(expression.Member.Name); - } +public static class DeltaExtensions { + public static bool ContainsChangedProperty(this Delta value, Expression> action) where T : class, new() { + if (!value.GetChangedPropertyNames().Any()) + return false; + + var expression = action.Body as MemberExpression ?? ((UnaryExpression)action.Body).Operand as MemberExpression; + return expression != null && value.GetChangedPropertyNames().Contains(expression.Member.Name); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Extensions/ExceptionlessStateExtensions.cs b/src/Exceptionless.Web/Extensions/ExceptionlessStateExtensions.cs index a36906c46f..f6542e685c 100644 --- a/src/Exceptionless.Web/Extensions/ExceptionlessStateExtensions.cs +++ b/src/Exceptionless.Web/Extensions/ExceptionlessStateExtensions.cs @@ -1,12 +1,10 @@ -using Microsoft.AspNetCore.Http; +namespace Microsoft.Extensions.Logging; -namespace Microsoft.Extensions.Logging { - public static class ExceptionlessStateExtensions { - /// - /// Marks the event as being a critical occurrence. - /// - public static ExceptionlessState SetHttpContext(this ExceptionlessState state, HttpContext context) { - return state.Property("HttpContext", context); - } +public static class ExceptionlessStateExtensions { + /// + /// Marks the event as being a critical occurrence. + /// + public static ExceptionlessState SetHttpContext(this ExceptionlessState state, HttpContext context) { + return state.Property("HttpContext", context); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Extensions/HttpExtensions.cs b/src/Exceptionless.Web/Extensions/HttpExtensions.cs index cb8371d05c..3a0b35eead 100644 --- a/src/Exceptionless.Web/Extensions/HttpExtensions.cs +++ b/src/Exceptionless.Web/Extensions/HttpExtensions.cs @@ -1,149 +1,145 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; +using System.Net; using System.Security.Claims; using System.Text; using Exceptionless.Core.Authorization; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; -using Microsoft.AspNetCore.Http; -namespace Exceptionless.Web.Extensions { - public static class HttpExtensions { - public static User GetUser(this HttpRequest request) { - return request.HttpContext.Items.TryGetAndReturn("User") as User; - } +namespace Exceptionless.Web.Extensions; - public static void SetUser(this HttpRequest request, User user) { - if (request == null) - throw new ArgumentNullException(nameof(request)); +public static class HttpExtensions { + public static User GetUser(this HttpRequest request) { + return request.HttpContext.Items.TryGetAndReturn("User") as User; + } - if (user != null) - request.HttpContext.Items["User"] = user; - } + public static void SetUser(this HttpRequest request, User user) { + if (request == null) + throw new ArgumentNullException(nameof(request)); - public static Organization GetOrganization(this HttpRequest request) { - return request?.HttpContext.Items.TryGetAndReturn("Organization") as Organization; - } + if (user != null) + request.HttpContext.Items["User"] = user; + } - public static void SetOrganization(this HttpRequest request, Organization organization) { - if (organization != null) - request.HttpContext.Items["Organization"] = organization; - } + public static Organization GetOrganization(this HttpRequest request) { + return request?.HttpContext.Items.TryGetAndReturn("Organization") as Organization; + } - public static Project GetProject(this HttpRequest request) { - return request?.HttpContext.Items.TryGetAndReturn("Project") as Project; - } + public static void SetOrganization(this HttpRequest request, Organization organization) { + if (organization != null) + request.HttpContext.Items["Organization"] = organization; + } - public static void SetProject(this HttpRequest request, Project project) { - if (project != null) - request.HttpContext.Items["Project"] = project; - } + public static Project GetProject(this HttpRequest request) { + return request?.HttpContext.Items.TryGetAndReturn("Project") as Project; + } - public static ClaimsPrincipal GetClaimsPrincipal(this HttpRequest request) { - return request.HttpContext.User; - } + public static void SetProject(this HttpRequest request, Project project) { + if (project != null) + request.HttpContext.Items["Project"] = project; + } - public static AuthType GetAuthType(this HttpRequest request) { - var principal = request.GetClaimsPrincipal(); - return principal.GetAuthType(); - } + public static ClaimsPrincipal GetClaimsPrincipal(this HttpRequest request) { + return request.HttpContext.User; + } - public static bool CanAccessOrganization(this HttpRequest request, string organizationId) { - if (request.IsInOrganization(organizationId)) - return true; + public static AuthType GetAuthType(this HttpRequest request) { + var principal = request.GetClaimsPrincipal(); + return principal.GetAuthType(); + } - return request.IsGlobalAdmin(); - } + public static bool CanAccessOrganization(this HttpRequest request, string organizationId) { + if (request.IsInOrganization(organizationId)) + return true; - public static bool IsGlobalAdmin(this HttpRequest request) { - var principal = request.GetClaimsPrincipal(); - return principal != null && principal.IsInRole(AuthorizationRoles.GlobalAdmin); - } + return request.IsGlobalAdmin(); + } - public static bool IsInOrganization(this HttpRequest request, string organizationId) { - if (String.IsNullOrEmpty(organizationId)) - return false; + public static bool IsGlobalAdmin(this HttpRequest request) { + var principal = request.GetClaimsPrincipal(); + return principal != null && principal.IsInRole(AuthorizationRoles.GlobalAdmin); + } - return request.GetAssociatedOrganizationIds().Contains(organizationId); - } + public static bool IsInOrganization(this HttpRequest request, string organizationId) { + if (String.IsNullOrEmpty(organizationId)) + return false; - public static ICollection GetAssociatedOrganizationIds(this HttpRequest request) { - var principal = request.GetClaimsPrincipal(); - return principal?.GetOrganizationIds(); - } + return request.GetAssociatedOrganizationIds().Contains(organizationId); + } - public static string GetTokenOrganizationId(this HttpRequest request) { - var principal = request.GetClaimsPrincipal(); - return principal?.GetTokenOrganizationId(); - } + public static ICollection GetAssociatedOrganizationIds(this HttpRequest request) { + var principal = request.GetClaimsPrincipal(); + return principal?.GetOrganizationIds(); + } - public static string GetDefaultOrganizationId(this HttpRequest request) { - return request?.GetAssociatedOrganizationIds().FirstOrDefault(); - } + public static string GetTokenOrganizationId(this HttpRequest request) { + var principal = request.GetClaimsPrincipal(); + return principal?.GetTokenOrganizationId(); + } - public static string GetDefaultProjectId(this HttpRequest request) { - // TODO: Use project id from url. E.G., /api/v{apiVersion:int=2}/projects/{projectId:objectid}/events - //var path = request.Path.Value; + public static string GetDefaultOrganizationId(this HttpRequest request) { + return request?.GetAssociatedOrganizationIds().FirstOrDefault(); + } - var principal = request.GetClaimsPrincipal(); - return principal?.GetDefaultProjectId(); - } + public static string GetDefaultProjectId(this HttpRequest request) { + // TODO: Use project id from url. E.G., /api/v{apiVersion:int=2}/projects/{projectId:objectid}/events + //var path = request.Path.Value; - public static string GetClientIpAddress(this HttpRequest request) { - return request.HttpContext.Connection.RemoteIpAddress?.ToString(); - } + var principal = request.GetClaimsPrincipal(); + return principal?.GetDefaultProjectId(); + } - public static string GetQueryString(this HttpRequest request, string key) { - if (request.Query.TryGetValue(key, out var queryStrings)) - return queryStrings; + public static string GetClientIpAddress(this HttpRequest request) { + return request.HttpContext.Connection.RemoteIpAddress?.ToString(); + } - return null; - } + public static string GetQueryString(this HttpRequest request, string key) { + if (request.Query.TryGetValue(key, out var queryStrings)) + return queryStrings; - public static AuthInfo GetBasicAuth(this HttpRequest request) { - string authHeader = request.Headers.TryGetAndReturn("Authorization"); - if (authHeader == null || !authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase)) - return null; - - string token = authHeader.Substring(6).Trim(); - string credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token)); - string[] credentials = credentialstring.Split(':', StringSplitOptions.RemoveEmptyEntries); - if (credentials.Length != 2) - return null; - - return new AuthInfo { - Username = credentials[0], - Password = credentials[1] - }; - } - - public static bool IsLocal(this HttpRequest request) { - if (request.Host.Host.Contains("localtest.me", StringComparison.OrdinalIgnoreCase) || - request.Host.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) - return true; + return null; + } + + public static AuthInfo GetBasicAuth(this HttpRequest request) { + string authHeader = request.Headers.TryGetAndReturn("Authorization"); + if (authHeader == null || !authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase)) + return null; - var connection = request.HttpContext.Connection; + string token = authHeader.Substring(6).Trim(); + string credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token)); + string[] credentials = credentialstring.Split(':', StringSplitOptions.RemoveEmptyEntries); + if (credentials.Length != 2) + return null; - if (IsSet(connection.RemoteIpAddress)) { - return IsSet(connection.LocalIpAddress) - ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress) - : IPAddress.IsLoopback(connection.RemoteIpAddress); - } + return new AuthInfo { + Username = credentials[0], + Password = credentials[1] + }; + } + public static bool IsLocal(this HttpRequest request) { + if (request.Host.Host.Contains("localtest.me", StringComparison.OrdinalIgnoreCase) || + request.Host.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) return true; - } - private const string NullIpAddress = "::1"; - - private static bool IsSet(IPAddress address) { - return address != null && address.ToString() != NullIpAddress; + var connection = request.HttpContext.Connection; + + if (IsSet(connection.RemoteIpAddress)) { + return IsSet(connection.LocalIpAddress) + ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress) + : IPAddress.IsLoopback(connection.RemoteIpAddress); } + + return true; } - public class AuthInfo { - public string Username { get; set; } - public string Password { get; set; } + private const string NullIpAddress = "::1"; + + private static bool IsSet(IPAddress address) { + return address != null && address.ToString() != NullIpAddress; } } + +public class AuthInfo { + public string Username { get; set; } + public string Password { get; set; } +} diff --git a/src/Exceptionless.Web/Extensions/LoggerExtensions.cs b/src/Exceptionless.Web/Extensions/LoggerExtensions.cs index 17effd2392..ee5f52e73e 100644 --- a/src/Exceptionless.Web/Extensions/LoggerExtensions.cs +++ b/src/Exceptionless.Web/Extensions/LoggerExtensions.cs @@ -1,171 +1,168 @@ -using System; -using Microsoft.Extensions.Logging; - -#nullable enable - -namespace Exceptionless.Web.Extensions { - internal static class LoggerExtensions { - - private static readonly Action _projectRouteDoesNotMatch = - LoggerMessage.Define( - LogLevel.Information, - new EventId(0, nameof(ProjectRouteDoesNotMatch)), - "Project {RequestProjectId} from request doesn't match project route id {RouteProjectId}"); - - private static readonly Action _removingZapierUrls = - LoggerMessage.Define( - LogLevel.Information, - new EventId(1, nameof(RemovingZapierUrls)), - "Removing {Count} zapier urls matching: {Url}"); - - private static readonly Action _removedTokens = - LoggerMessage.Define( - LogLevel.Information, - new EventId(2, nameof(RemovedTokens)), - "Removed {RemovedCount} tokens for user: {UserId}"); - - private static readonly Action _submissionTooLarge = - LoggerMessage.Define( - LogLevel.Information, - new EventId(3, nameof(SubmissionTooLarge)), - "Event submission discarded for being too large: {@value} bytes."); - - private static readonly Action _userDeletingProject = - LoggerMessage.Define( - LogLevel.Information, - new EventId(4, nameof(UserDeletingProject)), - "User {User} deleting project: {ProjectName}."); - - private static readonly Action _userDeletingOrganization = - LoggerMessage.Define( - LogLevel.Information, - new EventId(5, nameof(UserDeletingOrganization)), - "User {User} deleting organization: {OrganizationName} ({OrganizationId})"); - - private static readonly Action _userLoggedIn = - LoggerMessage.Define( - LogLevel.Information, - new EventId(6, nameof(UserLoggedIn)), - "{EmailAddress} logged in."); - - private static readonly Action _userSignedUp = - LoggerMessage.Define( - LogLevel.Information, - new EventId(7, nameof(UserSignedUp)), - "{EmailAddress} signed up."); - - private static readonly Action _userChangedPassword = - LoggerMessage.Define( - LogLevel.Information, - new EventId(8, nameof(UserChangedPassword)), - "{EmailAddress} changed their password."); - - private static readonly Action _userForgotPassword = - LoggerMessage.Define( - LogLevel.Information, - new EventId(9, nameof(UserForgotPassword)), - "{EmailAddress} forgot their password."); - - private static readonly Action _userResetPassword = - LoggerMessage.Define( - LogLevel.Information, - new EventId(10, nameof(UserResetPassword)), - "{EmailAddress} reset their password."); - - private static readonly Action _userCanceledResetPassword = - LoggerMessage.Define( - LogLevel.Information, - new EventId(11, nameof(UserCanceledResetPassword)), - "{EmailAddress} canceled the reset password."); - - private static readonly Action _changedUserPassword = - LoggerMessage.Define( - LogLevel.Information, - new EventId(12, nameof(ChangedUserPassword)), - "Changed password for {EmailAddress}"); - - private static readonly Action _userJoinedFromInvite = - LoggerMessage.Define( - LogLevel.Information, - new EventId(13, nameof(UserJoinedFromInvite)), - "{EmailAddress} joined from invite."); - - private static readonly Action _markedInvitedUserAsVerified = - LoggerMessage.Define( - LogLevel.Information, - new EventId(14, nameof(MarkedInvitedUserAsVerified)), - "Marking the invited users email address {EmailAddress} as verified."); - - private static readonly Action _userRemovedExternalLogin = - LoggerMessage.Define( - LogLevel.Information, - new EventId(15, nameof(UserRemovedExternalLogin)), - "{EmailAddress} removed an external login: {ProviderName}"); - - private static readonly Action _unableToAddInvitedUserInvalidToken = - LoggerMessage.Define( - LogLevel.Information, - new EventId(16, nameof(UnableToAddInvitedUserInvalidToken)), - "Unable to add the invited user {EmailAddress}. Invalid invite token: {Token}"); - - private static readonly Action _removedUserTokens = - LoggerMessage.Define( - LogLevel.Information, - new EventId(17, nameof(RemovedUserTokens)), - "Removed user {TokenCount} tokens for {EmailAddress}"); - - public static void ProjectRouteDoesNotMatch(this ILogger logger, string? requestProjectId, string targetUrl) - => _projectRouteDoesNotMatch(logger, requestProjectId, targetUrl, null); - - public static void RemovingZapierUrls(this ILogger logger, int count, string targetUrl) - => _removingZapierUrls(logger, count, targetUrl, null); - - public static void RemovedTokens(this ILogger logger, long removedCount, string userId) - => _removedTokens(logger, removedCount, userId, null); - - public static void SubmissionTooLarge(this ILogger logger, long size) - => _submissionTooLarge(logger, size, null); - - public static void UserDeletingProject(this ILogger logger, string user, string projectName) - => _userDeletingProject(logger, user, projectName, null); - - public static void UserDeletingOrganization(this ILogger logger, string user, string organizationName, string organizationId) - => _userDeletingOrganization(logger, user, organizationName, organizationId, null); - - public static void UserLoggedIn(this ILogger logger, string email) - => _userLoggedIn(logger, email, null); - - public static void UserSignedUp(this ILogger logger, string email) - => _userSignedUp(logger, email, null); - - public static void UserChangedPassword(this ILogger logger, string email) - => _userChangedPassword(logger, email, null); - - public static void UserForgotPassword(this ILogger logger, string email) - => _userForgotPassword(logger, email, null); - - public static void UserResetPassword(this ILogger logger, string email) - => _userResetPassword(logger, email, null); - - public static void UserCanceledResetPassword(this ILogger logger, string email) - => _userCanceledResetPassword(logger, email, null); - - public static void ChangedUserPassword(this ILogger logger, string email) - => _changedUserPassword(logger, email, null); - - public static void UserJoinedFromInvite(this ILogger logger, string email) - => _userJoinedFromInvite(logger, email, null); - - public static void MarkedInvitedUserAsVerified(this ILogger logger, string email) - => _markedInvitedUserAsVerified(logger, email, null); - - public static void UserRemovedExternalLogin(this ILogger logger, string email, string providerName) - => _userRemovedExternalLogin(logger, email, providerName, null); - - public static void UnableToAddInvitedUserInvalidToken(this ILogger logger, string email, string token) - => _unableToAddInvitedUserInvalidToken(logger, email, token, null); - - public static void RemovedUserTokens(this ILogger logger, long total, string email) - => _removedUserTokens(logger, total, email, null); - } -} \ No newline at end of file +#nullable enable + +namespace Exceptionless.Web.Extensions; + +internal static class LoggerExtensions { + + private static readonly Action _projectRouteDoesNotMatch = + LoggerMessage.Define( + LogLevel.Information, + new EventId(0, nameof(ProjectRouteDoesNotMatch)), + "Project {RequestProjectId} from request doesn't match project route id {RouteProjectId}"); + + private static readonly Action _removingZapierUrls = + LoggerMessage.Define( + LogLevel.Information, + new EventId(1, nameof(RemovingZapierUrls)), + "Removing {Count} zapier urls matching: {Url}"); + + private static readonly Action _removedTokens = + LoggerMessage.Define( + LogLevel.Information, + new EventId(2, nameof(RemovedTokens)), + "Removed {RemovedCount} tokens for user: {UserId}"); + + private static readonly Action _submissionTooLarge = + LoggerMessage.Define( + LogLevel.Information, + new EventId(3, nameof(SubmissionTooLarge)), + "Event submission discarded for being too large: {@value} bytes."); + + private static readonly Action _userDeletingProject = + LoggerMessage.Define( + LogLevel.Information, + new EventId(4, nameof(UserDeletingProject)), + "User {User} deleting project: {ProjectName}."); + + private static readonly Action _userDeletingOrganization = + LoggerMessage.Define( + LogLevel.Information, + new EventId(5, nameof(UserDeletingOrganization)), + "User {User} deleting organization: {OrganizationName} ({OrganizationId})"); + + private static readonly Action _userLoggedIn = + LoggerMessage.Define( + LogLevel.Information, + new EventId(6, nameof(UserLoggedIn)), + "{EmailAddress} logged in."); + + private static readonly Action _userSignedUp = + LoggerMessage.Define( + LogLevel.Information, + new EventId(7, nameof(UserSignedUp)), + "{EmailAddress} signed up."); + + private static readonly Action _userChangedPassword = + LoggerMessage.Define( + LogLevel.Information, + new EventId(8, nameof(UserChangedPassword)), + "{EmailAddress} changed their password."); + + private static readonly Action _userForgotPassword = + LoggerMessage.Define( + LogLevel.Information, + new EventId(9, nameof(UserForgotPassword)), + "{EmailAddress} forgot their password."); + + private static readonly Action _userResetPassword = + LoggerMessage.Define( + LogLevel.Information, + new EventId(10, nameof(UserResetPassword)), + "{EmailAddress} reset their password."); + + private static readonly Action _userCanceledResetPassword = + LoggerMessage.Define( + LogLevel.Information, + new EventId(11, nameof(UserCanceledResetPassword)), + "{EmailAddress} canceled the reset password."); + + private static readonly Action _changedUserPassword = + LoggerMessage.Define( + LogLevel.Information, + new EventId(12, nameof(ChangedUserPassword)), + "Changed password for {EmailAddress}"); + + private static readonly Action _userJoinedFromInvite = + LoggerMessage.Define( + LogLevel.Information, + new EventId(13, nameof(UserJoinedFromInvite)), + "{EmailAddress} joined from invite."); + + private static readonly Action _markedInvitedUserAsVerified = + LoggerMessage.Define( + LogLevel.Information, + new EventId(14, nameof(MarkedInvitedUserAsVerified)), + "Marking the invited users email address {EmailAddress} as verified."); + + private static readonly Action _userRemovedExternalLogin = + LoggerMessage.Define( + LogLevel.Information, + new EventId(15, nameof(UserRemovedExternalLogin)), + "{EmailAddress} removed an external login: {ProviderName}"); + + private static readonly Action _unableToAddInvitedUserInvalidToken = + LoggerMessage.Define( + LogLevel.Information, + new EventId(16, nameof(UnableToAddInvitedUserInvalidToken)), + "Unable to add the invited user {EmailAddress}. Invalid invite token: {Token}"); + + private static readonly Action _removedUserTokens = + LoggerMessage.Define( + LogLevel.Information, + new EventId(17, nameof(RemovedUserTokens)), + "Removed user {TokenCount} tokens for {EmailAddress}"); + + public static void ProjectRouteDoesNotMatch(this ILogger logger, string? requestProjectId, string targetUrl) + => _projectRouteDoesNotMatch(logger, requestProjectId, targetUrl, null); + + public static void RemovingZapierUrls(this ILogger logger, int count, string targetUrl) + => _removingZapierUrls(logger, count, targetUrl, null); + + public static void RemovedTokens(this ILogger logger, long removedCount, string userId) + => _removedTokens(logger, removedCount, userId, null); + + public static void SubmissionTooLarge(this ILogger logger, long size) + => _submissionTooLarge(logger, size, null); + + public static void UserDeletingProject(this ILogger logger, string user, string projectName) + => _userDeletingProject(logger, user, projectName, null); + + public static void UserDeletingOrganization(this ILogger logger, string user, string organizationName, string organizationId) + => _userDeletingOrganization(logger, user, organizationName, organizationId, null); + + public static void UserLoggedIn(this ILogger logger, string email) + => _userLoggedIn(logger, email, null); + + public static void UserSignedUp(this ILogger logger, string email) + => _userSignedUp(logger, email, null); + + public static void UserChangedPassword(this ILogger logger, string email) + => _userChangedPassword(logger, email, null); + + public static void UserForgotPassword(this ILogger logger, string email) + => _userForgotPassword(logger, email, null); + + public static void UserResetPassword(this ILogger logger, string email) + => _userResetPassword(logger, email, null); + + public static void UserCanceledResetPassword(this ILogger logger, string email) + => _userCanceledResetPassword(logger, email, null); + + public static void ChangedUserPassword(this ILogger logger, string email) + => _changedUserPassword(logger, email, null); + + public static void UserJoinedFromInvite(this ILogger logger, string email) + => _userJoinedFromInvite(logger, email, null); + + public static void MarkedInvitedUserAsVerified(this ILogger logger, string email) + => _markedInvitedUserAsVerified(logger, email, null); + + public static void UserRemovedExternalLogin(this ILogger logger, string email, string providerName) + => _userRemovedExternalLogin(logger, email, providerName, null); + + public static void UnableToAddInvitedUserInvalidToken(this ILogger logger, string email, string token) + => _unableToAddInvitedUserInvalidToken(logger, email, token, null); + + public static void RemovedUserTokens(this ILogger logger, long total, string email) + => _removedUserTokens(logger, total, email, null); +} diff --git a/src/Exceptionless.Web/Extensions/OAuth2Extensions.cs b/src/Exceptionless.Web/Extensions/OAuth2Extensions.cs index 98fcae2bc1..19273ddd1c 100644 --- a/src/Exceptionless.Web/Extensions/OAuth2Extensions.cs +++ b/src/Exceptionless.Web/Extensions/OAuth2Extensions.cs @@ -1,18 +1,16 @@ -using System; using System.Collections.Specialized; -using System.Threading.Tasks; using OAuth2.Client; using OAuth2.Models; -namespace Exceptionless.Web.Extensions { - public static class OAuth2Extensions { - public static Task GetUserInfoAsync(this OAuth2Client client, string code, string redirectUri) { - return client.GetUserInfoAsync(new NameValueCollection { { "code", code }, { "redirect_uri", redirectUri } }); - } +namespace Exceptionless.Web.Extensions; - public static string GetFullName(this UserInfo user) { - string name = (user.FirstName + " " + user.LastName).Trim(); - return !String.IsNullOrEmpty(name) ? name : user.Email; - } +public static class OAuth2Extensions { + public static Task GetUserInfoAsync(this OAuth2Client client, string code, string redirectUri) { + return client.GetUserInfoAsync(new NameValueCollection { { "code", code }, { "redirect_uri", redirectUri } }); } -} \ No newline at end of file + + public static string GetFullName(this UserInfo user) { + string name = (user.FirstName + " " + user.LastName).Trim(); + return !String.IsNullOrEmpty(name) ? name : user.Email; + } +} diff --git a/src/Exceptionless.Web/Extensions/SerilogExtensions.cs b/src/Exceptionless.Web/Extensions/SerilogExtensions.cs index a076844bb3..f373e1af18 100644 --- a/src/Exceptionless.Web/Extensions/SerilogExtensions.cs +++ b/src/Exceptionless.Web/Extensions/SerilogExtensions.cs @@ -1,9 +1,7 @@ -using Microsoft.Extensions.Logging; +namespace Exceptionless.Web.Extensions; -namespace Exceptionless.Web.Extensions { - public static class SerilogExtensions { - public static ILoggerFactory ToLoggerFactory(this Serilog.ILogger logger) { - return new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); - } +public static class SerilogExtensions { + public static ILoggerFactory ToLoggerFactory(this Serilog.ILogger logger) { + return new Serilog.Extensions.Logging.SerilogLoggerFactory(logger); } } diff --git a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs index 0141aa4327..f59cce669d 100644 --- a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs +++ b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs @@ -1,176 +1,170 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core; +using Exceptionless.Core; using Exceptionless.Core.Messaging.Models; using Exceptionless.Core.Models; using Exceptionless.Core.Utility; using Foundatio.Extensions.Hosting.Startup; using Foundatio.Messaging; using Foundatio.Repositories.Models; -using Microsoft.Extensions.Logging; - -namespace Exceptionless.Web.Hubs { - public sealed class MessageBusBroker : IStartupAction { - private static readonly string TokenTypeName = nameof(Token); - private static readonly string UserTypeName = nameof(User); - private readonly WebSocketConnectionManager _connectionManager; - private readonly IConnectionMapping _connectionMapping; - private readonly IMessageSubscriber _subscriber; - private readonly AppOptions _options; - private readonly ILogger _logger; - - public MessageBusBroker(WebSocketConnectionManager connectionManager, IConnectionMapping connectionMapping, IMessageSubscriber subscriber, AppOptions options, ILogger logger) { - _connectionManager = connectionManager; - _connectionMapping = connectionMapping; - _subscriber = subscriber; - _options = options; - _logger = logger; - } - public async Task RunAsync(CancellationToken shutdownToken = default) { - if (!_options.EnableWebSockets) - return; +namespace Exceptionless.Web.Hubs; + +public sealed class MessageBusBroker : IStartupAction { + private static readonly string TokenTypeName = nameof(Token); + private static readonly string UserTypeName = nameof(User); + private readonly WebSocketConnectionManager _connectionManager; + private readonly IConnectionMapping _connectionMapping; + private readonly IMessageSubscriber _subscriber; + private readonly AppOptions _options; + private readonly ILogger _logger; + + public MessageBusBroker(WebSocketConnectionManager connectionManager, IConnectionMapping connectionMapping, IMessageSubscriber subscriber, AppOptions options, ILogger logger) { + _connectionManager = connectionManager; + _connectionMapping = connectionMapping; + _subscriber = subscriber; + _options = options; + _logger = logger; + } - _logger.LogDebug("Subscribing to message bus notifications"); - await Task.WhenAll( - _subscriber.SubscribeAsync(OnEntityChangedAsync, shutdownToken), - _subscriber.SubscribeAsync(OnPlanChangedAsync, shutdownToken), - _subscriber.SubscribeAsync(OnPlanOverageAsync, shutdownToken), - _subscriber.SubscribeAsync(OnUserMembershipChangedAsync, shutdownToken), - _subscriber.SubscribeAsync(OnReleaseNotificationAsync, shutdownToken), - _subscriber.SubscribeAsync(OnSystemNotificationAsync, shutdownToken) - ); - _logger.LogDebug("Subscribed to message bus notifications"); + public async Task RunAsync(CancellationToken shutdownToken = default) { + if (!_options.EnableWebSockets) + return; + + _logger.LogDebug("Subscribing to message bus notifications"); + await Task.WhenAll( + _subscriber.SubscribeAsync(OnEntityChangedAsync, shutdownToken), + _subscriber.SubscribeAsync(OnPlanChangedAsync, shutdownToken), + _subscriber.SubscribeAsync(OnPlanOverageAsync, shutdownToken), + _subscriber.SubscribeAsync(OnUserMembershipChangedAsync, shutdownToken), + _subscriber.SubscribeAsync(OnReleaseNotificationAsync, shutdownToken), + _subscriber.SubscribeAsync(OnSystemNotificationAsync, shutdownToken) + ); + _logger.LogDebug("Subscribed to message bus notifications"); + } + + private async Task OnUserMembershipChangedAsync(UserMembershipChanged userMembershipChanged, CancellationToken cancellationToken = default) { + if (String.IsNullOrEmpty(userMembershipChanged?.OrganizationId)) { + _logger.LogTrace("Ignoring User Membership Changed message: No organization id."); + return; } - private async Task OnUserMembershipChangedAsync(UserMembershipChanged userMembershipChanged, CancellationToken cancellationToken = default) { - if (String.IsNullOrEmpty(userMembershipChanged?.OrganizationId)) { - _logger.LogTrace("Ignoring User Membership Changed message: No organization id."); - return; - } + // manage user organization group membership + var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(userMembershipChanged.UserId); + _logger.LogTrace("Attempting to update user {User} active groups for {UserConnectionCount} connections", userMembershipChanged.UserId, userConnectionIds.Count); + foreach (string connectionId in userConnectionIds) { + if (userMembershipChanged.ChangeType == ChangeType.Added) + await _connectionMapping.GroupAddAsync(userMembershipChanged.OrganizationId, connectionId); + else if (userMembershipChanged.ChangeType == ChangeType.Removed) + await _connectionMapping.GroupRemoveAsync(userMembershipChanged.OrganizationId, connectionId); + } - // manage user organization group membership - var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(userMembershipChanged.UserId); - _logger.LogTrace("Attempting to update user {User} active groups for {UserConnectionCount} connections", userMembershipChanged.UserId, userConnectionIds.Count); - foreach (string connectionId in userConnectionIds) { - if (userMembershipChanged.ChangeType == ChangeType.Added) - await _connectionMapping.GroupAddAsync(userMembershipChanged.OrganizationId, connectionId) ; - else if (userMembershipChanged.ChangeType == ChangeType.Removed) - await _connectionMapping.GroupRemoveAsync(userMembershipChanged.OrganizationId, connectionId); - } + await GroupSendAsync(userMembershipChanged.OrganizationId, userMembershipChanged); + } - await GroupSendAsync(userMembershipChanged.OrganizationId, userMembershipChanged); - } + private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken cancellationToken = default) { + if (ec == null) + return; - private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken cancellationToken = default) { - if (ec == null) + var entityChanged = ExtendedEntityChanged.Create(ec); + if (UserTypeName == entityChanged.Type) { + // It's pointless to send a user added message to the new user. + if (entityChanged.ChangeType == ChangeType.Added) { + _logger.LogTrace("Ignoring {UserTypeName} message for added user: {user}.", UserTypeName, entityChanged.Id); return; + } + + var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(entityChanged.Id); + _logger.LogTrace("Sending {UserTypeName} message to user: {user} (to {UserConnectionCount} connections)", UserTypeName, entityChanged.Id, userConnectionIds.Count); + foreach (string connectionId in userConnectionIds) + await TypedSendAsync(connectionId, entityChanged); - var entityChanged = ExtendedEntityChanged.Create(ec); - if (UserTypeName == entityChanged.Type) { - // It's pointless to send a user added message to the new user. - if (entityChanged.ChangeType == ChangeType.Added) { - _logger.LogTrace("Ignoring {UserTypeName} message for added user: {user}.", UserTypeName, entityChanged.Id); - return; - } + return; + } - var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(entityChanged.Id); - _logger.LogTrace("Sending {UserTypeName} message to user: {user} (to {UserConnectionCount} connections)", UserTypeName, entityChanged.Id, userConnectionIds.Count); + // Only allow specific token messages to be sent down to the client. + if (TokenTypeName == entityChanged.Type) { + string userId = entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.UserId); + if (userId != null) { + var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(userId); + _logger.LogTrace("Sending {TokenTypeName} message for added user: {user} (to {UserConnectionCount} connections)", TokenTypeName, userId, userConnectionIds.Count); foreach (string connectionId in userConnectionIds) await TypedSendAsync(connectionId, entityChanged); return; } - // Only allow specific token messages to be sent down to the client. - if (TokenTypeName == entityChanged.Type) { - string userId = entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.UserId); - if (userId != null) { - var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(userId); - _logger.LogTrace("Sending {TokenTypeName} message for added user: {user} (to {UserConnectionCount} connections)", TokenTypeName, userId, userConnectionIds.Count); - foreach (string connectionId in userConnectionIds) - await TypedSendAsync(connectionId, entityChanged); - - return; - } - - if (entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.IsAuthenticationToken)) { - _logger.LogTrace("Ignoring {TokenTypeName} Authentication Token message: {user}.", TokenTypeName, entityChanged.Id); - return; - } - - entityChanged.Data.Clear(); + if (entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.IsAuthenticationToken)) { + _logger.LogTrace("Ignoring {TokenTypeName} Authentication Token message: {user}.", TokenTypeName, entityChanged.Id); + return; } - if (!String.IsNullOrEmpty(entityChanged.OrganizationId)) { - _logger.LogTrace("Sending {MessageType} message to organization: {organization}", entityChanged.Type, entityChanged.OrganizationId); - await GroupSendAsync(entityChanged.OrganizationId, entityChanged); - } + entityChanged.Data.Clear(); } - private Task OnPlanOverageAsync(PlanOverage planOverage, CancellationToken cancellationToken = default) { - if (planOverage != null) { - _logger.LogTrace("Sending plan overage message to organization: {organization}", planOverage.OrganizationId); - return GroupSendAsync(planOverage.OrganizationId, planOverage); - } - - return Task.CompletedTask; + if (!String.IsNullOrEmpty(entityChanged.OrganizationId)) { + _logger.LogTrace("Sending {MessageType} message to organization: {organization}", entityChanged.Type, entityChanged.OrganizationId); + await GroupSendAsync(entityChanged.OrganizationId, entityChanged); } + } - private Task OnPlanChangedAsync(PlanChanged planChanged, CancellationToken cancellationToken = default) { - if (planChanged != null) { - _logger.LogTrace("Sending plan changed message to organization: {organization}", planChanged.OrganizationId); - return GroupSendAsync(planChanged.OrganizationId, planChanged); - } - - return Task.CompletedTask; + private Task OnPlanOverageAsync(PlanOverage planOverage, CancellationToken cancellationToken = default) { + if (planOverage != null) { + _logger.LogTrace("Sending plan overage message to organization: {organization}", planOverage.OrganizationId); + return GroupSendAsync(planOverage.OrganizationId, planOverage); } - private Task OnReleaseNotificationAsync(ReleaseNotification notification, CancellationToken cancellationToken = default) { - _logger.LogTrace("Sending release notification message: {Message}", notification.Message); - return TypedBroadcastAsync(notification); - } + return Task.CompletedTask; + } - private Task OnSystemNotificationAsync(SystemNotification notification, CancellationToken cancellationToken = default) { - _logger.LogTrace("Sending system notification message: {Message}", notification.Message); - return TypedBroadcastAsync(notification); + private Task OnPlanChangedAsync(PlanChanged planChanged, CancellationToken cancellationToken = default) { + if (planChanged != null) { + _logger.LogTrace("Sending plan changed message to organization: {organization}", planChanged.OrganizationId); + return GroupSendAsync(planChanged.OrganizationId, planChanged); } - private async Task GroupSendAsync(string group, object value) { - var connectionIds = await _connectionMapping.GetGroupConnectionsAsync(group); - if (connectionIds.Count == 0) { - _logger.LogTrace("Ignoring group message to {Group}: No Connections", group); - return; - } + return Task.CompletedTask; + } - await TypedSendAsync(connectionIds.ToList(), value); - } + private Task OnReleaseNotificationAsync(ReleaseNotification notification, CancellationToken cancellationToken = default) { + _logger.LogTrace("Sending release notification message: {Message}", notification.Message); + return TypedBroadcastAsync(notification); + } - public Task TypedSendAsync(string connectionId, object value) { - return _connectionManager.SendMessageAsync(connectionId, new TypedMessage { Type = GetMessageType(value), Message = value }); - } + private Task OnSystemNotificationAsync(SystemNotification notification, CancellationToken cancellationToken = default) { + _logger.LogTrace("Sending system notification message: {Message}", notification.Message); + return TypedBroadcastAsync(notification); + } - public Task TypedSendAsync(IList connectionIds, object value) { - return _connectionManager.SendMessageAsync(connectionIds, new TypedMessage { Type = GetMessageType(value), Message = value }); + private async Task GroupSendAsync(string group, object value) { + var connectionIds = await _connectionMapping.GetGroupConnectionsAsync(group); + if (connectionIds.Count == 0) { + _logger.LogTrace("Ignoring group message to {Group}: No Connections", group); + return; } - public Task TypedBroadcastAsync(object value) { - return _connectionManager.SendMessageToAllAsync(new TypedMessage { Type = GetMessageType(value), Message = value }); - } + await TypedSendAsync(connectionIds.ToList(), value); + } + + public Task TypedSendAsync(string connectionId, object value) { + return _connectionManager.SendMessageAsync(connectionId, new TypedMessage { Type = GetMessageType(value), Message = value }); + } - private static string GetMessageType(object value) { - if (value is EntityChanged) - return String.Concat(((EntityChanged)value).Type, "Changed"); + public Task TypedSendAsync(IList connectionIds, object value) { + return _connectionManager.SendMessageAsync(connectionIds, new TypedMessage { Type = GetMessageType(value), Message = value }); + } - return value.GetType().Name; - } + public Task TypedBroadcastAsync(object value) { + return _connectionManager.SendMessageToAllAsync(new TypedMessage { Type = GetMessageType(value), Message = value }); } - public class TypedMessage { - public string Type { get; set; } - public object Message { get; set; } + private static string GetMessageType(object value) { + if (value is EntityChanged) + return String.Concat(((EntityChanged)value).Type, "Changed"); + + return value.GetType().Name; } -} \ No newline at end of file +} + +public class TypedMessage { + public string Type { get; set; } + public object Message { get; set; } +} diff --git a/src/Exceptionless.Web/Hubs/MessageBusBrokerMiddleware.cs b/src/Exceptionless.Web/Hubs/MessageBusBrokerMiddleware.cs index 242d989530..aa2f34def4 100644 --- a/src/Exceptionless.Web/Hubs/MessageBusBrokerMiddleware.cs +++ b/src/Exceptionless.Web/Hubs/MessageBusBrokerMiddleware.cs @@ -1,131 +1,129 @@ -using System; -using System.IO; -using System.Net.WebSockets; +using System.Net.WebSockets; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Utility; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Exceptionless.Web.Hubs { - public class MessageBusBrokerMiddleware { - private readonly ILogger _logger; - private readonly WebSocketConnectionManager _connectionManager; - private readonly IConnectionMapping _connectionMapping; - private readonly RequestDelegate _next; - - public MessageBusBrokerMiddleware(RequestDelegate next, WebSocketConnectionManager connectionManager, IConnectionMapping connectionMapping, ILogger logger) { - _next = next; - _connectionManager = connectionManager; - _connectionMapping = connectionMapping; - _logger = logger; - } - public async Task Invoke(HttpContext context) { - if (!context.WebSockets.IsWebSocketRequest || !context.User.Identity.IsAuthenticated) { - await _next(context); - return; - } +namespace Exceptionless.Web.Hubs; - using(var socket = await context.WebSockets.AcceptWebSocketAsync()) { - string connectionId = _connectionManager.AddWebSocket(socket); - await OnConnected(context, socket, connectionId); - bool disconnected = false; - - try { - await ReceiveAsync(socket, async (result, data) => { - switch (result.MessageType) { - case WebSocketMessageType.Text: - _logger.LogTrace("WebSocket got message {ConnectionId}", connectionId); - // ignore incoming messages - return; - case WebSocketMessageType.Close: - await OnDisconnected(context, socket, connectionId); - await _connectionManager.RemoveWebSocketAsync(connectionId); - disconnected = true; - return; - } - }); - } catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { } - - // This will be hit when the connection is lost. - if (!disconnected) { - await OnDisconnected(context, socket, connectionId); - await _connectionManager.RemoveWebSocketAsync(connectionId); - } - } +public class MessageBusBrokerMiddleware { + private readonly ILogger _logger; + private readonly WebSocketConnectionManager _connectionManager; + private readonly IConnectionMapping _connectionMapping; + private readonly RequestDelegate _next; + + public MessageBusBrokerMiddleware(RequestDelegate next, WebSocketConnectionManager connectionManager, IConnectionMapping connectionMapping, ILogger logger) { + _next = next; + _connectionManager = connectionManager; + _connectionMapping = connectionMapping; + _logger = logger; + } + + public async Task Invoke(HttpContext context) { + if (!context.WebSockets.IsWebSocketRequest || !context.User.Identity.IsAuthenticated) { + await _next(context); + return; } - private async Task OnConnected(HttpContext context, WebSocket socket, string connectionId) { - _logger.LogTrace("WebSocket connected {ConnectionId} ({State})", connectionId, socket?.State); + using (var socket = await context.WebSockets.AcceptWebSocketAsync()) { + string connectionId = _connectionManager.AddWebSocket(socket); + await OnConnected(context, socket, connectionId); + bool disconnected = false; try { - foreach (string organizationId in context.User.GetOrganizationIds()) - await _connectionMapping.GroupAddAsync(organizationId, connectionId); + await ReceiveAsync(socket, async (result, data) => { + switch (result.MessageType) { + case WebSocketMessageType.Text: + _logger.LogTrace("WebSocket got message {ConnectionId}", connectionId); + // ignore incoming messages + return; + case WebSocketMessageType.Close: + await OnDisconnected(context, socket, connectionId); + await _connectionManager.RemoveWebSocketAsync(connectionId); + disconnected = true; + return; + } + }); + } + catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { } - await _connectionMapping.UserIdAddAsync(context.User.GetUserId(), connectionId); - } catch (Exception ex) { - _logger.LogError(ex, "OnConnected Error: {Message}", ex.Message); - throw; + // This will be hit when the connection is lost. + if (!disconnected) { + await OnDisconnected(context, socket, connectionId); + await _connectionManager.RemoveWebSocketAsync(connectionId); } } + } - private async Task OnDisconnected(HttpContext context, WebSocket socket, string connectionId) { - _logger.LogTrace("WebSocket disconnected {ConnectionId} ({State})", connectionId, socket?.State); + private async Task OnConnected(HttpContext context, WebSocket socket, string connectionId) { + _logger.LogTrace("WebSocket connected {ConnectionId} ({State})", connectionId, socket?.State); - try { - foreach (string organizationId in context.User.GetOrganizationIds()) - await _connectionMapping.GroupRemoveAsync(organizationId, connectionId); + try { + foreach (string organizationId in context.User.GetOrganizationIds()) + await _connectionMapping.GroupAddAsync(organizationId, connectionId); - await _connectionMapping.UserIdRemoveAsync(context.User.GetUserId(), connectionId); - } catch (Exception ex) { - _logger.LogError(ex, "OnDisconnected Error: {Message}", ex.Message); - throw; - } + await _connectionMapping.UserIdAddAsync(context.User.GetUserId(), connectionId); + } + catch (Exception ex) { + _logger.LogError(ex, "OnConnected Error: {Message}", ex.Message); + throw; } + } - private async Task ReceiveAsync(WebSocket socket, Func handleMessage) { - var buffer = new ArraySegment(new byte[1024 * 4]); - var result = await socket.ReceiveAsync(buffer, CancellationToken.None); - LogFrame(result, buffer.Array); + private async Task OnDisconnected(HttpContext context, WebSocket socket, string connectionId) { + _logger.LogTrace("WebSocket disconnected {ConnectionId} ({State})", connectionId, socket?.State); - while (!result.CloseStatus.HasValue) { - string data; + try { + foreach (string organizationId in context.User.GetOrganizationIds()) + await _connectionMapping.GroupRemoveAsync(organizationId, connectionId); - using (var ms = new MemoryStream()) { - do { - result = await socket.ReceiveAsync(buffer, CancellationToken.None); - LogFrame(result, buffer.Array); + await _connectionMapping.UserIdRemoveAsync(context.User.GetUserId(), connectionId); + } + catch (Exception ex) { + _logger.LogError(ex, "OnDisconnected Error: {Message}", ex.Message); + throw; + } + } - await ms.WriteAsync(buffer.Array, buffer.Offset, result.Count); - } while (!result.EndOfMessage); + private async Task ReceiveAsync(WebSocket socket, Func handleMessage) { + var buffer = new ArraySegment(new byte[1024 * 4]); + var result = await socket.ReceiveAsync(buffer, CancellationToken.None); + LogFrame(result, buffer.Array); - ms.Seek(0, SeekOrigin.Begin); + while (!result.CloseStatus.HasValue) { + string data; - using (var reader = new StreamReader(ms, Encoding.UTF8)) - data = await reader.ReadToEndAsync(); - } + using (var ms = new MemoryStream()) { + do { + result = await socket.ReceiveAsync(buffer, CancellationToken.None); + LogFrame(result, buffer.Array); - await handleMessage(result, data); + await ms.WriteAsync(buffer.Array, buffer.Offset, result.Count); + } while (!result.EndOfMessage); + + ms.Seek(0, SeekOrigin.Begin); + + using (var reader = new StreamReader(ms, Encoding.UTF8)) + data = await reader.ReadToEndAsync(); } - } - private void LogFrame(WebSocketReceiveResult frame, byte[] buffer) { - if (!_logger.IsEnabled(LogLevel.Debug)) - return; + await handleMessage(result, data); + } + } - if (frame.CloseStatus.HasValue) { - _logger.LogDebug("Close: {CloseStatus} {CloseStatusDescription}", frame.CloseStatus.Value, frame.CloseStatusDescription); - } else { - string content = "<>"; - if (frame.MessageType == WebSocketMessageType.Text) - content = Encoding.UTF8.GetString(buffer, 0, frame.Count); + private void LogFrame(WebSocketReceiveResult frame, byte[] buffer) { + if (!_logger.IsEnabled(LogLevel.Debug)) + return; - _logger.LogDebug("Received Frame {MessageType}: length={FrameCount}, end={FrameEndOfMessage}: {Content}", frame.MessageType, frame.Count, frame.EndOfMessage, content); - } + if (frame.CloseStatus.HasValue) { + _logger.LogDebug("Close: {CloseStatus} {CloseStatusDescription}", frame.CloseStatus.Value, frame.CloseStatusDescription); + } + else { + string content = "<>"; + if (frame.MessageType == WebSocketMessageType.Text) + content = Encoding.UTF8.GetString(buffer, 0, frame.Count); + _logger.LogDebug("Received Frame {MessageType}: length={FrameCount}, end={FrameEndOfMessage}: {Content}", frame.MessageType, frame.Count, frame.EndOfMessage, content); } + } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs b/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs index 91bbf3a118..b4ba41c98c 100644 --- a/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs +++ b/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs @@ -1,153 +1,154 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Net.WebSockets; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Exceptionless.Web.Hubs { - public class WebSocketConnectionManager : IDisposable { - private static readonly ArraySegment _keepAliveMessage = new ArraySegment(Encoding.ASCII.GetBytes("{}"), 0, 2); - private readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); - private readonly Timer _timer; - private readonly JsonSerializerSettings _serializerSettings; - private readonly ILogger _logger; - - public WebSocketConnectionManager(AppOptions options, JsonSerializerSettings serializerSettings, ILoggerFactory loggerFactory) { - _serializerSettings = serializerSettings; - _logger = loggerFactory.CreateLogger(); - if (!options.EnableWebSockets) - return; +namespace Exceptionless.Web.Hubs; - _timer = new Timer(KeepAlive, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); - } +public class WebSocketConnectionManager : IDisposable { + private static readonly ArraySegment _keepAliveMessage = new ArraySegment(Encoding.ASCII.GetBytes("{}"), 0, 2); + private readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); + private readonly Timer _timer; + private readonly JsonSerializerSettings _serializerSettings; + private readonly ILogger _logger; + + public WebSocketConnectionManager(AppOptions options, JsonSerializerSettings serializerSettings, ILoggerFactory loggerFactory) { + _serializerSettings = serializerSettings; + _logger = loggerFactory.CreateLogger(); + if (!options.EnableWebSockets) + return; + + _timer = new Timer(KeepAlive, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); + } + + private void KeepAlive(object state) { + if (_connections.IsEmpty && _connections.Count == 0) + return; - private void KeepAlive(object state) { - if (_connections.IsEmpty && _connections.Count == 0) - return; - - Task.Factory.StartNew(async () => { - var sockets = GetAll(); - var openSockets = sockets.Where(s => s.State == WebSocketState.Open).ToArray(); - _logger.LogTrace("Sending web socket keep alive to {OpenSocketsCount} open connections of {SocketCount} total connections", openSockets.Length, sockets.Count); - - foreach (var socket in openSockets) { - try { - await socket.SendAsync(buffer: _keepAliveMessage, - messageType: WebSocketMessageType.Text, - endOfMessage: true, - cancellationToken: CancellationToken.None); - } catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { - // NOTE: This will not remove it from the ConnectionMappings. - await RemoveWebSocketAsync(socket); - } catch (Exception ex) { - _logger.LogError(ex, "Error sending keep alive socket message: {Message}", ex.Message); - } + Task.Factory.StartNew(async () => { + var sockets = GetAll(); + var openSockets = sockets.Where(s => s.State == WebSocketState.Open).ToArray(); + _logger.LogTrace("Sending web socket keep alive to {OpenSocketsCount} open connections of {SocketCount} total connections", openSockets.Length, sockets.Count); + + foreach (var socket in openSockets) { + try { + await socket.SendAsync(buffer: _keepAliveMessage, + messageType: WebSocketMessageType.Text, + endOfMessage: true, + cancellationToken: CancellationToken.None); } - }); - } + catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { + // NOTE: This will not remove it from the ConnectionMappings. + await RemoveWebSocketAsync(socket); + } + catch (Exception ex) { + _logger.LogError(ex, "Error sending keep alive socket message: {Message}", ex.Message); + } + } + }); + } - public WebSocket GetWebSocketById(string connectionId) { - return _connections.TryGetValue(connectionId, out var socket) ? socket : null; - } + public WebSocket GetWebSocketById(string connectionId) { + return _connections.TryGetValue(connectionId, out var socket) ? socket : null; + } - public ICollection GetAll() { - return _connections.Values; - } + public ICollection GetAll() { + return _connections.Values; + } - public string GetConnectionId(WebSocket socket) { - return _connections.FirstOrDefault(p => p.Value == socket).Key; - } + public string GetConnectionId(WebSocket socket) { + return _connections.FirstOrDefault(p => p.Value == socket).Key; + } - public string AddWebSocket(WebSocket socket) { - string connectionId = Guid.NewGuid().ToString("N"); - _connections.TryAdd(connectionId, socket); - return connectionId; - } + public string AddWebSocket(WebSocket socket) { + string connectionId = Guid.NewGuid().ToString("N"); + _connections.TryAdd(connectionId, socket); + return connectionId; + } - private Task RemoveWebSocketAsync(WebSocket socket) { - string id = GetConnectionId(socket); - if (String.IsNullOrEmpty(id) || !_connections.TryRemove(id, out var _)) - return Task.CompletedTask; + private Task RemoveWebSocketAsync(WebSocket socket) { + string id = GetConnectionId(socket); + if (String.IsNullOrEmpty(id) || !_connections.TryRemove(id, out var _)) + return Task.CompletedTask; - return CloseWebSocketAsync(socket); - } + return CloseWebSocketAsync(socket); + } + + public Task RemoveWebSocketAsync(string id) { + if (!_connections.TryRemove(id, out var socket)) + return Task.CompletedTask; + + return CloseWebSocketAsync(socket); + } - public Task RemoveWebSocketAsync(string id) { - if (!_connections.TryRemove(id, out var socket)) - return Task.CompletedTask; + private async Task CloseWebSocketAsync(WebSocket socket) { + if (!CanSendWebSocketMessage(socket)) + return; - return CloseWebSocketAsync(socket); + try { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by manager", CancellationToken.None); } + catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { + } + catch (Exception ex) { + _logger.LogError(ex, "Error closing web socket: {Message}", ex.Message); + } + } + + private Task SendMessageAsync(WebSocket socket, object message) { + if (!CanSendWebSocketMessage(socket)) + return Task.CompletedTask; - private async Task CloseWebSocketAsync(WebSocket socket) { + string serializedMessage = JsonConvert.SerializeObject(message, _serializerSettings); + Task.Factory.StartNew(async () => { if (!CanSendWebSocketMessage(socket)) return; try { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by manager", CancellationToken.None); - } catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { - } catch (Exception ex) { - _logger.LogError(ex, "Error closing web socket: {Message}", ex.Message); + await socket.SendAsync(buffer: new ArraySegment(Encoding.ASCII.GetBytes(serializedMessage), 0, serializedMessage.Length), + messageType: WebSocketMessageType.Text, + endOfMessage: true, + cancellationToken: CancellationToken.None); } - } - - private Task SendMessageAsync(WebSocket socket, object message) { - if (!CanSendWebSocketMessage(socket)) - return Task.CompletedTask; + catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { + } + catch (Exception ex) { + _logger.LogError(ex, "Error sending socket message: {Message}", ex.Message); + } + }); - string serializedMessage = JsonConvert.SerializeObject(message, _serializerSettings); - Task.Factory.StartNew(async () => { - if (!CanSendWebSocketMessage(socket)) - return; - - try { - await socket.SendAsync(buffer: new ArraySegment(Encoding.ASCII.GetBytes(serializedMessage), 0, serializedMessage.Length), - messageType: WebSocketMessageType.Text, - endOfMessage: true, - cancellationToken: CancellationToken.None); - } catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { - } catch (Exception ex) { - _logger.LogError(ex, "Error sending socket message: {Message}", ex.Message); - } - }); - - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task SendMessageAsync(string connectionId, object message) { - return SendMessageAsync(GetWebSocketById(connectionId), message); - } + public Task SendMessageAsync(string connectionId, object message) { + return SendMessageAsync(GetWebSocketById(connectionId), message); + } - public Task SendMessageAsync(IEnumerable connectionIds, object message) { - return Task.WhenAll(connectionIds.Select(id => SendMessageAsync(GetWebSocketById(id), message))); - } + public Task SendMessageAsync(IEnumerable connectionIds, object message) { + return Task.WhenAll(connectionIds.Select(id => SendMessageAsync(GetWebSocketById(id), message))); + } - public async Task SendMessageToAllAsync(object message, bool throwOnError = true) { - foreach (var socket in GetAll()) { - if (!CanSendWebSocketMessage(socket)) - continue; + public async Task SendMessageToAllAsync(object message, bool throwOnError = true) { + foreach (var socket in GetAll()) { + if (!CanSendWebSocketMessage(socket)) + continue; - try { - await SendMessageAsync(socket, message); - } catch (Exception) { - if (throwOnError) - throw; - } + try { + await SendMessageAsync(socket, message); + } + catch (Exception) { + if (throwOnError) + throw; } } + } - private bool CanSendWebSocketMessage(WebSocket socket) { - return socket != null && socket.State != WebSocketState.Aborted && socket.State != WebSocketState.Closed && socket.State != WebSocketState.CloseSent; - } + private bool CanSendWebSocketMessage(WebSocket socket) { + return socket != null && socket.State != WebSocketState.Aborted && socket.State != WebSocketState.Closed && socket.State != WebSocketState.CloseSent; + } - public void Dispose() { - _timer?.Dispose(); - } + public void Dispose() { + _timer?.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Models/Auth/ChangePasswordModel.cs b/src/Exceptionless.Web/Models/Auth/ChangePasswordModel.cs index ba87b152cc..eb14cc27f8 100644 --- a/src/Exceptionless.Web/Models/Auth/ChangePasswordModel.cs +++ b/src/Exceptionless.Web/Models/Auth/ChangePasswordModel.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Web.Models { - public class ChangePasswordModel { - public string CurrentPassword { get; set; } - public string Password { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class ChangePasswordModel { + public string CurrentPassword { get; set; } + public string Password { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs b/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs index 4bcf4e1d4b..991e837999 100644 --- a/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs +++ b/src/Exceptionless.Web/Models/Auth/ExternalAuthInfo.cs @@ -1,8 +1,8 @@ -namespace Exceptionless.Web.Models { - public class ExternalAuthInfo { - public string ClientId { get; set; } - public string Code { get; set; } - public string RedirectUri { get; set; } - public string InviteToken { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class ExternalAuthInfo { + public string ClientId { get; set; } + public string Code { get; set; } + public string RedirectUri { get; set; } + public string InviteToken { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Auth/LoginModel.cs b/src/Exceptionless.Web/Models/Auth/LoginModel.cs index 7ba155a11f..3e3024813d 100644 --- a/src/Exceptionless.Web/Models/Auth/LoginModel.cs +++ b/src/Exceptionless.Web/Models/Auth/LoginModel.cs @@ -1,7 +1,7 @@ -namespace Exceptionless.Web.Models { - public class LoginModel { - public string Email { get; set; } - public string Password { get; set; } - public string InviteToken { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class LoginModel { + public string Email { get; set; } + public string Password { get; set; } + public string InviteToken { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Auth/ResetPasswordModel.cs b/src/Exceptionless.Web/Models/Auth/ResetPasswordModel.cs index b5075138cb..74e67fb624 100644 --- a/src/Exceptionless.Web/Models/Auth/ResetPasswordModel.cs +++ b/src/Exceptionless.Web/Models/Auth/ResetPasswordModel.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Web.Models { - public class ResetPasswordModel { - public string PasswordResetToken { get; set; } - public string Password { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class ResetPasswordModel { + public string PasswordResetToken { get; set; } + public string Password { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Auth/SignupModel.cs b/src/Exceptionless.Web/Models/Auth/SignupModel.cs index 01acac3d20..727b569aeb 100644 --- a/src/Exceptionless.Web/Models/Auth/SignupModel.cs +++ b/src/Exceptionless.Web/Models/Auth/SignupModel.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Web.Models { - public class SignupModel : LoginModel { - public string Name { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class SignupModel : LoginModel { + public string Name { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Auth/StatusResult.cs b/src/Exceptionless.Web/Models/Auth/StatusResult.cs index 08af7bdde4..8476b51e0e 100644 --- a/src/Exceptionless.Web/Models/Auth/StatusResult.cs +++ b/src/Exceptionless.Web/Models/Auth/StatusResult.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Web.Models { - public class TokenResult { - public string Token { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class TokenResult { + public string Token { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Event/UpdateEvent.cs b/src/Exceptionless.Web/Models/Event/UpdateEvent.cs index 3f959ddd90..43bdbf6faa 100644 --- a/src/Exceptionless.Web/Models/Event/UpdateEvent.cs +++ b/src/Exceptionless.Web/Models/Event/UpdateEvent.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Web.Models { - public class UpdateEvent { - public string EmailAddress { get; set; } - public string Description { get; set; } - } +namespace Exceptionless.Web.Models; + +public class UpdateEvent { + public string EmailAddress { get; set; } + public string Description { get; set; } } diff --git a/src/Exceptionless.Web/Models/Organization/Invoice.cs b/src/Exceptionless.Web/Models/Organization/Invoice.cs index 32c728bd06..fa368c10d8 100644 --- a/src/Exceptionless.Web/Models/Organization/Invoice.cs +++ b/src/Exceptionless.Web/Models/Organization/Invoice.cs @@ -1,20 +1,17 @@ -using System; -using System.Collections.Generic; +namespace Exceptionless.Web.Models; -namespace Exceptionless.Web.Models { - public class Invoice { - public Invoice() { - Items = new List(); - } +public class Invoice { + public Invoice() { + Items = new List(); + } - public string Id { get; set; } - public string OrganizationId { get; set; } - public string OrganizationName { get; set; } + public string Id { get; set; } + public string OrganizationId { get; set; } + public string OrganizationName { get; set; } - public DateTime Date { get; set; } - public bool Paid { get; set; } - public decimal Total { get; set; } + public DateTime Date { get; set; } + public bool Paid { get; set; } + public decimal Total { get; set; } - public IList Items { get; set; } - } -} \ No newline at end of file + public IList Items { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Organization/InvoiceGridModel.cs b/src/Exceptionless.Web/Models/Organization/InvoiceGridModel.cs index dbeab59a4b..aeae26be2e 100644 --- a/src/Exceptionless.Web/Models/Organization/InvoiceGridModel.cs +++ b/src/Exceptionless.Web/Models/Organization/InvoiceGridModel.cs @@ -1,9 +1,7 @@ -using System; +namespace Exceptionless.Web.Models; -namespace Exceptionless.Web.Models { - public class InvoiceGridModel { - public string Id { get; set; } - public DateTime Date { get; set; } - public bool Paid { get; set; } - } -} \ No newline at end of file +public class InvoiceGridModel { + public string Id { get; set; } + public DateTime Date { get; set; } + public bool Paid { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Organization/InvoiceLineItem.cs b/src/Exceptionless.Web/Models/Organization/InvoiceLineItem.cs index c69a17afd4..b88b6b16cf 100644 --- a/src/Exceptionless.Web/Models/Organization/InvoiceLineItem.cs +++ b/src/Exceptionless.Web/Models/Organization/InvoiceLineItem.cs @@ -1,7 +1,7 @@ -namespace Exceptionless.Web.Models { - public class InvoiceLineItem { - public string Description { get; set; } - public string Date { get; set; } - public decimal Amount { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class InvoiceLineItem { + public string Description { get; set; } + public string Date { get; set; } + public decimal Amount { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Organization/NewOrganization.cs b/src/Exceptionless.Web/Models/Organization/NewOrganization.cs index bd9d4b1e69..792e142ccc 100644 --- a/src/Exceptionless.Web/Models/Organization/NewOrganization.cs +++ b/src/Exceptionless.Web/Models/Organization/NewOrganization.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Web.Models { - public class NewOrganization { - public string Name { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class NewOrganization { + public string Name { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Organization/ViewOrganization.cs b/src/Exceptionless.Web/Models/Organization/ViewOrganization.cs index 13a038bd12..424e4febdc 100644 --- a/src/Exceptionless.Web/Models/Organization/ViewOrganization.cs +++ b/src/Exceptionless.Web/Models/Organization/ViewOrganization.cs @@ -1,43 +1,41 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories.Models; -namespace Exceptionless.Web.Models { - public class ViewOrganization : IIdentity, IData, IHaveCreatedDate { - public string Id { get; set; } - public DateTime CreatedUtc { get; set; } - public string Name { get; set; } - public string PlanId { get; set; } - public string PlanName { get; set; } - public string PlanDescription { get; set; } - public string CardLast4 { get; set; } - public DateTime? SubscribeDate { get; set; } - public DateTime? BillingChangeDate { get; set; } - public string BillingChangedByUserId { get; set; } - public BillingStatus BillingStatus { get; set; } - public decimal BillingPrice { get; set; } - public int MaxEventsPerMonth { get; set; } - public int BonusEventsPerMonth { get; set; } - public DateTime? BonusExpiration { get; set; } - public int RetentionDays { get; set; } - public bool IsSuspended { get; set; } - public string SuspensionCode { get; set; } - public string SuspensionNotes { get; set; } - public DateTime? SuspensionDate { get; set; } - public bool HasPremiumFeatures { get; set; } - public int MaxUsers { get; set; } - public int MaxProjects { get; set; } - public long ProjectCount { get; set; } - public long StackCount { get; set; } - public long EventCount { get; set; } - public ICollection Invites { get; set; } - public ICollection OverageHours { get; set; } - public ICollection Usage { get; set; } - public DataDictionary Data { get; set; } +namespace Exceptionless.Web.Models; - public bool IsOverHourlyLimit { get; set; } - public bool IsOverMonthlyLimit { get; set; } - public bool IsOverRequestLimit { get; set; } - } -} \ No newline at end of file +public class ViewOrganization : IIdentity, IData, IHaveCreatedDate { + public string Id { get; set; } + public DateTime CreatedUtc { get; set; } + public string Name { get; set; } + public string PlanId { get; set; } + public string PlanName { get; set; } + public string PlanDescription { get; set; } + public string CardLast4 { get; set; } + public DateTime? SubscribeDate { get; set; } + public DateTime? BillingChangeDate { get; set; } + public string BillingChangedByUserId { get; set; } + public BillingStatus BillingStatus { get; set; } + public decimal BillingPrice { get; set; } + public int MaxEventsPerMonth { get; set; } + public int BonusEventsPerMonth { get; set; } + public DateTime? BonusExpiration { get; set; } + public int RetentionDays { get; set; } + public bool IsSuspended { get; set; } + public string SuspensionCode { get; set; } + public string SuspensionNotes { get; set; } + public DateTime? SuspensionDate { get; set; } + public bool HasPremiumFeatures { get; set; } + public int MaxUsers { get; set; } + public int MaxProjects { get; set; } + public long ProjectCount { get; set; } + public long StackCount { get; set; } + public long EventCount { get; set; } + public ICollection Invites { get; set; } + public ICollection OverageHours { get; set; } + public ICollection Usage { get; set; } + public DataDictionary Data { get; set; } + + public bool IsOverHourlyLimit { get; set; } + public bool IsOverMonthlyLimit { get; set; } + public bool IsOverRequestLimit { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Project/NewProject.cs b/src/Exceptionless.Web/Models/Project/NewProject.cs index 88418c9cdb..4b75e1b4c6 100644 --- a/src/Exceptionless.Web/Models/Project/NewProject.cs +++ b/src/Exceptionless.Web/Models/Project/NewProject.cs @@ -1,7 +1,7 @@ using Exceptionless.Core.Models; -namespace Exceptionless.Web.Models { - public class NewProject : UpdateProject, IOwnedByOrganization { - public string OrganizationId { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class NewProject : UpdateProject, IOwnedByOrganization { + public string OrganizationId { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Project/UpdateProject.cs b/src/Exceptionless.Web/Models/Project/UpdateProject.cs index 00b6b3bb4f..0b329277c2 100644 --- a/src/Exceptionless.Web/Models/Project/UpdateProject.cs +++ b/src/Exceptionless.Web/Models/Project/UpdateProject.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Web.Models { - public class UpdateProject { - public string Name { get; set; } - public bool DeleteBotDataEnabled { get; set; } - } +namespace Exceptionless.Web.Models; + +public class UpdateProject { + public string Name { get; set; } + public bool DeleteBotDataEnabled { get; set; } } diff --git a/src/Exceptionless.Web/Models/Project/ViewProject.cs b/src/Exceptionless.Web/Models/Project/ViewProject.cs index f7ac1dc356..204e31d633 100644 --- a/src/Exceptionless.Web/Models/Project/ViewProject.cs +++ b/src/Exceptionless.Web/Models/Project/ViewProject.cs @@ -1,24 +1,22 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Foundatio.Repositories.Models; -namespace Exceptionless.Web.Models { - public class ViewProject : IIdentity, IData, IHaveCreatedDate { - public string Id { get; set; } - public DateTime CreatedUtc { get; set; } - public string OrganizationId { get; set; } - public string OrganizationName { get; set; } - public string Name { get; set; } - public bool DeleteBotDataEnabled { get; set; } - public DataDictionary Data { get; set; } - public HashSet PromotedTabs { get; set; } - public bool? IsConfigured { get; set; } - public long StackCount { get; set; } - public long EventCount { get; set; } - public bool HasPremiumFeatures { get; set; } - public bool HasSlackIntegration { get; set; } - public ICollection OverageHours { get; set; } - public ICollection Usage { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class ViewProject : IIdentity, IData, IHaveCreatedDate { + public string Id { get; set; } + public DateTime CreatedUtc { get; set; } + public string OrganizationId { get; set; } + public string OrganizationName { get; set; } + public string Name { get; set; } + public bool DeleteBotDataEnabled { get; set; } + public DataDictionary Data { get; set; } + public HashSet PromotedTabs { get; set; } + public bool? IsConfigured { get; set; } + public long StackCount { get; set; } + public long EventCount { get; set; } + public bool HasPremiumFeatures { get; set; } + public bool HasSlackIntegration { get; set; } + public ICollection OverageHours { get; set; } + public ICollection Usage { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Token/NewToken.cs b/src/Exceptionless.Web/Models/Token/NewToken.cs index 3615565242..29b488effa 100644 --- a/src/Exceptionless.Web/Models/Token/NewToken.cs +++ b/src/Exceptionless.Web/Models/Token/NewToken.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; -namespace Exceptionless.Web.Models { - public class NewToken : IOwnedByOrganizationAndProject { - public NewToken() { - Scopes = new HashSet(); - } +namespace Exceptionless.Web.Models; - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string DefaultProjectId { get; set; } - public HashSet Scopes { get; set; } - public DateTime? ExpiresUtc { get; set; } - public string Notes { get; set; } +public class NewToken : IOwnedByOrganizationAndProject { + public NewToken() { + Scopes = new HashSet(); } -} \ No newline at end of file + + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string DefaultProjectId { get; set; } + public HashSet Scopes { get; set; } + public DateTime? ExpiresUtc { get; set; } + public string Notes { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Token/UpdateToken.cs b/src/Exceptionless.Web/Models/Token/UpdateToken.cs index ba22ac1ee2..e2b0101024 100644 --- a/src/Exceptionless.Web/Models/Token/UpdateToken.cs +++ b/src/Exceptionless.Web/Models/Token/UpdateToken.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Web.Models { - public class UpdateToken { - public bool IsDisabled { get; set; } - public string Notes { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class UpdateToken { + public bool IsDisabled { get; set; } + public string Notes { get; set; } +} diff --git a/src/Exceptionless.Web/Models/Token/ViewToken.cs b/src/Exceptionless.Web/Models/Token/ViewToken.cs index 2f4fae3476..fcba0d84ee 100644 --- a/src/Exceptionless.Web/Models/Token/ViewToken.cs +++ b/src/Exceptionless.Web/Models/Token/ViewToken.cs @@ -1,19 +1,17 @@ -using System; -using System.Collections.Generic; -using Foundatio.Repositories.Models; +using Foundatio.Repositories.Models; -namespace Exceptionless.Web.Models { - public class ViewToken : IIdentity, IHaveDates { - public string Id { get; set; } - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string UserId { get; set; } - public string DefaultProjectId { get; set; } - public HashSet Scopes { get; set; } - public DateTime? ExpiresUtc { get; set; } - public string Notes { get; set; } - public bool IsDisabled { get; set; } - public DateTime CreatedUtc { get; set; } - public DateTime UpdatedUtc { get; set; } - } +namespace Exceptionless.Web.Models; + +public class ViewToken : IIdentity, IHaveDates { + public string Id { get; set; } + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string UserId { get; set; } + public string DefaultProjectId { get; set; } + public HashSet Scopes { get; set; } + public DateTime? ExpiresUtc { get; set; } + public string Notes { get; set; } + public bool IsDisabled { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } } diff --git a/src/Exceptionless.Web/Models/User/ChangePlanResult.cs b/src/Exceptionless.Web/Models/User/ChangePlanResult.cs index c0e77c596b..2d26195358 100644 --- a/src/Exceptionless.Web/Models/User/ChangePlanResult.cs +++ b/src/Exceptionless.Web/Models/User/ChangePlanResult.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Web.Models { - public class UpdateEmailAddressResult { - public bool IsVerified { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class UpdateEmailAddressResult { + public bool IsVerified { get; set; } +} diff --git a/src/Exceptionless.Web/Models/User/UpdateUser.cs b/src/Exceptionless.Web/Models/User/UpdateUser.cs index 5d2d722040..906084ded5 100644 --- a/src/Exceptionless.Web/Models/User/UpdateUser.cs +++ b/src/Exceptionless.Web/Models/User/UpdateUser.cs @@ -1,6 +1,6 @@ -namespace Exceptionless.Web.Models { - public class UpdateUser { - public string FullName { get; set; } - public bool EmailNotificationsEnabled { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class UpdateUser { + public string FullName { get; set; } + public bool EmailNotificationsEnabled { get; set; } +} diff --git a/src/Exceptionless.Web/Models/User/ViewCurrentUser.cs b/src/Exceptionless.Web/Models/User/ViewCurrentUser.cs index a858ba35d2..8d290fd303 100644 --- a/src/Exceptionless.Web/Models/User/ViewCurrentUser.cs +++ b/src/Exceptionless.Web/Models/User/ViewCurrentUser.cs @@ -1,47 +1,45 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Text; using Exceptionless.Core.Configuration; using Exceptionless.Core.Models; -namespace Exceptionless.Web.Models { - public class ViewCurrentUser : ViewUser { - public ViewCurrentUser(User user, IntercomOptions options) { - Id = user.Id; - OrganizationIds = user.OrganizationIds; - FullName = user.FullName; - EmailAddress = user.EmailAddress; - EmailNotificationsEnabled = user.EmailNotificationsEnabled; - IsEmailAddressVerified = user.IsEmailAddressVerified; - IsActive = user.IsActive; - Roles = user.Roles; - - Hash = HMACSHA256HashString(user.Id, options); - HasLocalAccount = !String.IsNullOrWhiteSpace(user.Password); - OAuthAccounts = user.OAuthAccounts; - } +namespace Exceptionless.Web.Models; + +public class ViewCurrentUser : ViewUser { + public ViewCurrentUser(User user, IntercomOptions options) { + Id = user.Id; + OrganizationIds = user.OrganizationIds; + FullName = user.FullName; + EmailAddress = user.EmailAddress; + EmailNotificationsEnabled = user.EmailNotificationsEnabled; + IsEmailAddressVerified = user.IsEmailAddressVerified; + IsActive = user.IsActive; + Roles = user.Roles; + + Hash = HMACSHA256HashString(user.Id, options); + HasLocalAccount = !String.IsNullOrWhiteSpace(user.Password); + OAuthAccounts = user.OAuthAccounts; + } - public string Hash { get; set; } - public bool HasLocalAccount { get; set; } - public ICollection OAuthAccounts { get; set; } + public string Hash { get; set; } + public bool HasLocalAccount { get; set; } + public ICollection OAuthAccounts { get; set; } - private string HMACSHA256HashString(string value, IntercomOptions options) { - if (!options.EnableIntercom) - return null; + private string HMACSHA256HashString(string value, IntercomOptions options) { + if (!options.EnableIntercom) + return null; - byte[] secretKey = Encoding.UTF8.GetBytes(options.IntercomSecret); - byte[] bytes = Encoding.UTF8.GetBytes(value); - using (var hmac = new HMACSHA256(secretKey)) { - hmac.ComputeHash(bytes); - byte[] data = hmac.Hash; + byte[] secretKey = Encoding.UTF8.GetBytes(options.IntercomSecret); + byte[] bytes = Encoding.UTF8.GetBytes(value); + using (var hmac = new HMACSHA256(secretKey)) { + hmac.ComputeHash(bytes); + byte[] data = hmac.Hash; - var builder = new StringBuilder(); - for (int i = 0; i < data.Length; i++) - builder.Append(data[i].ToString("x2")); + var builder = new StringBuilder(); + for (int i = 0; i < data.Length; i++) + builder.Append(data[i].ToString("x2")); - return builder.ToString(); - } + return builder.ToString(); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Models/User/ViewUser.cs b/src/Exceptionless.Web/Models/User/ViewUser.cs index a1a1757ad9..7dd928f349 100644 --- a/src/Exceptionless.Web/Models/User/ViewUser.cs +++ b/src/Exceptionless.Web/Models/User/ViewUser.cs @@ -1,16 +1,15 @@ -using System.Collections.Generic; -using Foundatio.Repositories.Models; +using Foundatio.Repositories.Models; -namespace Exceptionless.Web.Models { - public class ViewUser : IIdentity { - public string Id { get; set; } - public ICollection OrganizationIds { get; set; } - public string FullName { get; set; } - public string EmailAddress { get; set; } - public bool EmailNotificationsEnabled { get; set; } - public bool IsEmailAddressVerified { get; set; } - public bool IsActive { get; set; } - public bool IsInvite { get; set; } - public ICollection Roles { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class ViewUser : IIdentity { + public string Id { get; set; } + public ICollection OrganizationIds { get; set; } + public string FullName { get; set; } + public string EmailAddress { get; set; } + public bool EmailNotificationsEnabled { get; set; } + public bool IsEmailAddressVerified { get; set; } + public bool IsActive { get; set; } + public bool IsInvite { get; set; } + public ICollection Roles { get; set; } +} diff --git a/src/Exceptionless.Web/Models/ValueFromBody.cs b/src/Exceptionless.Web/Models/ValueFromBody.cs index b2925c40e2..dc87dec183 100644 --- a/src/Exceptionless.Web/Models/ValueFromBody.cs +++ b/src/Exceptionless.Web/Models/ValueFromBody.cs @@ -1,14 +1,14 @@ using System.Diagnostics; -namespace Exceptionless.Web.Models { - [DebuggerDisplay("{Value}")] - public class ValueFromBody { - private ValueFromBody() {} - - public ValueFromBody(T value) { - Value = value; - } - - public T Value { get; set; } +namespace Exceptionless.Web.Models; + +[DebuggerDisplay("{Value}")] +public class ValueFromBody { + private ValueFromBody() { } + + public ValueFromBody(T value) { + Value = value; } + + public T Value { get; set; } } diff --git a/src/Exceptionless.Web/Models/WebHook/NewWebHook.cs b/src/Exceptionless.Web/Models/WebHook/NewWebHook.cs index de35085ad6..50d4d9d27e 100644 --- a/src/Exceptionless.Web/Models/WebHook/NewWebHook.cs +++ b/src/Exceptionless.Web/Models/WebHook/NewWebHook.cs @@ -1,16 +1,15 @@ -using System; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; -namespace Exceptionless.Web.Models { - public class NewWebHook : IOwnedByOrganizationAndProject { - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string Url { get; set; } - public string[] EventTypes { get; set; } +namespace Exceptionless.Web.Models; - /// - /// The schema version that should be used. - /// - public Version Version { get; set; } - } -} \ No newline at end of file +public class NewWebHook : IOwnedByOrganizationAndProject { + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string Url { get; set; } + public string[] EventTypes { get; set; } + + /// + /// The schema version that should be used. + /// + public Version Version { get; set; } +} diff --git a/src/Exceptionless.Web/Models/WebHook/UpdateWebHook.cs b/src/Exceptionless.Web/Models/WebHook/UpdateWebHook.cs index b8ebabe5be..cad06cef6f 100644 --- a/src/Exceptionless.Web/Models/WebHook/UpdateWebHook.cs +++ b/src/Exceptionless.Web/Models/WebHook/UpdateWebHook.cs @@ -1,5 +1,5 @@ -namespace Exceptionless.Web.Models { - public class UpdateWebHook : NewWebHook { - public bool IsEnabled { get; set; } - } -} \ No newline at end of file +namespace Exceptionless.Web.Models; + +public class UpdateWebHook : NewWebHook { + public bool IsEnabled { get; set; } +} diff --git a/src/Exceptionless.Web/Program.cs b/src/Exceptionless.Web/Program.cs index 555ed349b2..9996718397 100644 --- a/src/Exceptionless.Web/Program.cs +++ b/src/Exceptionless.Web/Program.cs @@ -1,7 +1,4 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; +using System.Diagnostics; using App.Metrics; using App.Metrics.AspNetCore; using App.Metrics.Formatters; @@ -10,110 +7,109 @@ using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; using Exceptionless.Insulation.Configuration; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using OpenTelemetry; using Serilog; using Serilog.Enrichers.Span; using Serilog.Events; using Serilog.Sinks.Exceptionless; -namespace Exceptionless.Web { - public class Program { - public static async Task Main(string[] args) { - try { - await CreateHostBuilder(args).Build().RunAsync(); - return 0; - } catch (Exception ex) { - Log.Fatal(ex, "Job host terminated unexpectedly"); - return 1; - } finally { - Log.CloseAndFlush(); - await ExceptionlessClient.Default.ProcessQueueAsync(); - - if (Debugger.IsAttached) - Console.ReadKey(); - } - } - - public static IHostBuilder CreateHostBuilder(string[] args) { - string environment = Environment.GetEnvironmentVariable("EX_AppMode"); - if (String.IsNullOrWhiteSpace(environment)) - environment = "Production"; - - var config = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddYamlFile("appsettings.yml", optional: true, reloadOnChange: true) - .AddYamlFile($"appsettings.{environment}.yml", optional: true, reloadOnChange: true) - .AddEnvironmentVariables("EX_") - .AddEnvironmentVariables("ASPNETCORE_") - .AddCommandLine(args) - .Build(); +namespace Exceptionless.Web; - return CreateHostBuilder(config, environment); +public class Program { + public static async Task Main(string[] args) { + try { + await CreateHostBuilder(args).Build().RunAsync(); + return 0; } + catch (Exception ex) { + Log.Fatal(ex, "Job host terminated unexpectedly"); + return 1; + } + finally { + Log.CloseAndFlush(); + await ExceptionlessClient.Default.ProcessQueueAsync(); - public static IHostBuilder CreateHostBuilder(IConfigurationRoot config, string environment) { - Console.Title = "Exceptionless Web"; - - var options = AppOptions.ReadFromConfiguration(config); - - var loggerConfig = new LoggerConfiguration().ReadFrom.Configuration(config) - .Enrich.FromLogContext() - .Enrich.WithMachineName() - .Enrich.WithSpan(); - - if (!String.IsNullOrEmpty(options.ExceptionlessApiKey)) - loggerConfig.WriteTo.Sink(new ExceptionlessSink(), LogEventLevel.Information); - - Log.Logger = loggerConfig.CreateBootstrapLogger(); - var configDictionary = config.ToDictionary("Serilog"); - Log.Information("Bootstrapping Exceptionless Web in {AppMode} mode ({InformationalVersion}) on {MachineName} with settings {@Settings}", environment, options.InformationalVersion, Environment.MachineName, configDictionary); - - var builder = Host.CreateDefaultBuilder() - .UseEnvironment(environment) - .UseSerilog() - .ConfigureWebHostDefaults(webBuilder => { - webBuilder - .UseConfiguration(config) - .ConfigureKestrel(c => { - c.AddServerHeader = false; - - if (options.MaximumEventPostSize > 0) - c.Limits.MaxRequestBodySize = options.MaximumEventPostSize; - }) - .UseStartup(); - }) - .ConfigureServices((ctx, services) => { - services.AddSingleton(config); - services.AddAppOptions(options); - services.AddHttpContextAccessor(); - services.AddApm(new ApmConfig(config, "Exceptionless.Web", "Exceptionless", options.InformationalVersion, options.CacheOptions.Provider == "redis")); - }); - - if (!String.IsNullOrEmpty(options.MetricOptions.Provider)) - ConfigureMetricsReporting(builder, options.MetricOptions); - - return builder; + if (Debugger.IsAttached) + Console.ReadKey(); } + } + + public static IHostBuilder CreateHostBuilder(string[] args) { + string environment = Environment.GetEnvironmentVariable("EX_AppMode"); + if (String.IsNullOrWhiteSpace(environment)) + environment = "Production"; + + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddYamlFile("appsettings.yml", optional: true, reloadOnChange: true) + .AddYamlFile($"appsettings.{environment}.yml", optional: true, reloadOnChange: true) + .AddEnvironmentVariables("EX_") + .AddEnvironmentVariables("ASPNETCORE_") + .AddCommandLine(args) + .Build(); + + return CreateHostBuilder(config, environment); + } + + public static IHostBuilder CreateHostBuilder(IConfigurationRoot config, string environment) { + Console.Title = "Exceptionless Web"; + + var options = AppOptions.ReadFromConfiguration(config); - private static void ConfigureMetricsReporting(IHostBuilder builder, MetricOptions options) { - if (String.Equals(options.Provider, "prometheus")) { - var metrics = AppMetrics.CreateDefaultBuilder() - .OutputMetrics.AsPrometheusPlainText() - .OutputMetrics.AsPrometheusProtobuf() - .Build(); - builder.ConfigureMetrics(metrics).UseMetrics(o => { - o.EndpointOptions = endpointsOptions => { - endpointsOptions.MetricsTextEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); - endpointsOptions.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); - }; - }); - } else if (!String.Equals(options.Provider, "statsd")) { - builder.UseMetrics(); - } + var loggerConfig = new LoggerConfiguration().ReadFrom.Configuration(config) + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .Enrich.WithSpan(); + + if (!String.IsNullOrEmpty(options.ExceptionlessApiKey)) + loggerConfig.WriteTo.Sink(new ExceptionlessSink(), LogEventLevel.Information); + + Log.Logger = loggerConfig.CreateBootstrapLogger(); + var configDictionary = config.ToDictionary("Serilog"); + Log.Information("Bootstrapping Exceptionless Web in {AppMode} mode ({InformationalVersion}) on {MachineName} with settings {@Settings}", environment, options.InformationalVersion, Environment.MachineName, configDictionary); + + var builder = Host.CreateDefaultBuilder() + .UseEnvironment(environment) + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => { + webBuilder + .UseConfiguration(config) + .ConfigureKestrel(c => { + c.AddServerHeader = false; + + if (options.MaximumEventPostSize > 0) + c.Limits.MaxRequestBodySize = options.MaximumEventPostSize; + }) + .UseStartup(); + }) + .ConfigureServices((ctx, services) => { + services.AddSingleton(config); + services.AddAppOptions(options); + services.AddHttpContextAccessor(); + services.AddApm(new ApmConfig(config, "Exceptionless.Web", "Exceptionless", options.InformationalVersion, options.CacheOptions.Provider == "redis")); + }); + + if (!String.IsNullOrEmpty(options.MetricOptions.Provider)) + ConfigureMetricsReporting(builder, options.MetricOptions); + + return builder; + } + + private static void ConfigureMetricsReporting(IHostBuilder builder, MetricOptions options) { + if (String.Equals(options.Provider, "prometheus")) { + var metrics = AppMetrics.CreateDefaultBuilder() + .OutputMetrics.AsPrometheusPlainText() + .OutputMetrics.AsPrometheusProtobuf() + .Build(); + builder.ConfigureMetrics(metrics).UseMetrics(o => { + o.EndpointOptions = endpointsOptions => { + endpointsOptions.MetricsTextEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); + endpointsOptions.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters.GetType(); + }; + }); + } + else if (!String.Equals(options.Provider, "statsd")) { + builder.UseMetrics(); } } } diff --git a/src/Exceptionless.Web/Security/ApiKeyAuthenticationHandler.cs b/src/Exceptionless.Web/Security/ApiKeyAuthenticationHandler.cs index 09ca55035d..59f54dee0a 100644 --- a/src/Exceptionless.Web/Security/ApiKeyAuthenticationHandler.cs +++ b/src/Exceptionless.Web/Security/ApiKeyAuthenticationHandler.cs @@ -1,150 +1,149 @@ -using System; -using System.Linq; using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Encodings.Web; -using System.Threading.Tasks; using Exceptionless.Web.Extensions; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; using Foundatio.Repositories; using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Exceptionless.Web.Security { - public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions { - public const string ApiKeySchema = "ApiKey"; +namespace Exceptionless.Web.Security; - public string AuthenticationScheme { get; } = ApiKeySchema; - } +public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions { + public const string ApiKeySchema = "ApiKey"; - public class ApiKeyAuthenticationHandler : AuthenticationHandler { - public const string BearerScheme = "bearer"; - public const string BasicScheme = "basic"; - public const string TokenScheme = "token"; + public string AuthenticationScheme { get; } = ApiKeySchema; +} - private readonly ITokenRepository _tokenRepository; - private readonly IUserRepository _userRepository; +public class ApiKeyAuthenticationHandler : AuthenticationHandler { + public const string BearerScheme = "bearer"; + public const string BasicScheme = "basic"; + public const string TokenScheme = "token"; - public ApiKeyAuthenticationHandler(ITokenRepository tokenRepository, IUserRepository userRepository, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { - _tokenRepository = tokenRepository; - _userRepository = userRepository; - } + private readonly ITokenRepository _tokenRepository; + private readonly IUserRepository _userRepository; + + public ApiKeyAuthenticationHandler(ITokenRepository tokenRepository, IUserRepository userRepository, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { + _tokenRepository = tokenRepository; + _userRepository = userRepository; + } + + protected override async Task HandleAuthenticateAsync() { + string authHeaderValue = Request.Headers.TryGetAndReturn("Authorization").FirstOrDefault(); + AuthenticationHeaderValue authHeader = null; + if (!String.IsNullOrEmpty(authHeaderValue) && !AuthenticationHeaderValue.TryParse(authHeaderValue, out authHeader)) + return AuthenticateResult.Fail("Unable to parse header"); - protected override async Task HandleAuthenticateAsync() { - string authHeaderValue = Request.Headers.TryGetAndReturn("Authorization").FirstOrDefault(); - AuthenticationHeaderValue authHeader = null; - if (!String.IsNullOrEmpty(authHeaderValue) && !AuthenticationHeaderValue.TryParse(authHeaderValue, out authHeader)) - return AuthenticateResult.Fail("Unable to parse header"); - - string scheme = authHeader?.Scheme.ToLower(); - string token = null; - if (authHeader != null && (scheme == BearerScheme || scheme == TokenScheme)) { - token = authHeader.Parameter; - } else if (authHeader != null && scheme == BasicScheme) { - var authInfo = Request.GetBasicAuth(); - if (authInfo != null) { - if (authInfo.Username.ToLower() == "client") - token = authInfo.Password; - else if (authInfo.Password.ToLower() == "x-oauth-basic" || String.IsNullOrEmpty(authInfo.Password)) - token = authInfo.Username; - else { - User user; - try { - user = await _userRepository.GetByEmailAddressAsync(authInfo.Username); - } catch (Exception ex) { - return AuthenticateResult.Fail(ex); - } - - if (user == null || !user.IsActive) - return AuthenticateResult.Fail("User is not valid"); - - if (String.IsNullOrEmpty(user.Salt)) - return AuthenticateResult.Fail("User is not valid"); - - string encodedPassword = authInfo.Password.ToSaltedHash(user.Salt); - if (!String.Equals(encodedPassword, user.Password)) - return AuthenticateResult.Fail("User is not valid"); - - return AuthenticateResult.Success(CreateUserAuthenticationTicket(user)); + string scheme = authHeader?.Scheme.ToLower(); + string token = null; + if (authHeader != null && (scheme == BearerScheme || scheme == TokenScheme)) { + token = authHeader.Parameter; + } + else if (authHeader != null && scheme == BasicScheme) { + var authInfo = Request.GetBasicAuth(); + if (authInfo != null) { + if (authInfo.Username.ToLower() == "client") + token = authInfo.Password; + else if (authInfo.Password.ToLower() == "x-oauth-basic" || String.IsNullOrEmpty(authInfo.Password)) + token = authInfo.Username; + else { + User user; + try { + user = await _userRepository.GetByEmailAddressAsync(authInfo.Username); } - } - } else { - token = Request.GetQueryString("access_token"); - if (String.IsNullOrEmpty(token)) - token = Request.GetQueryString("api_key"); + catch (Exception ex) { + return AuthenticateResult.Fail(ex); + } + + if (user == null || !user.IsActive) + return AuthenticateResult.Fail("User is not valid"); + + if (String.IsNullOrEmpty(user.Salt)) + return AuthenticateResult.Fail("User is not valid"); - if (String.IsNullOrEmpty(token)) - token = Request.GetQueryString("apikey"); + string encodedPassword = authInfo.Password.ToSaltedHash(user.Salt); + if (!String.Equals(encodedPassword, user.Password)) + return AuthenticateResult.Fail("User is not valid"); + + return AuthenticateResult.Success(CreateUserAuthenticationTicket(user)); + } } + } + else { + token = Request.GetQueryString("access_token"); + if (String.IsNullOrEmpty(token)) + token = Request.GetQueryString("api_key"); if (String.IsNullOrEmpty(token)) - return AuthenticateResult.NoResult(); + token = Request.GetQueryString("apikey"); + } - Request.HttpContext.Items["ApiKey"] = token; - var tokenRecord = await _tokenRepository.GetByIdAsync(token, o => o.Cache()); - if (tokenRecord == null) { - using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) - Logger.LogWarning("Token {Token} for {Path} not found.", token, Request.Path); + if (String.IsNullOrEmpty(token)) + return AuthenticateResult.NoResult(); - return AuthenticateResult.Fail("Token is not valid"); - } - - if (tokenRecord.IsDisabled) { - using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) - Logger.LogWarning("Token {Token} is disabled for {Path}.", token, Request.Path); + Request.HttpContext.Items["ApiKey"] = token; + var tokenRecord = await _tokenRepository.GetByIdAsync(token, o => o.Cache()); + if (tokenRecord == null) { + using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) + Logger.LogWarning("Token {Token} for {Path} not found.", token, Request.Path); - return AuthenticateResult.Fail("Token is not valid"); - } + return AuthenticateResult.Fail("Token is not valid"); + } - if (tokenRecord.ExpiresUtc.HasValue && tokenRecord.ExpiresUtc.Value < Foundatio.Utility.SystemClock.UtcNow) { - using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) - Logger.LogWarning("Token {Token} for {Path} expired on {TokenExpiresUtc}.", token, Request.Path, tokenRecord.ExpiresUtc.Value); + if (tokenRecord.IsDisabled) { + using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) + Logger.LogWarning("Token {Token} is disabled for {Path}.", token, Request.Path); - return AuthenticateResult.Fail("Token is not valid"); - } + return AuthenticateResult.Fail("Token is not valid"); + } - if (!String.IsNullOrEmpty(tokenRecord.UserId)) { - var user = await _userRepository.GetByIdAsync(tokenRecord.UserId, o => o.Cache()); - if (user == null) { - using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) - Logger.LogWarning("Could not find user for token {Token} with user {user} for {Path}.", token, tokenRecord.UserId, Request.Path); + if (tokenRecord.ExpiresUtc.HasValue && tokenRecord.ExpiresUtc.Value < Foundatio.Utility.SystemClock.UtcNow) { + using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) + Logger.LogWarning("Token {Token} for {Path} expired on {TokenExpiresUtc}.", token, Request.Path, tokenRecord.ExpiresUtc.Value); - return AuthenticateResult.Fail("Token is not valid"); - } + return AuthenticateResult.Fail("Token is not valid"); + } + + if (!String.IsNullOrEmpty(tokenRecord.UserId)) { + var user = await _userRepository.GetByIdAsync(tokenRecord.UserId, o => o.Cache()); + if (user == null) { + using (Logger.BeginScope(new ExceptionlessState().Property("Headers", Request.Headers))) + Logger.LogWarning("Could not find user for token {Token} with user {user} for {Path}.", token, tokenRecord.UserId, Request.Path); - return AuthenticateResult.Success(CreateUserAuthenticationTicket(user, tokenRecord)); + return AuthenticateResult.Fail("Token is not valid"); } - return AuthenticateResult.Success(CreateTokenAuthenticationTicket(tokenRecord)); + return AuthenticateResult.Success(CreateUserAuthenticationTicket(user, tokenRecord)); } - private AuthenticationTicket CreateUserAuthenticationTicket(User user, Token token = null) { - Request.SetUser(user); + return AuthenticateResult.Success(CreateTokenAuthenticationTicket(tokenRecord)); + } - var principal = new ClaimsPrincipal(user.ToIdentity(token)); - return new AuthenticationTicket(principal, CreateAuthenticationProperties(token), Options.AuthenticationScheme); - } + private AuthenticationTicket CreateUserAuthenticationTicket(User user, Token token = null) { + Request.SetUser(user); - private AuthenticationTicket CreateTokenAuthenticationTicket(Token token) { - var principal = new ClaimsPrincipal(token.ToIdentity()); - return new AuthenticationTicket(principal, CreateAuthenticationProperties(token), Options.AuthenticationScheme); - } + var principal = new ClaimsPrincipal(user.ToIdentity(token)); + return new AuthenticationTicket(principal, CreateAuthenticationProperties(token), Options.AuthenticationScheme); + } - private AuthenticationProperties CreateAuthenticationProperties(Token token) { - var utcNow = Foundatio.Utility.SystemClock.UtcNow; - return new AuthenticationProperties { - ExpiresUtc = token?.ExpiresUtc ?? utcNow.AddHours(12), - IssuedUtc = token?.CreatedUtc ?? utcNow - }; - } + private AuthenticationTicket CreateTokenAuthenticationTicket(Token token) { + var principal = new ClaimsPrincipal(token.ToIdentity()); + return new AuthenticationTicket(principal, CreateAuthenticationProperties(token), Options.AuthenticationScheme); } - public static class ApiKeyAuthMiddlewareExtensions { - public static AuthenticationBuilder AddApiKeyAuthentication(this AuthenticationBuilder builder) { - return builder.AddScheme(ApiKeyAuthenticationOptions.ApiKeySchema, null, _ => { }); - } + private AuthenticationProperties CreateAuthenticationProperties(Token token) { + var utcNow = Foundatio.Utility.SystemClock.UtcNow; + return new AuthenticationProperties { + ExpiresUtc = token?.ExpiresUtc ?? utcNow.AddHours(12), + IssuedUtc = token?.CreatedUtc ?? utcNow + }; + } +} + +public static class ApiKeyAuthMiddlewareExtensions { + public static AuthenticationBuilder AddApiKeyAuthentication(this AuthenticationBuilder builder) { + return builder.AddScheme(ApiKeyAuthenticationOptions.ApiKeySchema, null, _ => { }); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Startup.cs b/src/Exceptionless.Web/Startup.cs index 4c92245399..2268585dd3 100644 --- a/src/Exceptionless.Web/Startup.cs +++ b/src/Exceptionless.Web/Startup.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.IO; using System.Security.Claims; using Exceptionless.Core; using Exceptionless.Core.Authorization; @@ -9,12 +6,8 @@ using Exceptionless.Web.Utility; using Exceptionless.Web.Utility.Handlers; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Joonasw.AspNetCore.SecurityHeaders; -using System.Collections.Generic; using Exceptionless.Web.Extensions; using Foundatio.Extensions.Hosting.Startup; using Microsoft.AspNetCore.Diagnostics.HealthChecks; @@ -23,85 +16,85 @@ using Newtonsoft.Json; using Serilog; using Serilog.Events; -using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore.Hosting.Server.Features; -namespace Exceptionless.Web { - public class Startup { - public Startup(IConfiguration configuration) { - Configuration = configuration; - } +namespace Exceptionless.Web; - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) { - services.AddCors(b => b.AddPolicy("AllowAny", p => p - .AllowAnyHeader() - .AllowAnyMethod() - .SetIsOriginAllowed(isOriginAllowed: _ => true) - .AllowCredentials() - .SetPreflightMaxAge(TimeSpan.FromMinutes(5)) - .WithExposedHeaders("ETag", "Link", Headers.RateLimit, Headers.RateLimitRemaining, "X-Result-Count", Headers.LegacyConfigurationVersion, Headers.ConfigurationVersion))); - - services.Configure(options => { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - options.RequireHeaderSymmetry = false; - options.KnownNetworks.Clear(); - options.KnownProxies.Clear(); - }); +public class Startup { + public Startup(IConfiguration configuration) { + Configuration = configuration; + } - services.AddControllers(o => { - o.Filters.Add(); - o.ModelBinderProviders.Insert(0, new CustomAttributesModelBinderProvider()); - o.InputFormatters.Insert(0, new RawRequestBodyFormatter()); - }).AddNewtonsoftJson(o => { - o.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include; - o.SerializerSettings.NullValueHandling = NullValueHandling.Include; - o.SerializerSettings.Formatting = Formatting.Indented; - o.SerializerSettings.ContractResolver = Core.Bootstrapper.GetJsonContractResolver(); // TODO: See if we can resolve this from the di. + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) { + services.AddCors(b => b.AddPolicy("AllowAny", p => p + .AllowAnyHeader() + .AllowAnyMethod() + .SetIsOriginAllowed(isOriginAllowed: _ => true) + .AllowCredentials() + .SetPreflightMaxAge(TimeSpan.FromMinutes(5)) + .WithExposedHeaders("ETag", "Link", Headers.RateLimit, Headers.RateLimitRemaining, "X-Result-Count", Headers.LegacyConfigurationVersion, Headers.ConfigurationVersion))); + + services.Configure(options => { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.RequireHeaderSymmetry = false; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + }); + + services.AddControllers(o => { + o.Filters.Add(); + o.ModelBinderProviders.Insert(0, new CustomAttributesModelBinderProvider()); + o.InputFormatters.Insert(0, new RawRequestBodyFormatter()); + }).AddNewtonsoftJson(o => { + o.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include; + o.SerializerSettings.NullValueHandling = NullValueHandling.Include; + o.SerializerSettings.Formatting = Formatting.Indented; + o.SerializerSettings.ContractResolver = Core.Bootstrapper.GetJsonContractResolver(); // TODO: See if we can resolve this from the di. + }); + + services.AddAuthentication(ApiKeyAuthenticationOptions.ApiKeySchema).AddApiKeyAuthentication(); + services.AddAuthorization(options => { + options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + options.AddPolicy(AuthorizationRoles.ClientPolicy, policy => policy.RequireClaim(ClaimTypes.Role, AuthorizationRoles.Client)); + options.AddPolicy(AuthorizationRoles.UserPolicy, policy => policy.RequireClaim(ClaimTypes.Role, AuthorizationRoles.User)); + options.AddPolicy(AuthorizationRoles.GlobalAdminPolicy, policy => policy.RequireClaim(ClaimTypes.Role, AuthorizationRoles.GlobalAdmin)); + }); + + services.AddRouting(r => { + r.LowercaseUrls = true; + r.ConstraintMap.Add("identifier", typeof(IdentifierRouteConstraint)); + r.ConstraintMap.Add("identifiers", typeof(IdentifiersRouteConstraint)); + r.ConstraintMap.Add("objectid", typeof(ObjectIdRouteConstraint)); + r.ConstraintMap.Add("objectids", typeof(ObjectIdsRouteConstraint)); + r.ConstraintMap.Add("token", typeof(TokenRouteConstraint)); + r.ConstraintMap.Add("tokens", typeof(TokensRouteConstraint)); + }); + services.AddSwaggerGen(c => { + c.SwaggerDoc("v2", new OpenApiInfo { + Title = "Exceptionless API", + Version = "v2" }); - services.AddAuthentication(ApiKeyAuthenticationOptions.ApiKeySchema).AddApiKeyAuthentication(); - services.AddAuthorization(options => { - options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); - options.AddPolicy(AuthorizationRoles.ClientPolicy, policy => policy.RequireClaim(ClaimTypes.Role, AuthorizationRoles.Client)); - options.AddPolicy(AuthorizationRoles.UserPolicy, policy => policy.RequireClaim(ClaimTypes.Role, AuthorizationRoles.User)); - options.AddPolicy(AuthorizationRoles.GlobalAdminPolicy, policy => policy.RequireClaim(ClaimTypes.Role, AuthorizationRoles.GlobalAdmin)); + c.AddSecurityDefinition("Basic", new OpenApiSecurityScheme { + Description = "Basic HTTP Authentication", + Scheme = "basic", + Type = SecuritySchemeType.Http }); - - services.AddRouting(r => { - r.LowercaseUrls = true; - r.ConstraintMap.Add("identifier", typeof(IdentifierRouteConstraint)); - r.ConstraintMap.Add("identifiers", typeof(IdentifiersRouteConstraint)); - r.ConstraintMap.Add("objectid", typeof(ObjectIdRouteConstraint)); - r.ConstraintMap.Add("objectids", typeof(ObjectIdsRouteConstraint)); - r.ConstraintMap.Add("token", typeof(TokenRouteConstraint)); - r.ConstraintMap.Add("tokens", typeof(TokensRouteConstraint)); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { + Description = "Authorization token. Example: \"Bearer {apikey}\"", + Scheme = "bearer", + Type = SecuritySchemeType.Http + }); + c.AddSecurityDefinition("Token", new OpenApiSecurityScheme { + Description = "Authorization token. Example: \"Bearer {apikey}\"", + Name = "access_token", + In = ParameterLocation.Query, + Type = SecuritySchemeType.ApiKey }); - services.AddSwaggerGen(c => { - c.SwaggerDoc("v2", new OpenApiInfo { - Title = "Exceptionless API", - Version = "v2" - }); - - c.AddSecurityDefinition("Basic", new OpenApiSecurityScheme { - Description = "Basic HTTP Authentication", - Scheme = "basic", - Type = SecuritySchemeType.Http - }); - c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { - Description = "Authorization token. Example: \"Bearer {apikey}\"", - Scheme = "bearer", - Type = SecuritySchemeType.Http - }); - c.AddSecurityDefinition("Token", new OpenApiSecurityScheme { - Description = "Authorization token. Example: \"Bearer {apikey}\"", - Name = "access_token", - In = ParameterLocation.Query, - Type = SecuritySchemeType.ApiKey - }); - c.AddSecurityRequirement(new OpenApiSecurityRequirement { + c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Basic" } @@ -122,161 +115,160 @@ public void ConfigureServices(IServiceCollection services) { } }); - string xmlDocPath = Path.Combine(AppContext.BaseDirectory, "Exceptionless.Web.xml"); - if (File.Exists(xmlDocPath)) - c.IncludeXmlComments(xmlDocPath); - - c.IgnoreObsoleteActions(); - c.OperationFilter(); - }); - - var appOptions = AppOptions.ReadFromConfiguration(Configuration); - Bootstrapper.RegisterServices(services, appOptions, Log.Logger.ToLoggerFactory()); - services.AddSingleton(s => { - return new ThrottlingOptions { - MaxRequestsForUserIdentifierFunc = userIdentifier => appOptions.ApiThrottleLimit, - Period = TimeSpan.FromMinutes(15) - }; - }); - } - - public void Configure(IApplicationBuilder app) { - var options = app.ApplicationServices.GetRequiredService(); - Core.Bootstrapper.LogConfiguration(app.ApplicationServices, options, Log.Logger.ToLoggerFactory().CreateLogger()); - - app.UseSerilogRequestLogging(o => { - o.MessageTemplate = "traceID={TraceId} HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; - o.GetLevel = (context, duration, ex) => { - if (ex != null || context.Response.StatusCode > 499) - return LogEventLevel.Error; - - return duration < 1000 && context.Response.StatusCode < 400 ? LogEventLevel.Debug : LogEventLevel.Information; - }; - }); - - app.UseMiddleware(); - - app.UseHealthChecks("/health", new HealthCheckOptions { - Predicate = hcr => hcr.Tags.Contains("Critical") || (options.RunJobsInProcess && hcr.Tags.Contains("AllJobs")) - }); - - var readyTags = new List { "Critical" }; - if (!options.EventSubmissionDisabled) - readyTags.Add("Storage"); - app.UseReadyHealthChecks(readyTags.ToArray()); - app.UseWaitForStartupActionsBeforeServingRequests(); - - if (!String.IsNullOrEmpty(options.ExceptionlessApiKey) && !String.IsNullOrEmpty(options.ExceptionlessServerUrl)) - app.UseExceptionless(ExceptionlessClient.Default); - - app.UseCsp(csp => { - csp.ByDefaultAllow.FromSelf() - .From("https://js.stripe.com") - .From("http://js.stripe.com"); - csp.AllowFonts.FromSelf() - .From("https://fonts.gstatic.com") - .From("http://fonts.gstatic.com") - .From("https://cdn.jsdelivr.net") - .From("http://cdn.jsdelivr.net"); - csp.AllowImages.FromSelf() - .From("data:") - .From("https://q.stripe.com") - .From("http://q.stripe.com") - .From("https://www.gravatar.com") - .From("http://www.gravatar.com"); - csp.AllowScripts.FromSelf() - .AllowUnsafeInline() - .AllowUnsafeEval() - .From("https://js.stripe.com") - .From("http://js.stripe.com") - .From("https://cdn.jsdelivr.net") - .From("http://cdn.jsdelivr.net"); - csp.AllowStyles.FromSelf() - .AllowUnsafeInline() - .From("https://fonts.googleapis.com") - .From("http://fonts.googleapis.com") - .From("https://cdn.jsdelivr.net") - .From("http://cdn.jsdelivr.net"); - }); - - app.Use(async (context, next) => { - if (options.AppMode != AppMode.Development && context.Request.IsLocal() == false) - context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - - context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin"); - context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); - context.Response.Headers.Add("X-Frame-Options", "DENY"); - context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); - context.Response.Headers.Remove("X-Powered-By"); - - await next(); - }); + string xmlDocPath = Path.Combine(AppContext.BaseDirectory, "Exceptionless.Web.xml"); + if (File.Exists(xmlDocPath)) + c.IncludeXmlComments(xmlDocPath); + + c.IgnoreObsoleteActions(); + c.OperationFilter(); + }); + + var appOptions = AppOptions.ReadFromConfiguration(Configuration); + Bootstrapper.RegisterServices(services, appOptions, Log.Logger.ToLoggerFactory()); + services.AddSingleton(s => { + return new ThrottlingOptions { + MaxRequestsForUserIdentifierFunc = userIdentifier => appOptions.ApiThrottleLimit, + Period = TimeSpan.FromMinutes(15) + }; + }); + } - var serverAddressesFeature = app.ServerFeatures.Get(); - if (options.AppMode != AppMode.Development && serverAddressesFeature != null && serverAddressesFeature.Addresses.Any(a => a.StartsWith("https://"))) - app.UseHttpsRedirection(); + public void Configure(IApplicationBuilder app) { + var options = app.ApplicationServices.GetRequiredService(); + Core.Bootstrapper.LogConfiguration(app.ApplicationServices, options, Log.Logger.ToLoggerFactory().CreateLogger()); - app.UseSerilogRequestLogging(o => o.GetLevel = (context, duration, ex) => { + app.UseSerilogRequestLogging(o => { + o.MessageTemplate = "traceID={TraceId} HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; + o.GetLevel = (context, duration, ex) => { if (ex != null || context.Response.StatusCode > 499) return LogEventLevel.Error; - if (context.Response.StatusCode > 399) - return LogEventLevel.Information; + return duration < 1000 && context.Response.StatusCode < 400 ? LogEventLevel.Debug : LogEventLevel.Information; + }; + }); + + app.UseMiddleware(); + + app.UseHealthChecks("/health", new HealthCheckOptions { + Predicate = hcr => hcr.Tags.Contains("Critical") || (options.RunJobsInProcess && hcr.Tags.Contains("AllJobs")) + }); + + var readyTags = new List { "Critical" }; + if (!options.EventSubmissionDisabled) + readyTags.Add("Storage"); + app.UseReadyHealthChecks(readyTags.ToArray()); + app.UseWaitForStartupActionsBeforeServingRequests(); + + if (!String.IsNullOrEmpty(options.ExceptionlessApiKey) && !String.IsNullOrEmpty(options.ExceptionlessServerUrl)) + app.UseExceptionless(ExceptionlessClient.Default); + + app.UseCsp(csp => { + csp.ByDefaultAllow.FromSelf() + .From("https://js.stripe.com") + .From("http://js.stripe.com"); + csp.AllowFonts.FromSelf() + .From("https://fonts.gstatic.com") + .From("http://fonts.gstatic.com") + .From("https://cdn.jsdelivr.net") + .From("http://cdn.jsdelivr.net"); + csp.AllowImages.FromSelf() + .From("data:") + .From("https://q.stripe.com") + .From("http://q.stripe.com") + .From("https://www.gravatar.com") + .From("http://www.gravatar.com"); + csp.AllowScripts.FromSelf() + .AllowUnsafeInline() + .AllowUnsafeEval() + .From("https://js.stripe.com") + .From("http://js.stripe.com") + .From("https://cdn.jsdelivr.net") + .From("http://cdn.jsdelivr.net"); + csp.AllowStyles.FromSelf() + .AllowUnsafeInline() + .From("https://fonts.googleapis.com") + .From("http://fonts.googleapis.com") + .From("https://cdn.jsdelivr.net") + .From("http://cdn.jsdelivr.net"); + }); + + app.Use(async (context, next) => { + if (options.AppMode != AppMode.Development && context.Request.IsLocal() == false) + context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + + context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin"); + context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); + context.Response.Headers.Add("X-Frame-Options", "DENY"); + context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); + context.Response.Headers.Remove("X-Powered-By"); + + await next(); + }); + + var serverAddressesFeature = app.ServerFeatures.Get(); + if (options.AppMode != AppMode.Development && serverAddressesFeature != null && serverAddressesFeature.Addresses.Any(a => a.StartsWith("https://"))) + app.UseHttpsRedirection(); + + app.UseSerilogRequestLogging(o => o.GetLevel = (context, duration, ex) => { + if (ex != null || context.Response.StatusCode > 499) + return LogEventLevel.Error; + + if (context.Response.StatusCode > 399) + return LogEventLevel.Information; - if (duration < 1000 || context.Request.Path.StartsWithSegments("/api/v2/push")) - return LogEventLevel.Debug; + if (duration < 1000 || context.Request.Path.StartsWithSegments("/api/v2/push")) + return LogEventLevel.Debug; - return LogEventLevel.Information; - }); - app.UseStaticFiles(new StaticFileOptions { - ContentTypeProvider = new FileExtensionContentTypeProvider { - Mappings = { + return LogEventLevel.Information; + }); + app.UseStaticFiles(new StaticFileOptions { + ContentTypeProvider = new FileExtensionContentTypeProvider { + Mappings = { [".less"] = "plain/text" } - } - }); - - app.UseDefaultFiles(); - app.UseFileServer(); - app.UseRouting(); - app.UseCors("AllowAny"); - app.UseHttpMethodOverride(); - app.UseForwardedHeaders(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseMiddleware(); - app.UseMiddleware(); - - if (options.ApiThrottleLimit < Int32.MaxValue) { - // Throttle api calls to X every 15 minutes by IP address. - app.UseMiddleware(); } + }); - // Reject event posts in organizations over their max event limits. - app.UseMiddleware(); + app.UseDefaultFiles(); + app.UseFileServer(); + app.UseRouting(); + app.UseCors("AllowAny"); + app.UseHttpMethodOverride(); + app.UseForwardedHeaders(); - app.UseSwagger(c => { - c.RouteTemplate = "docs/{documentName}/swagger.json"; - // TODO: Remove once 5.6.4+ is released - c.PreSerializeFilters.Add((doc, _) => doc.Servers?.Clear()); - }); - app.UseSwaggerUI(s => { - s.RoutePrefix = "docs"; - s.SwaggerEndpoint("/docs/v2/swagger.json", "Exceptionless API"); - s.InjectStylesheet("/docs.css"); - }); + app.UseAuthentication(); + app.UseAuthorization(); - if (options.EnableWebSockets) { - app.UseWebSockets(); - app.UseMiddleware(); - } + app.UseMiddleware(); + app.UseMiddleware(); - app.UseEndpoints(endpoints => { - endpoints.MapControllers(); - endpoints.MapFallbackToFile("{**slug:nonfile}", "index.html"); - }); + if (options.ApiThrottleLimit < Int32.MaxValue) { + // Throttle api calls to X every 15 minutes by IP address. + app.UseMiddleware(); } + + // Reject event posts in organizations over their max event limits. + app.UseMiddleware(); + + app.UseSwagger(c => { + c.RouteTemplate = "docs/{documentName}/swagger.json"; + // TODO: Remove once 5.6.4+ is released + c.PreSerializeFilters.Add((doc, _) => doc.Servers?.Clear()); + }); + app.UseSwaggerUI(s => { + s.RoutePrefix = "docs"; + s.SwaggerEndpoint("/docs/v2/swagger.json", "Exceptionless API"); + s.InjectStylesheet("/docs.css"); + }); + + if (options.EnableWebSockets) { + app.UseWebSockets(); + app.UseMiddleware(); + } + + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + endpoints.MapFallbackToFile("{**slug:nonfile}", "index.html"); + }); } } diff --git a/src/Exceptionless.Web/Utility/Bindings/CustomAttributesModelBinder.cs b/src/Exceptionless.Web/Utility/Bindings/CustomAttributesModelBinder.cs index d63935a305..836fab0f55 100644 --- a/src/Exceptionless.Web/Utility/Bindings/CustomAttributesModelBinder.cs +++ b/src/Exceptionless.Web/Utility/Bindings/CustomAttributesModelBinder.cs @@ -1,92 +1,87 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Exceptionless.Web.Utility { - public class CustomAttributesModelBinder : IModelBinder { - private readonly SimpleTypeModelBinder _simpleModelBinder; +namespace Exceptionless.Web.Utility; - public CustomAttributesModelBinder(Type type, ILoggerFactory loggerFactory) { - _simpleModelBinder = new SimpleTypeModelBinder(type, loggerFactory); - } +public class CustomAttributesModelBinder : IModelBinder { + private readonly SimpleTypeModelBinder _simpleModelBinder; - public Task BindModelAsync(ModelBindingContext bindingContext) { - if (bindingContext == null) - throw new ArgumentNullException(nameof(bindingContext)); - - if (!(bindingContext.ActionContext.ActionDescriptor.Parameters.FirstOrDefault(p => p.Name == bindingContext.FieldName) is ControllerParameterDescriptor parameter)) - return _simpleModelBinder.BindModelAsync(bindingContext); - - if (bindingContext.ModelType == typeof(string)) { - if (parameter.ParameterInfo.GetCustomAttributes(typeof(IpAddressAttribute), false).Any()) { - bindingContext.Result = ModelBindingResult.Success(bindingContext.HttpContext.Connection.RemoteIpAddress.ToString()); - return Task.CompletedTask; - } - - if (parameter.ParameterInfo.GetCustomAttributes(typeof(ContentTypeAttribute), false).Any()) { - string contentType = bindingContext.HttpContext.Request.Headers[HeaderNames.ContentType].ToString(); - bindingContext.Result = ModelBindingResult.Success(contentType); - return Task.CompletedTask; - } - - if (parameter.ParameterInfo.GetCustomAttributes(typeof(UserAgentAttribute), false).Any()) { - string userAgent; - if (bindingContext.HttpContext.Request.Headers.TryGetValue(Headers.Client, out var values) && values.Count > 0) - userAgent = values; - else - userAgent = bindingContext.HttpContext.Request.Headers[HeaderNames.UserAgent].ToString(); - bindingContext.Result = ModelBindingResult.Success(userAgent); - return Task.CompletedTask; - } - - if (parameter.ParameterInfo.GetCustomAttributes(typeof(ReferrerAttribute), false).Any()) { - string urlReferrer = bindingContext.HttpContext.Request.Headers[HeaderNames.Referer].ToString(); - bindingContext.Result = ModelBindingResult.Success(urlReferrer); - return Task.CompletedTask; - } - } else { - if (parameter.ParameterInfo.GetCustomAttributes(typeof(QueryStringParametersAttribute), false).Any()) { - var query = bindingContext.HttpContext.Request.Query; - bindingContext.Result = ModelBindingResult.Success(query); - return Task.CompletedTask; - } - } + public CustomAttributesModelBinder(Type type, ILoggerFactory loggerFactory) { + _simpleModelBinder = new SimpleTypeModelBinder(type, loggerFactory); + } + + public Task BindModelAsync(ModelBindingContext bindingContext) { + if (bindingContext == null) + throw new ArgumentNullException(nameof(bindingContext)); + if (!(bindingContext.ActionContext.ActionDescriptor.Parameters.FirstOrDefault(p => p.Name == bindingContext.FieldName) is ControllerParameterDescriptor parameter)) return _simpleModelBinder.BindModelAsync(bindingContext); + + if (bindingContext.ModelType == typeof(string)) { + if (parameter.ParameterInfo.GetCustomAttributes(typeof(IpAddressAttribute), false).Any()) { + bindingContext.Result = ModelBindingResult.Success(bindingContext.HttpContext.Connection.RemoteIpAddress.ToString()); + return Task.CompletedTask; + } + + if (parameter.ParameterInfo.GetCustomAttributes(typeof(ContentTypeAttribute), false).Any()) { + string contentType = bindingContext.HttpContext.Request.Headers[HeaderNames.ContentType].ToString(); + bindingContext.Result = ModelBindingResult.Success(contentType); + return Task.CompletedTask; + } + + if (parameter.ParameterInfo.GetCustomAttributes(typeof(UserAgentAttribute), false).Any()) { + string userAgent; + if (bindingContext.HttpContext.Request.Headers.TryGetValue(Headers.Client, out var values) && values.Count > 0) + userAgent = values; + else + userAgent = bindingContext.HttpContext.Request.Headers[HeaderNames.UserAgent].ToString(); + bindingContext.Result = ModelBindingResult.Success(userAgent); + return Task.CompletedTask; + } + + if (parameter.ParameterInfo.GetCustomAttributes(typeof(ReferrerAttribute), false).Any()) { + string urlReferrer = bindingContext.HttpContext.Request.Headers[HeaderNames.Referer].ToString(); + bindingContext.Result = ModelBindingResult.Success(urlReferrer); + return Task.CompletedTask; + } } + else { + if (parameter.ParameterInfo.GetCustomAttributes(typeof(QueryStringParametersAttribute), false).Any()) { + var query = bindingContext.HttpContext.Request.Query; + bindingContext.Result = ModelBindingResult.Success(query); + return Task.CompletedTask; + } + } + + return _simpleModelBinder.BindModelAsync(bindingContext); } +} - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class IpAddressAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class IpAddressAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class UserAgentAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class UserAgentAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class ReferrerAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class ReferrerAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class QueryStringParametersAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class QueryStringParametersAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class ContentTypeAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class ContentTypeAttribute : Attribute { } - public class CustomAttributesModelBinderProvider : IModelBinderProvider { - public IModelBinder GetBinder(ModelBinderProviderContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); +public class CustomAttributesModelBinderProvider : IModelBinderProvider { + public IModelBinder GetBinder(ModelBinderProviderContext context) { + if (context == null) + throw new ArgumentNullException(nameof(context)); - if (context.Metadata.ModelType == typeof(string) || context.Metadata.ModelType == typeof(IQueryCollection)) - return new CustomAttributesModelBinder(context.Metadata.ModelType, context.Services.GetService()); + if (context.Metadata.ModelType == typeof(string) || context.Metadata.ModelType == typeof(IQueryCollection)) + return new CustomAttributesModelBinder(context.Metadata.ModelType, context.Services.GetService()); - return null; - } + return null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Bindings/DelimitedQueryStringValueProvider.cs b/src/Exceptionless.Web/Utility/Bindings/DelimitedQueryStringValueProvider.cs index 59ad53d28e..0111f623aa 100644 --- a/src/Exceptionless.Web/Utility/Bindings/DelimitedQueryStringValueProvider.cs +++ b/src/Exceptionless.Web/Utility/Bindings/DelimitedQueryStringValueProvider.cs @@ -1,100 +1,96 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using System.Globalization; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Primitives; -namespace Exceptionless.Web.Utility { - public class DelimitedQueryStringValueProvider : QueryStringValueProvider { - private readonly CultureInfo _culture; - private readonly char[] _delimiters; - private readonly IQueryCollection _queryCollection; +namespace Exceptionless.Web.Utility; - public DelimitedQueryStringValueProvider(BindingSource bindingSource, IQueryCollection values, CultureInfo culture, char[] delimiters) : base(bindingSource, values, culture) { - _queryCollection = values; - _culture = culture; - _delimiters = delimiters; - } +public class DelimitedQueryStringValueProvider : QueryStringValueProvider { + private readonly CultureInfo _culture; + private readonly char[] _delimiters; + private readonly IQueryCollection _queryCollection; - public char[] Delimiters => _delimiters; + public DelimitedQueryStringValueProvider(BindingSource bindingSource, IQueryCollection values, CultureInfo culture, char[] delimiters) : base(bindingSource, values, culture) { + _queryCollection = values; + _culture = culture; + _delimiters = delimiters; + } - public override ValueProviderResult GetValue(string key) { - if (key == null) - throw new ArgumentNullException(nameof(key)); + public char[] Delimiters => _delimiters; - var values = _queryCollection[key]; - if (values.Count == 0) - return ValueProviderResult.None; + public override ValueProviderResult GetValue(string key) { + if (key == null) + throw new ArgumentNullException(nameof(key)); - if (!values.Any(x => _delimiters.Any(x.Contains))) - return new ValueProviderResult(values, _culture); + var values = _queryCollection[key]; + if (values.Count == 0) + return ValueProviderResult.None; - var stringValues = new StringValues(values.SelectMany(x => x.Split(_delimiters, StringSplitOptions.RemoveEmptyEntries)).ToArray()); + if (!values.Any(x => _delimiters.Any(x.Contains))) + return new ValueProviderResult(values, _culture); - return new ValueProviderResult(stringValues, _culture); - } + var stringValues = new StringValues(values.SelectMany(x => x.Split(_delimiters, StringSplitOptions.RemoveEmptyEntries)).ToArray()); + + return new ValueProviderResult(stringValues, _culture); } +} - public class DelimitedQueryStringValueProviderFactory : IValueProviderFactory { - private static readonly char[] DefaultDelimiters = { ',' }; - private readonly char[] _delimiters; +public class DelimitedQueryStringValueProviderFactory : IValueProviderFactory { + private static readonly char[] DefaultDelimiters = { ',' }; + private readonly char[] _delimiters; - public DelimitedQueryStringValueProviderFactory() : this(DefaultDelimiters) { } + public DelimitedQueryStringValueProviderFactory() : this(DefaultDelimiters) { } - public DelimitedQueryStringValueProviderFactory(params char[] delimiters) { - if (delimiters == null || delimiters.Length == 0) - _delimiters = DefaultDelimiters; - else - _delimiters = delimiters; - } + public DelimitedQueryStringValueProviderFactory(params char[] delimiters) { + if (delimiters == null || delimiters.Length == 0) + _delimiters = DefaultDelimiters; + else + _delimiters = delimiters; + } - public Task CreateValueProviderAsync(ValueProviderFactoryContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + public Task CreateValueProviderAsync(ValueProviderFactoryContext context) { + if (context == null) + throw new ArgumentNullException(nameof(context)); - var valueProvider = new DelimitedQueryStringValueProvider(BindingSource.Query, context.ActionContext.HttpContext.Request.Query, CultureInfo.InvariantCulture, _delimiters); + var valueProvider = new DelimitedQueryStringValueProvider(BindingSource.Query, context.ActionContext.HttpContext.Request.Query, CultureInfo.InvariantCulture, _delimiters); - context.ValueProviders.Add(valueProvider); + context.ValueProviders.Add(valueProvider); - return Task.CompletedTask; - } + return Task.CompletedTask; } +} - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] - public class DelimitedQueryStringAttribute : Attribute, IResourceFilter { - private readonly char[] _delimiters; +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public class DelimitedQueryStringAttribute : Attribute, IResourceFilter { + private readonly char[] _delimiters; - public DelimitedQueryStringAttribute(params char[] delimiters) { - _delimiters = delimiters; - } + public DelimitedQueryStringAttribute(params char[] delimiters) { + _delimiters = delimiters; + } - public void OnResourceExecuted(ResourceExecutedContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); - } + public void OnResourceExecuted(ResourceExecutedContext context) { + if (context == null) + throw new ArgumentNullException(nameof(context)); + } - public void OnResourceExecuting(ResourceExecutingContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + public void OnResourceExecuting(ResourceExecutingContext context) { + if (context == null) + throw new ArgumentNullException(nameof(context)); - context.ValueProviderFactories.AddDelimitedValueProviderFactory(_delimiters); - } + context.ValueProviderFactories.AddDelimitedValueProviderFactory(_delimiters); } +} - public static class ValueProviderFactoriesExtensions { - public static void AddDelimitedValueProviderFactory(this IList valueProviderFactories, char[] delimiters) { - var queryStringValueProviderFactory = valueProviderFactories.OfType().FirstOrDefault(); +public static class ValueProviderFactoriesExtensions { + public static void AddDelimitedValueProviderFactory(this IList valueProviderFactories, char[] delimiters) { + var queryStringValueProviderFactory = valueProviderFactories.OfType().FirstOrDefault(); - if (queryStringValueProviderFactory == null) { - valueProviderFactories.Insert(0, new DelimitedQueryStringValueProviderFactory(delimiters)); - } else { - valueProviderFactories.Insert(valueProviderFactories.IndexOf(queryStringValueProviderFactory), new DelimitedQueryStringValueProviderFactory(delimiters)); - valueProviderFactories.Remove(queryStringValueProviderFactory); - } + if (queryStringValueProviderFactory == null) { + valueProviderFactories.Insert(0, new DelimitedQueryStringValueProviderFactory(delimiters)); + } + else { + valueProviderFactories.Insert(valueProviderFactories.IndexOf(queryStringValueProviderFactory), new DelimitedQueryStringValueProviderFactory(delimiters)); + valueProviderFactories.Remove(queryStringValueProviderFactory); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/ConfigurationResponseFilterAttribute.cs b/src/Exceptionless.Web/Utility/ConfigurationResponseFilterAttribute.cs index 121bb5062f..724b9b5ffa 100644 --- a/src/Exceptionless.Web/Utility/ConfigurationResponseFilterAttribute.cs +++ b/src/Exceptionless.Web/Utility/ConfigurationResponseFilterAttribute.cs @@ -1,28 +1,26 @@ -using System; -using Exceptionless.Web.Extensions; -using Microsoft.AspNetCore.Http; +using Exceptionless.Web.Extensions; using Microsoft.AspNetCore.Mvc.Filters; -namespace Exceptionless.Web.Utility { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class ConfigurationResponseFilterAttribute : ActionFilterAttribute { - public override void OnActionExecuted(ActionExecutedContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); +namespace Exceptionless.Web.Utility; - if (context.HttpContext.Response == null || (context.HttpContext.Response.StatusCode != StatusCodes.Status200OK && context.HttpContext.Response.StatusCode != StatusCodes.Status202Accepted)) - return; +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class ConfigurationResponseFilterAttribute : ActionFilterAttribute { + public override void OnActionExecuted(ActionExecutedContext context) { + if (context == null) + throw new ArgumentNullException(nameof(context)); - var project = context.HttpContext.Request?.GetProject(); - if (project == null) - return; + if (context.HttpContext.Response == null || (context.HttpContext.Response.StatusCode != StatusCodes.Status200OK && context.HttpContext.Response.StatusCode != StatusCodes.Status202Accepted)) + return; - string headerName = Headers.ConfigurationVersion; - if (context.HttpContext.Request.Path.Value.StartsWith("/api/v1")) - headerName = Headers.LegacyConfigurationVersion; + var project = context.HttpContext.Request?.GetProject(); + if (project == null) + return; - // add the current configuration version to the response headers so the client will know if it should update its config. - context.HttpContext.Response.Headers.Add(headerName, project.Configuration.Version.ToString()); - } + string headerName = Headers.ConfigurationVersion; + if (context.HttpContext.Request.Path.Value.StartsWith("/api/v1")) + headerName = Headers.LegacyConfigurationVersion; + + // add the current configuration version to the response headers so the client will know if it should update its config. + context.HttpContext.Response.Headers.Add(headerName, project.Configuration.Version.ToString()); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Constraints/IdentifierRouteConstraint.cs b/src/Exceptionless.Web/Utility/Constraints/IdentifierRouteConstraint.cs index a2858f4280..4c5cceb8ca 100644 --- a/src/Exceptionless.Web/Utility/Constraints/IdentifierRouteConstraint.cs +++ b/src/Exceptionless.Web/Utility/Constraints/IdentifierRouteConstraint.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Routing.Constraints; -namespace Exceptionless.Web.Utility { - public class IdentifierRouteConstraint : RegexRouteConstraint { - public IdentifierRouteConstraint() : base(@"^[a-zA-Z\d-]{8,100}$") { } - } -} \ No newline at end of file +namespace Exceptionless.Web.Utility; + +public class IdentifierRouteConstraint : RegexRouteConstraint { + public IdentifierRouteConstraint() : base(@"^[a-zA-Z\d-]{8,100}$") { } +} diff --git a/src/Exceptionless.Web/Utility/Constraints/IdentifiersRouteConstraint.cs b/src/Exceptionless.Web/Utility/Constraints/IdentifiersRouteConstraint.cs index 7ec8ef25b9..e916d389d5 100644 --- a/src/Exceptionless.Web/Utility/Constraints/IdentifiersRouteConstraint.cs +++ b/src/Exceptionless.Web/Utility/Constraints/IdentifiersRouteConstraint.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Routing.Constraints; -namespace Exceptionless.Web.Utility { - public class IdentifiersRouteConstraint : RegexRouteConstraint { - public IdentifiersRouteConstraint() : base(@"^[a-zA-Z\d-]{8,100}(,[a-zA-Z\d-]{8,100})*$") { } - } -} \ No newline at end of file +namespace Exceptionless.Web.Utility; + +public class IdentifiersRouteConstraint : RegexRouteConstraint { + public IdentifiersRouteConstraint() : base(@"^[a-zA-Z\d-]{8,100}(,[a-zA-Z\d-]{8,100})*$") { } +} diff --git a/src/Exceptionless.Web/Utility/Constraints/ObjectIdRouteConstraint.cs b/src/Exceptionless.Web/Utility/Constraints/ObjectIdRouteConstraint.cs index f283e4db27..1d1969e588 100644 --- a/src/Exceptionless.Web/Utility/Constraints/ObjectIdRouteConstraint.cs +++ b/src/Exceptionless.Web/Utility/Constraints/ObjectIdRouteConstraint.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Routing.Constraints; -namespace Exceptionless.Web.Utility { - public class ObjectIdRouteConstraint : RegexRouteConstraint { - public ObjectIdRouteConstraint() : base(@"^[a-zA-Z\d]{24,36}$") {} - } -} \ No newline at end of file +namespace Exceptionless.Web.Utility; + +public class ObjectIdRouteConstraint : RegexRouteConstraint { + public ObjectIdRouteConstraint() : base(@"^[a-zA-Z\d]{24,36}$") { } +} diff --git a/src/Exceptionless.Web/Utility/Constraints/ObjectIdsRouteConstraint.cs b/src/Exceptionless.Web/Utility/Constraints/ObjectIdsRouteConstraint.cs index dad5b217e6..7b93c90f06 100644 --- a/src/Exceptionless.Web/Utility/Constraints/ObjectIdsRouteConstraint.cs +++ b/src/Exceptionless.Web/Utility/Constraints/ObjectIdsRouteConstraint.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Routing.Constraints; -namespace Exceptionless.Web.Utility { - public class ObjectIdsRouteConstraint : RegexRouteConstraint { - public ObjectIdsRouteConstraint() : base(@"^[a-zA-Z\d]{24,36}(,[a-zA-Z\d]{24,36})*$") { } - } -} \ No newline at end of file +namespace Exceptionless.Web.Utility; + +public class ObjectIdsRouteConstraint : RegexRouteConstraint { + public ObjectIdsRouteConstraint() : base(@"^[a-zA-Z\d]{24,36}(,[a-zA-Z\d]{24,36})*$") { } +} diff --git a/src/Exceptionless.Web/Utility/Constraints/TokenRouteConstraint.cs b/src/Exceptionless.Web/Utility/Constraints/TokenRouteConstraint.cs index f8469e4ea1..2aea34a1ae 100644 --- a/src/Exceptionless.Web/Utility/Constraints/TokenRouteConstraint.cs +++ b/src/Exceptionless.Web/Utility/Constraints/TokenRouteConstraint.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Routing.Constraints; -namespace Exceptionless.Web.Utility { - public class TokenRouteConstraint : RegexRouteConstraint { - public TokenRouteConstraint() : base(@"^[a-zA-Z\d-]{24,40}$") { } - } -} \ No newline at end of file +namespace Exceptionless.Web.Utility; + +public class TokenRouteConstraint : RegexRouteConstraint { + public TokenRouteConstraint() : base(@"^[a-zA-Z\d-]{24,40}$") { } +} diff --git a/src/Exceptionless.Web/Utility/Constraints/TokensRouteConstraint.cs b/src/Exceptionless.Web/Utility/Constraints/TokensRouteConstraint.cs index f3ef2a5494..91ee79263f 100644 --- a/src/Exceptionless.Web/Utility/Constraints/TokensRouteConstraint.cs +++ b/src/Exceptionless.Web/Utility/Constraints/TokensRouteConstraint.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Routing.Constraints; -namespace Exceptionless.Web.Utility { - public class TokensRouteConstraint : RegexRouteConstraint { - public TokensRouteConstraint() : base(@"^[a-zA-Z\d-]{24,40}(,[a-zA-Z\d-]{24,40})*$") { } - } -} \ No newline at end of file +namespace Exceptionless.Web.Utility; + +public class TokensRouteConstraint : RegexRouteConstraint { + public TokensRouteConstraint() : base(@"^[a-zA-Z\d-]{24,40}(,[a-zA-Z\d-]{24,40})*$") { } +} diff --git a/src/Exceptionless.Web/Utility/Delta/Delta.cs b/src/Exceptionless.Web/Utility/Delta/Delta.cs index 7d96633d6d..27ef2b3066 100644 --- a/src/Exceptionless.Web/Utility/Delta/Delta.cs +++ b/src/Exceptionless.Web/Utility/Delta/Delta.cs @@ -1,351 +1,350 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Dynamic; -using System.Linq; using Exceptionless.Core.Extensions; using Exceptionless.Core.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Exceptionless.Web.Utility { +namespace Exceptionless.Web.Utility; + +/// +/// A class the tracks changes (i.e. the Delta) for a particular . +/// +/// TEntityType is the base type of entity this delta tracks changes for. +public class Delta : DynamicObject /*, IDelta */ where TEntityType : class { + // cache property accessors for this type and all its derived types. + private static readonly ConcurrentDictionary> _propertyCache = new ConcurrentDictionary>(); + + private Dictionary _propertiesThatExist; + private readonly Dictionary _unknownProperties = new Dictionary(); + private HashSet _changedProperties; + private TEntityType _entity; + private Type _entityType; + /// - /// A class the tracks changes (i.e. the Delta) for a particular . + /// Initializes a new instance of . /// - /// TEntityType is the base type of entity this delta tracks changes for. - public class Delta : DynamicObject /*, IDelta */ where TEntityType : class { - // cache property accessors for this type and all its derived types. - private static readonly ConcurrentDictionary> _propertyCache = new ConcurrentDictionary>(); - - private Dictionary _propertiesThatExist; - private readonly Dictionary _unknownProperties = new Dictionary(); - private HashSet _changedProperties; - private TEntityType _entity; - private Type _entityType; - - /// - /// Initializes a new instance of . - /// - public Delta() : this(typeof(TEntityType)) {} - - /// - /// Initializes a new instance of . - /// - /// - /// The derived entity type for which the changes would be tracked. - /// should be assignable to instances of . - /// - public Delta(Type entityType) { - Initialize(entityType); - } + public Delta() : this(typeof(TEntityType)) { } + + /// + /// Initializes a new instance of . + /// + /// + /// The derived entity type for which the changes would be tracked. + /// should be assignable to instances of . + /// + public Delta(Type entityType) { + Initialize(entityType); + } - /// - /// The actual type of the entity for which the changes are tracked. - /// - public Type EntityType => _entityType; + /// + /// The actual type of the entity for which the changes are tracked. + /// + public Type EntityType => _entityType; - /// - /// Clears the Delta and resets the underlying Entity. - /// - public void Clear() { - Initialize(_entityType); - } + /// + /// Clears the Delta and resets the underlying Entity. + /// + public void Clear() { + Initialize(_entityType); + } - /// - /// Attempts to set the Property called to the specified. - /// - /// Only properties that exist on can be set. - /// If there is a type mismatch the request will fail. - /// - /// - /// The name of the Property - /// The new value of the Property - /// The target entity to set the value on - /// True if successful - public bool TrySetPropertyValue(string name, object value, TEntityType target = null) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - if (!_propertiesThatExist.ContainsKey(name)) - return false; + /// + /// Attempts to set the Property called to the specified. + /// + /// Only properties that exist on can be set. + /// If there is a type mismatch the request will fail. + /// + /// + /// The name of the Property + /// The new value of the Property + /// The target entity to set the value on + /// True if successful + public bool TrySetPropertyValue(string name, object value, TEntityType target = null) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (!_propertiesThatExist.ContainsKey(name)) + return false; - var cacheHit = _propertiesThatExist[name]; + var cacheHit = _propertiesThatExist[name]; - if (value == null && !IsNullable(cacheHit.MemberType)) - return false; - - if (value != null) { - if (value is JToken) { - try { - value = JsonConvert.DeserializeObject(value.ToString(), cacheHit.MemberType); - } catch (Exception) { - return false; - } - } else { - bool isGuid = cacheHit.MemberType == typeof(Guid) && value is string; - bool isEnum = cacheHit.MemberType.IsEnum && value is Int64 && (long)value <= Int32.MaxValue; - bool isInt32 = cacheHit.MemberType == typeof(int) && value is Int64 && (long)value <= Int32.MaxValue; - - if (!cacheHit.MemberType.IsPrimitive && !isGuid && !isEnum && !cacheHit.MemberType.IsInstanceOfType(value)) - return false; - - if (isGuid) - value = new Guid((string)value); - if (isInt32) - value = (int)(long)value; - if (isEnum) - value = Enum.Parse(cacheHit.MemberType, value.ToString()); + if (value == null && !IsNullable(cacheHit.MemberType)) + return false; + + if (value != null) { + if (value is JToken) { + try { + value = JsonConvert.DeserializeObject(value.ToString(), cacheHit.MemberType); + } + catch (Exception) { + return false; } } + else { + bool isGuid = cacheHit.MemberType == typeof(Guid) && value is string; + bool isEnum = cacheHit.MemberType.IsEnum && value is Int64 && (long)value <= Int32.MaxValue; + bool isInt32 = cacheHit.MemberType == typeof(int) && value is Int64 && (long)value <= Int32.MaxValue; + + if (!cacheHit.MemberType.IsPrimitive && !isGuid && !isEnum && !cacheHit.MemberType.IsInstanceOfType(value)) + return false; + + if (isGuid) + value = new Guid((string)value); + if (isInt32) + value = (int)(long)value; + if (isEnum) + value = Enum.Parse(cacheHit.MemberType, value.ToString()); + } + } + + //.Setter.Invoke(_entity, new object[] { value }); + cacheHit.SetValue(_entity ?? target, value); + _changedProperties.Add(name); + return true; + } - //.Setter.Invoke(_entity, new object[] { value }); - cacheHit.SetValue(_entity ?? target, value); - _changedProperties.Add(name); + /// + /// Attempts to get the value of the Property called from the underlying Entity. + /// + /// Only properties that exist on can be retrieved. + /// Both modified and unmodified properties can be retrieved. + /// + /// + /// The name of the Property + /// The value of the Property + /// The target entity to get the value from + /// True if the Property was found + public bool TryGetPropertyValue(string name, out object value, TEntityType target = null) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (_propertiesThatExist.ContainsKey(name)) { + var cacheHit = _propertiesThatExist[name]; + value = cacheHit.GetValue(target ?? _entity); return true; } - /// - /// Attempts to get the value of the Property called from the underlying Entity. - /// - /// Only properties that exist on can be retrieved. - /// Both modified and unmodified properties can be retrieved. - /// - /// - /// The name of the Property - /// The value of the Property - /// The target entity to get the value from - /// True if the Property was found - public bool TryGetPropertyValue(string name, out object value, TEntityType target = null) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - if (_propertiesThatExist.ContainsKey(name)) { - var cacheHit = _propertiesThatExist[name]; - value = cacheHit.GetValue(target ?? _entity); - return true; - } + value = null; + return false; + } - value = null; - return false; + /// + /// Attempts to get the of the Property called from the underlying Entity. + /// + /// Only properties that exist on can be retrieved. + /// Both modified and unmodified properties can be retrieved. + /// + /// + /// The name of the Property + /// The type of the Property + /// Returns true if the Property was found and false if not. + public bool TryGetPropertyType(string name, out Type type) { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + if (_propertiesThatExist.TryGetValue(name, out var value)) { + type = value.MemberType; + return true; } - /// - /// Attempts to get the of the Property called from the underlying Entity. - /// - /// Only properties that exist on can be retrieved. - /// Both modified and unmodified properties can be retrieved. - /// - /// - /// The name of the Property - /// The type of the Property - /// Returns true if the Property was found and false if not. - public bool TryGetPropertyType(string name, out Type type) { - if (name == null) - throw new ArgumentNullException(nameof(name)); - - if (_propertiesThatExist.TryGetValue(name, out var value)) { - type = value.MemberType; - return true; - } + type = null; + return false; + } - type = null; - return false; - } + /// + /// A dictionary of values that were set on the delta that don't exist in TEntityType. + /// + public IDictionary UnknownProperties => _unknownProperties; - /// - /// A dictionary of values that were set on the delta that don't exist in TEntityType. - /// - public IDictionary UnknownProperties => _unknownProperties; - - /// - /// Overrides the DynamicObject TrySetMember method, so that only the properties - /// of can be set. - /// - public override bool TrySetMember(SetMemberBinder binder, object value) { - if (binder == null) - throw new ArgumentNullException(nameof(binder)); - - // add properties that don't exist to the unknown properties collect - if (!_propertiesThatExist.ContainsKey(binder.Name)) { - _unknownProperties[binder.Name] = value; - return true; - } + /// + /// Overrides the DynamicObject TrySetMember method, so that only the properties + /// of can be set. + /// + public override bool TrySetMember(SetMemberBinder binder, object value) { + if (binder == null) + throw new ArgumentNullException(nameof(binder)); - return TrySetPropertyValue(binder.Name, value); + // add properties that don't exist to the unknown properties collect + if (!_propertiesThatExist.ContainsKey(binder.Name)) { + _unknownProperties[binder.Name] = value; + return true; } - /// - /// Overrides the DynamicObject TryGetMember method, so that only the properties - /// of can be got. - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) { - if (binder == null) - throw new ArgumentNullException(nameof(binder)); + return TrySetPropertyValue(binder.Name, value); + } - return TryGetPropertyValue(binder.Name, out result); - } + /// + /// Overrides the DynamicObject TryGetMember method, so that only the properties + /// of can be got. + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) { + if (binder == null) + throw new ArgumentNullException(nameof(binder)); - /// - /// Returns the instance - /// that holds all the changes (and original values) being tracked by this Delta. - /// - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Not appropriate to be a property")] - public TEntityType GetEntity() { - return _entity; - } + return TryGetPropertyValue(binder.Name, out result); + } - /// - /// Returns the Properties that have been modified through this Delta as an - /// enumeration of Property Names - /// - public IEnumerable GetChangedPropertyNames() { - return _changedProperties; - } + /// + /// Returns the instance + /// that holds all the changes (and original values) being tracked by this Delta. + /// + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Not appropriate to be a property")] + public TEntityType GetEntity() { + return _entity; + } - /// - /// Returns the Properties that have been modified from their original values through this Delta as an - /// enumeration of Property Names - /// - public IEnumerable GetChangedPropertyNames(TEntityType original) { - if (original == null) - return _changedProperties; + /// + /// Returns the Properties that have been modified through this Delta as an + /// enumeration of Property Names + /// + public IEnumerable GetChangedPropertyNames() { + return _changedProperties; + } - var changedPropertyNames = new HashSet(); + /// + /// Returns the Properties that have been modified from their original values through this Delta as an + /// enumeration of Property Names + /// + public IEnumerable GetChangedPropertyNames(TEntityType original) { + if (original == null) + return _changedProperties; - foreach (string propertyName in _changedProperties) { - if (!TryGetPropertyValue(propertyName, out object originalValue, original)) - changedPropertyNames.Add(propertyName); + var changedPropertyNames = new HashSet(); - if (!TryGetPropertyValue(propertyName, out object newValue)) - continue; + foreach (string propertyName in _changedProperties) { + if (!TryGetPropertyValue(propertyName, out object originalValue, original)) + changedPropertyNames.Add(propertyName); - if (originalValue == null && newValue == null) - continue; + if (!TryGetPropertyValue(propertyName, out object newValue)) + continue; - if (newValue == null || !newValue.Equals(originalValue)) - changedPropertyNames.Add(propertyName); - } + if (originalValue == null && newValue == null) + continue; - return changedPropertyNames; + if (newValue == null || !newValue.Equals(originalValue)) + changedPropertyNames.Add(propertyName); } - /// - /// Returns the Properties that have not been modified through this Delta as an - /// enumeration of Property Names - /// - public IEnumerable GetUnchangedPropertyNames() { - return _propertiesThatExist.Keys.Except(GetChangedPropertyNames()); - } + return changedPropertyNames; + } - /// - /// Copies any changed property values that match up from the underlying entity (accessible via ) - /// to the entity. - /// - /// The target entity to be updated. - public void CopyChangedValues(object target) { - if (target == null) - throw new ArgumentNullException(nameof(target)); - - var targetType = target.GetType(); - if (!_propertyCache.ContainsKey(targetType)) - CachePropertyAccessors(targetType); - - var propertiesToCopy = GetChangedPropertyNames().Select(s => _propertiesThatExist[s]).ToArray(); - - foreach (var sourceProperty in propertiesToCopy) { - object value = sourceProperty.GetValue(_entity); - if (!_propertyCache[targetType].TryGetValue(sourceProperty.Name, out var targetAccessor)) - continue; - - if (!targetAccessor.MemberType.IsInstanceOfType(value)) - continue; - - targetAccessor.SetValue(target, value); - } + /// + /// Returns the Properties that have not been modified through this Delta as an + /// enumeration of Property Names + /// + public IEnumerable GetUnchangedPropertyNames() { + return _propertiesThatExist.Keys.Except(GetChangedPropertyNames()); + } + + /// + /// Copies any changed property values that match up from the underlying entity (accessible via ) + /// to the entity. + /// + /// The target entity to be updated. + public void CopyChangedValues(object target) { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + var targetType = target.GetType(); + if (!_propertyCache.ContainsKey(targetType)) + CachePropertyAccessors(targetType); + + var propertiesToCopy = GetChangedPropertyNames().Select(s => _propertiesThatExist[s]).ToArray(); + + foreach (var sourceProperty in propertiesToCopy) { + object value = sourceProperty.GetValue(_entity); + if (!_propertyCache[targetType].TryGetValue(sourceProperty.Name, out var targetAccessor)) + continue; + + if (!targetAccessor.MemberType.IsInstanceOfType(value)) + continue; + + targetAccessor.SetValue(target, value); } + } - /// - /// Copies the unchanged property values from the underlying entity (accessible via ) - /// to the entity. - /// - /// The entity to be updated. - public void CopyUnchangedValues(object target) { - if (target == null) - throw new ArgumentNullException(nameof(target)); + /// + /// Copies the unchanged property values from the underlying entity (accessible via ) + /// to the entity. + /// + /// The entity to be updated. + public void CopyUnchangedValues(object target) { + if (target == null) + throw new ArgumentNullException(nameof(target)); - var targetType = target.GetType(); - if (!_propertyCache.ContainsKey(targetType)) - CachePropertyAccessors(targetType); + var targetType = target.GetType(); + if (!_propertyCache.ContainsKey(targetType)) + CachePropertyAccessors(targetType); - var propertiesToCopy = GetUnchangedPropertyNames().Select(s => _propertiesThatExist[s]).ToArray(); + var propertiesToCopy = GetUnchangedPropertyNames().Select(s => _propertiesThatExist[s]).ToArray(); - foreach (var sourceProperty in propertiesToCopy) { - object value = sourceProperty.GetValue(_entity); - if (!_propertyCache[targetType].TryGetValue(sourceProperty.Name, out var targetAccessor)) - continue; + foreach (var sourceProperty in propertiesToCopy) { + object value = sourceProperty.GetValue(_entity); + if (!_propertyCache[targetType].TryGetValue(sourceProperty.Name, out var targetAccessor)) + continue; - if (!targetAccessor.MemberType.IsInstanceOfType(value)) - continue; + if (!targetAccessor.MemberType.IsInstanceOfType(value)) + continue; - targetAccessor.SetValue(target, value); - } + targetAccessor.SetValue(target, value); } + } - /// - /// Overwrites the entity with the changes tracked by this Delta. - /// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. - /// - /// The entity to be updated. - public void Patch(object target) { - CopyChangedValues(target); - } + /// + /// Overwrites the entity with the changes tracked by this Delta. + /// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. + /// + /// The entity to be updated. + public void Patch(object target) { + CopyChangedValues(target); + } - /// - /// Overwrites the entity with the values stored in this Delta. - /// The semantics of this operation are equivalent to a HTTP PUT operation, hence the name. - /// - /// The entity to be updated. - public void Put(object target) { - CopyChangedValues(target); - CopyUnchangedValues(target); - } + /// + /// Overwrites the entity with the values stored in this Delta. + /// The semantics of this operation are equivalent to a HTTP PUT operation, hence the name. + /// + /// The entity to be updated. + public void Put(object target) { + CopyChangedValues(target); + CopyUnchangedValues(target); + } - private void Initialize(Type entityType) { - if (entityType == null) - throw new ArgumentNullException(nameof(entityType)); + private void Initialize(Type entityType) { + if (entityType == null) + throw new ArgumentNullException(nameof(entityType)); - if (!typeof(TEntityType).IsAssignableFrom(entityType)) - throw new InvalidOperationException("Delta Entity Type Not Assignable"); + if (!typeof(TEntityType).IsAssignableFrom(entityType)) + throw new InvalidOperationException("Delta Entity Type Not Assignable"); - _entity = Activator.CreateInstance(entityType) as TEntityType; - _changedProperties = new HashSet(); - _entityType = entityType; - CachePropertyAccessors(entityType); - _propertiesThatExist = _propertyCache[entityType]; - } + _entity = Activator.CreateInstance(entityType) as TEntityType; + _changedProperties = new HashSet(); + _entityType = entityType; + CachePropertyAccessors(entityType); + _propertiesThatExist = _propertyCache[entityType]; + } - private void CachePropertyAccessors(Type type) { - _propertyCache.GetOrAdd(type, t => { - var properties = t.GetProperties() - .Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null) - .Select(LateBinder.GetPropertyAccessor).ToList(); + private void CachePropertyAccessors(Type type) { + _propertyCache.GetOrAdd(type, t => { + var properties = t.GetProperties() + .Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null) + .Select(LateBinder.GetPropertyAccessor).ToList(); - var items = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var p in properties) { - items[p.Name] = p; - items[p.Name.ToLowerUnderscoredWords()] = p; - } + var items = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var p in properties) { + items[p.Name] = p; + items[p.Name.ToLowerUnderscoredWords()] = p; + } - return items; - }); - } + return items; + }); + } - public static bool IsNullable(Type type) { - return !type.IsValueType || Nullable.GetUnderlyingType(type) != null; - } + public static bool IsNullable(Type type) { + return !type.IsValueType || Nullable.GetUnderlyingType(type) != null; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs b/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs index 0b53e72b8b..fe1fc47d3f 100644 --- a/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs +++ b/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs @@ -1,52 +1,52 @@ -using System; -using System.Threading.Tasks; -using Foundatio.Parsers.LuceneQueries; +using Foundatio.Parsers.LuceneQueries; using Foundatio.Parsers.LuceneQueries.Visitors; using Foundatio.Parsers.LuceneQueries.Nodes; -namespace Exceptionless.Web.Utility { - public class GetFilterScopeVisitor : QueryNodeVisitorWithResultBase { - private readonly FilterScope _scope = new FilterScope(); - private static readonly LuceneQueryParser _parser = new LuceneQueryParser(); +namespace Exceptionless.Web.Utility; - public override void Visit(TermNode node, IQueryVisitorContext context) { - if (String.IsNullOrEmpty(node.Field) || !_scope.IsScopable) - return; +public class GetFilterScopeVisitor : QueryNodeVisitorWithResultBase { + private readonly FilterScope _scope = new FilterScope(); + private static readonly LuceneQueryParser _parser = new LuceneQueryParser(); - if (node.Field.Equals("organization")) { - if (!_scope.HasScope) - _scope.OrganizationId = node.UnescapedTerm; - else // found dupe, mark filter as not scopable - _scope.IsScopable = false; - } else if (node.Field.Equals("project")) { - if (!_scope.HasScope) - _scope.ProjectId = node.UnescapedTerm; - else // found dupe, mark filter as not scopable - _scope.IsScopable = false; - } else if (node.Field.Equals("stack")) { - if (!_scope.HasScope) - _scope.StackId = node.UnescapedTerm; - else // found dupe, mark filter as not scopable - _scope.IsScopable = false; - } - } + public override void Visit(TermNode node, IQueryVisitorContext context) { + if (String.IsNullOrEmpty(node.Field) || !_scope.IsScopable) + return; - public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) { - node.AcceptAsync(this, context); - return Task.FromResult(_scope); + if (node.Field.Equals("organization")) { + if (!_scope.HasScope) + _scope.OrganizationId = node.UnescapedTerm; + else // found dupe, mark filter as not scopable + _scope.IsScopable = false; } - - public static FilterScope Run(string filter) { - var node = _parser.Parse(filter); - return new GetFilterScopeVisitor().AcceptAsync(node, null).GetAwaiter().GetResult(); + else if (node.Field.Equals("project")) { + if (!_scope.HasScope) + _scope.ProjectId = node.UnescapedTerm; + else // found dupe, mark filter as not scopable + _scope.IsScopable = false; + } + else if (node.Field.Equals("stack")) { + if (!_scope.HasScope) + _scope.StackId = node.UnescapedTerm; + else // found dupe, mark filter as not scopable + _scope.IsScopable = false; } } - public class FilterScope { - public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string StackId { get; set; } - public bool IsScopable { get; set; } = true; - public bool HasScope => OrganizationId != null || ProjectId != null || StackId != null; + public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) { + node.AcceptAsync(this, context); + return Task.FromResult(_scope); } + + public static FilterScope Run(string filter) { + var node = _parser.Parse(filter); + return new GetFilterScopeVisitor().AcceptAsync(node, null).GetAwaiter().GetResult(); + } +} + +public class FilterScope { + public string OrganizationId { get; set; } + public string ProjectId { get; set; } + public string StackId { get; set; } + public bool IsScopable { get; set; } = true; + public bool HasScope => OrganizationId != null || ProjectId != null || StackId != null; } diff --git a/src/Exceptionless.Web/Utility/Handlers/AllowSynchronousIOMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/AllowSynchronousIOMiddleware.cs index 7b378dd38c..904455c285 100644 --- a/src/Exceptionless.Web/Utility/Handlers/AllowSynchronousIOMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/AllowSynchronousIOMiddleware.cs @@ -1,21 +1,19 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Exceptionless.Web.Utility.Handlers { - public class AllowSynchronousIOMiddleware { - private readonly RequestDelegate _next; +namespace Exceptionless.Web.Utility.Handlers; - public AllowSynchronousIOMiddleware(RequestDelegate next) { - _next = next; - } +public class AllowSynchronousIOMiddleware { + private readonly RequestDelegate _next; - public Task Invoke(HttpContext context) { - var syncIOFeature = context.Features.Get(); - if (syncIOFeature != null) - syncIOFeature.AllowSynchronousIO = true; + public AllowSynchronousIOMiddleware(RequestDelegate next) { + _next = next; + } + + public Task Invoke(HttpContext context) { + var syncIOFeature = context.Features.Get(); + if (syncIOFeature != null) + syncIOFeature.AllowSynchronousIO = true; - return _next(context); - } + return _next(context); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Handlers/ApiError.cs b/src/Exceptionless.Web/Utility/Handlers/ApiError.cs index 5d9718c54b..2d474a3974 100644 --- a/src/Exceptionless.Web/Utility/Handlers/ApiError.cs +++ b/src/Exceptionless.Web/Utility/Handlers/ApiError.cs @@ -1,45 +1,43 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation; +using FluentValidation; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Exceptionless.Web.Utility.Handlers { - public class ApiError { - public string Message { get; set; } - public string ReferenceId { get; set; } - public bool IsError => true; - public string Detail { get; set; } - public ICollection Errors { get; set; } +namespace Exceptionless.Web.Utility.Handlers; - public ApiError(string message, string referenceId) { - Message = message; - ReferenceId = referenceId; - Errors = new List(); - } +public class ApiError { + public string Message { get; set; } + public string ReferenceId { get; set; } + public bool IsError => true; + public string Detail { get; set; } + public ICollection Errors { get; set; } + + public ApiError(string message, string referenceId) { + Message = message; + ReferenceId = referenceId; + Errors = new List(); + } - public ApiError(ModelStateDictionary modelState) { - if (modelState != null && modelState.Any(m => m.Value.Errors.Count > 0)) { - Message = "Please correct the specified errors and try again."; - //errors = modelState.SelectMany(m => m.Value.Errors).ToDictionary(m => m.Key, m=> m.ErrorMessage); - //errors = modelState.SelectMany(m => m.Value.Errors.Select( me => new KeyValuePair( m.Key,me.ErrorMessage) )); - //errors = modelState.SelectMany(m => m.Value.Errors.Select(me => new ModelError { FieldName = m.Key, ErrorMessage = me.ErrorMessage })); - } - } - public ApiError(ValidationException ex, string referenceId) { + public ApiError(ModelStateDictionary modelState) { + if (modelState != null && modelState.Any(m => m.Value.Errors.Count > 0)) { Message = "Please correct the specified errors and try again."; - ReferenceId = referenceId; - Errors = ex.Errors.Select(error => new ApiErrorItem { - PropertyName = error.PropertyName, - Message = error.ErrorMessage, - AttemptedValue = error.AttemptedValue - }).ToList(); + //errors = modelState.SelectMany(m => m.Value.Errors).ToDictionary(m => m.Key, m=> m.ErrorMessage); + //errors = modelState.SelectMany(m => m.Value.Errors.Select( me => new KeyValuePair( m.Key,me.ErrorMessage) )); + //errors = modelState.SelectMany(m => m.Value.Errors.Select(me => new ModelError { FieldName = m.Key, ErrorMessage = me.ErrorMessage })); } } - - public class ApiErrorItem { - public string PropertyName { get; set; } - public string Message { get; set; } - public object AttemptedValue { get; set; } + public ApiError(ValidationException ex, string referenceId) { + Message = "Please correct the specified errors and try again."; + ReferenceId = referenceId; + Errors = ex.Errors.Select(error => new ApiErrorItem { + PropertyName = error.PropertyName, + Message = error.ErrorMessage, + AttemptedValue = error.AttemptedValue + }).ToList(); } } + +public class ApiErrorItem { + public string PropertyName { get; set; } + public string Message { get; set; } + public object AttemptedValue { get; set; } +} diff --git a/src/Exceptionless.Web/Utility/Handlers/ApiException.cs b/src/Exceptionless.Web/Utility/Handlers/ApiException.cs index 55fd171944..3a6052e2c0 100644 --- a/src/Exceptionless.Web/Utility/Handlers/ApiException.cs +++ b/src/Exceptionless.Web/Utility/Handlers/ApiException.cs @@ -1,19 +1,15 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; +namespace Exceptionless.Web.Utility.Handlers; -namespace Exceptionless.Web.Utility.Handlers { - public class ApiException : Exception { - public ApiException(string message, int statusCode = StatusCodes.Status500InternalServerError, ICollection errors = null) : base(message) { - StatusCode = statusCode; - Errors = errors; - } - - public ApiException(Exception ex, int statusCode = StatusCodes.Status500InternalServerError) : base(ex.Message) { - StatusCode = statusCode; - } +public class ApiException : Exception { + public ApiException(string message, int statusCode = StatusCodes.Status500InternalServerError, ICollection errors = null) : base(message) { + StatusCode = statusCode; + Errors = errors; + } - public int StatusCode { get; set; } - public ICollection Errors { get; set; } + public ApiException(Exception ex, int statusCode = StatusCodes.Status500InternalServerError) : base(ex.Message) { + StatusCode = statusCode; } -} \ No newline at end of file + + public int StatusCode { get; set; } + public ICollection Errors { get; set; } +} diff --git a/src/Exceptionless.Web/Utility/Handlers/ApiExceptionFilter.cs b/src/Exceptionless.Web/Utility/Handlers/ApiExceptionFilter.cs index fb6a4346f8..9d620c0a4b 100644 --- a/src/Exceptionless.Web/Utility/Handlers/ApiExceptionFilter.cs +++ b/src/Exceptionless.Web/Utility/Handlers/ApiExceptionFilter.cs @@ -1,66 +1,67 @@ -using System; -using Exceptionless.Plugins; +using Exceptionless.Plugins; using FluentValidation; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Logging; -namespace Exceptionless.Web.Utility.Handlers { - public class ApiExceptionFilter : ExceptionFilterAttribute { - private readonly ILogger _logger; +namespace Exceptionless.Web.Utility.Handlers; - public ApiExceptionFilter(ILoggerFactory loggerFactory) { - _logger = loggerFactory.CreateLogger(); - } - - public override void OnException(ExceptionContext context) { - var contextData = new ContextData(); - contextData.MarkAsUnhandledError(); - contextData.SetSubmissionMethod(nameof(ApiExceptionFilter)); - var builder = context.Exception.ToExceptionless(contextData).SetHttpContext(context.HttpContext); - builder.Submit(); +public class ApiExceptionFilter : ExceptionFilterAttribute { + private readonly ILogger _logger; + + public ApiExceptionFilter(ILoggerFactory loggerFactory) { + _logger = loggerFactory.CreateLogger(); + } - // TODO: pull the reference id using the reference id manager. - string referenceId = builder.Target.ReferenceId; - using (_logger.BeginScope(new ExceptionlessState().Property("Reference", referenceId))) { - _logger.LogError(context.Exception, "Unhandled error: {Message}", context.Exception.Message); - } + public override void OnException(ExceptionContext context) { + var contextData = new ContextData(); + contextData.MarkAsUnhandledError(); + contextData.SetSubmissionMethod(nameof(ApiExceptionFilter)); + var builder = context.Exception.ToExceptionless(contextData).SetHttpContext(context.HttpContext); + builder.Submit(); - ApiError apiError; - int statusCode = StatusCodes.Status500InternalServerError; + // TODO: pull the reference id using the reference id manager. + string referenceId = builder.Target.ReferenceId; + using (_logger.BeginScope(new ExceptionlessState().Property("Reference", referenceId))) { + _logger.LogError(context.Exception, "Unhandled error: {Message}", context.Exception.Message); + } - if (context.Exception is ApiException apiException) { - apiError = new ApiError(apiException.Message, referenceId) { - Errors = apiException.Errors - }; + ApiError apiError; + int statusCode = StatusCodes.Status500InternalServerError; - statusCode = apiException.StatusCode; - } else if (context.Exception is UnauthorizedAccessException unauthorizedAccessException) { - apiError = new ApiError(unauthorizedAccessException.Message, referenceId); - statusCode = StatusCodes.Status401Unauthorized; - } else if (context.Exception is ValidationException validationException) { - apiError = new ApiError(validationException, referenceId); - statusCode = StatusCodes.Status400BadRequest; - } else if (context.Exception is ApplicationException applicationException && applicationException.Message.Contains("version_conflict")) { - apiError = new ApiError(applicationException.Message, referenceId); - statusCode = StatusCodes.Status400BadRequest; - } else { + if (context.Exception is ApiException apiException) { + apiError = new ApiError(apiException.Message, referenceId) { + Errors = apiException.Errors + }; + + statusCode = apiException.StatusCode; + } + else if (context.Exception is UnauthorizedAccessException unauthorizedAccessException) { + apiError = new ApiError(unauthorizedAccessException.Message, referenceId); + statusCode = StatusCodes.Status401Unauthorized; + } + else if (context.Exception is ValidationException validationException) { + apiError = new ApiError(validationException, referenceId); + statusCode = StatusCodes.Status400BadRequest; + } + else if (context.Exception is ApplicationException applicationException && applicationException.Message.Contains("version_conflict")) { + apiError = new ApiError(applicationException.Message, referenceId); + statusCode = StatusCodes.Status400BadRequest; + } + else { #if DEBUG - apiError = new ApiError(context.Exception.GetBaseException().Message, referenceId) { - Detail = context.Exception.StackTrace - }; + apiError = new ApiError(context.Exception.GetBaseException().Message, referenceId) { + Detail = context.Exception.StackTrace + }; #else apiError = new ApiError("An error occurred while serving your request.", referenceId); #endif - } + } - context.Result = new ObjectResult(apiError) { - StatusCode = statusCode - }; + context.Result = new ObjectResult(apiError) { + StatusCode = statusCode + }; - base.OnException(context); - } + base.OnException(context); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs index 6cb1031bf6..7817d1793c 100644 --- a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Web.Extensions; +using Exceptionless.Web.Extensions; using Exceptionless.Core; using Exceptionless.Core.AppStats; using Exceptionless.Core.Extensions; @@ -8,105 +6,103 @@ using Exceptionless.Core.Services; using Foundatio.Metrics; using Foundatio.Repositories; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Exceptionless.Web.Utility { - public sealed class OverageMiddleware { - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly UsageService _usageService; - private readonly IMetricsClient _metricsClient; - private readonly AppOptions _appOptions; - private readonly ILogger _logger; - private readonly RequestDelegate _next; - - public OverageMiddleware(RequestDelegate next, IOrganizationRepository organizationRepository, IProjectRepository projectRepository, UsageService usageService, IMetricsClient metricsClient, AppOptions appOptions, ILogger logger) { - _next = next; - _organizationRepository = organizationRepository; - _projectRepository = projectRepository; - _usageService = usageService; - _metricsClient = metricsClient; - _appOptions = appOptions; - _logger = logger; - } - private bool IsEventPost(HttpContext context) { - string method = context.Request.Method; - if (String.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) - return context.Request.Path.Value.Contains("/events/submit"); +namespace Exceptionless.Web.Utility; + +public sealed class OverageMiddleware { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly UsageService _usageService; + private readonly IMetricsClient _metricsClient; + private readonly AppOptions _appOptions; + private readonly ILogger _logger; + private readonly RequestDelegate _next; + + public OverageMiddleware(RequestDelegate next, IOrganizationRepository organizationRepository, IProjectRepository projectRepository, UsageService usageService, IMetricsClient metricsClient, AppOptions appOptions, ILogger logger) { + _next = next; + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _usageService = usageService; + _metricsClient = metricsClient; + _appOptions = appOptions; + _logger = logger; + } - if (!String.Equals(method, "POST", StringComparison.OrdinalIgnoreCase)) - return false; + private bool IsEventPost(HttpContext context) { + string method = context.Request.Method; + if (String.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) + return context.Request.Path.Value.Contains("/events/submit"); - string absolutePath = context.Request.Path.Value; - if (absolutePath.EndsWith("/")) - absolutePath = absolutePath.Substring(0, absolutePath.Length - 1); + if (!String.Equals(method, "POST", StringComparison.OrdinalIgnoreCase)) + return false; - return absolutePath.EndsWith("/events", StringComparison.OrdinalIgnoreCase) - || String.Equals(absolutePath, "/api/v1/error", StringComparison.OrdinalIgnoreCase); - } + string absolutePath = context.Request.Path.Value; + if (absolutePath.EndsWith("/")) + absolutePath = absolutePath.Substring(0, absolutePath.Length - 1); - public async Task Invoke(HttpContext context) { - if (!IsEventPost(context)) { - await _next(context); - return; - } + return absolutePath.EndsWith("/events", StringComparison.OrdinalIgnoreCase) + || String.Equals(absolutePath, "/api/v1/error", StringComparison.OrdinalIgnoreCase); + } - if (_appOptions.EventSubmissionDisabled) { - context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - return; - } + public async Task Invoke(HttpContext context) { + if (!IsEventPost(context)) { + await _next(context); + return; + } - string organizationId = context.Request.GetDefaultOrganizationId(); - var organizationTask = _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); + if (_appOptions.EventSubmissionDisabled) { + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + return; + } - string projectId = context.Request.GetDefaultProjectId(); - var projectTask = _projectRepository.GetByIdAsync(projectId, o => o.Cache()); + string organizationId = context.Request.GetDefaultOrganizationId(); + var organizationTask = _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); - bool tooBig = false; - if (String.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && context.Request.Headers != null) { - if (context.Request.Headers.ContentLength.HasValue && context.Request.Headers.ContentLength.Value <= 0) { - //_metricsClient.Counter(MetricNames.PostsBlocked); - context.Response.StatusCode = StatusCodes.Status411LengthRequired; - await Task.WhenAll(organizationTask, projectTask); - return; - } + string projectId = context.Request.GetDefaultProjectId(); + var projectTask = _projectRepository.GetByIdAsync(projectId, o => o.Cache()); - long size = context.Request.Headers.ContentLength.GetValueOrDefault(); - if (size > 0) - _metricsClient.Gauge(MetricNames.PostsSize, size); + bool tooBig = false; + if (String.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && context.Request.Headers != null) { + if (context.Request.Headers.ContentLength.HasValue && context.Request.Headers.ContentLength.Value <= 0) { + //_metricsClient.Counter(MetricNames.PostsBlocked); + context.Response.StatusCode = StatusCodes.Status411LengthRequired; + await Task.WhenAll(organizationTask, projectTask); + return; + } - if (size > _appOptions.MaximumEventPostSize) { - if (_logger.IsEnabled(LogLevel.Warning)) { - using (_logger.BeginScope(new ExceptionlessState().Value(size).Tag(context.Request.Headers.TryGetAndReturn(Headers.ContentEncoding)))) - _logger.SubmissionTooLarge(size); - } + long size = context.Request.Headers.ContentLength.GetValueOrDefault(); + if (size > 0) + _metricsClient.Gauge(MetricNames.PostsSize, size); - _metricsClient.Counter(MetricNames.PostsDiscarded); - tooBig = true; + if (size > _appOptions.MaximumEventPostSize) { + if (_logger.IsEnabled(LogLevel.Warning)) { + using (_logger.BeginScope(new ExceptionlessState().Value(size).Tag(context.Request.Headers.TryGetAndReturn(Headers.ContentEncoding)))) + _logger.SubmissionTooLarge(size); } - } - var organization = await organizationTask; - var project = await projectTask; - bool overLimit = await _usageService.IncrementUsageAsync(organization, project, tooBig); - - // block large submissions, client should break them up or remove some of the data. - if (tooBig) { - context.Response.StatusCode = StatusCodes.Status413RequestEntityTooLarge; - return; + _metricsClient.Counter(MetricNames.PostsDiscarded); + tooBig = true; } + } - if (overLimit) { - _metricsClient.Counter(MetricNames.PostsBlocked); - context.Response.StatusCode = StatusCodes.Status402PaymentRequired; - return; - } + var organization = await organizationTask; + var project = await projectTask; + bool overLimit = await _usageService.IncrementUsageAsync(organization, project, tooBig); - context.Request.SetOrganization(organization); - context.Request.SetProject(project); - await _next(context); + // block large submissions, client should break them up or remove some of the data. + if (tooBig) { + context.Response.StatusCode = StatusCodes.Status413RequestEntityTooLarge; + return; } + + if (overLimit) { + _metricsClient.Counter(MetricNames.PostsBlocked); + context.Response.StatusCode = StatusCodes.Status402PaymentRequired; + return; + } + + context.Request.SetOrganization(organization); + context.Request.SetProject(project); + await _next(context); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Handlers/ProjectConfigMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/ProjectConfigMiddleware.cs index f3080e3020..d15891f0b4 100644 --- a/src/Exceptionless.Web/Utility/Handlers/ProjectConfigMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/ProjectConfigMiddleware.cs @@ -1,63 +1,60 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Repositories; +using Exceptionless.Core.Repositories; using Exceptionless.Web.Extensions; using Foundatio.Repositories; using Foundatio.Serializer; -using Microsoft.AspNetCore.Http; - -namespace Exceptionless.Web.Utility { - public sealed class ProjectConfigMiddleware { - private readonly IProjectRepository _projectRepository; - private readonly ITextSerializer _serializer; - private readonly RequestDelegate _next; - private static readonly PathString _v1Path = new PathString("/api/v1/project/config"); - private static readonly PathString _v2Path = new PathString("/api/v2/projects/config"); - - public ProjectConfigMiddleware(RequestDelegate next, IProjectRepository projectRepository, ITextSerializer serializer) { - _next = next; - _projectRepository = projectRepository; - _serializer = serializer; - } - private bool IsProjectConfigRoute(HttpContext context) { - if (!context.Request.Method.Equals(HttpMethods.Get, StringComparison.Ordinal)) - return false; +namespace Exceptionless.Web.Utility; + +public sealed class ProjectConfigMiddleware { + private readonly IProjectRepository _projectRepository; + private readonly ITextSerializer _serializer; + private readonly RequestDelegate _next; + private static readonly PathString _v1Path = new PathString("/api/v1/project/config"); + private static readonly PathString _v2Path = new PathString("/api/v2/projects/config"); - if (context.Request.Path.StartsWithSegments(_v2Path, StringComparison.Ordinal) - || context.Request.Path.StartsWithSegments(_v1Path, StringComparison.Ordinal)) - return true; + public ProjectConfigMiddleware(RequestDelegate next, IProjectRepository projectRepository, ITextSerializer serializer) { + _next = next; + _projectRepository = projectRepository; + _serializer = serializer; + } + private bool IsProjectConfigRoute(HttpContext context) { + if (!context.Request.Method.Equals(HttpMethods.Get, StringComparison.Ordinal)) return false; + + if (context.Request.Path.StartsWithSegments(_v2Path, StringComparison.Ordinal) + || context.Request.Path.StartsWithSegments(_v1Path, StringComparison.Ordinal)) + return true; + + return false; + } + + public async Task Invoke(HttpContext context) { + if (!IsProjectConfigRoute(context)) { + await _next(context); + return; + } + + string projectId = context.Request.GetDefaultProjectId(); + if (String.IsNullOrEmpty(projectId)) { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; } - public async Task Invoke(HttpContext context) { - if (!IsProjectConfigRoute(context)) { - await _next(context); - return; - } - - string projectId = context.Request.GetDefaultProjectId(); - if (String.IsNullOrEmpty(projectId)) { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - return; - } - - var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache()); - if (project == null) { - context.Response.StatusCode = StatusCodes.Status404NotFound; - return; - } - - if (context.Request.Query.TryGetValue("v", out var v) && Int32.TryParse(v, out int version) && version == project.Configuration.Version) { - context.Response.StatusCode = StatusCodes.Status304NotModified; - return; - } - - string json = _serializer.SerializeToString(project.Configuration); - context.Response.StatusCode = StatusCodes.Status200OK; - context.Response.ContentType = "application/json; charset=utf-8"; - await context.Response.WriteAsync(json); + var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache()); + if (project == null) { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; } + + if (context.Request.Query.TryGetValue("v", out var v) && Int32.TryParse(v, out int version) && version == project.Configuration.Version) { + context.Response.StatusCode = StatusCodes.Status304NotModified; + return; + } + + string json = _serializer.SerializeToString(project.Configuration); + context.Response.StatusCode = StatusCodes.Status200OK; + context.Response.ContentType = "application/json; charset=utf-8"; + await context.Response.WriteAsync(json); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Handlers/RecordSessionHeartbeatMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/RecordSessionHeartbeatMiddleware.cs index 5bd5700c83..a881d23e2d 100644 --- a/src/Exceptionless.Web/Utility/Handlers/RecordSessionHeartbeatMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/RecordSessionHeartbeatMiddleware.cs @@ -1,71 +1,68 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Web.Extensions; +using Exceptionless.Web.Extensions; using Exceptionless.Core; using Exceptionless.Core.Extensions; using Foundatio.Caching; using Foundatio.Utility; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -namespace Exceptionless.Web.Utility { - public sealed class RecordSessionHeartbeatMiddleware { - private readonly ICacheClient _cache; - private readonly AppOptions _appOptions; - private readonly ILogger _logger; - private readonly RequestDelegate _next; - private static readonly PathString _heartbeatPath = new PathString("/api/v2/events/session/heartbeat"); +namespace Exceptionless.Web.Utility; - public RecordSessionHeartbeatMiddleware(RequestDelegate next, ICacheClient cache, AppOptions appOptions, ILogger logger) { - _next = next; - _cache = cache; - _appOptions = appOptions; - _logger = logger; - } +public sealed class RecordSessionHeartbeatMiddleware { + private readonly ICacheClient _cache; + private readonly AppOptions _appOptions; + private readonly ILogger _logger; + private readonly RequestDelegate _next; + private static readonly PathString _heartbeatPath = new PathString("/api/v2/events/session/heartbeat"); - private bool IsHeartbeatRoute(HttpContext context) { - if (!context.Request.Method.Equals(HttpMethods.Get, StringComparison.Ordinal)) - return false; + public RecordSessionHeartbeatMiddleware(RequestDelegate next, ICacheClient cache, AppOptions appOptions, ILogger logger) { + _next = next; + _cache = cache; + _appOptions = appOptions; + _logger = logger; + } - return context.Request.Path.StartsWithSegments(_heartbeatPath, StringComparison.Ordinal); - } + private bool IsHeartbeatRoute(HttpContext context) { + if (!context.Request.Method.Equals(HttpMethods.Get, StringComparison.Ordinal)) + return false; - public async Task Invoke(HttpContext context) { - if (!IsHeartbeatRoute(context)) { - await _next(context); - return; - } + return context.Request.Path.StartsWithSegments(_heartbeatPath, StringComparison.Ordinal); + } - string projectId = context.Request.GetDefaultProjectId(); - if (String.IsNullOrEmpty(projectId)) { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - return; - } + public async Task Invoke(HttpContext context) { + if (!IsHeartbeatRoute(context)) { + await _next(context); + return; + } - if (_appOptions.EventSubmissionDisabled || !context.Request.Query.TryGetValue("id", out var id) || String.IsNullOrEmpty(id)) { - context.Response.StatusCode = StatusCodes.Status200OK; - return; - } + string projectId = context.Request.GetDefaultProjectId(); + if (String.IsNullOrEmpty(projectId)) { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } - string identityHash = id.ToSHA1(); - string heartbeatCacheKey = String.Concat("Project:", projectId, ":heartbeat:", identityHash); - bool close = context.Request.Query.TryGetValue("close", out var c) && Boolean.TryParse(c, out bool closed) && closed; - try { - await Task.WhenAll( - _cache.SetAsync(heartbeatCacheKey, SystemClock.UtcNow, TimeSpan.FromHours(2)), - close ? _cache.SetAsync(String.Concat(heartbeatCacheKey, "-close"), true, TimeSpan.FromHours(2)) : Task.CompletedTask - ); - } catch (Exception ex) { - if (projectId != _appOptions.InternalProjectId) { - using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Property("Id", id).Property("Close", close).SetHttpContext(context))) - _logger.LogError(ex, "Error enqueuing session heartbeat."); - } + if (_appOptions.EventSubmissionDisabled || !context.Request.Query.TryGetValue("id", out var id) || String.IsNullOrEmpty(id)) { + context.Response.StatusCode = StatusCodes.Status200OK; + return; + } - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - return; + string identityHash = id.ToSHA1(); + string heartbeatCacheKey = String.Concat("Project:", projectId, ":heartbeat:", identityHash); + bool close = context.Request.Query.TryGetValue("close", out var c) && Boolean.TryParse(c, out bool closed) && closed; + try { + await Task.WhenAll( + _cache.SetAsync(heartbeatCacheKey, SystemClock.UtcNow, TimeSpan.FromHours(2)), + close ? _cache.SetAsync(String.Concat(heartbeatCacheKey, "-close"), true, TimeSpan.FromHours(2)) : Task.CompletedTask + ); + } + catch (Exception ex) { + if (projectId != _appOptions.InternalProjectId) { + using (_logger.BeginScope(new ExceptionlessState().Project(projectId).Property("Id", id).Property("Close", close).SetHttpContext(context))) + _logger.LogError(ex, "Error enqueuing session heartbeat."); } - context.Response.StatusCode = StatusCodes.Status200OK; + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + return; } + + context.Response.StatusCode = StatusCodes.Status200OK; } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Handlers/ThrottlingMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/ThrottlingMiddleware.cs index f415401a72..3b877cdaee 100644 --- a/src/Exceptionless.Web/Utility/Handlers/ThrottlingMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/ThrottlingMiddleware.cs @@ -1,106 +1,104 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Web.Extensions; +using Exceptionless.Web.Extensions; using Exceptionless.Core.Extensions; using Exceptionless.DateTimeExtensions; using Foundatio.Caching; using Foundatio.Utility; -using Microsoft.AspNetCore.Http; -namespace Exceptionless.Web.Utility.Handlers { - public class ThrottlingOptions { - public Func MaxRequestsForUserIdentifierFunc { get; set; } - public TimeSpan Period { get; set; } - public string Message { get; set; } = "The allowed number of requests has been exceeded."; - } +namespace Exceptionless.Web.Utility.Handlers; + +public class ThrottlingOptions { + public Func MaxRequestsForUserIdentifierFunc { get; set; } + public TimeSpan Period { get; set; } + public string Message { get; set; } = "The allowed number of requests has been exceeded."; +} - public class ThrottlingMiddleware { - private readonly ICacheClient _cacheClient; - private readonly ThrottlingOptions _options; - private readonly RequestDelegate _next; - private static readonly PathString _v1ProjectConfigPath = new PathString("/api/v1/project/config"); - private static readonly PathString _v2ProjectConfigPath = new PathString("/api/v2/projects/config"); - private static readonly PathString _heartbeatPath = new PathString("/api/v2/events/session/heartbeat"); - private static readonly PathString _webSocketPath = new PathString("/api/v2/push"); +public class ThrottlingMiddleware { + private readonly ICacheClient _cacheClient; + private readonly ThrottlingOptions _options; + private readonly RequestDelegate _next; + private static readonly PathString _v1ProjectConfigPath = new PathString("/api/v1/project/config"); + private static readonly PathString _v2ProjectConfigPath = new PathString("/api/v2/projects/config"); + private static readonly PathString _heartbeatPath = new PathString("/api/v2/events/session/heartbeat"); + private static readonly PathString _webSocketPath = new PathString("/api/v2/push"); - public ThrottlingMiddleware(RequestDelegate next, ICacheClient cacheClient, ThrottlingOptions options) { - _next = next; - _cacheClient = cacheClient; - _options = options; + public ThrottlingMiddleware(RequestDelegate next, ICacheClient cacheClient, ThrottlingOptions options) { + _next = next; + _cacheClient = cacheClient; + _options = options; + } + + protected virtual string GetUserIdentifier(HttpRequest request) { + var authType = request.GetAuthType(); + if (authType == AuthType.Token) + return request.GetTokenOrganizationId(); + + if (authType == AuthType.User) { + var user = request.GetUser(); + if (user != null) + return user.Id; } - protected virtual string GetUserIdentifier(HttpRequest request) { - var authType = request.GetAuthType(); - if (authType == AuthType.Token) - return request.GetTokenOrganizationId(); + // fallback to using the IP address + string ip = request.GetClientIpAddress(); + if (String.IsNullOrEmpty(ip) || ip == "::1") + ip = "127.0.0.1"; - if (authType == AuthType.User) { - var user = request.GetUser(); - if (user != null) - return user.Id; - } + return ip; + } - // fallback to using the IP address - string ip = request.GetClientIpAddress(); - if (String.IsNullOrEmpty(ip) || ip == "::1") - ip = "127.0.0.1"; + private string GetCacheKey(string userIdentifier) { + return String.Concat("api:", userIdentifier, ":", SystemClock.UtcNow.Floor(_options.Period).Ticks); + } - return ip; + public async Task Invoke(HttpContext context) { + if (IsUnthrottledRoute(context)) { + await _next(context); + return; } - private string GetCacheKey(string userIdentifier) { - return String.Concat("api:", userIdentifier, ":", SystemClock.UtcNow.Floor(_options.Period).Ticks); + string identifier = GetUserIdentifier(context.Request); + if (String.IsNullOrEmpty(identifier)) { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return; } - public async Task Invoke(HttpContext context) { - if (IsUnthrottledRoute(context)) { - await _next(context); - return; - } - - string identifier = GetUserIdentifier(context.Request); - if (String.IsNullOrEmpty(identifier)) { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; - } - - long requestCount = 1; - try { - string cacheKey = GetCacheKey(identifier); - requestCount = await _cacheClient.IncrementAsync(cacheKey, 1); - if (requestCount == 1) - await _cacheClient.SetExpirationAsync(cacheKey, SystemClock.UtcNow.Ceiling(_options.Period)); - } catch { } - - long maxRequests = _options.MaxRequestsForUserIdentifierFunc(identifier); - if (requestCount > maxRequests) { - context.Response.StatusCode = StatusCodes.Status429TooManyRequests; - return; - } - - long remaining = maxRequests - requestCount; - if (remaining < 0) - remaining = 0; - - context.Response.OnStarting(() => { - context.Response.Headers.Add(Headers.RateLimit, maxRequests.ToString()); - context.Response.Headers.Add(Headers.RateLimitRemaining, remaining.ToString()); - - return Task.CompletedTask; - }); + long requestCount = 1; + try { + string cacheKey = GetCacheKey(identifier); + requestCount = await _cacheClient.IncrementAsync(cacheKey, 1); + if (requestCount == 1) + await _cacheClient.SetExpirationAsync(cacheKey, SystemClock.UtcNow.Ceiling(_options.Period)); + } + catch { } - await _next(context); + long maxRequests = _options.MaxRequestsForUserIdentifierFunc(identifier); + if (requestCount > maxRequests) { + context.Response.StatusCode = StatusCodes.Status429TooManyRequests; + return; } - private bool IsUnthrottledRoute(HttpContext context) { - if (!context.Request.Method.Equals(HttpMethods.Get, StringComparison.Ordinal)) - return false; + long remaining = maxRequests - requestCount; + if (remaining < 0) + remaining = 0; - return context.Request.Path.StartsWithSegments(_v2ProjectConfigPath, StringComparison.Ordinal) - || context.Request.Path.StartsWithSegments(_heartbeatPath, StringComparison.Ordinal) - || context.Request.Path.StartsWithSegments(_webSocketPath, StringComparison.Ordinal) - || context.Request.Path.StartsWithSegments(_v1ProjectConfigPath, StringComparison.Ordinal); - } + context.Response.OnStarting(() => { + context.Response.Headers.Add(Headers.RateLimit, maxRequests.ToString()); + context.Response.Headers.Add(Headers.RateLimitRemaining, remaining.ToString()); + + return Task.CompletedTask; + }); + + await _next(context); + } + + private bool IsUnthrottledRoute(HttpContext context) { + if (!context.Request.Method.Equals(HttpMethods.Get, StringComparison.Ordinal)) + return false; + + return context.Request.Path.StartsWithSegments(_v2ProjectConfigPath, StringComparison.Ordinal) + || context.Request.Path.StartsWithSegments(_heartbeatPath, StringComparison.Ordinal) + || context.Request.Path.StartsWithSegments(_webSocketPath, StringComparison.Ordinal) + || context.Request.Path.StartsWithSegments(_v1ProjectConfigPath, StringComparison.Ordinal); } } diff --git a/src/Exceptionless.Web/Utility/Headers.cs b/src/Exceptionless.Web/Utility/Headers.cs index e349b75a53..129e7c5bf2 100644 --- a/src/Exceptionless.Web/Utility/Headers.cs +++ b/src/Exceptionless.Web/Utility/Headers.cs @@ -1,13 +1,13 @@ -namespace Exceptionless.Web.Utility { - public static class Headers { - public const string Bearer = "Bearer"; - public const string LegacyConfigurationVersion = "v"; - public const string ConfigurationVersion = "X-Exceptionless-ConfigVersion"; - public const string Client = "X-Exceptionless-Client"; - public const string RateLimit = "X-RateLimit-Limit"; - public const string RateLimitRemaining = "X-RateLimit-Remaining"; - public const string LimitedByPlan = "X-LimitedByPlan"; +namespace Exceptionless.Web.Utility; - public const string ContentEncoding = "Content-Encoding"; - } -} \ No newline at end of file +public static class Headers { + public const string Bearer = "Bearer"; + public const string LegacyConfigurationVersion = "v"; + public const string ConfigurationVersion = "X-Exceptionless-ConfigVersion"; + public const string Client = "X-Exceptionless-Client"; + public const string RateLimit = "X-RateLimit-Limit"; + public const string RateLimitRemaining = "X-RateLimit-Remaining"; + public const string LimitedByPlan = "X-LimitedByPlan"; + + public const string ContentEncoding = "Content-Encoding"; +} diff --git a/src/Exceptionless.Web/Utility/RawRequestBodyFormatter.cs b/src/Exceptionless.Web/Utility/RawRequestBodyFormatter.cs index 6c02f3835e..57922c9521 100644 --- a/src/Exceptionless.Web/Utility/RawRequestBodyFormatter.cs +++ b/src/Exceptionless.Web/Utility/RawRequestBodyFormatter.cs @@ -1,49 +1,46 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; -namespace Exceptionless.Web.Utility { - public class RawRequestBodyFormatter : InputFormatter { - public RawRequestBodyFormatter() { - SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); - SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream")); - } +namespace Exceptionless.Web.Utility; + +public class RawRequestBodyFormatter : InputFormatter { + public RawRequestBodyFormatter() { + SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); + SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream")); + } - public override bool CanRead(InputFormatterContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + public override bool CanRead(InputFormatterContext context) { + if (context == null) + throw new ArgumentNullException(nameof(context)); - MediaTypeHeaderValue.TryParse(context.HttpContext.Request.ContentType, out var contentTypeHeader); - string contentType = contentTypeHeader?.MediaType.ToString(); - if (String.IsNullOrEmpty(contentType) || contentType == "text/plain" || contentType == "application/octet-stream") - return true; + MediaTypeHeaderValue.TryParse(context.HttpContext.Request.ContentType, out var contentTypeHeader); + string contentType = contentTypeHeader?.MediaType.ToString(); + if (String.IsNullOrEmpty(contentType) || contentType == "text/plain" || contentType == "application/octet-stream") + return true; - return false; - } + return false; + } - public override async Task ReadRequestBodyAsync(InputFormatterContext context) { - var request = context.HttpContext.Request; + public override async Task ReadRequestBodyAsync(InputFormatterContext context) { + var request = context.HttpContext.Request; - MediaTypeHeaderValue.TryParse(request.ContentType, out var contentTypeHeader); - string contentType = contentTypeHeader?.MediaType.ToString(); + MediaTypeHeaderValue.TryParse(request.ContentType, out var contentTypeHeader); + string contentType = contentTypeHeader?.MediaType.ToString(); - if (String.IsNullOrEmpty(contentType) || contentType == "text/plain") { - using (var reader = new StreamReader(request.Body)) { - string content = await reader.ReadToEndAsync(); - return await InputFormatterResult.SuccessAsync(content); - } + if (String.IsNullOrEmpty(contentType) || contentType == "text/plain") { + using (var reader = new StreamReader(request.Body)) { + string content = await reader.ReadToEndAsync(); + return await InputFormatterResult.SuccessAsync(content); } - if (contentType == "application/octet-stream") { - using (var ms = new MemoryStream(2048)) { - await request.Body.CopyToAsync(ms); - byte[] content = ms.ToArray(); - return await InputFormatterResult.SuccessAsync(content); - } + } + if (contentType == "application/octet-stream") { + using (var ms = new MemoryStream(2048)) { + await request.Body.CopyToAsync(ms); + byte[] content = ms.ToArray(); + return await InputFormatterResult.SuccessAsync(content); } - - return await InputFormatterResult.FailureAsync(); } + + return await InputFormatterResult.FailureAsync(); } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/RequestBodyOperationFilter.cs b/src/Exceptionless.Web/Utility/RequestBodyOperationFilter.cs index 5dfb0e026c..aa77677e26 100644 --- a/src/Exceptionless.Web/Utility/RequestBodyOperationFilter.cs +++ b/src/Exceptionless.Web/Utility/RequestBodyOperationFilter.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; diff --git a/src/Exceptionless.Web/Utility/Results/MessageContent.cs b/src/Exceptionless.Web/Utility/Results/MessageContent.cs index 1b352497a7..a01f37e631 100644 --- a/src/Exceptionless.Web/Utility/Results/MessageContent.cs +++ b/src/Exceptionless.Web/Utility/Results/MessageContent.cs @@ -1,13 +1,13 @@ -namespace Exceptionless.Web.Utility.Results { - public class MessageContent { - public MessageContent(string message) : this(null, message) {} +namespace Exceptionless.Web.Utility.Results; - public MessageContent(string id, string message) { - Id = id; - Message = message; - } +public class MessageContent { + public MessageContent(string message) : this(null, message) { } - public string Id { get; private set; } - public string Message { get; private set; } + public MessageContent(string id, string message) { + Id = id; + Message = message; } -} \ No newline at end of file + + public string Id { get; private set; } + public string Message { get; private set; } +} diff --git a/src/Exceptionless.Web/Utility/Results/ObjectWithHeadersResult.cs b/src/Exceptionless.Web/Utility/Results/ObjectWithHeadersResult.cs index a42e403620..e0617ad80a 100644 --- a/src/Exceptionless.Web/Utility/Results/ObjectWithHeadersResult.cs +++ b/src/Exceptionless.Web/Utility/Results/ObjectWithHeadersResult.cs @@ -1,22 +1,21 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; -namespace Exceptionless.Web.Utility.Results { - public class ObjectWithHeadersResult : ObjectResult { - public ObjectWithHeadersResult(object value, IHeaderDictionary headers) : base(value) { - Headers = headers ?? new HeaderDictionary(); - } +namespace Exceptionless.Web.Utility.Results; - public IHeaderDictionary Headers { get; set; } +public class ObjectWithHeadersResult : ObjectResult { + public ObjectWithHeadersResult(object value, IHeaderDictionary headers) : base(value) { + Headers = headers ?? new HeaderDictionary(); + } + + public IHeaderDictionary Headers { get; set; } - public override void OnFormatting(ActionContext context) { - base.OnFormatting(context); + public override void OnFormatting(ActionContext context) { + base.OnFormatting(context); - if (Headers == null) - return; + if (Headers == null) + return; - foreach (var header in Headers) - context.HttpContext.Response.Headers.Add(header.Key, header.Value); - } + foreach (var header in Headers) + context.HttpContext.Response.Headers.Add(header.Key, header.Value); } } diff --git a/src/Exceptionless.Web/Utility/Results/OkPaginatedResult.cs b/src/Exceptionless.Web/Utility/Results/OkPaginatedResult.cs index 97401becab..fe0fe0f1db 100644 --- a/src/Exceptionless.Web/Utility/Results/OkPaginatedResult.cs +++ b/src/Exceptionless.Web/Utility/Results/OkPaginatedResult.cs @@ -1,52 +1,49 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; -namespace Exceptionless.Web.Utility.Results { - public class OkPaginatedResult : ObjectWithHeadersResult { - public OkPaginatedResult(object content, bool hasMore, int page, long? total = null, IHeaderDictionary headers = null) : base(content, headers) { - StatusCode = StatusCodes.Status200OK; - HasMore = hasMore; - Page = page; - Total = total; - } +namespace Exceptionless.Web.Utility.Results; - public bool HasMore { get; set; } - public int Page { get; set; } - public long? Total { get; set; } +public class OkPaginatedResult : ObjectWithHeadersResult { + public OkPaginatedResult(object content, bool hasMore, int page, long? total = null, IHeaderDictionary headers = null) : base(content, headers) { + StatusCode = StatusCodes.Status200OK; + HasMore = hasMore; + Page = page; + Total = total; + } - public override void OnFormatting(ActionContext context) { - AddPageLinkHeaders(context.HttpContext.Request); + public bool HasMore { get; set; } + public int Page { get; set; } + public long? Total { get; set; } - if (Total.HasValue) - Headers.Add("X-Result-Count", Total.ToString()); + public override void OnFormatting(ActionContext context) { + AddPageLinkHeaders(context.HttpContext.Request); - base.OnFormatting(context); - } + if (Total.HasValue) + Headers.Add("X-Result-Count", Total.ToString()); - public void AddPageLinkHeaders(HttpRequest request) { - bool includePrevious = Page > 1; - bool includeNext = HasMore; + base.OnFormatting(context); + } + + public void AddPageLinkHeaders(HttpRequest request) { + bool includePrevious = Page > 1; + bool includeNext = HasMore; - if (!includePrevious && !includeNext) - return; + if (!includePrevious && !includeNext) + return; - if (includePrevious) { - var previousParameters = new Dictionary(request.Query) { - ["page"] = (Page - 1).ToString() - }; - Headers.Add("Link", String.Concat("<", request.Path, "?", String.Join('&', previousParameters.Values), ">; rel=\"previous\"")); - } + if (includePrevious) { + var previousParameters = new Dictionary(request.Query) { + ["page"] = (Page - 1).ToString() + }; + Headers.Add("Link", String.Concat("<", request.Path, "?", String.Join('&', previousParameters.Values), ">; rel=\"previous\"")); + } - if (includeNext) { - var nextParameters = new Dictionary(request.Query) { - ["page"] = (Page + 1).ToString() - }; + if (includeNext) { + var nextParameters = new Dictionary(request.Query) { + ["page"] = (Page + 1).ToString() + }; - Headers.Add("Link", String.Concat("<", request.Path, "?", String.Join('&', nextParameters.Values), ">; rel=\"next\"")); - } + Headers.Add("Link", String.Concat("<", request.Path, "?", String.Join('&', nextParameters.Values), ">; rel=\"next\"")); } } -} \ No newline at end of file +} diff --git a/src/Exceptionless.Web/Utility/Results/OkWithHeadersContentResult.cs b/src/Exceptionless.Web/Utility/Results/OkWithHeadersContentResult.cs index a3137c7b51..b80594fd26 100644 --- a/src/Exceptionless.Web/Utility/Results/OkWithHeadersContentResult.cs +++ b/src/Exceptionless.Web/Utility/Results/OkWithHeadersContentResult.cs @@ -1,135 +1,132 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; +using System.Collections.Specialized; using System.Web; using Exceptionless.Core.Extensions; using Foundatio.Repositories.Models; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -namespace Exceptionless.Web.Utility.Results { - public class OkWithHeadersContentResult : ObjectWithHeadersResult { - public OkWithHeadersContentResult(T content, IHeaderDictionary headers = null) : base(content, headers) { - StatusCode = StatusCodes.Status200OK; - } +namespace Exceptionless.Web.Utility.Results; + +public class OkWithHeadersContentResult : ObjectWithHeadersResult { + public OkWithHeadersContentResult(T content, IHeaderDictionary headers = null) : base(content, headers) { + StatusCode = StatusCodes.Status200OK; } +} - public class OkWithResourceLinks : OkWithHeadersContentResult> where TEntity : class { - //public OkWithResourceLinks(IEnumerable content, IHeaderDictionary headers = null) : base(content, headers) { } +public class OkWithResourceLinks : OkWithHeadersContentResult> where TEntity : class { + //public OkWithResourceLinks(IEnumerable content, IHeaderDictionary headers = null) : base(content, headers) { } - public OkWithResourceLinks(IEnumerable content, bool hasMore, int? page = null, Func pagePropertyAccessor = null, IHeaderDictionary headers = null, bool isDescending = false) - : this(content, hasMore, page, null, pagePropertyAccessor, headers, isDescending) {} + public OkWithResourceLinks(IEnumerable content, bool hasMore, int? page = null, Func pagePropertyAccessor = null, IHeaderDictionary headers = null, bool isDescending = false) + : this(content, hasMore, page, null, pagePropertyAccessor, headers, isDescending) { } - public OkWithResourceLinks(IEnumerable content, bool hasMore, int? page = null, long? total = null, Func pagePropertyAccessor = null, IHeaderDictionary headers = null, bool isDescending = false) : base(content, headers) { - Content = content; - HasMore = hasMore; - IsDescending = isDescending; - Page = page; - Total = total; - PagePropertyAccessor = pagePropertyAccessor; - } + public OkWithResourceLinks(IEnumerable content, bool hasMore, int? page = null, long? total = null, Func pagePropertyAccessor = null, IHeaderDictionary headers = null, bool isDescending = false) : base(content, headers) { + Content = content; + HasMore = hasMore; + IsDescending = isDescending; + Page = page; + Total = total; + PagePropertyAccessor = pagePropertyAccessor; + } - public IEnumerable Content { get; } - public bool HasMore { get; } - public bool IsDescending { get; } - public int? Page { get; } - public long? Total { get; } - public Func PagePropertyAccessor { get; } - - public override void OnFormatting(ActionContext context) { - if (Content != null) { - List links; - if (Page.HasValue) - links = GetPagedLinks(new Uri(context.HttpContext.Request.GetDisplayUrl()), Page.Value, HasMore); - else - links = GetBeforeAndAfterLinks(new Uri(context.HttpContext.Request.GetDisplayUrl()), Content, IsDescending, HasMore, PagePropertyAccessor); - - if (links.Count > 0) - Headers.Add("Link", links.ToArray()); - - if (Total.HasValue) - Headers.Add("X-Result-Count", Total.ToString()); - } - - base.OnFormatting(context); + public IEnumerable Content { get; } + public bool HasMore { get; } + public bool IsDescending { get; } + public int? Page { get; } + public long? Total { get; } + public Func PagePropertyAccessor { get; } + + public override void OnFormatting(ActionContext context) { + if (Content != null) { + List links; + if (Page.HasValue) + links = GetPagedLinks(new Uri(context.HttpContext.Request.GetDisplayUrl()), Page.Value, HasMore); + else + links = GetBeforeAndAfterLinks(new Uri(context.HttpContext.Request.GetDisplayUrl()), Content, IsDescending, HasMore, PagePropertyAccessor); + + if (links.Count > 0) + Headers.Add("Link", links.ToArray()); + + if (Total.HasValue) + Headers.Add("X-Result-Count", Total.ToString()); } - public static List GetPagedLinks(Uri url, int page, bool hasMore) { - bool includePrevious = page > 1; - bool includeNext = hasMore; + base.OnFormatting(context); + } - var previousParameters = HttpUtility.ParseQueryString(url.Query); - previousParameters["page"] = (page - 1).ToString(); - var nextParameters = new NameValueCollection(previousParameters) { - ["page"] = (page + 1).ToString() - }; + public static List GetPagedLinks(Uri url, int page, bool hasMore) { + bool includePrevious = page > 1; + bool includeNext = hasMore; - string baseUrl = url.GetBaseUrl(); + var previousParameters = HttpUtility.ParseQueryString(url.Query); + previousParameters["page"] = (page - 1).ToString(); + var nextParameters = new NameValueCollection(previousParameters) { + ["page"] = (page + 1).ToString() + }; - string previousLink = $"<{baseUrl}?{previousParameters.ToQueryString()}>; rel=\"previous\""; - string nextLink = $"<{baseUrl}?{nextParameters.ToQueryString()}>; rel=\"next\""; + string baseUrl = url.GetBaseUrl(); - var links = new List(); - if (includePrevious) - links.Add(previousLink); - if (includeNext) - links.Add(nextLink); + string previousLink = $"<{baseUrl}?{previousParameters.ToQueryString()}>; rel=\"previous\""; + string nextLink = $"<{baseUrl}?{nextParameters.ToQueryString()}>; rel=\"next\""; - return links; - } + var links = new List(); + if (includePrevious) + links.Add(previousLink); + if (includeNext) + links.Add(nextLink); - public static List GetBeforeAndAfterLinks(Uri url, IEnumerable content, bool isDescending, bool hasMore, Func pagePropertyAccessor) { - var contentList = content.ToList(); - if (pagePropertyAccessor == null && typeof(IIdentity).IsAssignableFrom(typeof(TEntity))) - pagePropertyAccessor = e => ((IIdentity)e).Id; - - if (pagePropertyAccessor == null) - return new List(); - - string firstId = contentList.Any() ? pagePropertyAccessor(!isDescending ? contentList.First() : contentList.Last()) : String.Empty; - string lastId = contentList.Any() ? pagePropertyAccessor(!isDescending ? contentList.Last() : contentList.First()) : String.Empty; - - bool hasBefore = false; - bool hasAfter = false; - - var previousParameters = HttpUtility.ParseQueryString(url.Query); - if (previousParameters["before"] != null) - hasBefore = true; - previousParameters.Remove("before"); - if (previousParameters["after"] != null) - hasAfter = true; - previousParameters.Remove("after"); - var nextParameters = new NameValueCollection(previousParameters); - - previousParameters.Add("before", firstId); - nextParameters.Add("after", lastId); - - bool includePrevious = hasBefore ? hasMore : true; - bool includeNext = !hasBefore ? hasMore : true; - if (hasBefore && !contentList.Any()) { - // are we currently before the first page? - includePrevious = false; - includeNext = true; - nextParameters.Remove("after"); - } else if (!hasBefore && !hasAfter) { - // are we at the first page? - includePrevious = false; - } - - string baseUrl = url.GetBaseUrl(); - - string previousLink = $"<{baseUrl}?{previousParameters.ToQueryString()}>; rel=\"previous\""; - string nextLink = $"<{baseUrl}?{nextParameters.ToQueryString()}>; rel=\"next\""; - - var links = new List(); - if (includePrevious) - links.Add(previousLink); - if (includeNext) - links.Add(nextLink); - - return links; + return links; + } + + public static List GetBeforeAndAfterLinks(Uri url, IEnumerable content, bool isDescending, bool hasMore, Func pagePropertyAccessor) { + var contentList = content.ToList(); + if (pagePropertyAccessor == null && typeof(IIdentity).IsAssignableFrom(typeof(TEntity))) + pagePropertyAccessor = e => ((IIdentity)e).Id; + + if (pagePropertyAccessor == null) + return new List(); + + string firstId = contentList.Any() ? pagePropertyAccessor(!isDescending ? contentList.First() : contentList.Last()) : String.Empty; + string lastId = contentList.Any() ? pagePropertyAccessor(!isDescending ? contentList.Last() : contentList.First()) : String.Empty; + + bool hasBefore = false; + bool hasAfter = false; + + var previousParameters = HttpUtility.ParseQueryString(url.Query); + if (previousParameters["before"] != null) + hasBefore = true; + previousParameters.Remove("before"); + if (previousParameters["after"] != null) + hasAfter = true; + previousParameters.Remove("after"); + var nextParameters = new NameValueCollection(previousParameters); + + previousParameters.Add("before", firstId); + nextParameters.Add("after", lastId); + + bool includePrevious = hasBefore ? hasMore : true; + bool includeNext = !hasBefore ? hasMore : true; + if (hasBefore && !contentList.Any()) { + // are we currently before the first page? + includePrevious = false; + includeNext = true; + nextParameters.Remove("after"); } + else if (!hasBefore && !hasAfter) { + // are we at the first page? + includePrevious = false; + } + + string baseUrl = url.GetBaseUrl(); + + string previousLink = $"<{baseUrl}?{previousParameters.ToQueryString()}>; rel=\"previous\""; + string nextLink = $"<{baseUrl}?{nextParameters.ToQueryString()}>; rel=\"next\""; + + var links = new List(); + if (includePrevious) + links.Add(previousLink); + if (includeNext) + links.Add(nextLink); + + return links; } } diff --git a/src/Exceptionless.Web/Utility/Results/PermissionResult.cs b/src/Exceptionless.Web/Utility/Results/PermissionResult.cs index 74821ed75b..48b09b1cc3 100644 --- a/src/Exceptionless.Web/Utility/Results/PermissionResult.cs +++ b/src/Exceptionless.Web/Utility/Results/PermissionResult.cs @@ -1,60 +1,58 @@ -using Microsoft.AspNetCore.Http; - -namespace Exceptionless.Web.Utility.Results { - public class PermissionResult { - public bool Allowed { get; set; } - public string Id { get; set; } - public string Message { get; set; } - - public int StatusCode { get; set; } - - public static PermissionResult Allow = new PermissionResult { Allowed = true, StatusCode = StatusCodes.Status200OK }; - - public static PermissionResult Deny = new PermissionResult { Allowed = false, StatusCode = StatusCodes.Status400BadRequest }; - - public static PermissionResult DenyWithNotFound(string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - StatusCode = StatusCodes.Status404NotFound - }; - } - - public static PermissionResult DenyWithMessage(string message, string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - Message = message, - StatusCode = StatusCodes.Status400BadRequest - }; - } - - public static PermissionResult DenyWithStatus(int statusCode, string message = null, string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - Message = message, - StatusCode = statusCode - }; - } - - public static PermissionResult DenyWithPlanLimitReached(string message, string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - Message = message, - StatusCode = StatusCodes.Status426UpgradeRequired - }; - } - - - public static PermissionResult DenyWithPNotImplemented(string message, string id = null) { - return new PermissionResult { - Allowed = false, - Id = id, - Message = message, - StatusCode = StatusCodes.Status501NotImplemented - }; - } +namespace Exceptionless.Web.Utility.Results; + +public class PermissionResult { + public bool Allowed { get; set; } + public string Id { get; set; } + public string Message { get; set; } + + public int StatusCode { get; set; } + + public static PermissionResult Allow = new PermissionResult { Allowed = true, StatusCode = StatusCodes.Status200OK }; + + public static PermissionResult Deny = new PermissionResult { Allowed = false, StatusCode = StatusCodes.Status400BadRequest }; + + public static PermissionResult DenyWithNotFound(string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + StatusCode = StatusCodes.Status404NotFound + }; + } + + public static PermissionResult DenyWithMessage(string message, string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + Message = message, + StatusCode = StatusCodes.Status400BadRequest + }; + } + + public static PermissionResult DenyWithStatus(int statusCode, string message = null, string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + Message = message, + StatusCode = statusCode + }; + } + + public static PermissionResult DenyWithPlanLimitReached(string message, string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + Message = message, + StatusCode = StatusCodes.Status426UpgradeRequired + }; + } + + + public static PermissionResult DenyWithPNotImplemented(string message, string id = null) { + return new PermissionResult { + Allowed = false, + Id = id, + Message = message, + StatusCode = StatusCodes.Status501NotImplemented + }; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/AppWebHostFactory.cs b/tests/Exceptionless.Tests/AppWebHostFactory.cs index 8291cb3482..bf30b986c9 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactory.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactory.cs @@ -1,25 +1,21 @@ -using System; using Exceptionless.Insulation.Configuration; using Exceptionless.Web; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -namespace Exceptionless.Tests { - public class AppWebHostFactory : WebApplicationFactory { - protected override void ConfigureWebHost(IWebHostBuilder builder) { - builder.UseSolutionRelativeContentRoot("src/Exceptionless.Web"); - } +namespace Exceptionless.Tests; - protected override IHostBuilder CreateHostBuilder() { - var config = new ConfigurationBuilder() - .SetBasePath(AppContext.BaseDirectory) - .AddYamlFile("appsettings.yml", optional: false, reloadOnChange: false) - .Build(); - - return Program.CreateHostBuilder(config, Environments.Development); - } +public class AppWebHostFactory : WebApplicationFactory { + protected override void ConfigureWebHost(IWebHostBuilder builder) { + builder.UseSolutionRelativeContentRoot("src/Exceptionless.Web"); + } + + protected override IHostBuilder CreateHostBuilder() { + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddYamlFile("appsettings.yml", optional: false, reloadOnChange: false) + .Build(); + + return Program.CreateHostBuilder(config, Environments.Development); } } diff --git a/tests/Exceptionless.Tests/Authentication/TestDomainLoginProvider.cs b/tests/Exceptionless.Tests/Authentication/TestDomainLoginProvider.cs index 69b4bdcc64..f60d506ffb 100644 --- a/tests/Exceptionless.Tests/Authentication/TestDomainLoginProvider.cs +++ b/tests/Exceptionless.Tests/Authentication/TestDomainLoginProvider.cs @@ -1,24 +1,24 @@ using Exceptionless.Core.Authentication; -namespace Exceptionless.Tests.Authentication { - internal class TestDomainLoginProvider : IDomainLoginProvider { - public const string ValidUsername = "user1"; - public const string ValidPassword = "password1!!"; +namespace Exceptionless.Tests.Authentication; - public bool Login(string username, string password) { - return username == ValidUsername && password == ValidPassword; - } +internal class TestDomainLoginProvider : IDomainLoginProvider { + public const string ValidUsername = "user1"; + public const string ValidPassword = "password1!!"; - public string GetEmailAddressFromUsername(string username) { - return $"{username}@domain.com"; - } + public bool Login(string username, string password) { + return username == ValidUsername && password == ValidPassword; + } + + public string GetEmailAddressFromUsername(string username) { + return $"{username}@domain.com"; + } - public string GetUserFullName(string username) { - return $"{username} {username.ToUpperInvariant()}"; - } + public string GetUserFullName(string username) { + return $"{username} {username.ToUpperInvariant()}"; + } - public string GetUsernameFromEmailAddress(string email) { - return email == GetEmailAddressFromUsername(ValidUsername) ? ValidUsername : null; - } + public string GetUsernameFromEmailAddress(string email) { + return email == GetEmailAddressFromUsername(ValidUsername) ? ValidUsername : null; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Billing/BillingManagerTests.cs b/tests/Exceptionless.Tests/Billing/BillingManagerTests.cs index 0e6c37af91..14d2e89aaf 100644 --- a/tests/Exceptionless.Tests/Billing/BillingManagerTests.cs +++ b/tests/Exceptionless.Tests/Billing/BillingManagerTests.cs @@ -2,39 +2,39 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Billing { - public class BillingManagerTests : TestWithServices { - public BillingManagerTests(ITestOutputHelper output) : base(output) {} - - [Fact] - public void GetBillingPlan() { - var billingManager = GetService(); - var plans = GetService(); - Assert.Equal(plans.FreePlan.Id, billingManager.GetBillingPlan(plans.FreePlan.Id).Id); - } - - [Fact] - public void GetBillingPlanByUpsellingRetentionPeriod() { - var billingManager = GetService(); - var plans = GetService(); - - var plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.FreePlan.RetentionDays); - Assert.NotNull(plan); - Assert.Equal(plans.SmallPlan.Id, plan.Id); - Assert.Equal(plans.SmallPlan.RetentionDays, plan.RetentionDays); - - plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.SmallPlan.RetentionDays); - Assert.NotNull(plan); - Assert.Equal(plans.MediumPlan.Id, plan.Id); - Assert.Equal(plans.MediumPlan.RetentionDays, plan.RetentionDays); - - plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.MediumPlan.RetentionDays); - Assert.NotNull(plan); - Assert.Equal(plans.LargePlan.Id, plan.Id); - Assert.Equal(plans.LargePlan.RetentionDays, plan.RetentionDays); - - plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.LargePlan.RetentionDays); - Assert.Null(plan); - } +namespace Exceptionless.Tests.Billing; + +public class BillingManagerTests : TestWithServices { + public BillingManagerTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void GetBillingPlan() { + var billingManager = GetService(); + var plans = GetService(); + Assert.Equal(plans.FreePlan.Id, billingManager.GetBillingPlan(plans.FreePlan.Id).Id); + } + + [Fact] + public void GetBillingPlanByUpsellingRetentionPeriod() { + var billingManager = GetService(); + var plans = GetService(); + + var plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.FreePlan.RetentionDays); + Assert.NotNull(plan); + Assert.Equal(plans.SmallPlan.Id, plan.Id); + Assert.Equal(plans.SmallPlan.RetentionDays, plan.RetentionDays); + + plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.SmallPlan.RetentionDays); + Assert.NotNull(plan); + Assert.Equal(plans.MediumPlan.Id, plan.Id); + Assert.Equal(plans.MediumPlan.RetentionDays, plan.RetentionDays); + + plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.MediumPlan.RetentionDays); + Assert.NotNull(plan); + Assert.Equal(plans.LargePlan.Id, plan.Id); + Assert.Equal(plans.LargePlan.RetentionDays, plan.RetentionDays); + + plan = billingManager.GetBillingPlanByUpsellingRetentionPeriod(plans.LargePlan.RetentionDays); + Assert.Null(plan); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs index c3fb282483..42594f172f 100644 --- a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Web.Models; +using Exceptionless.Web.Models; using Exceptionless.Tests.Authentication; using Exceptionless.Tests.Extensions; using Exceptionless.Core.Authorization; @@ -20,794 +17,794 @@ using User = Exceptionless.Core.Models.User; using FluentRest; -namespace Exceptionless.Tests.Controllers { - public class AuthControllerTests : IntegrationTestsBase { - private readonly AuthOptions _authOptions; - private readonly IUserRepository _userRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly ITokenRepository _tokenRepository; - - public AuthControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _authOptions = GetService(); - _authOptions.EnableAccountCreation = true; - _authOptions.EnableActiveDirectoryAuth = false; - - _organizationRepository = GetService(); - _projectRepository = GetService(); - _userRepository = GetService(); - _tokenRepository = GetService(); - } +namespace Exceptionless.Tests.Controllers; - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await CreateTestOrganizationAndProjectsAsync(); - } +public class AuthControllerTests : IntegrationTestsBase { + private readonly AuthOptions _authOptions; + private readonly IUserRepository _userRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly ITokenRepository _tokenRepository; - [Fact] - public async Task CannotSignupWithoutPassword() { - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = "test@domain.com", - Name = "hello" - }) - .StatusCodeShouldBeBadRequest() - ); - } + public AuthControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _authOptions = GetService(); + _authOptions.EnableAccountCreation = true; + _authOptions.EnableActiveDirectoryAuth = false; - [Theory] - [InlineData(true, TestDomainLoginProvider.ValidUsername, TestDomainLoginProvider.ValidPassword)] - [InlineData(true, "test1.2@exceptionless.io", TestDomainLoginProvider.ValidPassword)] - [InlineData(false, "test1@exceptionless.io", "Password1$")] - public async Task CannotSignupWhenAccountCreationDisabledWithNoTokenAsync(bool enableAdAuth, string email, string password) { - _authOptions.EnableAccountCreation = false; - _authOptions.EnableActiveDirectoryAuth = enableAdAuth; - - if (enableAdAuth && email == TestDomainLoginProvider.ValidUsername) { - var provider = new TestDomainLoginProvider(); - email = provider.GetEmailAddressFromUsername(email); - } - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = "", - Name = "Test", - Password = password - }) - .StatusCodeShouldBeBadRequest() - ); - } + _organizationRepository = GetService(); + _projectRepository = GetService(); + _userRepository = GetService(); + _tokenRepository = GetService(); + } - [Theory] - [InlineData(true, TestDomainLoginProvider.ValidUsername, TestDomainLoginProvider.ValidPassword)] - [InlineData(true, "test2.2@exceptionless.io", TestDomainLoginProvider.ValidPassword)] - [InlineData(false, "test2@exceptionless.io", "Password1$")] - public async Task CannotSignupWhenAccountCreationDisabledWithInvalidTokenAsync(bool enableAdAuth, string email, string password) { - _authOptions.EnableAccountCreation = false; - _authOptions.EnableActiveDirectoryAuth = enableAdAuth; - - if (enableAdAuth && email == TestDomainLoginProvider.ValidUsername) { - var provider = new TestDomainLoginProvider(); - email = provider.GetEmailAddressFromUsername(email); - } - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = StringExtensions.GetNewToken(), - Name = "Test", - Password = password - }) - .StatusCodeShouldBeBadRequest() - ); - } + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await CreateTestOrganizationAndProjectsAsync(); + } - [Theory] - [InlineData(true, TestDomainLoginProvider.ValidUsername, TestDomainLoginProvider.ValidPassword)] - [InlineData(false, "test3@exceptionless.io", "Password1$")] - public async Task CanSignupWhenAccountCreationDisabledWithValidTokenAsync(bool enableAdAuth, string email, string password) { - _authOptions.EnableAccountCreation = false; - _authOptions.EnableActiveDirectoryAuth = enableAdAuth; - - if (enableAdAuth && email == TestDomainLoginProvider.ValidUsername) { - var provider = new TestDomainLoginProvider(); - email = provider.GetEmailAddressFromUsername(email); - } - - var results = await _organizationRepository.GetAllAsync(); - var organization = results.Documents.First(); - - var invite = new Invite { - Token = StringExtensions.GetNewToken(), - EmailAddress = email.ToLowerInvariant(), - DateAdded = SystemClock.UtcNow - }; - organization.Invites.Add(invite); - organization = await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); - Assert.NotNull(organization.GetInvite(invite.Token)); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = invite.Token, - Name = "Test", - Password = password - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.False(String.IsNullOrEmpty(result.Token)); - } + [Fact] + public async Task CannotSignupWithoutPassword() { + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = "test@domain.com", + Name = "hello" + }) + .StatusCodeShouldBeBadRequest() + ); + } - [Fact] - public async Task CanSignupWhenAccountCreationDisabledWithValidTokenAndInvalidAdAccountAsync() { - _authOptions.EnableAccountCreation = false; - _authOptions.EnableActiveDirectoryAuth = true; - - string email = "testuser1@exceptionless.io"; - string password = "invalidAccount1"; - - var orgs = await _organizationRepository.GetAllAsync(); - var organization = orgs.Documents.First(); - var invite = new Invite { - Token = StringExtensions.GetNewToken(), - EmailAddress = email.ToLowerInvariant(), - DateAdded = SystemClock.UtcNow - }; - - organization.Invites.Add(invite); - await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); - Assert.NotNull(organization.GetInvite(invite.Token)); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = invite.Token, - Name = "Test", - Password = password - }) - .StatusCodeShouldBeBadRequest() - ); - } + [Theory] + [InlineData(true, TestDomainLoginProvider.ValidUsername, TestDomainLoginProvider.ValidPassword)] + [InlineData(true, "test1.2@exceptionless.io", TestDomainLoginProvider.ValidPassword)] + [InlineData(false, "test1@exceptionless.io", "Password1$")] + public async Task CannotSignupWhenAccountCreationDisabledWithNoTokenAsync(bool enableAdAuth, string email, string password) { + _authOptions.EnableAccountCreation = false; + _authOptions.EnableActiveDirectoryAuth = enableAdAuth; - [Fact] - public async Task CanSignupWhenAccountCreationEnabledWithNoTokenAsync() { - _authOptions.EnableAccountCreation = true; - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = "test4@exceptionless.io", - InviteToken = "", - Name = "Test", - Password = "Password1$" - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.False(String.IsNullOrEmpty(result.Token)); + if (enableAdAuth && email == TestDomainLoginProvider.ValidUsername) { + var provider = new TestDomainLoginProvider(); + email = provider.GetEmailAddressFromUsername(email); } - [Fact] - public async Task CanSignupWhenAccountCreationEnabledWithNoTokenAndValidAdAccountAsync() { - _authOptions.EnableAccountCreation = true; - _authOptions.EnableActiveDirectoryAuth = true; + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = "", + Name = "Test", + Password = password + }) + .StatusCodeShouldBeBadRequest() + ); + } - var provider = new TestDomainLoginProvider(); - string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = "", - Name = "Test", - Password = TestDomainLoginProvider.ValidPassword - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.False(String.IsNullOrEmpty(result.Token)); - } + [Theory] + [InlineData(true, TestDomainLoginProvider.ValidUsername, TestDomainLoginProvider.ValidPassword)] + [InlineData(true, "test2.2@exceptionless.io", TestDomainLoginProvider.ValidPassword)] + [InlineData(false, "test2@exceptionless.io", "Password1$")] + public async Task CannotSignupWhenAccountCreationDisabledWithInvalidTokenAsync(bool enableAdAuth, string email, string password) { + _authOptions.EnableAccountCreation = false; + _authOptions.EnableActiveDirectoryAuth = enableAdAuth; - [Fact] - public async Task CanSignupWhenAccountCreationEnabledWithNoTokenAndInvalidAdAccountAsync() { - _authOptions.EnableAccountCreation = true; - _authOptions.EnableActiveDirectoryAuth = true; - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = "testuser2@exceptionless.io", - InviteToken = "", - Name = "Test", - Password = "literallydoesntmatter" - }) - .StatusCodeShouldBeBadRequest() - ); + if (enableAdAuth && email == TestDomainLoginProvider.ValidUsername) { + var provider = new TestDomainLoginProvider(); + email = provider.GetEmailAddressFromUsername(email); } - [Fact] - public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAsync() { - _authOptions.EnableAccountCreation = true; - - var orgs = await _organizationRepository.GetAllAsync(); - var organization = orgs.Documents.First(); - const string email = "test5@exceptionless.io"; - const string name = "Test"; - const string password = "Password1$"; - - var invite = new Invite { - Token = StringExtensions.GetNewToken(), - EmailAddress = email.ToLowerInvariant(), - DateAdded = SystemClock.UtcNow - }; - - organization.Invites.Clear(); - organization.Invites.Add(invite); - await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); - Assert.NotNull(organization.GetInvite(invite.Token)); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = invite.Token, - Name = name, - Password = password - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.False(String.IsNullOrEmpty(result.Token)); - - await RefreshDataAsync(); - - var user = await _userRepository.GetByEmailAddressAsync(email); - Assert.NotNull(user); - Assert.Equal("Test", user.FullName); - Assert.NotEmpty(user.OrganizationIds); - Assert.True(user.IsEmailAddressVerified); - Assert.Equal(password.ToSaltedHash(user.Salt), user.Password); - Assert.Contains(organization.Id, user.OrganizationIds); - - organization = await _organizationRepository.GetByIdAsync(organization.Id); - Assert.Empty(organization.Invites); - - var token = await _tokenRepository.GetByIdAsync(result.Token); - Assert.NotNull(token); - Assert.Equal(user.Id, token.UserId); - Assert.Equal(TokenType.Access, token.Type); - - var mailQueue = GetService>() as InMemoryQueue; - Assert.Equal(0, (await mailQueue.GetQueueStatsAsync()).Enqueued); - } + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = StringExtensions.GetNewToken(), + Name = "Test", + Password = password + }) + .StatusCodeShouldBeBadRequest() + ); + } - [Fact] - public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAndValidAdAccountAsync() { - _authOptions.EnableAccountCreation = true; - _authOptions.EnableActiveDirectoryAuth = true; + [Theory] + [InlineData(true, TestDomainLoginProvider.ValidUsername, TestDomainLoginProvider.ValidPassword)] + [InlineData(false, "test3@exceptionless.io", "Password1$")] + public async Task CanSignupWhenAccountCreationDisabledWithValidTokenAsync(bool enableAdAuth, string email, string password) { + _authOptions.EnableAccountCreation = false; + _authOptions.EnableActiveDirectoryAuth = enableAdAuth; + if (enableAdAuth && email == TestDomainLoginProvider.ValidUsername) { var provider = new TestDomainLoginProvider(); - string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); - - var results = await _organizationRepository.GetAllAsync(); - var organization = results.Documents.First(); - var invite = new Invite { - Token = StringExtensions.GetNewToken(), - EmailAddress = email.ToLowerInvariant(), - DateAdded = SystemClock.UtcNow - }; - organization.Invites.Add(invite); - await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); - Assert.NotNull(organization.GetInvite(invite.Token)); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = invite.Token, - Name = "Test", - Password = TestDomainLoginProvider.ValidPassword - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.False(String.IsNullOrEmpty(result.Token)); + email = provider.GetEmailAddressFromUsername(email); } - [Fact] - public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAndInvalidAdAccountAsync() { - _authOptions.EnableAccountCreation = true; - _authOptions.EnableActiveDirectoryAuth = true; - - string email = "testuser4@exceptionless.io"; - var results = await _organizationRepository.GetAllAsync(); - var organization = results.Documents.First(); - var invite = new Invite { - Token = StringExtensions.GetNewToken(), - EmailAddress = email.ToLowerInvariant(), - DateAdded = SystemClock.UtcNow - }; - organization.Invites.Add(invite); - await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); - Assert.NotNull(organization.GetInvite(invite.Token)); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - InviteToken = invite.Token, - Name = "Test", - Password = TestDomainLoginProvider.ValidPassword - }) - .StatusCodeShouldBeBadRequest() - ); - } + var results = await _organizationRepository.GetAllAsync(); + var organization = results.Documents.First(); + + var invite = new Invite { + Token = StringExtensions.GetNewToken(), + EmailAddress = email.ToLowerInvariant(), + DateAdded = SystemClock.UtcNow + }; + organization.Invites.Add(invite); + organization = await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); + Assert.NotNull(organization.GetInvite(invite.Token)); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = invite.Token, + Name = "Test", + Password = password + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.False(String.IsNullOrEmpty(result.Token)); + } - [Fact] - public async Task SignupShouldFailWhenUsingExistingAccountWithNoPasswordOrInvalidPassword() { - var userRepo = GetService(); - - const string email = "test6@exceptionless.io"; - const string password = "Test6 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 6" - }; - await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - Name = "Random Name" - }) - .StatusCodeShouldBeBadRequest() - ); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/signup") - .Content(new SignupModel { - Email = email, - Name = "Random Name", - Password = "invalidPass", - }) - .StatusCodeShouldBeUnauthorized() - ); - } + [Fact] + public async Task CanSignupWhenAccountCreationDisabledWithValidTokenAndInvalidAdAccountAsync() { + _authOptions.EnableAccountCreation = false; + _authOptions.EnableActiveDirectoryAuth = true; + + string email = "testuser1@exceptionless.io"; + string password = "invalidAccount1"; + + var orgs = await _organizationRepository.GetAllAsync(); + var organization = orgs.Documents.First(); + var invite = new Invite { + Token = StringExtensions.GetNewToken(), + EmailAddress = email.ToLowerInvariant(), + DateAdded = SystemClock.UtcNow + }; + + organization.Invites.Add(invite); + await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); + Assert.NotNull(organization.GetInvite(invite.Token)); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = invite.Token, + Name = "Test", + Password = password + }) + .StatusCodeShouldBeBadRequest() + ); + } - [Fact] - public async Task LoginValidAsync() { - _authOptions.EnableActiveDirectoryAuth = false; - - const string email = "test6@exceptionless.io"; - const string password = "Test6 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 6" - }; - await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = password - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.False(String.IsNullOrEmpty(result.Token)); - } + [Fact] + public async Task CanSignupWhenAccountCreationEnabledWithNoTokenAsync() { + _authOptions.EnableAccountCreation = true; + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = "test4@exceptionless.io", + InviteToken = "", + Name = "Test", + Password = "Password1$" + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.False(String.IsNullOrEmpty(result.Token)); + } - [Fact] - public async Task LoginInvalidPasswordAsync() { - _authOptions.EnableActiveDirectoryAuth = false; - - const string email = "test7@exceptionless.io"; - const string password = "Test7 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 7" - }; - - await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = "This password ain't right" - }) - .StatusCodeShouldBeUnauthorized() - ); - } + [Fact] + public async Task CanSignupWhenAccountCreationEnabledWithNoTokenAndValidAdAccountAsync() { + _authOptions.EnableAccountCreation = true; + _authOptions.EnableActiveDirectoryAuth = true; + + var provider = new TestDomainLoginProvider(); + string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = "", + Name = "Test", + Password = TestDomainLoginProvider.ValidPassword + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.False(String.IsNullOrEmpty(result.Token)); + } - [Fact] - public async Task LoginNoSuchUserAsync() { - _authOptions.EnableActiveDirectoryAuth = false; - - const string email = "test8@exceptionless.io"; - const string password = "Test8 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 8" - }; - await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = "Thisguydoesntexist@exceptionless.io", - Password = "This password ain't right" - }) - .StatusCodeShouldBeUnauthorized() - ); - } + [Fact] + public async Task CanSignupWhenAccountCreationEnabledWithNoTokenAndInvalidAdAccountAsync() { + _authOptions.EnableAccountCreation = true; + _authOptions.EnableActiveDirectoryAuth = true; + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = "testuser2@exceptionless.io", + InviteToken = "", + Name = "Test", + Password = "literallydoesntmatter" + }) + .StatusCodeShouldBeBadRequest() + ); + } - [Fact] - public async Task LoginValidExistingActiveDirectoryAsync() { - _authOptions.EnableActiveDirectoryAuth = true; + [Fact] + public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAsync() { + _authOptions.EnableAccountCreation = true; + + var orgs = await _organizationRepository.GetAllAsync(); + var organization = orgs.Documents.First(); + const string email = "test5@exceptionless.io"; + const string name = "Test"; + const string password = "Password1$"; + + var invite = new Invite { + Token = StringExtensions.GetNewToken(), + EmailAddress = email.ToLowerInvariant(), + DateAdded = SystemClock.UtcNow + }; + + organization.Invites.Clear(); + organization.Invites.Add(invite); + await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); + Assert.NotNull(organization.GetInvite(invite.Token)); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = invite.Token, + Name = name, + Password = password + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.False(String.IsNullOrEmpty(result.Token)); + + await RefreshDataAsync(); + + var user = await _userRepository.GetByEmailAddressAsync(email); + Assert.NotNull(user); + Assert.Equal("Test", user.FullName); + Assert.NotEmpty(user.OrganizationIds); + Assert.True(user.IsEmailAddressVerified); + Assert.Equal(password.ToSaltedHash(user.Salt), user.Password); + Assert.Contains(organization.Id, user.OrganizationIds); + + organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.Empty(organization.Invites); + + var token = await _tokenRepository.GetByIdAsync(result.Token); + Assert.NotNull(token); + Assert.Equal(user.Id, token.UserId); + Assert.Equal(TokenType.Access, token.Type); + + var mailQueue = GetService>() as InMemoryQueue; + Assert.Equal(0, (await mailQueue.GetQueueStatsAsync()).Enqueued); + } - var provider = new TestDomainLoginProvider(); - string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); - var user = new User { - EmailAddress = email, - IsEmailAddressVerified = true, - FullName = "User 6" - }; - - await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = TestDomainLoginProvider.ValidPassword - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.False(String.IsNullOrEmpty(result.Token)); - } + [Fact] + public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAndValidAdAccountAsync() { + _authOptions.EnableAccountCreation = true; + _authOptions.EnableActiveDirectoryAuth = true; + + var provider = new TestDomainLoginProvider(); + string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); + + var results = await _organizationRepository.GetAllAsync(); + var organization = results.Documents.First(); + var invite = new Invite { + Token = StringExtensions.GetNewToken(), + EmailAddress = email.ToLowerInvariant(), + DateAdded = SystemClock.UtcNow + }; + organization.Invites.Add(invite); + await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); + Assert.NotNull(organization.GetInvite(invite.Token)); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = invite.Token, + Name = "Test", + Password = TestDomainLoginProvider.ValidPassword + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.False(String.IsNullOrEmpty(result.Token)); + } - [Fact] - public async Task LoginValidNonExistantActiveDirectoryAsync() { - _authOptions.EnableActiveDirectoryAuth = true; + [Fact] + public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAndInvalidAdAccountAsync() { + _authOptions.EnableAccountCreation = true; + _authOptions.EnableActiveDirectoryAuth = true; + + string email = "testuser4@exceptionless.io"; + var results = await _organizationRepository.GetAllAsync(); + var organization = results.Documents.First(); + var invite = new Invite { + Token = StringExtensions.GetNewToken(), + EmailAddress = email.ToLowerInvariant(), + DateAdded = SystemClock.UtcNow + }; + organization.Invites.Add(invite); + await _organizationRepository.SaveAsync(organization, o => o.ImmediateConsistency()); + Assert.NotNull(organization.GetInvite(invite.Token)); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + InviteToken = invite.Token, + Name = "Test", + Password = TestDomainLoginProvider.ValidPassword + }) + .StatusCodeShouldBeBadRequest() + ); + } - var provider = new TestDomainLoginProvider(); - string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = TestDomainLoginProvider.ValidPassword - }) - .StatusCodeShouldBeUnauthorized() - ); - } + [Fact] + public async Task SignupShouldFailWhenUsingExistingAccountWithNoPasswordOrInvalidPassword() { + var userRepo = GetService(); + + const string email = "test6@exceptionless.io"; + const string password = "Test6 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 6" + }; + await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + Name = "Random Name" + }) + .StatusCodeShouldBeBadRequest() + ); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/signup") + .Content(new SignupModel { + Email = email, + Name = "Random Name", + Password = "invalidPass", + }) + .StatusCodeShouldBeUnauthorized() + ); + } - [Fact] - public async Task LoginInvalidNonExistantActiveDirectoryAsync() { - _authOptions.EnableActiveDirectoryAuth = true; - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = TestDomainLoginProvider.ValidUsername + ".au", - Password = "Totallywrongpassword1234" - }) - .StatusCodeShouldBeUnauthorized() - ); - - // Verify that a user account was not added - var provider = new TestDomainLoginProvider(); - string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); - var user = await _userRepository.GetByEmailAddressAsync(email + ".au"); - Assert.Null(user); - } + [Fact] + public async Task LoginValidAsync() { + _authOptions.EnableActiveDirectoryAuth = false; + + const string email = "test6@exceptionless.io"; + const string password = "Test6 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 6" + }; + await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = password + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.False(String.IsNullOrEmpty(result.Token)); + } - [Fact] - public async Task LoginInvalidExistingActiveDirectoryAsync() { - _authOptions.EnableActiveDirectoryAuth = true; + [Fact] + public async Task LoginInvalidPasswordAsync() { + _authOptions.EnableActiveDirectoryAuth = false; + + const string email = "test7@exceptionless.io"; + const string password = "Test7 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 7" + }; + + await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = "This password ain't right" + }) + .StatusCodeShouldBeUnauthorized() + ); + } - var provider = new TestDomainLoginProvider(); - string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); - var user = new User { - EmailAddress = email, - IsEmailAddressVerified = true, - FullName = "User 6" - }; - await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); - - await SendRequestAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = TestDomainLoginProvider.ValidUsername, - Password = "Totallywrongpassword1234" - }) - .StatusCodeShouldBeUnauthorized() - ); - } + [Fact] + public async Task LoginNoSuchUserAsync() { + _authOptions.EnableActiveDirectoryAuth = false; + + const string email = "test8@exceptionless.io"; + const string password = "Test8 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 8" + }; + await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = "Thisguydoesntexist@exceptionless.io", + Password = "This password ain't right" + }) + .StatusCodeShouldBeUnauthorized() + ); + } - [Fact] - public async Task CanChangePasswordAsync() { - const string email = "test6@exceptionless.io"; - const string password = "Test6 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 6", - Roles = AuthorizationRoles.AllScopes - }; - - await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = password, - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.NotEmpty(result.Token); - - var token = await _tokenRepository.GetByIdAsync(result.Token); - Assert.NotNull(token); - - var actualUser = await _userRepository.GetByIdAsync(token.UserId); - Assert.NotNull(actualUser); - Assert.Equal(email, actualUser.EmailAddress); - - const string newPassword = "NewP@ssword2"; - var changePasswordResult = await SendRequestAsAsync(r => r - .Post() - .BasicAuthorization(email, password) - .AppendPath("auth/change-password") - .Content(new ChangePasswordModel { - CurrentPassword = password, - Password = newPassword - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(changePasswordResult); - Assert.NotEmpty(changePasswordResult.Token); - - Assert.Null(await _tokenRepository.GetByIdAsync(result.Token)); - Assert.NotNull(await _tokenRepository.GetByIdAsync(changePasswordResult.Token)); - } + [Fact] + public async Task LoginValidExistingActiveDirectoryAsync() { + _authOptions.EnableActiveDirectoryAuth = true; + + var provider = new TestDomainLoginProvider(); + string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); + var user = new User { + EmailAddress = email, + IsEmailAddressVerified = true, + FullName = "User 6" + }; + + await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = TestDomainLoginProvider.ValidPassword + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.False(String.IsNullOrEmpty(result.Token)); + } - [Fact] - public async Task ChangePasswordShouldFailWithCurrentPasswordAsync() { - const string email = "test6@exceptionless.io"; - const string password = "Test6 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 6", - Roles = AuthorizationRoles.AllScopes - }; - - await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = password, - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.NotEmpty(result.Token); - - var token = await _tokenRepository.GetByIdAsync(result.Token); - Assert.NotNull(token); - - var actualUser = await _userRepository.GetByIdAsync(token.UserId); - Assert.NotNull(actualUser); - Assert.Equal(email, actualUser.EmailAddress); - - await SendRequestAsync(r => r - .Post() - .BasicAuthorization(email, password) - .AppendPath("auth/change-password") - .Content(new ChangePasswordModel { - CurrentPassword = password, - Password = password - }) - .StatusCodeShouldBeBadRequest() - ); - - Assert.NotNull(await _tokenRepository.GetByIdAsync(result.Token)); - } + [Fact] + public async Task LoginValidNonExistantActiveDirectoryAsync() { + _authOptions.EnableActiveDirectoryAuth = true; + + var provider = new TestDomainLoginProvider(); + string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = TestDomainLoginProvider.ValidPassword + }) + .StatusCodeShouldBeUnauthorized() + ); + } - [Fact] - public async Task CanResetPasswordAsync() { - const string email = "test6@exceptionless.io"; - const string password = "Test6 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 6", - Roles = AuthorizationRoles.AllScopes - }; - - user.CreatePasswordResetToken(); - await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = password, - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.NotEmpty(result.Token); - - var token = await _tokenRepository.GetByIdAsync(result.Token); - Assert.NotNull(token); - - var actualUser = await _userRepository.GetByIdAsync(token.UserId); - Assert.NotNull(actualUser); - Assert.Equal(email, actualUser.EmailAddress); - - const string newPassword = "NewP@ssword2"; - await SendRequestAsync(r => r - .Post() - .BasicAuthorization(email, password) - .AppendPath("auth/reset-password") - .Content(new ResetPasswordModel { - PasswordResetToken = user.PasswordResetToken, - Password = newPassword - }) - .StatusCodeShouldBeOk() - ); - - Assert.Null(await _tokenRepository.GetByIdAsync(result.Token)); - } + [Fact] + public async Task LoginInvalidNonExistantActiveDirectoryAsync() { + _authOptions.EnableActiveDirectoryAuth = true; + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = TestDomainLoginProvider.ValidUsername + ".au", + Password = "Totallywrongpassword1234" + }) + .StatusCodeShouldBeUnauthorized() + ); + + // Verify that a user account was not added + var provider = new TestDomainLoginProvider(); + string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); + var user = await _userRepository.GetByEmailAddressAsync(email + ".au"); + Assert.Null(user); + } - [Fact] - public async Task ResetPasswordShouldFailWithCurrentPasswordAsync() { - const string email = "test6@exceptionless.io"; - const string password = "Test6 password"; - const string salt = "1234567890123456"; - string passwordHash = password.ToSaltedHash(salt); - - var user = new User { - EmailAddress = email, - Password = passwordHash, - Salt = salt, - IsEmailAddressVerified = true, - FullName = "User 6", - Roles = AuthorizationRoles.AllScopes - }; - - user.CreatePasswordResetToken(); - await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); - - var result = await SendRequestAsAsync(r => r - .Post() - .AppendPath("auth/login") - .Content(new LoginModel { - Email = email, - Password = password, - }) - .StatusCodeShouldBeOk() - ); - - Assert.NotNull(result); - Assert.NotEmpty(result.Token); - - var token = await _tokenRepository.GetByIdAsync(result.Token); - Assert.NotNull(token); - - var actualUser = await _userRepository.GetByIdAsync(token.UserId); - Assert.NotNull(actualUser); - Assert.Equal(email, actualUser.EmailAddress); - - await SendRequestAsync(r => r - .Post() - .BasicAuthorization(email, password) - .AppendPath("auth/reset-password") - .Content(new ResetPasswordModel { - PasswordResetToken = user.PasswordResetToken, - Password = password - }) - .StatusCodeShouldBeBadRequest() - ); - - Assert.NotNull(await _tokenRepository.GetByIdAsync(result.Token)); - } + [Fact] + public async Task LoginInvalidExistingActiveDirectoryAsync() { + _authOptions.EnableActiveDirectoryAuth = true; + + var provider = new TestDomainLoginProvider(); + string email = provider.GetEmailAddressFromUsername(TestDomainLoginProvider.ValidUsername); + var user = new User { + EmailAddress = email, + IsEmailAddressVerified = true, + FullName = "User 6" + }; + await _userRepository.AddAsync(user, o => o.ImmediateConsistency()); + + await SendRequestAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = TestDomainLoginProvider.ValidUsername, + Password = "Totallywrongpassword1234" + }) + .StatusCodeShouldBeUnauthorized() + ); + } - private Task CreateTestOrganizationAndProjectsAsync() { - return Task.WhenAll( - _organizationRepository.AddAsync(OrganizationData.GenerateSampleOrganizations(GetService(), GetService()), o => o.ImmediateConsistency()), - _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.ImmediateConsistency()) - ); - } + [Fact] + public async Task CanChangePasswordAsync() { + const string email = "test6@exceptionless.io"; + const string password = "Test6 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 6", + Roles = AuthorizationRoles.AllScopes + }; + + await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = password, + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.NotEmpty(result.Token); + + var token = await _tokenRepository.GetByIdAsync(result.Token); + Assert.NotNull(token); + + var actualUser = await _userRepository.GetByIdAsync(token.UserId); + Assert.NotNull(actualUser); + Assert.Equal(email, actualUser.EmailAddress); + + const string newPassword = "NewP@ssword2"; + var changePasswordResult = await SendRequestAsAsync(r => r + .Post() + .BasicAuthorization(email, password) + .AppendPath("auth/change-password") + .Content(new ChangePasswordModel { + CurrentPassword = password, + Password = newPassword + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(changePasswordResult); + Assert.NotEmpty(changePasswordResult.Token); + + Assert.Null(await _tokenRepository.GetByIdAsync(result.Token)); + Assert.NotNull(await _tokenRepository.GetByIdAsync(changePasswordResult.Token)); + } + + [Fact] + public async Task ChangePasswordShouldFailWithCurrentPasswordAsync() { + const string email = "test6@exceptionless.io"; + const string password = "Test6 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 6", + Roles = AuthorizationRoles.AllScopes + }; + + await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = password, + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.NotEmpty(result.Token); + + var token = await _tokenRepository.GetByIdAsync(result.Token); + Assert.NotNull(token); + + var actualUser = await _userRepository.GetByIdAsync(token.UserId); + Assert.NotNull(actualUser); + Assert.Equal(email, actualUser.EmailAddress); + + await SendRequestAsync(r => r + .Post() + .BasicAuthorization(email, password) + .AppendPath("auth/change-password") + .Content(new ChangePasswordModel { + CurrentPassword = password, + Password = password + }) + .StatusCodeShouldBeBadRequest() + ); + + Assert.NotNull(await _tokenRepository.GetByIdAsync(result.Token)); + } + + [Fact] + public async Task CanResetPasswordAsync() { + const string email = "test6@exceptionless.io"; + const string password = "Test6 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 6", + Roles = AuthorizationRoles.AllScopes + }; + + user.CreatePasswordResetToken(); + await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = password, + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.NotEmpty(result.Token); + + var token = await _tokenRepository.GetByIdAsync(result.Token); + Assert.NotNull(token); + + var actualUser = await _userRepository.GetByIdAsync(token.UserId); + Assert.NotNull(actualUser); + Assert.Equal(email, actualUser.EmailAddress); + + const string newPassword = "NewP@ssword2"; + await SendRequestAsync(r => r + .Post() + .BasicAuthorization(email, password) + .AppendPath("auth/reset-password") + .Content(new ResetPasswordModel { + PasswordResetToken = user.PasswordResetToken, + Password = newPassword + }) + .StatusCodeShouldBeOk() + ); + + Assert.Null(await _tokenRepository.GetByIdAsync(result.Token)); + } + + [Fact] + public async Task ResetPasswordShouldFailWithCurrentPasswordAsync() { + const string email = "test6@exceptionless.io"; + const string password = "Test6 password"; + const string salt = "1234567890123456"; + string passwordHash = password.ToSaltedHash(salt); + + var user = new User { + EmailAddress = email, + Password = passwordHash, + Salt = salt, + IsEmailAddressVerified = true, + FullName = "User 6", + Roles = AuthorizationRoles.AllScopes + }; + + user.CreatePasswordResetToken(); + await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); + + var result = await SendRequestAsAsync(r => r + .Post() + .AppendPath("auth/login") + .Content(new LoginModel { + Email = email, + Password = password, + }) + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(result); + Assert.NotEmpty(result.Token); + + var token = await _tokenRepository.GetByIdAsync(result.Token); + Assert.NotNull(token); + + var actualUser = await _userRepository.GetByIdAsync(token.UserId); + Assert.NotNull(actualUser); + Assert.Equal(email, actualUser.EmailAddress); + + await SendRequestAsync(r => r + .Post() + .BasicAuthorization(email, password) + .AppendPath("auth/reset-password") + .Content(new ResetPasswordModel { + PasswordResetToken = user.PasswordResetToken, + Password = password + }) + .StatusCodeShouldBeBadRequest() + ); + + Assert.NotNull(await _tokenRepository.GetByIdAsync(result.Token)); + } + + private Task CreateTestOrganizationAndProjectsAsync() { + return Task.WhenAll( + _organizationRepository.AddAsync(OrganizationData.GenerateSampleOrganizations(GetService(), GetService()), o => o.ImmediateConsistency()), + _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.ImmediateConsistency()) + ); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs index e25cc83974..470c91f464 100644 --- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Threading.Tasks; using Exceptionless.Tests.Extensions; using Exceptionless.Web.Utility; using Exceptionless.Core.Jobs; @@ -22,7 +16,6 @@ using Exceptionless.Tests.Utility; using Foundatio.Jobs; using Foundatio.Queues; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; @@ -32,532 +25,491 @@ using Foundatio.Repositories.Models; using Exceptionless.Core.Repositories.Queries; -namespace Exceptionless.Tests.Controllers { - public class EventControllerTests : IntegrationTestsBase { - private readonly IEventRepository _eventRepository; - private readonly IQueue _eventQueue; - private readonly IQueue _eventUserDescriptionQueue; +namespace Exceptionless.Tests.Controllers; - public EventControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - Log.MinimumLevel = LogLevel.Warning; +public class EventControllerTests : IntegrationTestsBase { + private readonly IEventRepository _eventRepository; + private readonly IQueue _eventQueue; + private readonly IQueue _eventUserDescriptionQueue; - _eventRepository = GetService(); - _eventQueue = GetService>(); - _eventUserDescriptionQueue = GetService>(); - } - - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await _eventQueue.DeleteQueueAsync(); - - var service = GetService(); - await service.CreateDataAsync(); - } - - [Fact] - public async Task CanPostUserDescriptionAsync() { - const string json = "{\"message\":\"test\",\"reference_id\":\"TestReferenceId\",\"@user\":{\"identity\":\"Test user\",\"name\":null}}"; - await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events") - .Content(json, "application/json") - .StatusCodeShouldBeAccepted() - ); - - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); - - var processEventsJob = GetService(); - await processEventsJob.RunAsync(); - await RefreshDataAsync(); - - stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); + public EventControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + Log.MinimumLevel = LogLevel.Warning; - var events = await _eventRepository.GetAllAsync(); - var ev = events.Documents.Single(e => String.Equals(e.Type, Event.KnownTypes.Log)); - Assert.Equal("test", ev.Message); - Assert.Equal("TestReferenceId", ev.ReferenceId); - - var identity = ev.GetUserIdentity(); - Assert.Equal("Test user", identity.Identity); - Assert.Null(identity.Name); - Assert.Null(identity.Name); - Assert.Null(ev.GetUserDescription()); - - // post description - await _eventUserDescriptionQueue.DeleteQueueAsync(); - await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events/by-ref/TestReferenceId/user-description") - .Content(new EventUserDescription { Description = "Test Description", EmailAddress = TestConstants.UserEmail }) - .StatusCodeShouldBeAccepted() - ); - - stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); - - var userDescriptionJob = GetService(); - await userDescriptionJob.RunAsync(); - - stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Dequeued); - Assert.Equal(0, stats.Abandoned); - Assert.Equal(1, stats.Completed); - - ev = await _eventRepository.GetByIdAsync(ev.Id); - identity = ev.GetUserIdentity(); - Assert.Equal("Test user", identity.Identity); - Assert.Null(identity.Name); - Assert.Null(identity.Name); - - var description = ev.GetUserDescription(); - Assert.Equal("Test Description", description.Description); - Assert.Equal(TestConstants.UserEmail, description.EmailAddress); - } - - [Fact] - public async Task CanPostUserDescriptionWithNoMatchingEventAsync() { - await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events/by-ref/TestReferenceId/user-description") - .Content(new EventUserDescription { Description = "Test Description", EmailAddress = TestConstants.UserEmail }) - .StatusCodeShouldBeAccepted() - ); - - var stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); - - var userDescriptionJob = GetService(); - await userDescriptionJob.RunAsync(); - - stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Dequeued); - Assert.Equal(1, stats.Abandoned); // Event doesn't exist - } - - [Fact] - public async Task CanPostStringAsync() { - const string message = "simple string"; - await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events") - .Content(message, "text/plain") - .StatusCodeShouldBeAccepted() - ); - - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); - - var processEventsJob = GetService(); - await processEventsJob.RunAsync(); - await RefreshDataAsync(); + _eventRepository = GetService(); + _eventQueue = GetService>(); + _eventUserDescriptionQueue = GetService>(); + } - stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await _eventQueue.DeleteQueueAsync(); - var ev = (await _eventRepository.GetAllAsync()).Documents.Single(); - Assert.Equal(message, ev.Message); - } + var service = GetService(); + await service.CreateDataAsync(); + } - [Fact] - public async Task CanPostCompressedStringAsync() { - const string message = "simple string"; - - byte[] data = Encoding.UTF8.GetBytes(message); - var ms = new MemoryStream(); - using (var gzip = new GZipStream(ms, CompressionMode.Compress, true)) - gzip.Write(data, 0, data.Length); - ms.Position = 0; - - var content = new StreamContent(ms); - content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); - content.Headers.ContentEncoding.Add("gzip"); - _httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + TestConstants.ApiKey); - var response = await _httpClient.PostAsync("events", content); - Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); - Assert.True(response.Headers.Contains(Headers.ConfigurationVersion)); - - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); - - var processEventsJob = GetService(); - await processEventsJob.RunAsync(); - await RefreshDataAsync(); - - stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); - - var ev = (await _eventRepository.GetAllAsync()).Documents.Single(); - Assert.Equal(message, ev.Message); - } - - [Fact] - public async Task CanPostJsonWithUserInfoAsync() { - const string json = "{\"message\":\"test\",\"@user\":{\"identity\":\"Test user\",\"name\":null}}"; - await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events") - .Content(json, "application/json") - .StatusCodeShouldBeAccepted() - ); + [Fact] + public async Task CanPostUserDescriptionAsync() { + const string json = "{\"message\":\"test\",\"reference_id\":\"TestReferenceId\",\"@user\":{\"identity\":\"Test user\",\"name\":null}}"; + await SendRequestAsync(r => r + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events") + .Content(json, "application/json") + .StatusCodeShouldBeAccepted() + ); + + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var processEventsJob = GetService(); + await processEventsJob.RunAsync(); + await RefreshDataAsync(); + + stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + + var events = await _eventRepository.GetAllAsync(); + var ev = events.Documents.Single(e => String.Equals(e.Type, Event.KnownTypes.Log)); + Assert.Equal("test", ev.Message); + Assert.Equal("TestReferenceId", ev.ReferenceId); + + var identity = ev.GetUserIdentity(); + Assert.Equal("Test user", identity.Identity); + Assert.Null(identity.Name); + Assert.Null(identity.Name); + Assert.Null(ev.GetUserDescription()); + + // post description + await _eventUserDescriptionQueue.DeleteQueueAsync(); + await SendRequestAsync(r => r + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events/by-ref/TestReferenceId/user-description") + .Content(new EventUserDescription { Description = "Test Description", EmailAddress = TestConstants.UserEmail }) + .StatusCodeShouldBeAccepted() + ); + + stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var userDescriptionJob = GetService(); + await userDescriptionJob.RunAsync(); + + stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Dequeued); + Assert.Equal(0, stats.Abandoned); + Assert.Equal(1, stats.Completed); + + ev = await _eventRepository.GetByIdAsync(ev.Id); + identity = ev.GetUserIdentity(); + Assert.Equal("Test user", identity.Identity); + Assert.Null(identity.Name); + Assert.Null(identity.Name); + + var description = ev.GetUserDescription(); + Assert.Equal("Test Description", description.Description); + Assert.Equal(TestConstants.UserEmail, description.EmailAddress); + } - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); + [Fact] + public async Task CanPostUserDescriptionWithNoMatchingEventAsync() { + await SendRequestAsync(r => r + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events/by-ref/TestReferenceId/user-description") + .Content(new EventUserDescription { Description = "Test Description", EmailAddress = TestConstants.UserEmail }) + .StatusCodeShouldBeAccepted() + ); + + var stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var userDescriptionJob = GetService(); + await userDescriptionJob.RunAsync(); + + stats = await _eventUserDescriptionQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Dequeued); + Assert.Equal(1, stats.Abandoned); // Event doesn't exist + } - var processEventsJob = GetService(); - await processEventsJob.RunAsync(); - await RefreshDataAsync(); + [Fact] + public async Task CanPostStringAsync() { + const string message = "simple string"; + await SendRequestAsync(r => r + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events") + .Content(message, "text/plain") + .StatusCodeShouldBeAccepted() + ); + + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var processEventsJob = GetService(); + await processEventsJob.RunAsync(); + await RefreshDataAsync(); + + stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + + var ev = (await _eventRepository.GetAllAsync()).Documents.Single(); + Assert.Equal(message, ev.Message); + } - stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); + [Fact] + public async Task CanPostCompressedStringAsync() { + const string message = "simple string"; + + byte[] data = Encoding.UTF8.GetBytes(message); + var ms = new MemoryStream(); + using (var gzip = new GZipStream(ms, CompressionMode.Compress, true)) + gzip.Write(data, 0, data.Length); + ms.Position = 0; + + var content = new StreamContent(ms); + content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + content.Headers.ContentEncoding.Add("gzip"); + _httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + TestConstants.ApiKey); + var response = await _httpClient.PostAsync("events", content); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Assert.True(response.Headers.Contains(Headers.ConfigurationVersion)); + + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var processEventsJob = GetService(); + await processEventsJob.RunAsync(); + await RefreshDataAsync(); + + stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + + var ev = (await _eventRepository.GetAllAsync()).Documents.Single(); + Assert.Equal(message, ev.Message); + } - var events = await _eventRepository.GetAllAsync(); - var ev = events.Documents.Single(e => String.Equals(e.Type, Event.KnownTypes.Log)); - Assert.Equal("test", ev.Message); + [Fact] + public async Task CanPostJsonWithUserInfoAsync() { + const string json = "{\"message\":\"test\",\"@user\":{\"identity\":\"Test user\",\"name\":null}}"; + await SendRequestAsync(r => r + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events") + .Content(json, "application/json") + .StatusCodeShouldBeAccepted() + ); + + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var processEventsJob = GetService(); + await processEventsJob.RunAsync(); + await RefreshDataAsync(); + + stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + + var events = await _eventRepository.GetAllAsync(); + var ev = events.Documents.Single(e => String.Equals(e.Type, Event.KnownTypes.Log)); + Assert.Equal("test", ev.Message); + + var userInfo = ev.GetUserIdentity(); + Assert.NotNull(userInfo); + Assert.Equal("Test user", userInfo.Identity); + Assert.Null(userInfo.Name); + } - var userInfo = ev.GetUserIdentity(); - Assert.NotNull(userInfo); - Assert.Equal("Test user", userInfo.Identity); - Assert.Null(userInfo.Name); - } + [Fact] + public async Task CanPostEventAsync() { + var ev = new RandomEventGenerator().GeneratePersistent(false); + if (String.IsNullOrEmpty(ev.Message)) + ev.Message = "Generated message."; + + await SendRequestAsync(r => r + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events") + .Content(ev) + .StatusCodeShouldBeAccepted() + ); + + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var processEventsJob = GetService(); + await processEventsJob.RunAsync(); + await RefreshDataAsync(); + + stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); + + var actual = await _eventRepository.GetAllAsync(); + Assert.Single(actual.Documents); + Assert.Equal(ev.Message, actual.Documents.Single().Message); + } - [Fact] - public async Task CanPostEventAsync() { - var ev = new RandomEventGenerator().GeneratePersistent(false); - if (String.IsNullOrEmpty(ev.Message)) - ev.Message = "Generated message."; + [Fact] + public async Task CanPostManyEventsAsync() { + const int batchSize = 50; + const int batchCount = 10; + await Run.InParallelAsync(batchCount, async i => { + var events = new RandomEventGenerator().Generate(batchSize, false); await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events") - .Content(ev) - .StatusCodeShouldBeAccepted() + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events") + .Content(events) + .StatusCodeShouldBeAccepted() ); + }); + + await RefreshDataAsync(); + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(batchCount, stats.Enqueued); + Assert.Equal(0, stats.Completed); + + var processEventsJob = GetService(); + var sw = Stopwatch.StartNew(); + await processEventsJob.RunUntilEmptyAsync(); + sw.Stop(); + _logger.LogInformation("{Duration:g}", sw.Elapsed); + + await RefreshDataAsync(); + stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(batchCount, stats.Completed); + Assert.Equal(batchSize * batchCount, await _eventRepository.CountAsync()); + } - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); - - var processEventsJob = GetService(); - await processEventsJob.RunAsync(); - await RefreshDataAsync(); - - stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); - - var actual = await _eventRepository.GetAllAsync(); - Assert.Single(actual.Documents); - Assert.Equal(ev.Message, actual.Documents.Single().Message); - } - - [Fact] - public async Task CanPostManyEventsAsync() { - const int batchSize = 50; - const int batchCount = 10; - - await Run.InParallelAsync(batchCount, async i => { - var events = new RandomEventGenerator().Generate(batchSize, false); - await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events") - .Content(events) - .StatusCodeShouldBeAccepted() - ); - }); - - await RefreshDataAsync(); - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(batchCount, stats.Enqueued); - Assert.Equal(0, stats.Completed); - - var processEventsJob = GetService(); - var sw = Stopwatch.StartNew(); - await processEventsJob.RunUntilEmptyAsync(); - sw.Stop(); - _logger.LogInformation("{Duration:g}", sw.Elapsed); - - await RefreshDataAsync(); - stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(batchCount, stats.Completed); - Assert.Equal(batchSize * batchCount, await _eventRepository.CountAsync()); - } - - [Fact] - public async Task CanGetMostFrequentStackMode() { - await CreateStacksAndEventsAsync(); - - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} (status:open OR status:regressed)") - .QueryString("mode", "stack_frequent") - .QueryString("offset", "-300m") - .QueryString("limit", 20) - .StatusCodeShouldBeOk() - ); - - Assert.Equal(2, results.Count); - } - - [Fact] - public async Task CanGetProjectLevelMostFrequentStackMode() { - await CreateStacksAndEventsAsync(); - - string projectId = SampleDataService.TEST_PROJECT_ID; - - var results = await SendRequestAsAsync>(r => r - .AsTestOrganizationUser() - .AppendPath("projects", projectId, "events") - .QueryString("filter", $"project:{projectId} (status:open OR status:regressed)") - .QueryString("mode", "stack_frequent") - .QueryString("offset", "-300m") - .QueryString("limit", 20) - .StatusCodeShouldBeOk() - ); - - Assert.Equal(2, results.Count); - } - - [Fact] - public async Task CanGetFreeProjectLevelMostFrequentStackMode() { - await CreateStacksAndEventsAsync(); - - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - string projectId = SampleDataService.FREE_PROJECT_ID; - var results = await SendRequestAsAsync>(r => r - .AsFreeOrganizationUser() - .AppendPath("projects", projectId, "events") - .QueryString("filter", $"project:{projectId} (status:open OR status:regressed)") - .QueryString("mode", "stack_frequent") - .QueryString("offset", "-300m") - .QueryString("limit", 20) - .StatusCodeShouldBeOk() - ); + [Fact] + public async Task CanGetMostFrequentStackMode() { + await CreateStacksAndEventsAsync(); + + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} (status:open OR status:regressed)") + .QueryString("mode", "stack_frequent") + .QueryString("offset", "-300m") + .QueryString("limit", 20) + .StatusCodeShouldBeOk() + ); + + Assert.Equal(2, results.Count); + } - Assert.Equal(2, results.Count); - } + [Fact] + public async Task CanGetProjectLevelMostFrequentStackMode() { + await CreateStacksAndEventsAsync(); - [Fact] - public async Task CanGetNewStackMode() { - Log.MinimumLevel = LogLevel.Warning; - await CreateStacksAndEventsAsync(); + string projectId = SampleDataService.TEST_PROJECT_ID; - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} (status:open OR status:regressed)") - .QueryString("mode", "stack_new") - .QueryString("time", "last 12 hours") - .StatusCodeShouldBeOk() - ); + var results = await SendRequestAsAsync>(r => r + .AsTestOrganizationUser() + .AppendPath("projects", projectId, "events") + .QueryString("filter", $"project:{projectId} (status:open OR status:regressed)") + .QueryString("mode", "stack_frequent") + .QueryString("offset", "-300m") + .QueryString("limit", 20) + .StatusCodeShouldBeOk() + ); - Assert.Equal(2, results.Count); - } + Assert.Equal(2, results.Count); + } - [Fact] - public async Task GetRecentStackMode() { - await CreateStacksAndEventsAsync(); + [Fact] + public async Task CanGetFreeProjectLevelMostFrequentStackMode() { + await CreateStacksAndEventsAsync(); + + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + string projectId = SampleDataService.FREE_PROJECT_ID; + var results = await SendRequestAsAsync>(r => r + .AsFreeOrganizationUser() + .AppendPath("projects", projectId, "events") + .QueryString("filter", $"project:{projectId} (status:open OR status:regressed)") + .QueryString("mode", "stack_frequent") + .QueryString("offset", "-300m") + .QueryString("limit", 20) + .StatusCodeShouldBeOk() + ); + + Assert.Equal(2, results.Count); + } - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} (status:open OR status:regressed)") - .QueryString("mode", "stack_recent") - .StatusCodeShouldBeOk() - ); + [Fact] + public async Task CanGetNewStackMode() { + Log.MinimumLevel = LogLevel.Warning; + await CreateStacksAndEventsAsync(); + + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} (status:open OR status:regressed)") + .QueryString("mode", "stack_new") + .QueryString("time", "last 12 hours") + .StatusCodeShouldBeOk() + ); + + Assert.Equal(2, results.Count); + } - Assert.Equal(2, results.Count); - } + [Fact] + public async Task GetRecentStackMode() { + await CreateStacksAndEventsAsync(); - [Fact] - public async Task GetUsersStackMode() { - await CreateStacksAndEventsAsync(); + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} (status:open OR status:regressed)") + .QueryString("mode", "stack_recent") + .StatusCodeShouldBeOk() + ); - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} type:error (status:open OR status:regressed)") - .QueryString("mode", "stack_users") - .QueryString("offset", "-300m") - .StatusCodeShouldBeOk() - ); + Assert.Equal(2, results.Count); + } - Assert.Single(results); - } + [Fact] + public async Task GetUsersStackMode() { + await CreateStacksAndEventsAsync(); - [Fact] - public async Task WillExcludeDeletedSessions() { - await CreateDataAsync(d => { - d.Event().TestProject().Type(Event.KnownTypes.Session).Deleted(); - d.Event().TestProject().Type(Event.KnownTypes.Session); - }); - - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - var countResult = await SendRequestAsAsync(r => r - .AsGlobalAdminUser() - .AppendPath("projects", SampleDataService.TEST_PROJECT_ID, "events", "count") - .QueryString("filter", "type:session _missing_:data.sessionend") - .StatusCodeShouldBeOk() - ); + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", $"project:{SampleDataService.TEST_PROJECT_ID} type:error (status:open OR status:regressed)") + .QueryString("mode", "stack_users") + .QueryString("offset", "-300m") + .StatusCodeShouldBeOk() + ); - Assert.Equal(1, countResult.Total); + Assert.Single(results); + } - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("projects", SampleDataService.TEST_PROJECT_ID, "events", "sessions") - .QueryString("filter", "_missing_:data.sessionend") - .StatusCodeShouldBeOk() - ); + [Fact] + public async Task WillExcludeDeletedSessions() { + await CreateDataAsync(d => { + d.Event().TestProject().Type(Event.KnownTypes.Session).Deleted(); + d.Event().TestProject().Type(Event.KnownTypes.Session); + }); + + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + var countResult = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPath("projects", SampleDataService.TEST_PROJECT_ID, "events", "count") + .QueryString("filter", "type:session _missing_:data.sessionend") + .StatusCodeShouldBeOk() + ); + + Assert.Equal(1, countResult.Total); + + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("projects", SampleDataService.TEST_PROJECT_ID, "events", "sessions") + .QueryString("filter", "_missing_:data.sessionend") + .StatusCodeShouldBeOk() + ); + + Assert.Single(results); + } - Assert.Single(results); - } - - [Fact] - public async Task WillGetStackEvents() { - var (stacks, _) = await CreateDataAsync(d => { - d.Event().TestProject(); - }); - - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - var result = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("stacks", stacks.Single().Id, "events") - .StatusCodeShouldBeOk() - ); + [Fact] + public async Task WillGetStackEvents() { + var (stacks, _) = await CreateDataAsync(d => { + d.Event().TestProject(); + }); - Assert.Single(result); - } - - [Fact] - public async Task WillGetEventSessions() { - string sessionId = Guid.NewGuid().ToString("N"); - await CreateDataAsync(d => { - d.Event().TestProject().Type(Event.KnownTypes.Session).SessionId(sessionId); - d.Event().TestProject().Type(Event.KnownTypes.Log).SessionId(sessionId); - }); - - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - var result = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events/sessions", sessionId) - .QueryString("filter", "-type:heartbeat") - .QueryString("limit", "10") - .StatusCodeShouldBeOk() - ); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); - Assert.Equal(2, result.Count); - - result = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("projects", SampleDataService.TEST_PROJECT_ID, "events/sessions", sessionId) - .QueryString("filter", "-type:heartbeat") - .QueryString("limit", "10") - .QueryString("offset", "-360m") - .QueryString("time", $"{SystemClock.UtcNow.SubtractDays(180):s}-now") - .StatusCodeShouldBeOk() - ); + var result = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("stacks", stacks.Single().Id, "events") + .StatusCodeShouldBeOk() + ); - Assert.Equal(2, result.Count); - } + Assert.Single(result); + } - [Theory] - [InlineData("status:open", 1)] - [InlineData("status:regressed", 1)] - [InlineData("status:ignored", 1)] - [InlineData("(status:open OR status:regressed)", 2)] - [InlineData("is_fixed:true", 2)] - [InlineData("status:fixed", 2)] - [InlineData("status:discarded", 0)] - [InlineData("tags:old_tag", 0)] // Stack only tags won't be resolved - [InlineData("type:log status:fixed", 2)] - [InlineData("type:log version_fixed:1.2.3", 1)] - [InlineData("type:error is_hidden:false is_fixed:false is_regressed:true", 1)] - [InlineData("type:error hidden:false fixed:false", 1)] - [InlineData("type:log status:fixed version_fixed:1.2.3", 1)] - [InlineData("1ecd0826e447a44e78877ab1", 0)] // Stack Id - [InlineData("type:error", 1)] - public async Task CheckStackModeCounts(string filter, int expected) { - await CreateStacksAndEventsAsync(); - - string[] modes = new [] { "stack_recent", "stack_frequent", "stack_new", "stack_users" }; - foreach (string mode in modes) { - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", filter) - .QueryString("mode", mode) - .StatusCodeShouldBeOk() - ); - - Assert.Equal(expected, results.Count); - - // @! forces use of opposite of default filter inversion - results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", $"@!{filter}") - .QueryString("mode", mode) - .StatusCodeShouldBeOk() - ); - - Assert.Equal(expected, results.Count); - } - } - - [Theory] - [InlineData("status:open", 1)] - [InlineData("status:regressed", 3)] - [InlineData("status:ignored", 1)] - [InlineData("(status:open OR status:regressed)", 4)] - [InlineData("is_fixed:true", 2)] - [InlineData("status:fixed", 2)] - [InlineData("status:discarded", 0)] - [InlineData("tags:old_tag", 0)] // Stack only tags won't be resolved - [InlineData("type:log status:fixed", 2)] - [InlineData("type:log version_fixed:1.2.3", 1)] - [InlineData("type:error is_hidden:false is_fixed:false is_regressed:true", 2)] - [InlineData("type:log status:fixed version_fixed:1.2.3", 1)] - [InlineData("1ecd0826e447a44e78877ab1", 0)] // Stack Id - [InlineData("type:error", 2)] - public async Task CheckSummaryModeCounts(string filter, int expected) { - await CreateStacksAndEventsAsync(); - Log.SetLogLevel(LogLevel.Trace); + [Fact] + public async Task WillGetEventSessions() { + string sessionId = Guid.NewGuid().ToString("N"); + await CreateDataAsync(d => { + d.Event().TestProject().Type(Event.KnownTypes.Session).SessionId(sessionId); + d.Event().TestProject().Type(Event.KnownTypes.Log).SessionId(sessionId); + }); + + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + var result = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events/sessions", sessionId) + .QueryString("filter", "-type:heartbeat") + .QueryString("limit", "10") + .StatusCodeShouldBeOk() + ); + + Assert.Equal(2, result.Count); + + result = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("projects", SampleDataService.TEST_PROJECT_ID, "events/sessions", sessionId) + .QueryString("filter", "-type:heartbeat") + .QueryString("limit", "10") + .QueryString("offset", "-360m") + .QueryString("time", $"{SystemClock.UtcNow.SubtractDays(180):s}-now") + .StatusCodeShouldBeOk() + ); + + Assert.Equal(2, result.Count); + } + [Theory] + [InlineData("status:open", 1)] + [InlineData("status:regressed", 1)] + [InlineData("status:ignored", 1)] + [InlineData("(status:open OR status:regressed)", 2)] + [InlineData("is_fixed:true", 2)] + [InlineData("status:fixed", 2)] + [InlineData("status:discarded", 0)] + [InlineData("tags:old_tag", 0)] // Stack only tags won't be resolved + [InlineData("type:log status:fixed", 2)] + [InlineData("type:log version_fixed:1.2.3", 1)] + [InlineData("type:error is_hidden:false is_fixed:false is_regressed:true", 1)] + [InlineData("type:error hidden:false fixed:false", 1)] + [InlineData("type:log status:fixed version_fixed:1.2.3", 1)] + [InlineData("1ecd0826e447a44e78877ab1", 0)] // Stack Id + [InlineData("type:error", 1)] + public async Task CheckStackModeCounts(string filter, int expected) { + await CreateStacksAndEventsAsync(); + + string[] modes = new[] { "stack_recent", "stack_frequent", "stack_new", "stack_users" }; + foreach (string mode in modes) { var results = await SendRequestAsAsync>(r => r .AsGlobalAdminUser() .AppendPath("events") .QueryString("filter", filter) - .QueryString("mode", "summary") + .QueryString("mode", mode) .StatusCodeShouldBeOk() ); @@ -568,247 +520,288 @@ public async Task CheckSummaryModeCounts(string filter, int expected) { .AsGlobalAdminUser() .AppendPath("events") .QueryString("filter", $"@!{filter}") - .QueryString("mode", "summary") + .QueryString("mode", mode) .StatusCodeShouldBeOk() ); Assert.Equal(expected, results.Count); } + } - [InlineData(null)] - [InlineData("")] - [InlineData("@!")] - [InlineData("status:open OR status:regressed")] - [InlineData("(status:open OR status:regressed)")] - [InlineData("@!status:open OR status:regressed")] - [InlineData("@!(status:open OR status:regressed)")] - [Theory] - public async Task WillExcludeDeletedStacks(string filter) { - var utcNow = SystemClock.UtcNow; - - await CreateDataAsync(d => { - d.Event() - .TestProject() - .Type(Event.KnownTypes.Log) - .Status(StackStatus.Open) - .Deleted() - .TotalOccurrences(50) - .FirstOccurrence(utcNow.SubtractDays(1)); - - d.Event() - .TestProject() - .Type(Event.KnownTypes.Error) - .Status(StackStatus.Regressed) - .TotalOccurrences(10) - .FirstOccurrence(utcNow.SubtractDays(2)) - .StackReference("https://github.com/exceptionless/Exceptionless") - .Version("3.2.1-beta1"); - }); - - Log.MinimumLevel = LogLevel.Trace; - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryStringIf(() => !String.IsNullOrEmpty(filter), "filter", filter) - .QueryString("mode", "stack_new") - .StatusCodeShouldBeOk() - ); - - Assert.Single(results); - - var countResult = await SendRequestAsAsync(r => r - .AsGlobalAdminUser() - .AppendPath("events", "count") - .QueryStringIf(() => !String.IsNullOrEmpty(filter), "filter", filter) - .QueryString("aggregations", "date:(date cardinality:stack sum:count~1) cardinality:stack terms:(first @include:true) sum:count~1") - .StatusCodeShouldBeOk() - ); - - var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); - double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); - Assert.Equal(1, dateAggStackCount); - Assert.Equal(1, dateAggEventCount); - - double? total = countResult.Aggregations.Sum("sum_count")?.Value; - double newTotal = countResult.Aggregations.Terms("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0; - double uniqueTotal = countResult.Aggregations.Cardinality("cardinality_stack")?.Value ?? 0; - - Assert.Equal(1, total); - Assert.Equal(0, newTotal); - Assert.Equal(1, uniqueTotal); - } - - [Fact] - public async Task WillExcludeOldStacksForStackNewMode() { - var utcNow = SystemClock.UtcNow; - - await CreateDataAsync(d => { - d.Event() - .TestProject() - .Message("New stack - skip due to date filter") - .Type(Event.KnownTypes.Log) - .Status(StackStatus.Open) - .TotalOccurrences(50) - .IsFirstOccurrence() - .FirstOccurrence(utcNow.SubtractYears(1)) - .LastOccurrence(utcNow.SubtractMonths(5)); - - d.Event() - .TestProject() - .Message("Old stack - new event") - .Type(Event.KnownTypes.Log) - .Status(StackStatus.Regressed) - .TotalOccurrences(33) - .FirstOccurrence(utcNow.SubtractYears(1)) - .LastOccurrence(utcNow); - - d.Event() - .TestProject() - .Message("New Stack - event not marked as first occurrence") - .Type(Event.KnownTypes.Log) - .Status(StackStatus.Open) - .TotalOccurrences(15) - .FirstOccurrence(utcNow.SubtractDays(2)) - .Version("1.2.3"); - - d.Event() - .TestProject() - .Message("New Stack - event marked as first occurrence") - .Type(Event.KnownTypes.Error) - .Status(StackStatus.Regressed) - .TotalOccurrences(10) - .FirstOccurrence(utcNow.SubtractDays(2)) - .Date(utcNow.SubtractDays(2)) - .IsFirstOccurrence() - .StackReference("https://github.com/exceptionless/Exceptionless") - .Version("3.2.1-beta1"); - - d.Event() - .TestProject() - .Message("Deleted New stack - event is first occurrence") - .Type(Event.KnownTypes.FeatureUsage) - .Status(StackStatus.Open) - .TotalOccurrences(7) - .FirstOccurrence(utcNow.Date) - .IsFirstOccurrence() - .Date(utcNow.Date) - .Deleted(); - }); - - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - const string filter = "(status:open OR status:regressed)"; - const string time = "last week"; - - /* - _logger.LogInformation("Running non-inverted query"); - var invertedResults = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", "@!" + filter) - .QueryString("time", time) - .QueryString("mode", "stack_new") - .StatusCodeShouldBeOk() - );*/ - - //Assert.Equal(2, invertedResults.Count); - - _logger.LogInformation("Running inverted query"); - var results = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("events") - .QueryString("filter", filter) - .QueryString("time", time) - .QueryString("mode", "stack_new") - .StatusCodeShouldBeOk() - ); + [Theory] + [InlineData("status:open", 1)] + [InlineData("status:regressed", 3)] + [InlineData("status:ignored", 1)] + [InlineData("(status:open OR status:regressed)", 4)] + [InlineData("is_fixed:true", 2)] + [InlineData("status:fixed", 2)] + [InlineData("status:discarded", 0)] + [InlineData("tags:old_tag", 0)] // Stack only tags won't be resolved + [InlineData("type:log status:fixed", 2)] + [InlineData("type:log version_fixed:1.2.3", 1)] + [InlineData("type:error is_hidden:false is_fixed:false is_regressed:true", 2)] + [InlineData("type:log status:fixed version_fixed:1.2.3", 1)] + [InlineData("1ecd0826e447a44e78877ab1", 0)] // Stack Id + [InlineData("type:error", 2)] + public async Task CheckSummaryModeCounts(string filter, int expected) { + await CreateStacksAndEventsAsync(); + Log.SetLogLevel(LogLevel.Trace); + + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", filter) + .QueryString("mode", "summary") + .StatusCodeShouldBeOk() + ); + + Assert.Equal(expected, results.Count); + + // @! forces use of opposite of default filter inversion + results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", $"@!{filter}") + .QueryString("mode", "summary") + .StatusCodeShouldBeOk() + ); + + Assert.Equal(expected, results.Count); + } - Assert.Equal(2, results.Count); + [InlineData(null)] + [InlineData("")] + [InlineData("@!")] + [InlineData("status:open OR status:regressed")] + [InlineData("(status:open OR status:regressed)")] + [InlineData("@!status:open OR status:regressed")] + [InlineData("@!(status:open OR status:regressed)")] + [Theory] + public async Task WillExcludeDeletedStacks(string filter) { + var utcNow = SystemClock.UtcNow; + + await CreateDataAsync(d => { + d.Event() + .TestProject() + .Type(Event.KnownTypes.Log) + .Status(StackStatus.Open) + .Deleted() + .TotalOccurrences(50) + .FirstOccurrence(utcNow.SubtractDays(1)); + + d.Event() + .TestProject() + .Type(Event.KnownTypes.Error) + .Status(StackStatus.Regressed) + .TotalOccurrences(10) + .FirstOccurrence(utcNow.SubtractDays(2)) + .StackReference("https://github.com/exceptionless/Exceptionless") + .Version("3.2.1-beta1"); + }); + + Log.MinimumLevel = LogLevel.Trace; + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryStringIf(() => !String.IsNullOrEmpty(filter), "filter", filter) + .QueryString("mode", "stack_new") + .StatusCodeShouldBeOk() + ); + + Assert.Single(results); + + var countResult = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPath("events", "count") + .QueryStringIf(() => !String.IsNullOrEmpty(filter), "filter", filter) + .QueryString("aggregations", "date:(date cardinality:stack sum:count~1) cardinality:stack terms:(first @include:true) sum:count~1") + .StatusCodeShouldBeOk() + ); + + var dateAgg = countResult.Aggregations.DateHistogram("date_date"); + double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); + double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); + Assert.Equal(1, dateAggStackCount); + Assert.Equal(1, dateAggEventCount); + + double? total = countResult.Aggregations.Sum("sum_count")?.Value; + double newTotal = countResult.Aggregations.Terms("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0; + double uniqueTotal = countResult.Aggregations.Cardinality("cardinality_stack")?.Value ?? 0; + + Assert.Equal(1, total); + Assert.Equal(0, newTotal); + Assert.Equal(1, uniqueTotal); + } - _logger.LogInformation("Running normal count"); - var countResult = await SendRequestAsAsync(r => r - .AsGlobalAdminUser() - .AppendPath("events", "count") - .QueryString("filter", filter) - .QueryString("time", time) - .QueryString("mode", "stack_new") - .QueryString("aggregations", "date:(date cardinality:stack sum:count~1) cardinality:stack terms:(first @include:true) sum:count~1") - .StatusCodeShouldBeOk() - ); + [Fact] + public async Task WillExcludeOldStacksForStackNewMode() { + var utcNow = SystemClock.UtcNow; + + await CreateDataAsync(d => { + d.Event() + .TestProject() + .Message("New stack - skip due to date filter") + .Type(Event.KnownTypes.Log) + .Status(StackStatus.Open) + .TotalOccurrences(50) + .IsFirstOccurrence() + .FirstOccurrence(utcNow.SubtractYears(1)) + .LastOccurrence(utcNow.SubtractMonths(5)); + + d.Event() + .TestProject() + .Message("Old stack - new event") + .Type(Event.KnownTypes.Log) + .Status(StackStatus.Regressed) + .TotalOccurrences(33) + .FirstOccurrence(utcNow.SubtractYears(1)) + .LastOccurrence(utcNow); + + d.Event() + .TestProject() + .Message("New Stack - event not marked as first occurrence") + .Type(Event.KnownTypes.Log) + .Status(StackStatus.Open) + .TotalOccurrences(15) + .FirstOccurrence(utcNow.SubtractDays(2)) + .Version("1.2.3"); + + d.Event() + .TestProject() + .Message("New Stack - event marked as first occurrence") + .Type(Event.KnownTypes.Error) + .Status(StackStatus.Regressed) + .TotalOccurrences(10) + .FirstOccurrence(utcNow.SubtractDays(2)) + .Date(utcNow.SubtractDays(2)) + .IsFirstOccurrence() + .StackReference("https://github.com/exceptionless/Exceptionless") + .Version("3.2.1-beta1"); + + d.Event() + .TestProject() + .Message("Deleted New stack - event is first occurrence") + .Type(Event.KnownTypes.FeatureUsage) + .Status(StackStatus.Open) + .TotalOccurrences(7) + .FirstOccurrence(utcNow.Date) + .IsFirstOccurrence() + .Date(utcNow.Date) + .Deleted(); + }); + + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + const string filter = "(status:open OR status:regressed)"; + const string time = "last week"; + + /* + _logger.LogInformation("Running non-inverted query"); + var invertedResults = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", "@!" + filter) + .QueryString("time", time) + .QueryString("mode", "stack_new") + .StatusCodeShouldBeOk() + );*/ + + //Assert.Equal(2, invertedResults.Count); + + _logger.LogInformation("Running inverted query"); + var results = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("events") + .QueryString("filter", filter) + .QueryString("time", time) + .QueryString("mode", "stack_new") + .StatusCodeShouldBeOk() + ); + + Assert.Equal(2, results.Count); + + _logger.LogInformation("Running normal count"); + var countResult = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPath("events", "count") + .QueryString("filter", filter) + .QueryString("time", time) + .QueryString("mode", "stack_new") + .QueryString("aggregations", "date:(date cardinality:stack sum:count~1) cardinality:stack terms:(first @include:true) sum:count~1") + .StatusCodeShouldBeOk() + ); + + var dateAgg = countResult.Aggregations.DateHistogram("date_date"); + double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); + double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); + Assert.Equal(2, dateAggStackCount); + Assert.Equal(2, dateAggEventCount); + + double? total = countResult.Aggregations.Sum("sum_count")?.Value; + double newTotal = countResult.Aggregations.Terms("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0; + double uniqueTotal = countResult.Aggregations.Cardinality("cardinality_stack")?.Value ?? 0; + + Assert.Equal(2, total); + Assert.Equal(1, newTotal); + Assert.Equal(2, uniqueTotal); + } - var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); - double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); - Assert.Equal(2, dateAggStackCount); - Assert.Equal(2, dateAggEventCount); - - double? total = countResult.Aggregations.Sum("sum_count")?.Value; - double newTotal = countResult.Aggregations.Terms("terms_first")?.Buckets.FirstOrDefault()?.Total ?? 0; - double uniqueTotal = countResult.Aggregations.Cardinality("cardinality_stack")?.Value ?? 0; - - Assert.Equal(2, total); - Assert.Equal(1, newTotal); - Assert.Equal(2, uniqueTotal); - } - - private async Task CreateStacksAndEventsAsync() { - var utcNow = SystemClock.UtcNow; - - await CreateDataAsync(d => { - // matches event1.json / stack1.json - d.Event() - .FreeProject() - .Type(Event.KnownTypes.Log) - .Level("Error") - .Source("GET /Print") - .DateFixed() - .TotalOccurrences(5) - .StackReference("http://exceptionless.io") - .FirstOccurrence(utcNow.SubtractDays(1)) - .Tag("test", "Critical") - .Geo("40,-70") - .Value(1.0M) - .RequestInfoSample() - .UserIdentity("My-User-Identity", "test user") - .UserDescription("test@exceptionless.com", "my custom description") - .Version("1.2.3.0") - .ReferenceId("876554321"); - - // matches event2.json / stack2.json - var stack2 = d.Event() - .FreeProject() - .Type(Event.KnownTypes.Error) - .Status(StackStatus.Regressed) - .TotalOccurrences(50) - .FirstOccurrence(utcNow.SubtractDays(2)) - .StackReference("https://github.com/exceptionless/Exceptionless") - .Tag("Blake Niemyjski") - .RequestInfoSample() - .UserIdentity("example@exceptionless.com") - .Version("3.2.1-beta1"); - - // matches event3.json and using the same stack as the previous event - d.Event() - .FreeProject() - .Type(Event.KnownTypes.Error) - .Stack(stack2) - .Tag("Blake Niemyjski") - .RequestInfoSample() - .UserIdentity("example", "Blake") - .Version("4.0.1039 6f929bbe18"); - - // defaults everything - d.Event().FreeProject(); - }); - - Log.MinimumLevel = LogLevel.Warning; - await StackData.CreateSearchDataAsync(GetService(), GetService(), true); - await EventData.CreateSearchDataAsync(GetService(), _eventRepository, GetService(), true); - Log.MinimumLevel = LogLevel.Trace; - } + private async Task CreateStacksAndEventsAsync() { + var utcNow = SystemClock.UtcNow; + + await CreateDataAsync(d => { + // matches event1.json / stack1.json + d.Event() + .FreeProject() + .Type(Event.KnownTypes.Log) + .Level("Error") + .Source("GET /Print") + .DateFixed() + .TotalOccurrences(5) + .StackReference("http://exceptionless.io") + .FirstOccurrence(utcNow.SubtractDays(1)) + .Tag("test", "Critical") + .Geo("40,-70") + .Value(1.0M) + .RequestInfoSample() + .UserIdentity("My-User-Identity", "test user") + .UserDescription("test@exceptionless.com", "my custom description") + .Version("1.2.3.0") + .ReferenceId("876554321"); + + // matches event2.json / stack2.json + var stack2 = d.Event() + .FreeProject() + .Type(Event.KnownTypes.Error) + .Status(StackStatus.Regressed) + .TotalOccurrences(50) + .FirstOccurrence(utcNow.SubtractDays(2)) + .StackReference("https://github.com/exceptionless/Exceptionless") + .Tag("Blake Niemyjski") + .RequestInfoSample() + .UserIdentity("example@exceptionless.com") + .Version("3.2.1-beta1"); + + // matches event3.json and using the same stack as the previous event + d.Event() + .FreeProject() + .Type(Event.KnownTypes.Error) + .Stack(stack2) + .Tag("Blake Niemyjski") + .RequestInfoSample() + .UserIdentity("example", "Blake") + .Version("4.0.1039 6f929bbe18"); + + // defaults everything + d.Event().FreeProject(); + }); + + Log.MinimumLevel = LogLevel.Warning; + await StackData.CreateSearchDataAsync(GetService(), GetService(), true); + await EventData.CreateSearchDataAsync(GetService(), _eventRepository, GetService(), true); + Log.MinimumLevel = LogLevel.Trace; } } diff --git a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs index c68a82db3b..a6b481cac1 100644 --- a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Jobs; using Exceptionless.Core.Models; using Exceptionless.Tests.Extensions; @@ -9,104 +6,103 @@ using Exceptionless.Web.Models; using FluentRest; using Foundatio.Jobs; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Controllers { - public sealed class ProjectControllerTests : IntegrationTestsBase { - public ProjectControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - } - - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - var service = GetService(); - await service.CreateDataAsync(); - } - - [Fact] - public async Task CanGetProjectConfiguration() { - var response = await SendRequestAsync(r => r - .AsFreeOrganizationClientUser() - .AppendPath("projects/config") - .StatusCodeShouldBeOk() - ); - - Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType); - Assert.Equal("utf-8", response.Content.Headers.ContentType?.CharSet); - Assert.True(response.Content.Headers.ContentLength.HasValue); - Assert.True(response.Content.Headers.ContentLength > 0); - - var config = await response.DeserializeAsync(); - Assert.True(config.Settings.GetBoolean("IncludeConditionalData")); - Assert.Equal(0, config.Version); - } - - [Fact] - public async Task CanGetProjectListStats() { - var projects = await SendRequestAsAsync>(r => r - .AsTestOrganizationUser() - .AppendPath("projects") - .QueryString("mode", "stats") - .StatusCodeShouldBeOk() - ); - - var project = projects.Single(); - Assert.Equal(0, project.StackCount); - Assert.Equal(0, project.EventCount); - - var (stacks, events) = await CreateDataAsync(d => { - d.Event().Message("test"); - }); - - projects = await SendRequestAsAsync>(r => r - .AsTestOrganizationUser() - .AppendPath("projects") - .QueryString("mode", "stats") - .StatusCodeShouldBeOk() - ); - - project = projects.Single(); - Assert.Equal(stacks.Count, project.StackCount); - Assert.Equal(events.Count, project.EventCount); - - // Reset Project data and ensure soft deleted counts don't show up - var workItems = await SendRequestAsAsync(r => r - .AsTestOrganizationUser() - .AppendPath("projects", project.Id, "reset-data") - .StatusCodeShouldBeAccepted() - ); - - Assert.Single(workItems.Workers); - var workItemJob = GetService(); - await workItemJob.RunUntilEmptyAsync(); - await RefreshDataAsync(); - - projects = await SendRequestAsAsync>(r => r - .AsTestOrganizationUser() - .AppendPath("projects") - .QueryString("mode", "stats") - .StatusCodeShouldBeOk() - ); - - project = projects.Single(); - // Stacks and event counts include soft deleted (performance reasons) - Assert.Equal(stacks.Count, project.StackCount); - Assert.Equal(events.Count, project.EventCount); - - var cleanupJob = GetService(); - await cleanupJob.RunAsync(); - - projects = await SendRequestAsAsync>(r => r - .AsTestOrganizationUser() - .AppendPath("projects") - .QueryString("mode", "stats") - .StatusCodeShouldBeOk() - ); - - project = projects.Single(); - Assert.Equal(0, project.StackCount); - Assert.Equal(0, project.EventCount); - } +namespace Exceptionless.Tests.Controllers; + +public sealed class ProjectControllerTests : IntegrationTestsBase { + public ProjectControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + } + + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + var service = GetService(); + await service.CreateDataAsync(); + } + + [Fact] + public async Task CanGetProjectConfiguration() { + var response = await SendRequestAsync(r => r + .AsFreeOrganizationClientUser() + .AppendPath("projects/config") + .StatusCodeShouldBeOk() + ); + + Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType); + Assert.Equal("utf-8", response.Content.Headers.ContentType?.CharSet); + Assert.True(response.Content.Headers.ContentLength.HasValue); + Assert.True(response.Content.Headers.ContentLength > 0); + + var config = await response.DeserializeAsync(); + Assert.True(config.Settings.GetBoolean("IncludeConditionalData")); + Assert.Equal(0, config.Version); + } + + [Fact] + public async Task CanGetProjectListStats() { + var projects = await SendRequestAsAsync>(r => r + .AsTestOrganizationUser() + .AppendPath("projects") + .QueryString("mode", "stats") + .StatusCodeShouldBeOk() + ); + + var project = projects.Single(); + Assert.Equal(0, project.StackCount); + Assert.Equal(0, project.EventCount); + + var (stacks, events) = await CreateDataAsync(d => { + d.Event().Message("test"); + }); + + projects = await SendRequestAsAsync>(r => r + .AsTestOrganizationUser() + .AppendPath("projects") + .QueryString("mode", "stats") + .StatusCodeShouldBeOk() + ); + + project = projects.Single(); + Assert.Equal(stacks.Count, project.StackCount); + Assert.Equal(events.Count, project.EventCount); + + // Reset Project data and ensure soft deleted counts don't show up + var workItems = await SendRequestAsAsync(r => r + .AsTestOrganizationUser() + .AppendPath("projects", project.Id, "reset-data") + .StatusCodeShouldBeAccepted() + ); + + Assert.Single(workItems.Workers); + var workItemJob = GetService(); + await workItemJob.RunUntilEmptyAsync(); + await RefreshDataAsync(); + + projects = await SendRequestAsAsync>(r => r + .AsTestOrganizationUser() + .AppendPath("projects") + .QueryString("mode", "stats") + .StatusCodeShouldBeOk() + ); + + project = projects.Single(); + // Stacks and event counts include soft deleted (performance reasons) + Assert.Equal(stacks.Count, project.StackCount); + Assert.Equal(events.Count, project.EventCount); + + var cleanupJob = GetService(); + await cleanupJob.RunAsync(); + + projects = await SendRequestAsAsync>(r => r + .AsTestOrganizationUser() + .AppendPath("projects") + .QueryString("mode", "stats") + .StatusCodeShouldBeOk() + ); + + project = projects.Single(); + Assert.Equal(0, project.StackCount); + Assert.Equal(0, project.EventCount); } } diff --git a/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs b/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs index 98bed09fc9..f8f6462e01 100644 --- a/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Tests.Extensions; using Exceptionless.Core.Jobs; @@ -14,105 +11,104 @@ using Foundatio.Queues; using Xunit; using Xunit.Abstractions; -using System.Collections.Generic; - -namespace Exceptionless.Tests.Controllers { - public class StackControllerTests : IntegrationTestsBase { - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly IQueue _eventQueue; - private readonly IQueue _workItemQueue; - - public StackControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _stackRepository = GetService(); - _eventRepository = GetService(); - _eventQueue = GetService>(); - _workItemQueue = GetService>(); - } - - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await _eventQueue.DeleteQueueAsync(); - await _workItemQueue.DeleteQueueAsync(); - - var service = GetService(); - await service.CreateDataAsync(); - } - - [Fact] - public async Task CanSearchByNonPremiumFields() { - var ev = await SubmitErrorEventAsync(); - Assert.NotNull(ev.StackId); - - var stack = await _stackRepository.GetByIdAsync(ev.StackId); - Assert.NotNull(stack); - Assert.False(stack.IsFixed()); - - var result = await SendRequestAsAsync>(r => r - .AsGlobalAdminUser() - .AppendPath("stacks") - .QueryString("f", "status:fixed") - .StatusCodeShouldBeOk()); - - Assert.NotNull(result); - Assert.Equal(1, result.Count); - } - - [Theory] - [InlineData(null)] - [InlineData("1.0.0")] - [InlineData("1.0.0.0")] - public async Task CanMarkFixed(string version) { - var ev = await SubmitErrorEventAsync(); - Assert.NotNull(ev.StackId); - - var stack = await _stackRepository.GetByIdAsync(ev.StackId); - Assert.NotNull(stack); - Assert.False(stack.IsFixed()); - - await SendRequestAsAsync(r => r - .Post() - .AsGlobalAdminUser() - .AppendPath($"stacks/{stack.Id}/mark-fixed") - .QueryStringIf(() => !String.IsNullOrEmpty(version), "version", version) - .StatusCodeShouldBeOk()); - - stack = await _stackRepository.GetByIdAsync(ev.StackId); - Assert.NotNull(stack); - Assert.True(stack.IsFixed()); - } - - private async Task SubmitErrorEventAsync() { - const string message = "simple string"; - await SendRequestAsync(r => r - .Post() - .AsTestOrganizationClientUser() - .AppendPath("events") - .Content(new Event { - Message = message, - Type = Event.KnownTypes.Error, - Data = {{ Event.KnownDataKeys.SimpleError, new SimpleError { + +namespace Exceptionless.Tests.Controllers; + +public class StackControllerTests : IntegrationTestsBase { + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly IQueue _eventQueue; + private readonly IQueue _workItemQueue; + + public StackControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _stackRepository = GetService(); + _eventRepository = GetService(); + _eventQueue = GetService>(); + _workItemQueue = GetService>(); + } + + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await _eventQueue.DeleteQueueAsync(); + await _workItemQueue.DeleteQueueAsync(); + + var service = GetService(); + await service.CreateDataAsync(); + } + + [Fact] + public async Task CanSearchByNonPremiumFields() { + var ev = await SubmitErrorEventAsync(); + Assert.NotNull(ev.StackId); + + var stack = await _stackRepository.GetByIdAsync(ev.StackId); + Assert.NotNull(stack); + Assert.False(stack.IsFixed()); + + var result = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("stacks") + .QueryString("f", "status:fixed") + .StatusCodeShouldBeOk()); + + Assert.NotNull(result); + Assert.Equal(1, result.Count); + } + + [Theory] + [InlineData(null)] + [InlineData("1.0.0")] + [InlineData("1.0.0.0")] + public async Task CanMarkFixed(string version) { + var ev = await SubmitErrorEventAsync(); + Assert.NotNull(ev.StackId); + + var stack = await _stackRepository.GetByIdAsync(ev.StackId); + Assert.NotNull(stack); + Assert.False(stack.IsFixed()); + + await SendRequestAsAsync(r => r + .Post() + .AsGlobalAdminUser() + .AppendPath($"stacks/{stack.Id}/mark-fixed") + .QueryStringIf(() => !String.IsNullOrEmpty(version), "version", version) + .StatusCodeShouldBeOk()); + + stack = await _stackRepository.GetByIdAsync(ev.StackId); + Assert.NotNull(stack); + Assert.True(stack.IsFixed()); + } + + private async Task SubmitErrorEventAsync() { + const string message = "simple string"; + await SendRequestAsync(r => r + .Post() + .AsTestOrganizationClientUser() + .AppendPath("events") + .Content(new Event { + Message = message, + Type = Event.KnownTypes.Error, + Data = {{ Event.KnownDataKeys.SimpleError, new SimpleError { Message = message, Type = "Error", StackTrace = "test", } }} - }) - .StatusCodeShouldBeAccepted()); + }) + .StatusCodeShouldBeAccepted()); - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Enqueued); - Assert.Equal(0, stats.Completed); + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Enqueued); + Assert.Equal(0, stats.Completed); - var processEventsJob = GetService(); - await processEventsJob.RunAsync(); - await RefreshDataAsync(); + var processEventsJob = GetService(); + await processEventsJob.RunAsync(); + await RefreshDataAsync(); - stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Completed); + stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Completed); - var ev = (await _eventRepository.GetAllAsync()).Documents.Single(); - Assert.Equal(message, ev.Message); - return ev; - } + var ev = (await _eventRepository.GetAllAsync()).Documents.Single(); + Assert.Equal(message, ev.Message); + return ev; } } diff --git a/tests/Exceptionless.Tests/Controllers/StatusControllerTests.cs b/tests/Exceptionless.Tests/Controllers/StatusControllerTests.cs index 366ef0c54a..3a5f514ed0 100644 --- a/tests/Exceptionless.Tests/Controllers/StatusControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/StatusControllerTests.cs @@ -1,58 +1,56 @@ -using System; -using System.Threading.Tasks; using Exceptionless.Tests.Extensions; using Exceptionless.Core.Messaging.Models; using Exceptionless.Core.Utility; using Exceptionless.DateTimeExtensions; using Exceptionless.Web.Models; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Controllers { - public class StatusControllerTests : IntegrationTestsBase { - public StatusControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - } +namespace Exceptionless.Tests.Controllers; - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - - var service = GetService(); - await service.CreateDataAsync(); - } +public class StatusControllerTests : IntegrationTestsBase { + public StatusControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + } - [Theory] - [InlineData(null, false)] - [InlineData(null, true)] - //[InlineData(null, true, false)] // TODO: Resolve issue where you are required to pass a message via the body. - [InlineData("New Release!!", false)] - [InlineData("New Release!!", true)] - public async Task CanSendReleaseNotification(string message, bool critical, bool sendMessageAsContentIfEmpty = true) { - Log.MinimumLevel = LogLevel.Trace; - var utcNow = SystemClock.UtcNow; + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); - ReleaseNotification notification; - if (!String.IsNullOrEmpty(message) || sendMessageAsContentIfEmpty) { - notification = await SendRequestAsAsync(r => r - .Post() - .AsGlobalAdminUser() - .AppendPath("notifications/release") - .QueryStringIf(() => critical, "critical", critical) - .Content(new ValueFromBody(message)) - .StatusCodeShouldBeOk()); - } else { - notification = await SendRequestAsAsync(r => r - .Post() - .AsGlobalAdminUser() - .AppendPath("notifications/release") - .QueryStringIf(() => critical, "critical", critical) - .StatusCodeShouldBeOk()); - } + var service = GetService(); + await service.CreateDataAsync(); + } - Assert.Equal(message, notification.Message); - Assert.Equal(critical, notification.Critical); - Assert.True(notification.Date.IsAfterOrEqual(utcNow)); + [Theory] + [InlineData(null, false)] + [InlineData(null, true)] + //[InlineData(null, true, false)] // TODO: Resolve issue where you are required to pass a message via the body. + [InlineData("New Release!!", false)] + [InlineData("New Release!!", true)] + public async Task CanSendReleaseNotification(string message, bool critical, bool sendMessageAsContentIfEmpty = true) { + Log.MinimumLevel = LogLevel.Trace; + var utcNow = SystemClock.UtcNow; + + ReleaseNotification notification; + if (!String.IsNullOrEmpty(message) || sendMessageAsContentIfEmpty) { + notification = await SendRequestAsAsync(r => r + .Post() + .AsGlobalAdminUser() + .AppendPath("notifications/release") + .QueryStringIf(() => critical, "critical", critical) + .Content(new ValueFromBody(message)) + .StatusCodeShouldBeOk()); + } + else { + notification = await SendRequestAsAsync(r => r + .Post() + .AsGlobalAdminUser() + .AppendPath("notifications/release") + .QueryStringIf(() => critical, "critical", critical) + .StatusCodeShouldBeOk()); } + + Assert.Equal(message, notification.Message); + Assert.Equal(critical, notification.Critical); + Assert.True(notification.Date.IsAfterOrEqual(utcNow)); } } diff --git a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs index 5fc35d5221..298ca80e80 100644 --- a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Exceptionless.Core.Authorization; using Exceptionless.Tests.Extensions; using Exceptionless.Core.Repositories; @@ -9,70 +7,70 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Controllers { - public sealed class TokenControllerTests : IntegrationTestsBase { - public TokenControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - } +namespace Exceptionless.Tests.Controllers; - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - var service = GetService(); - await service.CreateDataAsync(); - } +public sealed class TokenControllerTests : IntegrationTestsBase { + public TokenControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + } + + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + var service = GetService(); + await service.CreateDataAsync(); + } - [Fact] - public async Task CanDisableApiKey() { - var token = await SendRequestAsAsync(r => r - .Post() - .AsGlobalAdminUser() - .AppendPath("tokens") - .Content(new NewToken { - OrganizationId = SampleDataService.TEST_ORG_ID, - ProjectId = SampleDataService.TEST_PROJECT_ID, - Scopes = new HashSet { AuthorizationRoles.Client, AuthorizationRoles.User } - }) - .StatusCodeShouldBeCreated() - ); + [Fact] + public async Task CanDisableApiKey() { + var token = await SendRequestAsAsync(r => r + .Post() + .AsGlobalAdminUser() + .AppendPath("tokens") + .Content(new NewToken { + OrganizationId = SampleDataService.TEST_ORG_ID, + ProjectId = SampleDataService.TEST_PROJECT_ID, + Scopes = new HashSet { AuthorizationRoles.Client, AuthorizationRoles.User } + }) + .StatusCodeShouldBeCreated() + ); - Assert.NotNull(token.Id); - Assert.False(token.IsDisabled); - Assert.Equal(2, token.Scopes.Count); + Assert.NotNull(token.Id); + Assert.False(token.IsDisabled); + Assert.Equal(2, token.Scopes.Count); - var updateToken = new UpdateToken { - IsDisabled = true, - Notes = "Disabling until next release" - }; + var updateToken = new UpdateToken { + IsDisabled = true, + Notes = "Disabling until next release" + }; - var updatedToken = await SendRequestAsAsync(r => r - .Patch() - .BearerToken(token.Id) - .AppendPath($"tokens/{token.Id}") - .Content(updateToken) - .StatusCodeShouldBeOk() - ); + var updatedToken = await SendRequestAsAsync(r => r + .Patch() + .BearerToken(token.Id) + .AppendPath($"tokens/{token.Id}") + .Content(updateToken) + .StatusCodeShouldBeOk() + ); - Assert.True(updatedToken.IsDisabled); - Assert.Equal(updateToken.Notes, updatedToken.Notes); + Assert.True(updatedToken.IsDisabled); + Assert.Equal(updateToken.Notes, updatedToken.Notes); - await SendRequestAsync(r => r - .BearerToken(token.Id) - .AppendPath($"tokens/{token.Id}") - .StatusCodeShouldBeUnauthorized() - ); + await SendRequestAsync(r => r + .BearerToken(token.Id) + .AppendPath($"tokens/{token.Id}") + .StatusCodeShouldBeUnauthorized() + ); - var repository = GetService(); - var actualToken = await repository.GetByIdAsync(token.Id); - Assert.NotNull(actualToken); - actualToken.IsDisabled = false; - await repository.SaveAsync(actualToken); + var repository = GetService(); + var actualToken = await repository.GetByIdAsync(token.Id); + Assert.NotNull(actualToken); + actualToken.IsDisabled = false; + await repository.SaveAsync(actualToken); - token = await SendRequestAsAsync(r => r - .BearerToken(token.Id) - .AppendPath($"tokens/{token.Id}") - .StatusCodeShouldBeOk() - ); + token = await SendRequestAsAsync(r => r + .BearerToken(token.Id) + .AppendPath($"tokens/{token.Id}") + .StatusCodeShouldBeOk() + ); - Assert.False(token.IsDisabled); - } + Assert.False(token.IsDisabled); } } diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index 6971a018f9..8f73b26cf7 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -1,6 +1,7 @@  + + - net6.0 False diff --git a/tests/Exceptionless.Tests/Extensions/RequestExtensions.cs b/tests/Exceptionless.Tests/Extensions/RequestExtensions.cs index 30ae01a995..03cd40ae67 100644 --- a/tests/Exceptionless.Tests/Extensions/RequestExtensions.cs +++ b/tests/Exceptionless.Tests/Extensions/RequestExtensions.cs @@ -1,43 +1,41 @@ -using System; -using System.Net; -using System.Net.Http; +using System.Net; using Exceptionless.Tests.Utility; -namespace Exceptionless.Tests.Extensions { - public static class RequestExtensions { - public static AppSendBuilder StatusCodeShouldBeOk(this AppSendBuilder builder) { - return builder.ExpectedStatus(HttpStatusCode.OK); - } +namespace Exceptionless.Tests.Extensions; - public static AppSendBuilder StatusCodeShouldBeAccepted(this AppSendBuilder builder) { - return builder.ExpectedStatus(HttpStatusCode.Accepted); - } +public static class RequestExtensions { + public static AppSendBuilder StatusCodeShouldBeOk(this AppSendBuilder builder) { + return builder.ExpectedStatus(HttpStatusCode.OK); + } - public static AppSendBuilder StatusCodeShouldBeBadRequest(this AppSendBuilder builder) { - return builder.ExpectedStatus(HttpStatusCode.BadRequest); - } + public static AppSendBuilder StatusCodeShouldBeAccepted(this AppSendBuilder builder) { + return builder.ExpectedStatus(HttpStatusCode.Accepted); + } - public static AppSendBuilder StatusCodeShouldBeCreated(this AppSendBuilder builder) { - return builder.ExpectedStatus(HttpStatusCode.Created); - } + public static AppSendBuilder StatusCodeShouldBeBadRequest(this AppSendBuilder builder) { + return builder.ExpectedStatus(HttpStatusCode.BadRequest); + } - public static AppSendBuilder StatusCodeShouldBeUnauthorized(this AppSendBuilder builder) { - return builder.ExpectedStatus(HttpStatusCode.Unauthorized); - } + public static AppSendBuilder StatusCodeShouldBeCreated(this AppSendBuilder builder) { + return builder.ExpectedStatus(HttpStatusCode.Created); + } - public static HttpStatusCode? GetExpectedStatus(this HttpRequestMessage requestMessage) { - if (requestMessage == null) - throw new ArgumentNullException(nameof(requestMessage)); + public static AppSendBuilder StatusCodeShouldBeUnauthorized(this AppSendBuilder builder) { + return builder.ExpectedStatus(HttpStatusCode.Unauthorized); + } + + public static HttpStatusCode? GetExpectedStatus(this HttpRequestMessage requestMessage) { + if (requestMessage == null) + throw new ArgumentNullException(nameof(requestMessage)); - requestMessage.Options.TryGetValue(AppSendBuilder.ExpectedStatusKey, out var propertyValue); - return propertyValue; - } + requestMessage.Options.TryGetValue(AppSendBuilder.ExpectedStatusKey, out var propertyValue); + return propertyValue; + } - public static void SetExpectedStatus(this HttpRequestMessage requestMessage, HttpStatusCode statusCode) { - if (requestMessage == null) - throw new ArgumentNullException(nameof(requestMessage)); + public static void SetExpectedStatus(this HttpRequestMessage requestMessage, HttpStatusCode statusCode) { + if (requestMessage == null) + throw new ArgumentNullException(nameof(requestMessage)); - requestMessage.Options.Set(AppSendBuilder.ExpectedStatusKey, statusCode); - } + requestMessage.Options.Set(AppSendBuilder.ExpectedStatusKey, statusCode); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Extensions/StringExtensionsTests.cs b/tests/Exceptionless.Tests/Extensions/StringExtensionsTests.cs index e24f65c337..07304322ba 100644 --- a/tests/Exceptionless.Tests/Extensions/StringExtensionsTests.cs +++ b/tests/Exceptionless.Tests/Extensions/StringExtensionsTests.cs @@ -2,45 +2,45 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Extensions { - public class StringExtensionsTests : TestWithServices { - public StringExtensionsTests(ITestOutputHelper output) : base(output) {} +namespace Exceptionless.Tests.Extensions; - [Fact] - public void ToAddress() { - Assert.Equal("::1", "::1".ToAddress()); - Assert.Equal("1.2.3.4", "1.2.3.4".ToAddress()); - Assert.Equal("1.2.3.4", "1.2.3.4:".ToAddress()); - Assert.Equal("1.2.3.4", "1.2.3.4:80".ToAddress()); - Assert.Equal("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8".ToAddress()); - Assert.Equal("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8:".ToAddress()); - Assert.Equal("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8:80".ToAddress()); - } +public class StringExtensionsTests : TestWithServices { + public StringExtensionsTests(ITestOutputHelper output) : base(output) { } - [Fact(Skip = "TODO: https://github.com/exceptionless/Exceptionless.Net/issues/2")] - public void LowerUnderscoredWords() { - Assert.Equal("enable_ssl", "EnableSSL".ToLowerUnderscoredWords()); - Assert.Equal("base_url", "BaseURL".ToLowerUnderscoredWords()); - Assert.Equal("website_mode", "WebsiteMode".ToLowerUnderscoredWords()); - Assert.Equal("google_app_id", "GoogleAppId".ToLowerUnderscoredWords()); + [Fact] + public void ToAddress() { + Assert.Equal("::1", "::1".ToAddress()); + Assert.Equal("1.2.3.4", "1.2.3.4".ToAddress()); + Assert.Equal("1.2.3.4", "1.2.3.4:".ToAddress()); + Assert.Equal("1.2.3.4", "1.2.3.4:80".ToAddress()); + Assert.Equal("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8".ToAddress()); + Assert.Equal("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8:".ToAddress()); + Assert.Equal("1:2:3:4:5:6:7:8", "1:2:3:4:5:6:7:8:80".ToAddress()); + } + + [Fact(Skip = "TODO: https://github.com/exceptionless/Exceptionless.Net/issues/2")] + public void LowerUnderscoredWords() { + Assert.Equal("enable_ssl", "EnableSSL".ToLowerUnderscoredWords()); + Assert.Equal("base_url", "BaseURL".ToLowerUnderscoredWords()); + Assert.Equal("website_mode", "WebsiteMode".ToLowerUnderscoredWords()); + Assert.Equal("google_app_id", "GoogleAppId".ToLowerUnderscoredWords()); - Assert.Equal("blake_niemyjski_1", "blakeNiemyjski 1".ToLowerUnderscoredWords()); - Assert.Equal("blake_niemyjski_2", "Blake Niemyjski 2".ToLowerUnderscoredWords()); - Assert.Equal("blake_niemyjski_3", "Blake_ niemyjski 3".ToLowerUnderscoredWords()); - Assert.Equal("blake_niemyjski4", "Blake_Niemyjski4".ToLowerUnderscoredWords()); - Assert.Equal("mp3_files_data", "MP3FilesData".ToLowerUnderscoredWords()); - Assert.Equal("flac", "FLAC".ToLowerUnderscoredWords()); - Assert.Equal("number_of_abcd_things", "NumberOfABCDThings".ToLowerUnderscoredWords()); - Assert.Equal("ip_address_2s", "IPAddress 2s".ToLowerUnderscoredWords()); - Assert.Equal("127.0.0.1", "127.0.0.1".ToLowerUnderscoredWords()); - Assert.Equal("", "".ToLowerUnderscoredWords()); - Assert.Equal("_type", "_type".ToLowerUnderscoredWords()); - Assert.Equal("__type", "__type".ToLowerUnderscoredWords()); - Assert.Equal("my_custom_type", "myCustom _type".ToLowerUnderscoredWords()); - Assert.Equal("my_custom_type", "myCustom_type".ToLowerUnderscoredWords()); - Assert.Equal("my_custom_type", "myCustom _type".ToLowerUnderscoredWords()); - Assert.Equal("node.data", "node.data".ToLowerUnderscoredWords()); - Assert.Equal("match_mapping_type", "match_mapping_type".ToLowerUnderscoredWords()); - } + Assert.Equal("blake_niemyjski_1", "blakeNiemyjski 1".ToLowerUnderscoredWords()); + Assert.Equal("blake_niemyjski_2", "Blake Niemyjski 2".ToLowerUnderscoredWords()); + Assert.Equal("blake_niemyjski_3", "Blake_ niemyjski 3".ToLowerUnderscoredWords()); + Assert.Equal("blake_niemyjski4", "Blake_Niemyjski4".ToLowerUnderscoredWords()); + Assert.Equal("mp3_files_data", "MP3FilesData".ToLowerUnderscoredWords()); + Assert.Equal("flac", "FLAC".ToLowerUnderscoredWords()); + Assert.Equal("number_of_abcd_things", "NumberOfABCDThings".ToLowerUnderscoredWords()); + Assert.Equal("ip_address_2s", "IPAddress 2s".ToLowerUnderscoredWords()); + Assert.Equal("127.0.0.1", "127.0.0.1".ToLowerUnderscoredWords()); + Assert.Equal("", "".ToLowerUnderscoredWords()); + Assert.Equal("_type", "_type".ToLowerUnderscoredWords()); + Assert.Equal("__type", "__type".ToLowerUnderscoredWords()); + Assert.Equal("my_custom_type", "myCustom _type".ToLowerUnderscoredWords()); + Assert.Equal("my_custom_type", "myCustom_type".ToLowerUnderscoredWords()); + Assert.Equal("my_custom_type", "myCustom _type".ToLowerUnderscoredWords()); + Assert.Equal("node.data", "node.data".ToLowerUnderscoredWords()); + Assert.Equal("match_mapping_type", "match_mapping_type".ToLowerUnderscoredWords()); } } diff --git a/tests/Exceptionless.Tests/Extensions/TaskExtensions.cs b/tests/Exceptionless.Tests/Extensions/TaskExtensions.cs index c75a0eaf8f..4eb9574bcc 100644 --- a/tests/Exceptionless.Tests/Extensions/TaskExtensions.cs +++ b/tests/Exceptionless.Tests/Extensions/TaskExtensions.cs @@ -1,12 +1,10 @@ -using System; -using System.Threading.Tasks; -using Foundatio.AsyncEx; +using Foundatio.AsyncEx; using Foundatio.Utility; -namespace Exceptionless.Tests.Extensions { - public static class TaskExtensions { - public static Task WaitAsync(this AsyncCountdownEvent countdownEvent, TimeSpan timeout) { - return Task.WhenAny(countdownEvent.WaitAsync(), SystemClock.SleepAsync(timeout)); - } +namespace Exceptionless.Tests.Extensions; + +public static class TaskExtensions { + public static Task WaitAsync(this AsyncCountdownEvent countdownEvent, TimeSpan timeout) { + return Task.WhenAny(countdownEvent.WaitAsync(), SystemClock.SleepAsync(timeout)); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Extensions/TestServerExtensions.cs b/tests/Exceptionless.Tests/Extensions/TestServerExtensions.cs index 76f112a244..5ff670662e 100644 --- a/tests/Exceptionless.Tests/Extensions/TestServerExtensions.cs +++ b/tests/Exceptionless.Tests/Extensions/TestServerExtensions.cs @@ -1,37 +1,34 @@ -using System; using System.Diagnostics; -using System.Threading.Tasks; using Foundatio.Extensions.Hosting.Startup; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -namespace Exceptionless.Tests { - public static class TestServerExtensions { - private static bool _alreadyWaited; +namespace Exceptionless.Tests; - public static async Task WaitForReadyAsync(this TestServer server) { - var startupContext = server.Services.GetService(); - var maxWaitTime = !_alreadyWaited ? TimeSpan.FromSeconds(30) : TimeSpan.FromSeconds(2); - if (Debugger.IsAttached) - maxWaitTime = maxWaitTime.Add(TimeSpan.FromMinutes(1)); +public static class TestServerExtensions { + private static bool _alreadyWaited; - _alreadyWaited = true; + public static async Task WaitForReadyAsync(this TestServer server) { + var startupContext = server.Services.GetService(); + var maxWaitTime = !_alreadyWaited ? TimeSpan.FromSeconds(30) : TimeSpan.FromSeconds(2); + if (Debugger.IsAttached) + maxWaitTime = maxWaitTime.Add(TimeSpan.FromMinutes(1)); - var client = server.CreateClient(); - var startTime = DateTime.Now; - do { - if (startupContext != null && startupContext.IsStartupComplete && startupContext.Result.Success == false) - throw new OperationCanceledException($"Startup action \"{startupContext.Result.FailedActionName}\" failed"); + _alreadyWaited = true; - var response = await client.GetAsync("/ready"); - if (response.IsSuccessStatusCode) - break; + var client = server.CreateClient(); + var startTime = DateTime.Now; + do { + if (startupContext != null && startupContext.IsStartupComplete && startupContext.Result.Success == false) + throw new OperationCanceledException($"Startup action \"{startupContext.Result.FailedActionName}\" failed"); - if (DateTime.Now.Subtract(startTime) > maxWaitTime) - throw new TimeoutException("Failed waiting for server to be ready."); + var response = await client.GetAsync("/ready"); + if (response.IsSuccessStatusCode) + break; - await Task.Delay(TimeSpan.FromMilliseconds(100)); - } while (true); - } + if (DateTime.Now.Subtract(startTime) > maxWaitTime) + throw new TimeoutException("Failed waiting for server to be ready."); + + await Task.Delay(TimeSpan.FromMilliseconds(100)); + } while (true); } } diff --git a/tests/Exceptionless.Tests/IntegrationTestsBase.cs b/tests/Exceptionless.Tests/IntegrationTestsBase.cs index 6b4ba87756..5679478d7d 100644 --- a/tests/Exceptionless.Tests/IntegrationTestsBase.cs +++ b/tests/Exceptionless.Tests/IntegrationTestsBase.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Exceptionless.Core.Authentication; +using Exceptionless.Core.Authentication; using Exceptionless.Core.Extensions; using Exceptionless.Core.Mail; using Exceptionless.Tests.Utility; @@ -13,7 +7,6 @@ using FluentRest; using Xunit.Abstractions; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; using Exceptionless.Tests.Extensions; using Exceptionless.Tests.Mail; using FluentRest.NewtonsoftJson; @@ -26,7 +19,6 @@ using Foundatio.Repositories.Elasticsearch.Extensions; using Foundatio.Storage; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Nest; using Newtonsoft.Json; using Xunit; @@ -36,198 +28,201 @@ using Foundatio.Repositories; using Foundatio.Repositories.Extensions; -namespace Exceptionless.Tests { - public abstract class IntegrationTestsBase : TestWithLoggingBase, Xunit.IAsyncLifetime, IClassFixture { - private static bool _indexesHaveBeenConfigured = false; - private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); - private readonly IDisposable _testSystemClock = TestSystemClock.Install(); - private readonly ExceptionlessElasticConfiguration _configuration; - protected readonly TestServer _server; - protected readonly FluentClient _client; - protected readonly HttpClient _httpClient; - protected readonly IList _disposables = new List(); - - public IntegrationTestsBase(ITestOutputHelper output, AppWebHostFactory factory) : base(output) { - Log.MinimumLevel = LogLevel.Information; - Log.SetLogLevel(LogLevel.Warning); - Log.SetLogLevel(LogLevel.Warning); - Log.SetLogLevel(LogLevel.Warning); - Log.SetLogLevel(LogLevel.Information); - Log.SetLogLevel("StartupActions", LogLevel.Warning); - Log.SetLogLevel(LogLevel.Warning); - - var configuredFactory = factory.Factories.FirstOrDefault(); - if (configuredFactory == null) { - configuredFactory = factory.WithWebHostBuilder(builder => { - builder.ConfigureTestServices(RegisterServices); // happens after normal container configure and overrides services - }); - } +namespace Exceptionless.Tests; + +public abstract class IntegrationTestsBase : TestWithLoggingBase, Xunit.IAsyncLifetime, IClassFixture { + private static bool _indexesHaveBeenConfigured = false; + private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private readonly IDisposable _testSystemClock = TestSystemClock.Install(); + private readonly ExceptionlessElasticConfiguration _configuration; + protected readonly TestServer _server; + protected readonly FluentClient _client; + protected readonly HttpClient _httpClient; + protected readonly IList _disposables = new List(); + + public IntegrationTestsBase(ITestOutputHelper output, AppWebHostFactory factory) : base(output) { + Log.MinimumLevel = LogLevel.Information; + Log.SetLogLevel(LogLevel.Warning); + Log.SetLogLevel(LogLevel.Warning); + Log.SetLogLevel(LogLevel.Warning); + Log.SetLogLevel(LogLevel.Information); + Log.SetLogLevel("StartupActions", LogLevel.Warning); + Log.SetLogLevel(LogLevel.Warning); + + var configuredFactory = factory.Factories.FirstOrDefault(); + if (configuredFactory == null) { + configuredFactory = factory.WithWebHostBuilder(builder => { + builder.ConfigureTestServices(RegisterServices); // happens after normal container configure and overrides services + }); + } - _disposables.Add(_testSystemClock); + _disposables.Add(_testSystemClock); - _httpClient = configuredFactory.CreateClient(); - _server = configuredFactory.Server; - _httpClient.BaseAddress = new Uri(_server.BaseAddress + "api/v2/", UriKind.Absolute); + _httpClient = configuredFactory.CreateClient(); + _server = configuredFactory.Server; + _httpClient.BaseAddress = new Uri(_server.BaseAddress + "api/v2/", UriKind.Absolute); - var testScope = configuredFactory.Services.CreateScope(); - _disposables.Add(testScope); - ServiceProvider = testScope.ServiceProvider; + var testScope = configuredFactory.Services.CreateScope(); + _disposables.Add(testScope); + ServiceProvider = testScope.ServiceProvider; - var settings = GetService(); - _client = new FluentClient(_httpClient, new NewtonsoftJsonSerializer(settings)); - _configuration = GetService(); - } + var settings = GetService(); + _client = new FluentClient(_httpClient, new NewtonsoftJsonSerializer(settings)); + _configuration = GetService(); + } - public virtual async Task InitializeAsync() { - Log.SetLogLevel("Microsoft.AspNetCore.Hosting.Internal.WebHost", LogLevel.Warning); - Log.SetLogLevel("Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService", LogLevel.None); - await _server.WaitForReadyAsync(); - Log.SetLogLevel("Microsoft.AspNetCore.Hosting.Internal.WebHost", LogLevel.Information); - Log.SetLogLevel("Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService", LogLevel.Information); + public virtual async Task InitializeAsync() { + Log.SetLogLevel("Microsoft.AspNetCore.Hosting.Internal.WebHost", LogLevel.Warning); + Log.SetLogLevel("Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService", LogLevel.None); + await _server.WaitForReadyAsync(); + Log.SetLogLevel("Microsoft.AspNetCore.Hosting.Internal.WebHost", LogLevel.Information); + Log.SetLogLevel("Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService", LogLevel.Information); - await ResetDataAsync(); - } + await ResetDataAsync(); + } - private IServiceProvider ServiceProvider { get; } + private IServiceProvider ServiceProvider { get; } - protected TService GetService() { - return ServiceProvider.GetRequiredService(); - } + protected TService GetService() { + return ServiceProvider.GetRequiredService(); + } + + protected virtual void RegisterServices(IServiceCollection services) { + // use xunit test logger + services.AddSingleton(Log); + services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); - protected virtual void RegisterServices(IServiceCollection services) { - // use xunit test logger - services.AddSingleton(Log); - services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); + services.AddSingleton(); + services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddTransient(); - services.AddTransient(); + services.ReplaceSingleton(s => _server.CreateHandler()); + } + + public async Task<(List Stacks, List Events)> CreateDataAsync(Action dataBuilderFunc) { + var eventBuilders = new List(); + + var dataBuilder = new DataBuilder(eventBuilders, ServiceProvider); + dataBuilderFunc(dataBuilder); - services.ReplaceSingleton(s => _server.CreateHandler()); + var eventRepository = GetService(); + var stackRepository = GetService(); + + var events = new HashSet(); + var stacks = new HashSet(); + + foreach (var builder in eventBuilders) { + var data = builder.Build(); + events.AddRange(data.Events); + stacks.Add(data.Stack); } - public async Task<(List Stacks, List Events)> CreateDataAsync(Action dataBuilderFunc) { - var eventBuilders = new List(); + await stackRepository.AddAsync(stacks, o => o.ImmediateConsistency()); + await eventRepository.AddAsync(events, o => o.ImmediateConsistency()); - var dataBuilder = new DataBuilder(eventBuilders, ServiceProvider); - dataBuilderFunc(dataBuilder); + await RefreshDataAsync(); - var eventRepository = GetService(); - var stackRepository = GetService(); + return (stacks.ToList(), events.ToList()); + } - var events = new HashSet(); - var stacks = new HashSet(); + protected virtual async Task ResetDataAsync() { + await _semaphoreSlim.WaitAsync(); + try { + var oldLoggingLevel = Log.MinimumLevel; + Log.MinimumLevel = LogLevel.Warning; - foreach (var builder in eventBuilders) { - var data = builder.Build(); - events.AddRange(data.Events); - stacks.Add(data.Stack); + await RefreshDataAsync(); + if (!_indexesHaveBeenConfigured) { + await _configuration.DeleteIndexesAsync(); + await _configuration.ConfigureIndexesAsync(); + _indexesHaveBeenConfigured = true; + } + else { + string indexes = String.Join(',', _configuration.Indexes.Select(i => i.Name)); + await _configuration.Client.DeleteByQueryAsync(new DeleteByQueryRequest(indexes) { + Query = new MatchAllQuery(), + IgnoreUnavailable = true, + Refresh = true + }); } - await stackRepository.AddAsync(stacks, o => o.ImmediateConsistency()); - await eventRepository.AddAsync(events, o => o.ImmediateConsistency()); + _logger.LogTrace("Configured Indexes"); - await RefreshDataAsync(); + foreach (var index in _configuration.Indexes) + index.QueryParser.Configuration.MappingResolver.RefreshMapping(); - return (stacks.ToList(), events.ToList()); - } + var cacheClient = GetService(); + await cacheClient.RemoveAllAsync(); - protected virtual async Task ResetDataAsync() { - await _semaphoreSlim.WaitAsync(); - try { - var oldLoggingLevel = Log.MinimumLevel; - Log.MinimumLevel = LogLevel.Warning; - - await RefreshDataAsync(); - if (!_indexesHaveBeenConfigured) { - await _configuration.DeleteIndexesAsync(); - await _configuration.ConfigureIndexesAsync(); - _indexesHaveBeenConfigured = true; - } else { - string indexes = String.Join(',', _configuration.Indexes.Select(i => i.Name)); - await _configuration.Client.DeleteByQueryAsync(new DeleteByQueryRequest(indexes) { - Query = new MatchAllQuery(), - IgnoreUnavailable = true, - Refresh = true - }); - } - - _logger.LogTrace("Configured Indexes"); - - foreach (var index in _configuration.Indexes) - index.QueryParser.Configuration.MappingResolver.RefreshMapping(); - - var cacheClient = GetService(); - await cacheClient.RemoveAllAsync(); - - var fileStorage = GetService(); - await fileStorage.DeleteFilesAsync(await fileStorage.GetFileListAsync()); - - await GetService>().DeleteQueueAsync(); - - Log.MinimumLevel = oldLoggingLevel; - } finally { - _semaphoreSlim.Release(); - _logger.LogDebug("Reset Data"); - } - } + var fileStorage = GetService(); + await fileStorage.DeleteFilesAsync(await fileStorage.GetFileListAsync()); - protected async Task RefreshDataAsync(Indices indices = null) { - var configuration = GetService(); - var response = await configuration.Client.Indices.RefreshAsync(indices ?? Indices.All); - _logger.LogRequest(response); + await GetService>().DeleteQueueAsync(); + + Log.MinimumLevel = oldLoggingLevel; + } + finally { + _semaphoreSlim.Release(); + _logger.LogDebug("Reset Data"); } + } - protected async Task SendRequestAsync(Action configure) { - var request = new HttpRequestMessage(HttpMethod.Get, _client.HttpClient.BaseAddress); - var builder = new AppSendBuilder(request); - configure(builder); + protected async Task RefreshDataAsync(Indices indices = null) { + var configuration = GetService(); + var response = await configuration.Client.Indices.RefreshAsync(indices ?? Indices.All); + _logger.LogRequest(response); + } - var response = await _client.SendAsync(request); + protected async Task SendRequestAsync(Action configure) { + var request = new HttpRequestMessage(HttpMethod.Get, _client.HttpClient.BaseAddress); + var builder = new AppSendBuilder(request); + configure(builder); - var expectedStatus = request.GetExpectedStatus(); - if (expectedStatus.HasValue && expectedStatus.Value != response.StatusCode) { - string content = await response.Content.ReadAsStringAsync(); - if (content.Length > 1000) - content = content.Substring(0, 1000); + var response = await _client.SendAsync(request); - throw new HttpRequestException($"Expected status code {expectedStatus.Value} but received status code {response.StatusCode} ({response.ReasonPhrase}).\n" + content); - } + var expectedStatus = request.GetExpectedStatus(); + if (expectedStatus.HasValue && expectedStatus.Value != response.StatusCode) { + string content = await response.Content.ReadAsStringAsync(); + if (content.Length > 1000) + content = content.Substring(0, 1000); - return response; + throw new HttpRequestException($"Expected status code {expectedStatus.Value} but received status code {response.StatusCode} ({response.ReasonPhrase}).\n" + content); } - protected async Task SendRequestAsAsync(Action configure) { - var response = await SendRequestAsync(configure); - return await response.DeserializeAsync(); - } + return response; + } - protected async Task SendGlobalAdminRequestAsync(Action configure) { - return await SendRequestAsync(b => { - b.AsGlobalAdminUser(); - configure(b); - }); - } + protected async Task SendRequestAsAsync(Action configure) { + var response = await SendRequestAsync(configure); + return await response.DeserializeAsync(); + } - protected async Task SendGlobalAdminRequestAsAsync(Action configure) { - var response = await SendGlobalAdminRequestAsync(configure); - return await response.DeserializeAsync(); - } + protected async Task SendGlobalAdminRequestAsync(Action configure) { + return await SendRequestAsync(b => { + b.AsGlobalAdminUser(); + configure(b); + }); + } - protected async Task DeserializeResponseAsync(HttpResponseMessage response) { - return await response.DeserializeAsync(); - } + protected async Task SendGlobalAdminRequestAsAsync(Action configure) { + var response = await SendGlobalAdminRequestAsync(configure); + return await response.DeserializeAsync(); + } + + protected async Task DeserializeResponseAsync(HttpResponseMessage response) { + return await response.DeserializeAsync(); + } - public virtual Task DisposeAsync() { - foreach (var disposable in _disposables) { - try { - disposable.Dispose(); - } catch (Exception ex) { - _logger?.LogError(ex, "Error disposing resource."); - } + public virtual Task DisposeAsync() { + foreach (var disposable in _disposables) { + try { + disposable.Dispose(); + } + catch (Exception ex) { + _logger?.LogError(ex, "Error disposing resource."); } - return Task.CompletedTask; } + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs index 121a9c24c4..ca086eac17 100644 --- a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs @@ -1,6 +1,4 @@ -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core; +using Exceptionless.Core; using Exceptionless.Core.Billing; using Exceptionless.Core.Jobs; using Exceptionless.Core.Repositories; @@ -12,124 +10,124 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Jobs { - public class CleanupDataJobTests : IntegrationTestsBase { - private readonly CleanupDataJob _job; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - - public CleanupDataJobTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _job = GetService(); - _organizationRepository = GetService(); - _projectRepository = GetService(); - _stackRepository = GetService(); - _eventRepository = GetService(); - _billingManager = GetService(); - _plans = GetService(); - } - - [Fact] - public async Task CanCleanupSoftDeletedOrganization() { - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - organization.IsDeleted = true; - await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); - - var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); - var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); - var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); - - await _job.RunAsync(); - - Assert.Null(await _organizationRepository.GetByIdAsync(organization.Id, o => o.IncludeSoftDeletes())); - Assert.Null(await _projectRepository.GetByIdAsync(project.Id, o => o.IncludeSoftDeletes())); - Assert.Null(await _stackRepository.GetByIdAsync(stack.Id, o => o.IncludeSoftDeletes())); - Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); - } - - [Fact] - public async Task CanCleanupSoftDeletedProject() { - var organization = await _organizationRepository.AddAsync(OrganizationData.GenerateSampleOrganization(_billingManager, _plans), o => o.ImmediateConsistency()); - - var project = ProjectData.GenerateSampleProject(); - project.IsDeleted = true; - await _projectRepository.AddAsync(project, o => o.ImmediateConsistency()); - - var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); - var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); - - await _job.RunAsync(); - - Assert.NotNull(await _organizationRepository.GetByIdAsync(organization.Id)); - Assert.Null(await _projectRepository.GetByIdAsync(project.Id, o => o.IncludeSoftDeletes())); - Assert.Null(await _stackRepository.GetByIdAsync(stack.Id, o => o.IncludeSoftDeletes())); - Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); - } - - [Fact] - public async Task CanCleanupSoftDeletedStack() { - var organization = await _organizationRepository.AddAsync(OrganizationData.GenerateSampleOrganization(_billingManager, _plans), o => o.ImmediateConsistency()); - var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); - - var stack = StackData.GenerateSampleStack(); - stack.IsDeleted = true; - await _stackRepository.AddAsync(stack, o => o.ImmediateConsistency()); - - var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); - - await _job.RunAsync(); - - Assert.NotNull(await _organizationRepository.GetByIdAsync(organization.Id)); - Assert.NotNull(await _projectRepository.GetByIdAsync(project.Id)); - Assert.Null(await _stackRepository.GetByIdAsync(stack.Id, o => o.IncludeSoftDeletes())); - Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); - } - - [Fact] - public async Task CanCleanupEventsOutsideOfRetentionPeriod() { - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - _billingManager.ApplyBillingPlan(organization, _plans.FreePlan); - await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); - - var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); - var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); - - var options = GetService(); - var date = SystemClock.OffsetUtcNow.SubtractDays(options.MaximumRetentionDays); - var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id, date, date, date), o => o.ImmediateConsistency()); - - await _job.RunAsync(); - - Assert.NotNull(await _organizationRepository.GetByIdAsync(organization.Id)); - Assert.NotNull(await _projectRepository.GetByIdAsync(project.Id)); - Assert.NotNull(await _stackRepository.GetByIdAsync(stack.Id)); - Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); - } - - [Fact] - public async Task CanDeleteOrphanedEventsByStack() { - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); - var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); - - var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); - await _eventRepository.AddAsync(EventData.GenerateEvents(5000, organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); - - var orphanedEvents = EventData.GenerateEvents(10000, organization.Id, project.Id).ToList(); - orphanedEvents.ForEach(e => e.StackId = ObjectId.GenerateNewId().ToString()); - - await _eventRepository.AddAsync(orphanedEvents, o => o.ImmediateConsistency()); - - var eventCount = await _eventRepository.CountAsync(o => o.IncludeSoftDeletes().ImmediateConsistency()); - Assert.Equal(15000, eventCount); - - await GetService().RunAsync(); - - eventCount = await _eventRepository.CountAsync(o => o.IncludeSoftDeletes().ImmediateConsistency()); - Assert.Equal(5000, eventCount); - } +namespace Exceptionless.Tests.Jobs; + +public class CleanupDataJobTests : IntegrationTestsBase { + private readonly CleanupDataJob _job; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + + public CleanupDataJobTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _job = GetService(); + _organizationRepository = GetService(); + _projectRepository = GetService(); + _stackRepository = GetService(); + _eventRepository = GetService(); + _billingManager = GetService(); + _plans = GetService(); + } + + [Fact] + public async Task CanCleanupSoftDeletedOrganization() { + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + organization.IsDeleted = true; + await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); + + var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); + var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); + var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); + + await _job.RunAsync(); + + Assert.Null(await _organizationRepository.GetByIdAsync(organization.Id, o => o.IncludeSoftDeletes())); + Assert.Null(await _projectRepository.GetByIdAsync(project.Id, o => o.IncludeSoftDeletes())); + Assert.Null(await _stackRepository.GetByIdAsync(stack.Id, o => o.IncludeSoftDeletes())); + Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); + } + + [Fact] + public async Task CanCleanupSoftDeletedProject() { + var organization = await _organizationRepository.AddAsync(OrganizationData.GenerateSampleOrganization(_billingManager, _plans), o => o.ImmediateConsistency()); + + var project = ProjectData.GenerateSampleProject(); + project.IsDeleted = true; + await _projectRepository.AddAsync(project, o => o.ImmediateConsistency()); + + var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); + var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); + + await _job.RunAsync(); + + Assert.NotNull(await _organizationRepository.GetByIdAsync(organization.Id)); + Assert.Null(await _projectRepository.GetByIdAsync(project.Id, o => o.IncludeSoftDeletes())); + Assert.Null(await _stackRepository.GetByIdAsync(stack.Id, o => o.IncludeSoftDeletes())); + Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); + } + + [Fact] + public async Task CanCleanupSoftDeletedStack() { + var organization = await _organizationRepository.AddAsync(OrganizationData.GenerateSampleOrganization(_billingManager, _plans), o => o.ImmediateConsistency()); + var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); + + var stack = StackData.GenerateSampleStack(); + stack.IsDeleted = true; + await _stackRepository.AddAsync(stack, o => o.ImmediateConsistency()); + + var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); + + await _job.RunAsync(); + + Assert.NotNull(await _organizationRepository.GetByIdAsync(organization.Id)); + Assert.NotNull(await _projectRepository.GetByIdAsync(project.Id)); + Assert.Null(await _stackRepository.GetByIdAsync(stack.Id, o => o.IncludeSoftDeletes())); + Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); + } + + [Fact] + public async Task CanCleanupEventsOutsideOfRetentionPeriod() { + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + _billingManager.ApplyBillingPlan(organization, _plans.FreePlan); + await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); + + var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); + var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); + + var options = GetService(); + var date = SystemClock.OffsetUtcNow.SubtractDays(options.MaximumRetentionDays); + var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id, date, date, date), o => o.ImmediateConsistency()); + + await _job.RunAsync(); + + Assert.NotNull(await _organizationRepository.GetByIdAsync(organization.Id)); + Assert.NotNull(await _projectRepository.GetByIdAsync(project.Id)); + Assert.NotNull(await _stackRepository.GetByIdAsync(stack.Id)); + Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); + } + + [Fact] + public async Task CanDeleteOrphanedEventsByStack() { + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); + var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); + + var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); + await _eventRepository.AddAsync(EventData.GenerateEvents(5000, organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); + + var orphanedEvents = EventData.GenerateEvents(10000, organization.Id, project.Id).ToList(); + orphanedEvents.ForEach(e => e.StackId = ObjectId.GenerateNewId().ToString()); + + await _eventRepository.AddAsync(orphanedEvents, o => o.ImmediateConsistency()); + + var eventCount = await _eventRepository.CountAsync(o => o.IncludeSoftDeletes().ImmediateConsistency()); + Assert.Equal(15000, eventCount); + + await GetService().RunAsync(); + + eventCount = await _eventRepository.CountAsync(o => o.IncludeSoftDeletes().ImmediateConsistency()); + Assert.Equal(5000, eventCount); } } diff --git a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs index 3815e64d5d..384f633efe 100644 --- a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Jobs; using Exceptionless.Core.Models; @@ -16,192 +13,193 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Jobs { - public class CloseInactiveSessionsJobTests : IntegrationTestsBase { - private readonly CloseInactiveSessionsJob _job; - private readonly ICacheClient _cache; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IEventRepository _eventRepository; - private readonly IUserRepository _userRepository; - private readonly EventPipeline _pipeline; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - - public CloseInactiveSessionsJobTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _job = GetService(); - _cache = GetService(); - _organizationRepository = GetService(); - _projectRepository = GetService(); - _eventRepository = GetService(); - _userRepository = GetService(); - _pipeline = GetService(); - _billingManager = GetService(); - _plans = GetService(); - } +namespace Exceptionless.Tests.Jobs; + +public class CloseInactiveSessionsJobTests : IntegrationTestsBase { + private readonly CloseInactiveSessionsJob _job; + private readonly ICacheClient _cache; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IEventRepository _eventRepository; + private readonly IUserRepository _userRepository; + private readonly EventPipeline _pipeline; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + + public CloseInactiveSessionsJobTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _job = GetService(); + _cache = GetService(); + _organizationRepository = GetService(); + _projectRepository = GetService(); + _eventRepository = GetService(); + _userRepository = GetService(); + _pipeline = GetService(); + _billingManager = GetService(); + _plans = GetService(); + } - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await CreateDataAsync(); - } + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await CreateDataAsync(); + } - [Fact] - public async Task CloseDuplicateIdentitySessions() { - const string userId = "blake@exceptionless.io"; - var event1 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId); - var event2 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId, sessionId: "123456789"); - - var contexts = await _pipeline.RunAsync(new []{ event1, event2 }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.True(contexts.All(c => !c.HasError)); - Assert.True(contexts.All(c => !c.IsCancelled)); - Assert.True(contexts.All(c => c.IsProcessed)); - - await RefreshDataAsync(); - var events = await _eventRepository.GetAllAsync(); - Assert.Equal(4, events.Total); - Assert.Equal(2, events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); - var sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Equal(0, sessionStarts.Sum(e => e.Value)); - Assert.DoesNotContain(sessionStarts, e => e.HasSessionEndTime()); - - var utcNow = SystemClock.UtcNow; - await _cache.SetAsync($"Project:{sessionStarts.First().ProjectId}:heartbeat:{userId.ToSHA1()}", utcNow.SubtractMinutes(1)); - - _job.DefaultInactivePeriod = TimeSpan.FromMinutes(3); - Assert.Equal(JobResult.Success, await _job.RunAsync()); - await RefreshDataAsync(); - events = await _eventRepository.GetAllAsync(); - Assert.Equal(4, events.Total); - - sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Equal(2, sessionStarts.Count); - Assert.Equal(1, sessionStarts.Count(e => !e.HasSessionEndTime())); - Assert.Equal(1, sessionStarts.Count(e => e.HasSessionEndTime())); - } + [Fact] + public async Task CloseDuplicateIdentitySessions() { + const string userId = "blake@exceptionless.io"; + var event1 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId); + var event2 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId, sessionId: "123456789"); + + var contexts = await _pipeline.RunAsync(new[] { event1, event2 }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.True(contexts.All(c => !c.HasError)); + Assert.True(contexts.All(c => !c.IsCancelled)); + Assert.True(contexts.All(c => c.IsProcessed)); + + await RefreshDataAsync(); + var events = await _eventRepository.GetAllAsync(); + Assert.Equal(4, events.Total); + Assert.Equal(2, events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); + var sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Equal(0, sessionStarts.Sum(e => e.Value)); + Assert.DoesNotContain(sessionStarts, e => e.HasSessionEndTime()); + + var utcNow = SystemClock.UtcNow; + await _cache.SetAsync($"Project:{sessionStarts.First().ProjectId}:heartbeat:{userId.ToSHA1()}", utcNow.SubtractMinutes(1)); + + _job.DefaultInactivePeriod = TimeSpan.FromMinutes(3); + Assert.Equal(JobResult.Success, await _job.RunAsync()); + await RefreshDataAsync(); + events = await _eventRepository.GetAllAsync(); + Assert.Equal(4, events.Total); + + sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Equal(2, sessionStarts.Count); + Assert.Equal(1, sessionStarts.Count(e => !e.HasSessionEndTime())); + Assert.Equal(1, sessionStarts.Count(e => e.HasSessionEndTime())); + } - [Fact] - public async Task WillNotCloseDuplicateIdentitySessionsWithSessionIdHeartbeat() { - const string userId = "blake@exceptionless.io"; - const string sessionId = "123456789"; - var event1 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId); - var event2 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId, sessionId: sessionId); - - var contexts = await _pipeline.RunAsync(new[] { event1, event2 }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.True(contexts.All(c => !c.HasError)); - Assert.True(contexts.All(c => !c.IsCancelled)); - Assert.True(contexts.All(c => c.IsProcessed)); - - await RefreshDataAsync(); - var events = await _eventRepository.GetAllAsync(); - Assert.Equal(4, events.Total); - Assert.Equal(2, events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); - var sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Equal(0, sessionStarts.Sum(e => e.Value)); - Assert.DoesNotContain(sessionStarts, e => e.HasSessionEndTime()); - - var utcNow = SystemClock.UtcNow; - await _cache.SetAsync($"Project:{sessionStarts.First().ProjectId}:heartbeat:{userId.ToSHA1()}", utcNow.SubtractMinutes(1)); - await _cache.SetAsync($"Project:{sessionStarts.First().ProjectId}:heartbeat:{sessionId.ToSHA1()}", utcNow.SubtractMinutes(1)); - - _job.DefaultInactivePeriod = TimeSpan.FromMinutes(3); - Assert.Equal(JobResult.Success, await _job.RunAsync()); - await RefreshDataAsync(); - events = await _eventRepository.GetAllAsync(); - Assert.Equal(4, events.Total); - - sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Equal(2, sessionStarts.Count); - Assert.Equal(2, sessionStarts.Count(e => !e.HasSessionEndTime())); - Assert.Equal(0, sessionStarts.Count(e => e.HasSessionEndTime())); + [Fact] + public async Task WillNotCloseDuplicateIdentitySessionsWithSessionIdHeartbeat() { + const string userId = "blake@exceptionless.io"; + const string sessionId = "123456789"; + var event1 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId); + var event2 = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId, sessionId: sessionId); + + var contexts = await _pipeline.RunAsync(new[] { event1, event2 }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.True(contexts.All(c => !c.HasError)); + Assert.True(contexts.All(c => !c.IsCancelled)); + Assert.True(contexts.All(c => c.IsProcessed)); + + await RefreshDataAsync(); + var events = await _eventRepository.GetAllAsync(); + Assert.Equal(4, events.Total); + Assert.Equal(2, events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); + var sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Equal(0, sessionStarts.Sum(e => e.Value)); + Assert.DoesNotContain(sessionStarts, e => e.HasSessionEndTime()); + + var utcNow = SystemClock.UtcNow; + await _cache.SetAsync($"Project:{sessionStarts.First().ProjectId}:heartbeat:{userId.ToSHA1()}", utcNow.SubtractMinutes(1)); + await _cache.SetAsync($"Project:{sessionStarts.First().ProjectId}:heartbeat:{sessionId.ToSHA1()}", utcNow.SubtractMinutes(1)); + + _job.DefaultInactivePeriod = TimeSpan.FromMinutes(3); + Assert.Equal(JobResult.Success, await _job.RunAsync()); + await RefreshDataAsync(); + events = await _eventRepository.GetAllAsync(); + Assert.Equal(4, events.Total); + + sessionStarts = events.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Equal(2, sessionStarts.Count); + Assert.Equal(2, sessionStarts.Count(e => !e.HasSessionEndTime())); + Assert.Equal(0, sessionStarts.Count(e => e.HasSessionEndTime())); + } + + [Theory] + [InlineData(1, true, null, false)] + [InlineData(1, true, 70, false)] + [InlineData(1, false, 50, false)] + [InlineData(1, true, 50, true)] + [InlineData(60, false, null, false)] + public async Task CloseInactiveSessions(int defaultInactivePeriodInMinutes, bool willCloseSession, int? sessionHeartbeatUpdatedAgoInSeconds, bool heartbeatClosesSession) { + const string userId = "blake@exceptionless.io"; + var ev = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId); + + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + Assert.False(context.IsCancelled); + Assert.True(context.IsProcessed); + + await RefreshDataAsync(); + var events = await _eventRepository.GetAllAsync(); + Assert.Equal(2, events.Total); + Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + var sessionStart = events.Documents.First(e => e.IsSessionStart()); + Assert.Equal(0, sessionStart.Value); + Assert.False(sessionStart.HasSessionEndTime()); + + var utcNow = SystemClock.UtcNow; + if (sessionHeartbeatUpdatedAgoInSeconds.HasValue) { + await _cache.SetAsync($"Project:{sessionStart.ProjectId}:heartbeat:{userId.ToSHA1()}", utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value)); + if (heartbeatClosesSession) + await _cache.SetAsync($"Project:{sessionStart.ProjectId}:heartbeat:{userId.ToSHA1()}-close", true); } - [Theory] - [InlineData(1, true, null, false)] - [InlineData(1, true, 70, false)] - [InlineData(1, false, 50, false)] - [InlineData(1, true, 50, true)] - [InlineData(60, false, null, false)] - public async Task CloseInactiveSessions(int defaultInactivePeriodInMinutes, bool willCloseSession, int? sessionHeartbeatUpdatedAgoInSeconds, bool heartbeatClosesSession) { - const string userId = "blake@exceptionless.io"; - var ev = GenerateEvent(SystemClock.OffsetNow.SubtractMinutes(5), userId); - - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - Assert.False(context.IsCancelled); - Assert.True(context.IsProcessed); - - await RefreshDataAsync(); - var events = await _eventRepository.GetAllAsync(); - Assert.Equal(2, events.Total); - Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - var sessionStart = events.Documents.First(e => e.IsSessionStart()); - Assert.Equal(0, sessionStart.Value); + _job.DefaultInactivePeriod = TimeSpan.FromMinutes(defaultInactivePeriodInMinutes); + Assert.Equal(JobResult.Success, await _job.RunAsync()); + await RefreshDataAsync(); + events = await _eventRepository.GetAllAsync(); + Assert.Equal(2, events.Total); + + sessionStart = events.Documents.First(e => e.IsSessionStart()); + decimal sessionStartDuration = (decimal)(sessionHeartbeatUpdatedAgoInSeconds.HasValue ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).TotalSeconds : 0); + if (willCloseSession) { + Assert.Equal(sessionStartDuration, sessionStart.Value); + Assert.True(sessionStart.HasSessionEndTime()); + } + else { + Assert.Equal(sessionStartDuration, sessionStart.Value); Assert.False(sessionStart.HasSessionEndTime()); + } + } - var utcNow = SystemClock.UtcNow; - if (sessionHeartbeatUpdatedAgoInSeconds.HasValue) { - await _cache.SetAsync($"Project:{sessionStart.ProjectId}:heartbeat:{userId.ToSHA1()}", utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value)); - if (heartbeatClosesSession) - await _cache.SetAsync($"Project:{sessionStart.ProjectId}:heartbeat:{userId.ToSHA1()}-close", true); + private async Task CreateDataAsync() { + foreach (var organization in OrganizationData.GenerateSampleOrganizations(_billingManager, _plans)) { + if (organization.Id == TestConstants.OrganizationId3) + _billingManager.ApplyBillingPlan(organization, _plans.FreePlan, UserData.GenerateSampleUser()); + else + _billingManager.ApplyBillingPlan(organization, _plans.SmallPlan, UserData.GenerateSampleUser()); + + organization.StripeCustomerId = Guid.NewGuid().ToString("N"); + organization.CardLast4 = "1234"; + organization.SubscribeDate = SystemClock.UtcNow; + + if (organization.IsSuspended) { + organization.SuspendedByUserId = TestConstants.UserId; + organization.SuspensionCode = SuspensionCode.Billing; + organization.SuspensionDate = SystemClock.UtcNow; } - _job.DefaultInactivePeriod = TimeSpan.FromMinutes(defaultInactivePeriodInMinutes); - Assert.Equal(JobResult.Success, await _job.RunAsync()); - await RefreshDataAsync(); - events = await _eventRepository.GetAllAsync(); - Assert.Equal(2, events.Total); - - sessionStart = events.Documents.First(e => e.IsSessionStart()); - decimal sessionStartDuration = (decimal)(sessionHeartbeatUpdatedAgoInSeconds.HasValue ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).TotalSeconds : 0); - if (willCloseSession) { - Assert.Equal(sessionStartDuration, sessionStart.Value); - Assert.True(sessionStart.HasSessionEndTime()); - } else { - Assert.Equal(sessionStartDuration, sessionStart.Value); - Assert.False(sessionStart.HasSessionEndTime()); - } + await _organizationRepository.AddAsync(organization, o => o.Cache()); } - private async Task CreateDataAsync() { - foreach (var organization in OrganizationData.GenerateSampleOrganizations(_billingManager, _plans)) { - if (organization.Id == TestConstants.OrganizationId3) - _billingManager.ApplyBillingPlan(organization, _plans.FreePlan, UserData.GenerateSampleUser()); - else - _billingManager.ApplyBillingPlan(organization, _plans.SmallPlan, UserData.GenerateSampleUser()); - - organization.StripeCustomerId = Guid.NewGuid().ToString("N"); - organization.CardLast4 = "1234"; - organization.SubscribeDate = SystemClock.UtcNow; - - if (organization.IsSuspended) { - organization.SuspendedByUserId = TestConstants.UserId; - organization.SuspensionCode = SuspensionCode.Billing; - organization.SuspensionDate = SystemClock.UtcNow; - } + await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.Cache()); - await _organizationRepository.AddAsync(organization, o => o.Cache()); + foreach (var user in UserData.GenerateSampleUsers()) { + if (user.Id == TestConstants.UserId) { + user.OrganizationIds.Add(TestConstants.OrganizationId2); + user.OrganizationIds.Add(TestConstants.OrganizationId3); } - await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.Cache()); - - foreach (var user in UserData.GenerateSampleUsers()) { - if (user.Id == TestConstants.UserId) { - user.OrganizationIds.Add(TestConstants.OrganizationId2); - user.OrganizationIds.Add(TestConstants.OrganizationId3); - } - - if (!user.IsEmailAddressVerified) - user.CreateVerifyEmailAddressToken(); + if (!user.IsEmailAddressVerified) + user.CreateVerifyEmailAddressToken(); - await _userRepository.AddAsync(user, o => o.Cache()); - } - - await RefreshDataAsync(); + await _userRepository.AddAsync(user, o => o.Cache()); } - private PersistentEvent GenerateEvent(DateTimeOffset? occurrenceDate = null, string userIdentity = null, string type = null, string sessionId = null) { - occurrenceDate ??= SystemClock.OffsetNow; - return EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: occurrenceDate, userIdentity: userIdentity, type: type, sessionId: sessionId); - } + await RefreshDataAsync(); + } + + private PersistentEvent GenerateEvent(DateTimeOffset? occurrenceDate = null, string userIdentity = null, string type = null, string sessionId = null) { + occurrenceDate ??= SystemClock.OffsetNow; + return EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: occurrenceDate, userIdentity: userIdentity, type: type, sessionId: sessionId); } } diff --git a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs index a0c2aedb34..4745cbc9b1 100644 --- a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; -using System.Threading.Tasks; using Exceptionless.Core; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; @@ -18,155 +15,155 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Jobs { - public class EventPostJobTests : IntegrationTestsBase { - private readonly EventPostsJob _job; - private readonly IFileStorage _storage; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IEventRepository _eventRepository; - private readonly IQueue _eventQueue; - private readonly IUserRepository _userRepository; - private readonly ITextSerializer _serializer; - private readonly EventPostService _eventPostService; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - private readonly AppOptions _options; - - public EventPostJobTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _job = GetService(); - _eventQueue = GetService >(); - _storage = GetService(); - _eventPostService = new EventPostService(_eventQueue, _storage, Log); - _organizationRepository = GetService(); - _projectRepository = GetService(); - _eventRepository = GetService(); - _userRepository = GetService(); - _serializer = GetService(); - _billingManager = GetService(); - _plans = GetService(); - _options = GetService(); - } - - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await _eventQueue.DeleteQueueAsync(); - await CreateDataAsync(); - } +namespace Exceptionless.Tests.Jobs; + +public class EventPostJobTests : IntegrationTestsBase { + private readonly EventPostsJob _job; + private readonly IFileStorage _storage; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IEventRepository _eventRepository; + private readonly IQueue _eventQueue; + private readonly IUserRepository _userRepository; + private readonly ITextSerializer _serializer; + private readonly EventPostService _eventPostService; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + private readonly AppOptions _options; + + public EventPostJobTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _job = GetService(); + _eventQueue = GetService>(); + _storage = GetService(); + _eventPostService = new EventPostService(_eventQueue, _storage, Log); + _organizationRepository = GetService(); + _projectRepository = GetService(); + _eventRepository = GetService(); + _userRepository = GetService(); + _serializer = GetService(); + _billingManager = GetService(); + _plans = GetService(); + _options = GetService(); + } - [Fact] - public async Task CanRunJob() { - var ev = GenerateEvent(); - Assert.NotNull(await EnqueueEventPostAsync(ev)); - Assert.Equal(1, (await _eventQueue.GetQueueStatsAsync()).Enqueued); - var files = await _storage.GetFileListAsync(); - Assert.Equal(1, files.Count); - - var result = await _job.RunAsync(); - Assert.True(result.IsSuccess); - - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Dequeued); - Assert.Equal(1, stats.Completed); - - await RefreshDataAsync(); - Assert.Equal(1, await _eventRepository.CountAsync()); - - files = await _storage.GetFileListAsync(); - Assert.Equal(0, files.Count); - } + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await _eventQueue.DeleteQueueAsync(); + await CreateDataAsync(); + } - [Fact] - public async Task CanRunJobWithMassiveEventAsync() { - var ev = GenerateEvent(); - for (int i = 1; i < 100; i++) - ev.Data[$"{i}MB"] = new string('0', 1024 * 1000); + [Fact] + public async Task CanRunJob() { + var ev = GenerateEvent(); + Assert.NotNull(await EnqueueEventPostAsync(ev)); + Assert.Equal(1, (await _eventQueue.GetQueueStatsAsync()).Enqueued); + var files = await _storage.GetFileListAsync(); + Assert.Equal(1, files.Count); - Assert.NotNull(await EnqueueEventPostAsync(ev)); - Assert.Equal(1, (await _eventQueue.GetQueueStatsAsync()).Enqueued); - var files = await _storage.GetFileListAsync(); - Assert.Equal(1, files.Count); + var result = await _job.RunAsync(); + Assert.True(result.IsSuccess); - var result = await _job.RunAsync(); - Assert.False(result.IsSuccess); + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Dequeued); + Assert.Equal(1, stats.Completed); - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Dequeued); - Assert.Equal(1, stats.Completed); + await RefreshDataAsync(); + Assert.Equal(1, await _eventRepository.CountAsync()); - files = await _storage.GetFileListAsync(); - Assert.Equal(0, files.Count); - } + files = await _storage.GetFileListAsync(); + Assert.Equal(0, files.Count); + } - [Fact] - public async Task CanRunJobWithNonExistingEventDataAsync() { - var ev = GenerateEvent(); - Assert.NotNull(await EnqueueEventPostAsync(ev)); - Assert.Equal(1, (await _eventQueue.GetQueueStatsAsync()).Enqueued); + [Fact] + public async Task CanRunJobWithMassiveEventAsync() { + var ev = GenerateEvent(); + for (int i = 1; i < 100; i++) + ev.Data[$"{i}MB"] = new string('0', 1024 * 1000); - await _storage.DeleteFilesAsync(await _storage.GetFileListAsync()); + Assert.NotNull(await EnqueueEventPostAsync(ev)); + Assert.Equal(1, (await _eventQueue.GetQueueStatsAsync()).Enqueued); + var files = await _storage.GetFileListAsync(); + Assert.Equal(1, files.Count); - var result = await _job.RunAsync(); - Assert.False(result.IsSuccess); + var result = await _job.RunAsync(); + Assert.False(result.IsSuccess); - var stats = await _eventQueue.GetQueueStatsAsync(); - Assert.Equal(1, stats.Dequeued); - Assert.Equal(1, stats.Abandoned); - } + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Dequeued); + Assert.Equal(1, stats.Completed); - private async Task CreateDataAsync() { - foreach (var organization in OrganizationData.GenerateSampleOrganizations(_billingManager, _plans)) { - if (organization.Id == TestConstants.OrganizationId3) - _billingManager.ApplyBillingPlan(organization, _plans.FreePlan, UserData.GenerateSampleUser()); - else - _billingManager.ApplyBillingPlan(organization, _plans.SmallPlan, UserData.GenerateSampleUser()); + files = await _storage.GetFileListAsync(); + Assert.Equal(0, files.Count); + } - organization.StripeCustomerId = Guid.NewGuid().ToString("N"); - organization.CardLast4 = "1234"; - organization.SubscribeDate = SystemClock.UtcNow; + [Fact] + public async Task CanRunJobWithNonExistingEventDataAsync() { + var ev = GenerateEvent(); + Assert.NotNull(await EnqueueEventPostAsync(ev)); + Assert.Equal(1, (await _eventQueue.GetQueueStatsAsync()).Enqueued); - if (organization.IsSuspended) { - organization.SuspendedByUserId = TestConstants.UserId; - organization.SuspensionCode = SuspensionCode.Billing; - organization.SuspensionDate = SystemClock.UtcNow; - } + await _storage.DeleteFilesAsync(await _storage.GetFileListAsync()); - await _organizationRepository.AddAsync(organization, o => o.Cache().ImmediateConsistency()); - } + var result = await _job.RunAsync(); + Assert.False(result.IsSuccess); - await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.Cache().ImmediateConsistency()); + var stats = await _eventQueue.GetQueueStatsAsync(); + Assert.Equal(1, stats.Dequeued); + Assert.Equal(1, stats.Abandoned); + } - foreach (var user in UserData.GenerateSampleUsers()) { - if (user.Id == TestConstants.UserId) { - user.OrganizationIds.Add(TestConstants.OrganizationId2); - user.OrganizationIds.Add(TestConstants.OrganizationId3); - } + private async Task CreateDataAsync() { + foreach (var organization in OrganizationData.GenerateSampleOrganizations(_billingManager, _plans)) { + if (organization.Id == TestConstants.OrganizationId3) + _billingManager.ApplyBillingPlan(organization, _plans.FreePlan, UserData.GenerateSampleUser()); + else + _billingManager.ApplyBillingPlan(organization, _plans.SmallPlan, UserData.GenerateSampleUser()); + + organization.StripeCustomerId = Guid.NewGuid().ToString("N"); + organization.CardLast4 = "1234"; + organization.SubscribeDate = SystemClock.UtcNow; + + if (organization.IsSuspended) { + organization.SuspendedByUserId = TestConstants.UserId; + organization.SuspensionCode = SuspensionCode.Billing; + organization.SuspensionDate = SystemClock.UtcNow; + } - if (!user.IsEmailAddressVerified) - user.CreateVerifyEmailAddressToken(); + await _organizationRepository.AddAsync(organization, o => o.Cache().ImmediateConsistency()); + } - await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); + await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.Cache().ImmediateConsistency()); + + foreach (var user in UserData.GenerateSampleUsers()) { + if (user.Id == TestConstants.UserId) { + user.OrganizationIds.Add(TestConstants.OrganizationId2); + user.OrganizationIds.Add(TestConstants.OrganizationId3); } - } - private async Task EnqueueEventPostAsync(PersistentEvent ev) { - var eventPostInfo = new EventPost(_options.EnableArchive) { - OrganizationId = ev.OrganizationId, - ProjectId = ev.ProjectId, - ApiVersion = 2, - CharSet = "utf-8", - ContentEncoding = "gzip", - MediaType = "application/json", - UserAgent = "exceptionless-test", - }; - - var stream = new MemoryStream(_serializer.SerializeToBytes(ev).Compress()); - return await _eventPostService.EnqueueAsync(eventPostInfo, stream).AnyContext(); - } + if (!user.IsEmailAddressVerified) + user.CreateVerifyEmailAddressToken(); - private PersistentEvent GenerateEvent(DateTimeOffset? occurrenceDate = null, string userIdentity = null, string type = null, string sessionId = null) { - occurrenceDate ??= SystemClock.OffsetNow; - return EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: occurrenceDate, userIdentity: userIdentity, type: type, sessionId: sessionId); + await _userRepository.AddAsync(user, o => o.Cache().ImmediateConsistency()); } } -} \ No newline at end of file + + private async Task EnqueueEventPostAsync(PersistentEvent ev) { + var eventPostInfo = new EventPost(_options.EnableArchive) { + OrganizationId = ev.OrganizationId, + ProjectId = ev.ProjectId, + ApiVersion = 2, + CharSet = "utf-8", + ContentEncoding = "gzip", + MediaType = "application/json", + UserAgent = "exceptionless-test", + }; + + var stream = new MemoryStream(_serializer.SerializeToBytes(ev).Compress()); + return await _eventPostService.EnqueueAsync(eventPostInfo, stream).AnyContext(); + } + + private PersistentEvent GenerateEvent(DateTimeOffset? occurrenceDate = null, string userIdentity = null, string type = null, string sessionId = null) { + occurrenceDate ??= SystemClock.OffsetNow; + return EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: occurrenceDate, userIdentity: userIdentity, type: type, sessionId: sessionId); + } +} diff --git a/tests/Exceptionless.Tests/Mail/MailerTests.cs b/tests/Exceptionless.Tests/Mail/MailerTests.cs index 78ecfdc7e5..b2a9382173 100644 --- a/tests/Exceptionless.Tests/Mail/MailerTests.cs +++ b/tests/Exceptionless.Tests/Mail/MailerTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core; +using Exceptionless.Core; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Jobs; @@ -14,45 +12,45 @@ using Foundatio.Metrics; using Foundatio.Queues; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Mail { - public sealed class MailerTests : TestWithServices { - private readonly IMailer _mailer; - private readonly AppOptions _options; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - - public MailerTests(ITestOutputHelper output) : base(output) { - _mailer = GetService(); - _options = GetService(); - _billingManager = GetService(); - _plans = GetService(); - - if (_mailer is NullMailer) - _mailer = new Mailer(GetService>(), GetService(), _options, GetService(), Log.CreateLogger()); - } +namespace Exceptionless.Tests.Mail; +public sealed class MailerTests : TestWithServices { + private readonly IMailer _mailer; + private readonly AppOptions _options; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + + public MailerTests(ITestOutputHelper output) : base(output) { + _mailer = GetService(); + _options = GetService(); + _billingManager = GetService(); + _plans = GetService(); + + if (_mailer is NullMailer) + _mailer = new Mailer(GetService>(), GetService(), _options, GetService(), Log.CreateLogger()); + } - [Fact] - public void CanParseSmtpUri() { - var uri = new SmtpUri("smtps://test%40test.com:testpass@smtp.test.com:587"); - Assert.NotNull(uri); - Assert.True(uri.IsSecure); - Assert.Equal("smtp.test.com", uri.Host); - Assert.Equal(587, uri.Port); - Assert.Equal("test@test.com", uri.User); - Assert.Equal("testpass", uri.Password); - } - [Fact] - public Task SendEventNoticeSimpleErrorAsync() { - var ex = GetException(); - return SendEventNoticeAsync(new PersistentEvent { - Type = Event.KnownTypes.Error, - Data = new Core.Models.DataDictionary { + [Fact] + public void CanParseSmtpUri() { + var uri = new SmtpUri("smtps://test%40test.com:testpass@smtp.test.com:587"); + Assert.NotNull(uri); + Assert.True(uri.IsSecure); + Assert.Equal("smtp.test.com", uri.Host); + Assert.Equal(587, uri.Port); + Assert.Equal("test@test.com", uri.User); + Assert.Equal("testpass", uri.Password); + } + + [Fact] + public Task SendEventNoticeSimpleErrorAsync() { + var ex = GetException(); + return SendEventNoticeAsync(new PersistentEvent { + Type = Event.KnownTypes.Error, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.SimpleError, new SimpleError { Message = ex.Message, @@ -61,281 +59,281 @@ public Task SendEventNoticeSimpleErrorAsync() { } } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeErrorAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Type = Event.KnownTypes.Error, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeErrorAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Type = Event.KnownTypes.Error, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Error, EventData.GenerateError() } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeErrorWithDetailsAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Type = Event.KnownTypes.Error, - Geo = "44.5241,-87.9056", - ReferenceId = "ex_blake_dreams_of_cookies", - Tags = new TagSet(new [] { "Out", "Of", "Cookies", "Critical" }), - Count = 2, - Value = 500, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeErrorWithDetailsAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Type = Event.KnownTypes.Error, + Geo = "44.5241,-87.9056", + ReferenceId = "ex_blake_dreams_of_cookies", + Tags = new TagSet(new[] { "Out", "Of", "Cookies", "Critical" }), + Count = 2, + Value = 500, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Error, EventData.GenerateError() }, { Event.KnownDataKeys.Version, "1.2.3" }, { Event.KnownDataKeys.UserInfo, new UserInfo("niemyjski", "Blake Niemyjski") }, { Event.KnownDataKeys.UserDescription, new UserDescription("noreply@exceptionless.io", "Blake ate two boxes of cookies and needs help") } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeNotFoundAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "[GET] /not-found?page=20", - Type = Event.KnownTypes.NotFound - }); - } + [Fact] + public Task SendEventNoticeNotFoundAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "[GET] /not-found?page=20", + Type = Event.KnownTypes.NotFound + }); + } - [Fact] - public Task SendEventNoticeFeatureAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "My Feature Usage", - Value = 1, - Type = Event.KnownTypes.FeatureUsage - }); - } + [Fact] + public Task SendEventNoticeFeatureAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "My Feature Usage", + Value = 1, + Type = Event.KnownTypes.FeatureUsage + }); + } - [Fact] - public Task SendEventNoticeEmptyLogEventAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Value = 1, - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeEmptyLogEventAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Value = 1, + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogMessageAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "Only Message", - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeLogMessageAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "Only Message", + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogSourceAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "Only Source", - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeLogSourceAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "Only Source", + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogReallyLongSourceAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "Soooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooorce", - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeLogReallyLongSourceAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "Soooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooorce", + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogMessageSourceLevelAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "My Message", - Source = "My Source", - Type = Event.KnownTypes.Log, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeLogMessageSourceLevelAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "My Message", + Source = "My Source", + Type = Event.KnownTypes.Log, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Level, "Warn" } } - }); - } - - [Fact] - public Task SendEventNoticeDefaultAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "Default Test Message", - Source = "Default Test Source" - }); - } + }); + } - private async Task SendEventNoticeAsync(PersistentEvent ev) { - var user = UserData.GenerateSampleUser(); - var project = ProjectData.GenerateSampleProject(); + [Fact] + public Task SendEventNoticeDefaultAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "Default Test Message", + Source = "Default Test Source" + }); + } - ev.Id = TestConstants.EventId; - ev.OrganizationId = TestConstants.OrganizationId; - ev.ProjectId = TestConstants.ProjectId; - ev.StackId = TestConstants.StackId; + private async Task SendEventNoticeAsync(PersistentEvent ev) { + var user = UserData.GenerateSampleUser(); + var project = ProjectData.GenerateSampleProject(); - await _mailer.SendEventNoticeAsync(user, ev, project, RandomData.GetBool(), RandomData.GetBool(), 1); - await RunMailJobAsync(); - } + ev.Id = TestConstants.EventId; + ev.OrganizationId = TestConstants.OrganizationId; + ev.ProjectId = TestConstants.ProjectId; + ev.StackId = TestConstants.StackId; - [Fact] - public async Task SendOrganizationAddedAsync() { - var user = UserData.GenerateSampleUser(); - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + await _mailer.SendEventNoticeAsync(user, ev, project, RandomData.GetBool(), RandomData.GetBool(), 1); + await RunMailJobAsync(); + } - await _mailer.SendOrganizationAddedAsync(user, organization, user); - await RunMailJobAsync(); - } + [Fact] + public async Task SendOrganizationAddedAsync() { + var user = UserData.GenerateSampleUser(); + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - [Fact] - public async Task SendOrganizationInviteAsync() { - var user = UserData.GenerateSampleUser(); - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + await _mailer.SendOrganizationAddedAsync(user, organization, user); + await RunMailJobAsync(); + } - await _mailer.SendOrganizationInviteAsync(user, organization, new Invite { - DateAdded = SystemClock.UtcNow, - EmailAddress = "test@exceptionless.com", - Token = "1" - }); + [Fact] + public async Task SendOrganizationInviteAsync() { + var user = UserData.GenerateSampleUser(); + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - await RunMailJobAsync(); + await _mailer.SendOrganizationInviteAsync(user, organization, new Invite { + DateAdded = SystemClock.UtcNow, + EmailAddress = "test@exceptionless.com", + Token = "1" + }); - var sender = GetService() as InMemoryMailSender; - Assert.NotNull(sender); + await RunMailJobAsync(); - Assert.Contains("Join Organization", sender.LastMessage.Body); - } + var sender = GetService() as InMemoryMailSender; + Assert.NotNull(sender); - [Fact] - public async Task SendOrganizationHourlyOverageNoticeAsync() { - var user = UserData.GenerateSampleUser(); - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + Assert.Contains("Join Organization", sender.LastMessage.Body); + } - await _mailer.SendOrganizationNoticeAsync(user, organization, false, true); - await RunMailJobAsync(); - } + [Fact] + public async Task SendOrganizationHourlyOverageNoticeAsync() { + var user = UserData.GenerateSampleUser(); + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - [Fact] - public async Task SendOrganizationMonthlyOverageNoticeAsync() { - var user = UserData.GenerateSampleUser(); - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + await _mailer.SendOrganizationNoticeAsync(user, organization, false, true); + await RunMailJobAsync(); + } - await _mailer.SendOrganizationNoticeAsync(user, organization, true, false); - await RunMailJobAsync(); - } + [Fact] + public async Task SendOrganizationMonthlyOverageNoticeAsync() { + var user = UserData.GenerateSampleUser(); + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - [Fact] - public async Task SendOrganizationPaymentFailedAsync() { - var user = UserData.GenerateSampleUser(); - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + await _mailer.SendOrganizationNoticeAsync(user, organization, true, false); + await RunMailJobAsync(); + } - await _mailer.SendOrganizationPaymentFailedAsync(user, organization); - await RunMailJobAsync(); - } + [Fact] + public async Task SendOrganizationPaymentFailedAsync() { + var user = UserData.GenerateSampleUser(); + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - [Fact] - public async Task SendProjectDailySummaryAsync() { - var user = UserData.GenerateSampleUser(); - var project = ProjectData.GenerateSampleProject(); - var mostFrequent = StackData.GenerateStacks(3, generateId: true, type: Event.KnownTypes.Error); + await _mailer.SendOrganizationPaymentFailedAsync(user, organization); + await RunMailJobAsync(); + } - await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, null, SystemClock.UtcNow.Date, true, 12, 1, 0, 1, 0, 0, false); - await RunMailJobAsync(); - } + [Fact] + public async Task SendProjectDailySummaryAsync() { + var user = UserData.GenerateSampleUser(); + var project = ProjectData.GenerateSampleProject(); + var mostFrequent = StackData.GenerateStacks(3, generateId: true, type: Event.KnownTypes.Error); - [Fact] - public async Task SendProjectDailySummaryWithAllBlockedAsync() { - var user = UserData.GenerateSampleUser(); - var project = ProjectData.GenerateSampleProject(); - var mostFrequent = StackData.GenerateStacks(3, generateId: true, type: Event.KnownTypes.Error); + await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, null, SystemClock.UtcNow.Date, true, 12, 1, 0, 1, 0, 0, false); + await RunMailJobAsync(); + } - await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, null, SystemClock.UtcNow.Date, true, 123456, 1, 0, 1, 123456, 0, false); - await RunMailJobAsync(); - } + [Fact] + public async Task SendProjectDailySummaryWithAllBlockedAsync() { + var user = UserData.GenerateSampleUser(); + var project = ProjectData.GenerateSampleProject(); + var mostFrequent = StackData.GenerateStacks(3, generateId: true, type: Event.KnownTypes.Error); - [Fact] - public async Task SendProjectDailySummaryNotConfiguredAsync() { - var user = UserData.GenerateSampleUser(); - var project = ProjectData.GenerateSampleProject(); + await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, null, SystemClock.UtcNow.Date, true, 123456, 1, 0, 1, 123456, 0, false); + await RunMailJobAsync(); + } - await _mailer.SendProjectDailySummaryAsync(user, project, null, null, SystemClock.UtcNow.Date, false, 0, 0, 0, 0, 0, 0, false); - await RunMailJobAsync(); - } + [Fact] + public async Task SendProjectDailySummaryNotConfiguredAsync() { + var user = UserData.GenerateSampleUser(); + var project = ProjectData.GenerateSampleProject(); - [Fact] - public async Task SendProjectDailySummaryWithNoEventsButHasFixedEventsAsync() { - var user = UserData.GenerateSampleUser(); - var project = ProjectData.GenerateSampleProject(); + await _mailer.SendProjectDailySummaryAsync(user, project, null, null, SystemClock.UtcNow.Date, false, 0, 0, 0, 0, 0, 0, false); + await RunMailJobAsync(); + } - await _mailer.SendProjectDailySummaryAsync(user, project, null, null, SystemClock.UtcNow.Date, true, 0, 0, 0, 10, 0, 0, false); - await RunMailJobAsync(); - } + [Fact] + public async Task SendProjectDailySummaryWithNoEventsButHasFixedEventsAsync() { + var user = UserData.GenerateSampleUser(); + var project = ProjectData.GenerateSampleProject(); - [Fact] - public async Task SendProjectDailySummaryWithNoEventsButHasFixedAndTooBigEventsAsync() { - var user = UserData.GenerateSampleUser(); - var project = ProjectData.GenerateSampleProject(); + await _mailer.SendProjectDailySummaryAsync(user, project, null, null, SystemClock.UtcNow.Date, true, 0, 0, 0, 10, 0, 0, false); + await RunMailJobAsync(); + } - await _mailer.SendProjectDailySummaryAsync(user, project, null, null, SystemClock.UtcNow.Date, true, 0, 0, 0, 10, 123456, 23, false); - await RunMailJobAsync(); - } + [Fact] + public async Task SendProjectDailySummaryWithNoEventsButHasFixedAndTooBigEventsAsync() { + var user = UserData.GenerateSampleUser(); + var project = ProjectData.GenerateSampleProject(); - [Fact] - public async Task SendProjectDailySummaryWithFreeProjectAsync() { - var user = UserData.GenerateSampleUser(); - var project = ProjectData.GenerateSampleProject(); - var mostFrequent = StackData.GenerateStacks(3, generateId: true, type: Event.KnownTypes.Error); - var newest = StackData.GenerateStacks(1, generateId: true, type: Event.KnownTypes.Error); + await _mailer.SendProjectDailySummaryAsync(user, project, null, null, SystemClock.UtcNow.Date, true, 0, 0, 0, 10, 123456, 23, false); + await RunMailJobAsync(); + } - await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, newest, SystemClock.UtcNow.Date, true, 12, 1, 1, 2, 0, 0, true); - await RunMailJobAsync(); - } + [Fact] + public async Task SendProjectDailySummaryWithFreeProjectAsync() { + var user = UserData.GenerateSampleUser(); + var project = ProjectData.GenerateSampleProject(); + var mostFrequent = StackData.GenerateStacks(3, generateId: true, type: Event.KnownTypes.Error); + var newest = StackData.GenerateStacks(1, generateId: true, type: Event.KnownTypes.Error); - [Fact] - public async Task SendUserPasswordResetAsync() { - var user = UserData.GenerateSampleUser(); - user.CreatePasswordResetToken(); + await _mailer.SendProjectDailySummaryAsync(user, project, mostFrequent, newest, SystemClock.UtcNow.Date, true, 12, 1, 1, 2, 0, 0, true); + await RunMailJobAsync(); + } - await _mailer.SendUserPasswordResetAsync(user); - await RunMailJobAsync(); - } + [Fact] + public async Task SendUserPasswordResetAsync() { + var user = UserData.GenerateSampleUser(); + user.CreatePasswordResetToken(); - [Fact] - public async Task SendUserEmailVerifyAsync() { - var user = UserData.GenerateSampleUser(); - user.CreateVerifyEmailAddressToken(); + await _mailer.SendUserPasswordResetAsync(user); + await RunMailJobAsync(); + } - await _mailer.SendUserEmailVerifyAsync(user); - await RunMailJobAsync(); - } + [Fact] + public async Task SendUserEmailVerifyAsync() { + var user = UserData.GenerateSampleUser(); + user.CreateVerifyEmailAddressToken(); - private async Task RunMailJobAsync() { - var job = GetService(); - await job.RunAsync(); + await _mailer.SendUserEmailVerifyAsync(user); + await RunMailJobAsync(); + } - if (!(GetService() is InMemoryMailSender sender)) - return; + private async Task RunMailJobAsync() { + var job = GetService(); + await job.RunAsync(); - _logger.LogTrace("To: {To}", sender.LastMessage.To); - _logger.LogTrace("Subject: {Subject}", sender.LastMessage.Subject); - _logger.LogTrace("Body:\n{Body}", sender.LastMessage.Body); - } + if (!(GetService() is InMemoryMailSender sender)) + return; - private Exception GetException() { - void TestInner() { - void TestInnerInner() { - throw new ApplicationException("Random Test Exception"); - } + _logger.LogTrace("To: {To}", sender.LastMessage.To); + _logger.LogTrace("Subject: {Subject}", sender.LastMessage.Subject); + _logger.LogTrace("Body:\n{Body}", sender.LastMessage.Body); + } - TestInnerInner(); + private Exception GetException() { + void TestInner() { + void TestInnerInner() { + throw new ApplicationException("Random Test Exception"); } - try { - TestInner(); - } catch (Exception ex) { - return ex; - } + TestInnerInner(); + } - return null; + try { + TestInner(); } + catch (Exception ex) { + return ex; + } + + return null; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Mail/NullMailer.cs b/tests/Exceptionless.Tests/Mail/NullMailer.cs index 33ebb404e7..1c3b750d0a 100644 --- a/tests/Exceptionless.Tests/Mail/NullMailer.cs +++ b/tests/Exceptionless.Tests/Mail/NullMailer.cs @@ -1,41 +1,38 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Mail; +using Exceptionless.Core.Mail; using Exceptionless.Core.Models; -namespace Exceptionless.Tests.Mail { - public class NullMailer : IMailer { - public Task SendEventNoticeAsync(User user, PersistentEvent ev, Project project, bool isNew, bool isRegression, int totalOccurrences) { - return Task.FromResult(true); - } +namespace Exceptionless.Tests.Mail; - public Task SendOrganizationAddedAsync(User sender, Organization organization, User user) { - return Task.CompletedTask; - } +public class NullMailer : IMailer { + public Task SendEventNoticeAsync(User user, PersistentEvent ev, Project project, bool isNew, bool isRegression, int totalOccurrences) { + return Task.FromResult(true); + } + + public Task SendOrganizationAddedAsync(User sender, Organization organization, User user) { + return Task.CompletedTask; + } - public Task SendOrganizationInviteAsync(User sender, Organization organization, Invite invite) { - return Task.CompletedTask; - } + public Task SendOrganizationInviteAsync(User sender, Organization organization, Invite invite) { + return Task.CompletedTask; + } - public Task SendOrganizationNoticeAsync(User user, Organization organization, bool isOverMonthlyLimit, bool isOverHourlyLimit) { - return Task.CompletedTask; - } + public Task SendOrganizationNoticeAsync(User user, Organization organization, bool isOverMonthlyLimit, bool isOverHourlyLimit) { + return Task.CompletedTask; + } - public Task SendOrganizationPaymentFailedAsync(User owner, Organization organization) { - return Task.CompletedTask; - } + public Task SendOrganizationPaymentFailedAsync(User owner, Organization organization) { + return Task.CompletedTask; + } - public Task SendProjectDailySummaryAsync(User user, Project project, IEnumerable mostFrequent, IEnumerable newest, DateTime startDate, bool hasSubmittedEvents, double count, double uniqueCount, double newCount, double fixedCount, int blockedCount, int tooBigCount, bool isFreePlan) { - return Task.CompletedTask; - } + public Task SendProjectDailySummaryAsync(User user, Project project, IEnumerable mostFrequent, IEnumerable newest, DateTime startDate, bool hasSubmittedEvents, double count, double uniqueCount, double newCount, double fixedCount, int blockedCount, int tooBigCount, bool isFreePlan) { + return Task.CompletedTask; + } - public Task SendUserEmailVerifyAsync(User user) { - return Task.CompletedTask; - } + public Task SendUserEmailVerifyAsync(User user) { + return Task.CompletedTask; + } - public Task SendUserPasswordResetAsync(User user) { - return Task.CompletedTask; - } + public Task SendUserPasswordResetAsync(User user) { + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs index 9b523d72f7..3ec00abcfb 100644 --- a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs @@ -1,5 +1,3 @@ -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core.Migrations; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; @@ -10,179 +8,178 @@ using Foundatio.Repositories.Migrations; using Foundatio.Repositories.Utility; using Foundatio.Utility; -using Microsoft.Extensions.DependencyInjection; using Nest; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Migrations { - public class FixDuplicateStacksMigrationTests : IntegrationTestsBase { - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - - public FixDuplicateStacksMigrationTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _stackRepository = GetService(); - _eventRepository = GetService(); - } - - protected override void RegisterServices(IServiceCollection services) { - services.AddTransient(); - services.AddSingleton(new EmptyLock()); - base.RegisterServices(services); - } - - [Fact] - public async Task WillMergeDuplicatedStacks() { - var utcNow = SystemClock.UtcNow; - var originalStack = StackData.GenerateStack(); - originalStack.Id = ObjectId.GenerateNewId().ToString(); - originalStack.TotalOccurrences = 100; - var duplicateStack = originalStack.DeepClone(); - duplicateStack.Id = ObjectId.GenerateNewId().ToString(); - duplicateStack.Status = StackStatus.Fixed; - duplicateStack.TotalOccurrences = 10; - duplicateStack.LastOccurrence = originalStack.LastOccurrence.AddMinutes(1); - duplicateStack.SnoozeUntilUtc = originalStack.SnoozeUntilUtc = null; - duplicateStack.DateFixed = duplicateStack.LastOccurrence.AddMinutes(1); - duplicateStack.Tags.Add("stack2"); - duplicateStack.References.Add("stack2"); - duplicateStack.OccurrencesAreCritical = true; - - originalStack = await _stackRepository.AddAsync(originalStack, o => o.ImmediateConsistency()); - duplicateStack = await _stackRepository.AddAsync(duplicateStack, o => o.ImmediateConsistency()); - - await _eventRepository.AddAsync(EventData.GenerateEvents(count: 100, stackId: originalStack.Id), o => o.ImmediateConsistency()); - await _eventRepository.AddAsync(EventData.GenerateEvents(count: 10, stackId: duplicateStack.Id), o => o.ImmediateConsistency()); - - var results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); - Assert.Equal(2, results.Total); - - var migration = GetService(); - var context = new MigrationContext(GetService(), _logger, CancellationToken.None); - await migration.RunAsync(context); - - await RefreshDataAsync(); - - results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); - Assert.Single(results.Documents); - - var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); - Assert.False(updatedOriginalStack.IsDeleted); - var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); - Assert.True(updatedDuplicateStack.IsDeleted); - - Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); - Assert.Equal(110, updatedOriginalStack.TotalOccurrences); - Assert.Equal(StackStatus.Fixed, updatedOriginalStack.Status); - Assert.Equal(duplicateStack.LastOccurrence, updatedOriginalStack.LastOccurrence); - Assert.Null(updatedOriginalStack.SnoozeUntilUtc); - Assert.Equal(duplicateStack.DateFixed, updatedOriginalStack.DateFixed); - Assert.Equal(originalStack.Tags.Count + 1, updatedOriginalStack.Tags.Count); - Assert.Contains("stack2", updatedOriginalStack.Tags); - Assert.Equal(originalStack.References.Count + 1, updatedOriginalStack.References.Count); - Assert.Contains("stack2", updatedOriginalStack.References); - Assert.True(updatedOriginalStack.OccurrencesAreCritical); - } - - [Fact] - public async Task WillMergeToStackWithMostEvents() { - var utcNow = SystemClock.UtcNow; - var originalStack = StackData.GenerateStack(); - originalStack.Id = ObjectId.GenerateNewId().ToString(); - originalStack.TotalOccurrences = 10; - var biggerStack = originalStack.DeepClone(); - biggerStack.Id = ObjectId.GenerateNewId().ToString(); - biggerStack.Status = StackStatus.Fixed; - biggerStack.TotalOccurrences = 100; - biggerStack.LastOccurrence = originalStack.LastOccurrence.AddMinutes(1); - biggerStack.SnoozeUntilUtc = originalStack.SnoozeUntilUtc = null; - biggerStack.DateFixed = biggerStack.LastOccurrence.AddMinutes(1); - biggerStack.Tags.Add("stack2"); - biggerStack.References.Add("stack2"); - biggerStack.OccurrencesAreCritical = true; - - originalStack = await _stackRepository.AddAsync(originalStack, o => o.ImmediateConsistency()); - biggerStack = await _stackRepository.AddAsync(biggerStack, o => o.ImmediateConsistency()); - - await _eventRepository.AddAsync(EventData.GenerateEvents(count: 10, stackId: originalStack.Id), o => o.ImmediateConsistency()); - await _eventRepository.AddAsync(EventData.GenerateEvents(count: 100, stackId: biggerStack.Id), o => o.ImmediateConsistency()); - - var results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); - Assert.Equal(2, results.Total); - - var migration = GetService(); - var context = new MigrationContext(GetService(), _logger, CancellationToken.None); - await migration.RunAsync(context); - - await RefreshDataAsync(); - - results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); - Assert.Single(results.Documents); - - var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); - Assert.True(updatedOriginalStack.IsDeleted); - var updatedBiggerStack = await _stackRepository.GetByIdAsync(biggerStack.Id, o => o.IncludeSoftDeletes()); - Assert.False(updatedBiggerStack.IsDeleted); - - Assert.Equal(originalStack.CreatedUtc, updatedBiggerStack.CreatedUtc); - Assert.Equal(110, updatedBiggerStack.TotalOccurrences); - Assert.Equal(StackStatus.Fixed, updatedBiggerStack.Status); - Assert.Equal(biggerStack.LastOccurrence, updatedBiggerStack.LastOccurrence); - Assert.Null(updatedBiggerStack.SnoozeUntilUtc); - Assert.Equal(biggerStack.DateFixed, updatedBiggerStack.DateFixed); - Assert.Equal(originalStack.Tags.Count + 1, updatedBiggerStack.Tags.Count); - Assert.Contains("stack2", updatedBiggerStack.Tags); - Assert.Equal(originalStack.References.Count + 1, updatedBiggerStack.References.Count); - Assert.Contains("stack2", updatedBiggerStack.References); - Assert.True(updatedBiggerStack.OccurrencesAreCritical); - } - - [Fact] - public async Task WillNotMergeDuplicatedDeletedStacks() { - var originalStack = StackData.GenerateStack(); - var duplicateStack = originalStack.DeepClone(); - duplicateStack.Id = ObjectId.GenerateNewId().ToString(); - duplicateStack.CreatedUtc = originalStack.CreatedUtc.AddMinutes(1); - duplicateStack.Status = StackStatus.Fixed; - duplicateStack.LastOccurrence = originalStack.LastOccurrence.AddMinutes(1); - duplicateStack.SnoozeUntilUtc = originalStack.SnoozeUntilUtc = null; - duplicateStack.DateFixed = duplicateStack.LastOccurrence.AddMinutes(1); - duplicateStack.UpdatedUtc = originalStack.UpdatedUtc.SubtractMinutes(1); - duplicateStack.Tags.Add("stack2"); - duplicateStack.References.Add("stack2"); - duplicateStack.OccurrencesAreCritical = true; - duplicateStack.IsDeleted = true; - - await _stackRepository.AddAsync(new []{ originalStack, duplicateStack }, o => o.ImmediateConsistency()); - - var results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); - Assert.Single(results.Documents); - - var migration = GetService(); - var context = new MigrationContext(GetService(), _logger, CancellationToken.None); - await migration.RunAsync(context); - - await RefreshDataAsync(); - - results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); - Assert.Single(results.Documents); - - var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); - Assert.False(updatedOriginalStack.IsDeleted); - var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); - Assert.True(updatedDuplicateStack.IsDeleted); - - Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); - Assert.Equal(originalStack.Status, updatedOriginalStack.Status); - Assert.Equal(originalStack.LastOccurrence, updatedOriginalStack.LastOccurrence); - Assert.Equal(originalStack.SnoozeUntilUtc, updatedOriginalStack.SnoozeUntilUtc); - Assert.Equal(originalStack.DateFixed, updatedOriginalStack.DateFixed); - Assert.Equal(originalStack.UpdatedUtc, updatedOriginalStack.UpdatedUtc); - Assert.Equal(originalStack.Tags.Count, updatedOriginalStack.Tags.Count); - Assert.DoesNotContain("stack2", updatedOriginalStack.Tags); - Assert.Equal(originalStack.References.Count , updatedOriginalStack.References.Count); - Assert.DoesNotContain("stack2", updatedOriginalStack.References); - Assert.False(updatedOriginalStack.OccurrencesAreCritical); - } +namespace Exceptionless.Tests.Migrations; + +public class FixDuplicateStacksMigrationTests : IntegrationTestsBase { + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + + public FixDuplicateStacksMigrationTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _stackRepository = GetService(); + _eventRepository = GetService(); + } + + protected override void RegisterServices(IServiceCollection services) { + services.AddTransient(); + services.AddSingleton(new EmptyLock()); + base.RegisterServices(services); + } + + [Fact] + public async Task WillMergeDuplicatedStacks() { + var utcNow = SystemClock.UtcNow; + var originalStack = StackData.GenerateStack(); + originalStack.Id = ObjectId.GenerateNewId().ToString(); + originalStack.TotalOccurrences = 100; + var duplicateStack = originalStack.DeepClone(); + duplicateStack.Id = ObjectId.GenerateNewId().ToString(); + duplicateStack.Status = StackStatus.Fixed; + duplicateStack.TotalOccurrences = 10; + duplicateStack.LastOccurrence = originalStack.LastOccurrence.AddMinutes(1); + duplicateStack.SnoozeUntilUtc = originalStack.SnoozeUntilUtc = null; + duplicateStack.DateFixed = duplicateStack.LastOccurrence.AddMinutes(1); + duplicateStack.Tags.Add("stack2"); + duplicateStack.References.Add("stack2"); + duplicateStack.OccurrencesAreCritical = true; + + originalStack = await _stackRepository.AddAsync(originalStack, o => o.ImmediateConsistency()); + duplicateStack = await _stackRepository.AddAsync(duplicateStack, o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync(EventData.GenerateEvents(count: 100, stackId: originalStack.Id), o => o.ImmediateConsistency()); + await _eventRepository.AddAsync(EventData.GenerateEvents(count: 10, stackId: duplicateStack.Id), o => o.ImmediateConsistency()); + + var results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); + Assert.Equal(2, results.Total); + + var migration = GetService(); + var context = new MigrationContext(GetService(), _logger, CancellationToken.None); + await migration.RunAsync(context); + + await RefreshDataAsync(); + + results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); + Assert.Single(results.Documents); + + var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.False(updatedOriginalStack.IsDeleted); + var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + Assert.True(updatedDuplicateStack.IsDeleted); + + Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); + Assert.Equal(110, updatedOriginalStack.TotalOccurrences); + Assert.Equal(StackStatus.Fixed, updatedOriginalStack.Status); + Assert.Equal(duplicateStack.LastOccurrence, updatedOriginalStack.LastOccurrence); + Assert.Null(updatedOriginalStack.SnoozeUntilUtc); + Assert.Equal(duplicateStack.DateFixed, updatedOriginalStack.DateFixed); + Assert.Equal(originalStack.Tags.Count + 1, updatedOriginalStack.Tags.Count); + Assert.Contains("stack2", updatedOriginalStack.Tags); + Assert.Equal(originalStack.References.Count + 1, updatedOriginalStack.References.Count); + Assert.Contains("stack2", updatedOriginalStack.References); + Assert.True(updatedOriginalStack.OccurrencesAreCritical); + } + + [Fact] + public async Task WillMergeToStackWithMostEvents() { + var utcNow = SystemClock.UtcNow; + var originalStack = StackData.GenerateStack(); + originalStack.Id = ObjectId.GenerateNewId().ToString(); + originalStack.TotalOccurrences = 10; + var biggerStack = originalStack.DeepClone(); + biggerStack.Id = ObjectId.GenerateNewId().ToString(); + biggerStack.Status = StackStatus.Fixed; + biggerStack.TotalOccurrences = 100; + biggerStack.LastOccurrence = originalStack.LastOccurrence.AddMinutes(1); + biggerStack.SnoozeUntilUtc = originalStack.SnoozeUntilUtc = null; + biggerStack.DateFixed = biggerStack.LastOccurrence.AddMinutes(1); + biggerStack.Tags.Add("stack2"); + biggerStack.References.Add("stack2"); + biggerStack.OccurrencesAreCritical = true; + + originalStack = await _stackRepository.AddAsync(originalStack, o => o.ImmediateConsistency()); + biggerStack = await _stackRepository.AddAsync(biggerStack, o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync(EventData.GenerateEvents(count: 10, stackId: originalStack.Id), o => o.ImmediateConsistency()); + await _eventRepository.AddAsync(EventData.GenerateEvents(count: 100, stackId: biggerStack.Id), o => o.ImmediateConsistency()); + + var results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); + Assert.Equal(2, results.Total); + + var migration = GetService(); + var context = new MigrationContext(GetService(), _logger, CancellationToken.None); + await migration.RunAsync(context); + + await RefreshDataAsync(); + + results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); + Assert.Single(results.Documents); + + var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.True(updatedOriginalStack.IsDeleted); + var updatedBiggerStack = await _stackRepository.GetByIdAsync(biggerStack.Id, o => o.IncludeSoftDeletes()); + Assert.False(updatedBiggerStack.IsDeleted); + + Assert.Equal(originalStack.CreatedUtc, updatedBiggerStack.CreatedUtc); + Assert.Equal(110, updatedBiggerStack.TotalOccurrences); + Assert.Equal(StackStatus.Fixed, updatedBiggerStack.Status); + Assert.Equal(biggerStack.LastOccurrence, updatedBiggerStack.LastOccurrence); + Assert.Null(updatedBiggerStack.SnoozeUntilUtc); + Assert.Equal(biggerStack.DateFixed, updatedBiggerStack.DateFixed); + Assert.Equal(originalStack.Tags.Count + 1, updatedBiggerStack.Tags.Count); + Assert.Contains("stack2", updatedBiggerStack.Tags); + Assert.Equal(originalStack.References.Count + 1, updatedBiggerStack.References.Count); + Assert.Contains("stack2", updatedBiggerStack.References); + Assert.True(updatedBiggerStack.OccurrencesAreCritical); + } + + [Fact] + public async Task WillNotMergeDuplicatedDeletedStacks() { + var originalStack = StackData.GenerateStack(); + var duplicateStack = originalStack.DeepClone(); + duplicateStack.Id = ObjectId.GenerateNewId().ToString(); + duplicateStack.CreatedUtc = originalStack.CreatedUtc.AddMinutes(1); + duplicateStack.Status = StackStatus.Fixed; + duplicateStack.LastOccurrence = originalStack.LastOccurrence.AddMinutes(1); + duplicateStack.SnoozeUntilUtc = originalStack.SnoozeUntilUtc = null; + duplicateStack.DateFixed = duplicateStack.LastOccurrence.AddMinutes(1); + duplicateStack.UpdatedUtc = originalStack.UpdatedUtc.SubtractMinutes(1); + duplicateStack.Tags.Add("stack2"); + duplicateStack.References.Add("stack2"); + duplicateStack.OccurrencesAreCritical = true; + duplicateStack.IsDeleted = true; + + await _stackRepository.AddAsync(new[] { originalStack, duplicateStack }, o => o.ImmediateConsistency()); + + var results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); + Assert.Single(results.Documents); + + var migration = GetService(); + var context = new MigrationContext(GetService(), _logger, CancellationToken.None); + await migration.RunAsync(context); + + await RefreshDataAsync(); + + results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); + Assert.Single(results.Documents); + + var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.False(updatedOriginalStack.IsDeleted); + var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + Assert.True(updatedDuplicateStack.IsDeleted); + + Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); + Assert.Equal(originalStack.Status, updatedOriginalStack.Status); + Assert.Equal(originalStack.LastOccurrence, updatedOriginalStack.LastOccurrence); + Assert.Equal(originalStack.SnoozeUntilUtc, updatedOriginalStack.SnoozeUntilUtc); + Assert.Equal(originalStack.DateFixed, updatedOriginalStack.DateFixed); + Assert.Equal(originalStack.UpdatedUtc, updatedOriginalStack.UpdatedUtc); + Assert.Equal(originalStack.Tags.Count, updatedOriginalStack.Tags.Count); + Assert.DoesNotContain("stack2", updatedOriginalStack.Tags); + Assert.Equal(originalStack.References.Count, updatedOriginalStack.References.Count); + Assert.DoesNotContain("stack2", updatedOriginalStack.References); + Assert.False(updatedOriginalStack.OccurrencesAreCritical); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs index 84af721b22..774c6765ee 100644 --- a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs @@ -1,5 +1,3 @@ -using System.Threading; -using System.Threading.Tasks; using Exceptionless.Core; using Exceptionless.Core.Migrations; using Exceptionless.Core.Models; @@ -9,46 +7,45 @@ using Foundatio.Repositories; using Foundatio.Repositories.Migrations; using Foundatio.Utility; -using Microsoft.Extensions.DependencyInjection; using Nest; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Migrations { - public class SetStackDuplicateSignatureMigrationTests : TestWithServices { - private readonly IStackRepository _repository; - - public SetStackDuplicateSignatureMigrationTests(ITestOutputHelper output) : base(output) { - _repository = GetService(); - } - - protected override void RegisterServices(IServiceCollection services, AppOptions options) { - services.AddTransient(); - services.AddSingleton(new EmptyLock()); - base.RegisterServices(services, options); - } - - [Fact] - public async Task WillSetStackDuplicateSignature() { - var stack = StackData.GenerateStack(); - stack.DuplicateSignature = null; - stack = await _repository.AddAsync(stack, o => o.ImmediateConsistency()); - Assert.NotEmpty(stack.ProjectId); - Assert.NotEmpty(stack.SignatureHash); - Assert.Null(stack.DuplicateSignature); - - var migration = GetService(); - var context = new MigrationContext(GetService(), _logger, CancellationToken.None); - await migration.RunAsync(context); - - string expectedDuplicateSignature = $"{stack.ProjectId}:{stack.SignatureHash}"; - var actualStack = await _repository.GetByIdAsync(stack.Id); - Assert.NotEmpty(actualStack.ProjectId); - Assert.NotEmpty(actualStack.SignatureHash); - Assert.Equal($"{actualStack.ProjectId}:{actualStack.SignatureHash}", actualStack.DuplicateSignature); - - var results = await _repository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, expectedDuplicateSignature))); - Assert.Single(results.Documents); - } +namespace Exceptionless.Tests.Migrations; + +public class SetStackDuplicateSignatureMigrationTests : TestWithServices { + private readonly IStackRepository _repository; + + public SetStackDuplicateSignatureMigrationTests(ITestOutputHelper output) : base(output) { + _repository = GetService(); + } + + protected override void RegisterServices(IServiceCollection services, AppOptions options) { + services.AddTransient(); + services.AddSingleton(new EmptyLock()); + base.RegisterServices(services, options); + } + + [Fact] + public async Task WillSetStackDuplicateSignature() { + var stack = StackData.GenerateStack(); + stack.DuplicateSignature = null; + stack = await _repository.AddAsync(stack, o => o.ImmediateConsistency()); + Assert.NotEmpty(stack.ProjectId); + Assert.NotEmpty(stack.SignatureHash); + Assert.Null(stack.DuplicateSignature); + + var migration = GetService(); + var context = new MigrationContext(GetService(), _logger, CancellationToken.None); + await migration.RunAsync(context); + + string expectedDuplicateSignature = $"{stack.ProjectId}:{stack.SignatureHash}"; + var actualStack = await _repository.GetByIdAsync(stack.Id); + Assert.NotEmpty(actualStack.ProjectId); + Assert.NotEmpty(actualStack.SignatureHash); + Assert.Equal($"{actualStack.ProjectId}:{actualStack.SignatureHash}", actualStack.DuplicateSignature); + + var results = await _repository.FindAsync(q => q.ElasticFilter(Query.Term(s => s.DuplicateSignature, expectedDuplicateSignature))); + Assert.Single(results.Documents); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Miscellaneous/DeltaTests.cs b/tests/Exceptionless.Tests/Miscellaneous/DeltaTests.cs index 826dee835a..a10dc59abb 100644 --- a/tests/Exceptionless.Tests/Miscellaneous/DeltaTests.cs +++ b/tests/Exceptionless.Tests/Miscellaneous/DeltaTests.cs @@ -1,35 +1,35 @@ using Exceptionless.Web.Utility; using Xunit; -namespace Exceptionless.Tests.Miscellaneous { - public class DeltaTests { - [Fact] - public void CanSetUnknownProperties() { - dynamic delta = new Delta(); - delta.Data = "Blah"; - delta.SomeUnknown = "yes"; - Assert.Equal(1, delta.UnknownProperties.Count); - } +namespace Exceptionless.Tests.Miscellaneous; - [Fact] - public void CanPatchUnrelatedTypes() { - dynamic delta = new Delta(); - delta.Data = "Blah"; +public class DeltaTests { + [Fact] + public void CanSetUnknownProperties() { + dynamic delta = new Delta(); + delta.Data = "Blah"; + delta.SomeUnknown = "yes"; + Assert.Equal(1, delta.UnknownProperties.Count); + } + + [Fact] + public void CanPatchUnrelatedTypes() { + dynamic delta = new Delta(); + delta.Data = "Blah"; - var msg = new SimpleMessageB { - Data = "Blah2" - }; - delta.Patch(msg); + var msg = new SimpleMessageB { + Data = "Blah2" + }; + delta.Patch(msg); - Assert.Equal(delta.Data, msg.Data); - } + Assert.Equal(delta.Data, msg.Data); + } - public class SimpleMessageA { - public string Data { get; set; } - } + public class SimpleMessageA { + public string Data { get; set; } + } - public class SimpleMessageB { - public string Data { get; set; } - } + public class SimpleMessageB { + public string Data { get; set; } } } diff --git a/tests/Exceptionless.Tests/Miscellaneous/EfficientPagingTests.cs b/tests/Exceptionless.Tests/Miscellaneous/EfficientPagingTests.cs index 77069b5f5b..2309924e69 100644 --- a/tests/Exceptionless.Tests/Miscellaneous/EfficientPagingTests.cs +++ b/tests/Exceptionless.Tests/Miscellaneous/EfficientPagingTests.cs @@ -1,63 +1,60 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Web.Utility.Results; +using Exceptionless.Web.Utility.Results; using Xunit; using Xunit.Abstractions; -using Microsoft.Extensions.Logging; - -namespace Exceptionless.Tests.Miscellaneous { - public class EfficientPagingTests : TestWithServices { - public EfficientPagingTests(ITestOutputHelper output) : base(output) {} - - [Theory] - [InlineData("http://localhost", false, false, false)] - [InlineData("http://localhost", true, false, true)] - [InlineData("http://localhost?after=1", false, true, false)] - [InlineData("http://localhost?after=1", true, true, true)] - [InlineData("http://localhost?before=11", false, false, true)] - [InlineData("http://localhost?before=11", true, true, true)] - public void CanBeforeAndAfterLinks(string url, bool hasMore, bool expectPrevious, bool expectNext){ - var data = new List { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" }; - - var links = OkWithResourceLinks.GetBeforeAndAfterLinks(new Uri(url), data, false, hasMore, s => s); - int expectedLinkCount = 0; - if (expectPrevious) - expectedLinkCount++; - if (expectNext) - expectedLinkCount++; - - foreach (string link in links) - _logger.LogInformation(link); - - Assert.Equal(expectedLinkCount, links.Count); - if (expectPrevious) - Assert.Contains(links, l => l.Contains("previous")); - if (expectNext) - Assert.Contains(links, l => l.Contains("next")); - } - - [Theory] - [InlineData("http://localhost", 0, false, false, false)] - [InlineData("http://localhost", 1, false, false, false)] - [InlineData("http://localhost", 2, false, true, false)] - [InlineData("http://localhost", 2, true, true, true)] - public void CanPageLinks(string url, int pageNumber, bool hasMore, bool expectPrevious, bool expectNext){ - var links = OkWithResourceLinks.GetPagedLinks(new Uri(url), pageNumber, hasMore); - - int expectedLinkCount = 0; - if (expectPrevious) - expectedLinkCount++; - if (expectNext) - expectedLinkCount++; - - foreach (string link in links) - _logger.LogInformation(link); - - Assert.Equal(expectedLinkCount, links.Count); - if (expectPrevious) - Assert.Contains(links, l => l.Contains("previous")); - if (expectNext) - Assert.Contains(links, l => l.Contains("next")); - } + +namespace Exceptionless.Tests.Miscellaneous; + +public class EfficientPagingTests : TestWithServices { + public EfficientPagingTests(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData("http://localhost", false, false, false)] + [InlineData("http://localhost", true, false, true)] + [InlineData("http://localhost?after=1", false, true, false)] + [InlineData("http://localhost?after=1", true, true, true)] + [InlineData("http://localhost?before=11", false, false, true)] + [InlineData("http://localhost?before=11", true, true, true)] + public void CanBeforeAndAfterLinks(string url, bool hasMore, bool expectPrevious, bool expectNext) { + var data = new List { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11" }; + + var links = OkWithResourceLinks.GetBeforeAndAfterLinks(new Uri(url), data, false, hasMore, s => s); + int expectedLinkCount = 0; + if (expectPrevious) + expectedLinkCount++; + if (expectNext) + expectedLinkCount++; + + foreach (string link in links) + _logger.LogInformation(link); + + Assert.Equal(expectedLinkCount, links.Count); + if (expectPrevious) + Assert.Contains(links, l => l.Contains("previous")); + if (expectNext) + Assert.Contains(links, l => l.Contains("next")); + } + + [Theory] + [InlineData("http://localhost", 0, false, false, false)] + [InlineData("http://localhost", 1, false, false, false)] + [InlineData("http://localhost", 2, false, true, false)] + [InlineData("http://localhost", 2, true, true, true)] + public void CanPageLinks(string url, int pageNumber, bool hasMore, bool expectPrevious, bool expectNext) { + var links = OkWithResourceLinks.GetPagedLinks(new Uri(url), pageNumber, hasMore); + + int expectedLinkCount = 0; + if (expectPrevious) + expectedLinkCount++; + if (expectNext) + expectedLinkCount++; + + foreach (string link in links) + _logger.LogInformation(link); + + Assert.Equal(expectedLinkCount, links.Count); + if (expectPrevious) + Assert.Contains(links, l => l.Contains("previous")); + if (expectNext) + Assert.Contains(links, l => l.Contains("next")); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs index b00184aac4..bcd089cabc 100644 --- a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs +++ b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Pipeline; @@ -23,170 +18,170 @@ using Foundatio.Storage; using Foundatio.Utility; using McSherry.SemanticVersioning; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; using DataDictionary = Exceptionless.Core.Models.DataDictionary; -namespace Exceptionless.Tests.Pipeline { - public sealed class EventPipelineTests : IntegrationTestsBase { - private readonly EventPipeline _pipeline; - private readonly IEventRepository _eventRepository; - private readonly IStackRepository _stackRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly IUserRepository _userRepository; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - - public EventPipelineTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _eventRepository = GetService(); - _stackRepository = GetService(); - _organizationRepository = GetService(); - _projectRepository = GetService(); - _userRepository = GetService(); - _pipeline = GetService(); - _billingManager = GetService(); - _plans = GetService(); - } +namespace Exceptionless.Tests.Pipeline; + +public sealed class EventPipelineTests : IntegrationTestsBase { + private readonly EventPipeline _pipeline; + private readonly IEventRepository _eventRepository; + private readonly IStackRepository _stackRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IUserRepository _userRepository; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + + public EventPipelineTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _eventRepository = GetService(); + _stackRepository = GetService(); + _organizationRepository = GetService(); + _projectRepository = GetService(); + _userRepository = GetService(); + _pipeline = GetService(); + _billingManager = GetService(); + _plans = GetService(); + } - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await CreateProjectDataAsync(); - } + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await CreateProjectDataAsync(); + } - [Fact] - public async Task NoFutureEventsAsync() { - var localTime = SystemClock.UtcNow; - var ev = GenerateEvent(localTime.AddMinutes(10)); + [Fact] + public async Task NoFutureEventsAsync() { + var localTime = SystemClock.UtcNow; + var ev = GenerateEvent(localTime.AddMinutes(10)); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); - ev = await _eventRepository.GetByIdAsync(ev.Id); - Assert.NotNull(ev); - Assert.True(ev.Date < localTime.AddMinutes(10)); - Assert.True(ev.Date - localTime < TimeSpan.FromSeconds(5)); - } + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); + Assert.True(ev.Date < localTime.AddMinutes(10)); + Assert.True(ev.Date - localTime < TimeSpan.FromSeconds(5)); + } - [Fact] - public Task CreateAutoSessionAsync() { - return CreateAutoSessionInternalAsync(SystemClock.OffsetNow); - } + [Fact] + public Task CreateAutoSessionAsync() { + return CreateAutoSessionInternalAsync(SystemClock.OffsetNow); + } - private async Task CreateAutoSessionInternalAsync(DateTimeOffset date) { - var ev = GenerateEvent(date, "blake@exceptionless.io"); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - Assert.False(context.IsCancelled); - Assert.True(context.IsProcessed); - - await RefreshDataAsync(); - var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(2, events.Total); - Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - - var sessionStart = events.Documents.First(e => e.IsSessionStart()); - Assert.Equal(0, sessionStart.Value); - Assert.False(sessionStart.HasSessionEndTime()); - } + private async Task CreateAutoSessionInternalAsync(DateTimeOffset date) { + var ev = GenerateEvent(date, "blake@exceptionless.io"); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + Assert.False(context.IsCancelled); + Assert.True(context.IsProcessed); + + await RefreshDataAsync(); + var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(2, events.Total); + Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + + var sessionStart = events.Documents.First(e => e.IsSessionStart()); + Assert.Equal(0, sessionStart.Value); + Assert.False(sessionStart.HasSessionEndTime()); + } - [Fact] - public async Task CanUpdateExistingAutoSessionAsync() { - var startDate = SystemClock.OffsetNow.SubtractMinutes(5); - await CreateAutoSessionInternalAsync(startDate); + [Fact] + public async Task CanUpdateExistingAutoSessionAsync() { + var startDate = SystemClock.OffsetNow.SubtractMinutes(5); + await CreateAutoSessionInternalAsync(startDate); - var ev = GenerateEvent(startDate.AddMinutes(4), "blake@exceptionless.io"); + var ev = GenerateEvent(startDate.AddMinutes(4), "blake@exceptionless.io"); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - Assert.False(context.IsCancelled); - Assert.True(context.IsProcessed); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + Assert.False(context.IsCancelled); + Assert.True(context.IsProcessed); - await RefreshDataAsync(); - var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(3, events.Total); - Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + await RefreshDataAsync(); + var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(3, events.Total); + Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - var sessionStart = events.Documents.Single(e => e.IsSessionStart()); - Assert.Equal(240, sessionStart.Value); - Assert.False(sessionStart.HasSessionEndTime()); - } + var sessionStart = events.Documents.Single(e => e.IsSessionStart()); + Assert.Equal(240, sessionStart.Value); + Assert.False(sessionStart.HasSessionEndTime()); + } - [Fact] - public async Task IgnoreAutoSessionsWithoutIdentityAsync() { - var ev = GenerateEvent(SystemClock.OffsetNow); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - Assert.False(context.IsCancelled); - Assert.True(context.IsProcessed); - - await RefreshDataAsync(); - var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(1, events.Total); - Assert.Equal(0, events.Documents.Count(e => e.IsSessionStart())); - Assert.Empty(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - } + [Fact] + public async Task IgnoreAutoSessionsWithoutIdentityAsync() { + var ev = GenerateEvent(SystemClock.OffsetNow); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + Assert.False(context.IsCancelled); + Assert.True(context.IsProcessed); + + await RefreshDataAsync(); + var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(1, events.Total); + Assert.Equal(0, events.Documents.Count(e => e.IsSessionStart())); + Assert.Empty(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + } - [Fact] - public async Task CreateAutoSessionStartEventsAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task CreateAutoSessionStartEventsAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate, "blake@exceptionless.io"), GenerateEvent(firstEventDate.AddSeconds(10), "blake@exceptionless.io", Event.KnownTypes.SessionEnd), GenerateEvent(firstEventDate.AddSeconds(20), "blake@exceptionless.io"), GenerateEvent(firstEventDate.AddSeconds(30), "blake@exceptionless.io", Event.KnownTypes.SessionEnd), }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.DoesNotContain(contexts, c => c.IsCancelled); - Assert.Contains(contexts, c => c.IsProcessed); - - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(6, results.Total); - Assert.Equal(2, results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); - Assert.Equal(2, results.Documents.Count(e => e.IsSessionEnd())); - - var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Equal(2, sessionStarts.Count); - foreach (var sessionStart in sessionStarts) { - Assert.Equal(10, sessionStart.Value); - Assert.True(sessionStart.HasSessionEndTime()); - } + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.DoesNotContain(contexts, c => c.IsCancelled); + Assert.Contains(contexts, c => c.IsProcessed); + + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(6, results.Total); + Assert.Equal(2, results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); + Assert.Equal(2, results.Documents.Count(e => e.IsSessionEnd())); + + var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Equal(2, sessionStarts.Count); + foreach (var sessionStart in sessionStarts) { + Assert.Equal(10, sessionStart.Value); + Assert.True(sessionStart.HasSessionEndTime()); } + } - [Fact] - public async Task UpdateAutoMultipleSessionStartEventDurationsAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task UpdateAutoMultipleSessionStartEventDurationsAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate, "blake@exceptionless.io", Event.KnownTypes.Session), GenerateEvent(firstEventDate.AddSeconds(10), "blake@exceptionless.io", Event.KnownTypes.Session), }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled)); - Assert.Contains(contexts, c => c.IsProcessed); + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled)); + Assert.Contains(contexts, c => c.IsProcessed); - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(1, results.Total); - Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - Assert.Equal(0, results.Documents.Count(e => e.IsSessionEnd())); + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(1, results.Total); + Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + Assert.Equal(0, results.Documents.Count(e => e.IsSessionEnd())); - var sessionStart = results.Documents.Single(e => e.IsSessionStart()); - Assert.Equal(10, sessionStart.Value); - Assert.False(sessionStart.HasSessionEndTime()); - } + var sessionStart = results.Documents.Single(e => e.IsSessionStart()); + Assert.Equal(10, sessionStart.Value); + Assert.False(sessionStart.HasSessionEndTime()); + } - [Fact] - public async Task UpdateAutoSessionLastActivityAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var lastEventDate = firstEventDate.Add(TimeSpan.FromMinutes(1)); + [Fact] + public async Task UpdateAutoSessionLastActivityAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var lastEventDate = firstEventDate.Add(TimeSpan.FromMinutes(1)); - var events = new List { + var events = new List { GenerateEvent(firstEventDate, "blake@exceptionless.io"), GenerateEvent(firstEventDate.AddSeconds(10), "blake@exceptionless.io"), GenerateEvent(lastEventDate, "blake@exceptionless.io"), @@ -197,190 +192,190 @@ public async Task UpdateAutoSessionLastActivityAsync() { GenerateEvent(lastEventDate, "eric@exceptionless.io") }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.DoesNotContain(contexts, c => c.IsCancelled); - Assert.Contains(contexts, c => c.IsProcessed); - - await RefreshDataAsync(); - var results = await _eventRepository.GetAllAsync(o => o.PageLimit(15)); - Assert.Equal(9, results.Total); - Assert.Equal(2, results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd() && e.GetUserIdentity()?.Identity == "blake@exceptionless.io")); - Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId()) && e.GetUserIdentity().Identity == "eric@exceptionless.io").Select(e => e.GetSessionId()).Distinct()); - Assert.Equal(1, results.Documents.Count(e => String.IsNullOrEmpty(e.GetSessionId()))); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); - - var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Equal(2, sessionStarts.Count); - - var firstUserSessionStartEvents = sessionStarts.Single(e => e.GetUserIdentity().Identity == "blake@exceptionless.io"); - Assert.Equal((decimal)(lastEventDate - firstEventDate).TotalSeconds, firstUserSessionStartEvents.Value); - Assert.True(firstUserSessionStartEvents.HasSessionEndTime()); - - var secondUserSessionStartEvents = sessionStarts.Single(e => e.GetUserIdentity().Identity == "eric@exceptionless.io"); - Assert.Equal((decimal)(lastEventDate - firstEventDate).TotalSeconds, secondUserSessionStartEvents.Value); - Assert.False(secondUserSessionStartEvents.HasSessionEndTime()); - } + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.DoesNotContain(contexts, c => c.IsCancelled); + Assert.Contains(contexts, c => c.IsProcessed); + + await RefreshDataAsync(); + var results = await _eventRepository.GetAllAsync(o => o.PageLimit(15)); + Assert.Equal(9, results.Total); + Assert.Equal(2, results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd() && e.GetUserIdentity()?.Identity == "blake@exceptionless.io")); + Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId()) && e.GetUserIdentity().Identity == "eric@exceptionless.io").Select(e => e.GetSessionId()).Distinct()); + Assert.Equal(1, results.Documents.Count(e => String.IsNullOrEmpty(e.GetSessionId()))); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); + + var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Equal(2, sessionStarts.Count); + + var firstUserSessionStartEvents = sessionStarts.Single(e => e.GetUserIdentity().Identity == "blake@exceptionless.io"); + Assert.Equal((decimal)(lastEventDate - firstEventDate).TotalSeconds, firstUserSessionStartEvents.Value); + Assert.True(firstUserSessionStartEvents.HasSessionEndTime()); + + var secondUserSessionStartEvents = sessionStarts.Single(e => e.GetUserIdentity().Identity == "eric@exceptionless.io"); + Assert.Equal((decimal)(lastEventDate - firstEventDate).TotalSeconds, secondUserSessionStartEvents.Value); + Assert.False(secondUserSessionStartEvents.HasSessionEndTime()); + } - [Fact] - public async Task CloseExistingAutoSessionAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - string identity = "blake@exceptionless.io"; - var events = new List { + [Fact] + public async Task CloseExistingAutoSessionAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + string identity = "blake@exceptionless.io"; + var events = new List { GenerateEvent(firstEventDate, identity), GenerateEvent(firstEventDate.AddSeconds(10), identity, Event.KnownTypes.SessionHeartbeat) }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); - Assert.Contains(contexts, c => c.IsProcessed); - - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(2, results.Total); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionStart())); - - events = new List { + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); + Assert.Contains(contexts, c => c.IsProcessed); + + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(2, results.Total); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionStart())); + + events = new List { GenerateEvent(firstEventDate.AddSeconds(10), identity, Event.KnownTypes.Session), GenerateEvent(firstEventDate.AddSeconds(20), identity, Event.KnownTypes.SessionEnd) }; - await RefreshDataAsync(); - contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled)); - Assert.Equal(1, contexts.Count(c => c.IsProcessed)); - - await RefreshDataAsync(); - results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(3, results.Total); - var sessionIds = results.Documents.Select(e => e.GetSessionId()).Distinct(); - Assert.Single(sessionIds); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); - - var sessionStart = results.Documents.Single(e => e.IsSessionStart()); - Assert.Equal(20, sessionStart.Value); - Assert.True(sessionStart.HasSessionEndTime()); - } + await RefreshDataAsync(); + contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled)); + Assert.Equal(1, contexts.Count(c => c.IsProcessed)); + + await RefreshDataAsync(); + results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(3, results.Total); + var sessionIds = results.Documents.Select(e => e.GetSessionId()).Distinct(); + Assert.Single(sessionIds); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); + + var sessionStart = results.Documents.Single(e => e.IsSessionStart()); + Assert.Equal(20, sessionStart.Value); + Assert.True(sessionStart.HasSessionEndTime()); + } - [Fact] - public async Task IgnoreDuplicateAutoEndSessionsAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task IgnoreDuplicateAutoEndSessionsAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate, "blake@exceptionless.io", Event.KnownTypes.SessionEnd), GenerateEvent(firstEventDate.AddSeconds(10), "blake@exceptionless.io", Event.KnownTypes.SessionEnd) }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(2, contexts.Count(c => c.IsCancelled)); - Assert.False(contexts.All(c => c.IsProcessed)); + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(2, contexts.Count(c => c.IsCancelled)); + Assert.False(contexts.All(c => c.IsProcessed)); - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(0, results.Total); - } + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(0, results.Total); + } - [Fact] - public async Task WillMarkAutoSessionHeartbeatStackHiddenAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task WillMarkAutoSessionHeartbeatStackHiddenAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate.AddSeconds(10), "blake@exceptionless.io", Event.KnownTypes.SessionHeartbeat) }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); - Assert.Equal(0, contexts.Count(c => c.IsProcessed)); + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); + Assert.Equal(0, contexts.Count(c => c.IsProcessed)); - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(1, results.Total); - var sessionStart = results.Documents.FirstOrDefault(e => e.IsSessionStart()); - Assert.NotNull(sessionStart); + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(1, results.Total); + var sessionStart = results.Documents.FirstOrDefault(e => e.IsSessionStart()); + Assert.NotNull(sessionStart); - var stack = await _stackRepository.GetByIdAsync(sessionStart.StackId); - Assert.NotNull(stack); - Assert.True(stack.Status == StackStatus.Ignored); - } + var stack = await _stackRepository.GetByIdAsync(sessionStart.StackId); + Assert.NotNull(stack); + Assert.True(stack.Status == StackStatus.Ignored); + } - [Fact] - public Task CreateManualSessionAsync() { - return CreateManualSessionInternalAsync(SystemClock.OffsetNow); - } + [Fact] + public Task CreateManualSessionAsync() { + return CreateManualSessionInternalAsync(SystemClock.OffsetNow); + } - private async Task CreateManualSessionInternalAsync(DateTimeOffset start) { - var ev = GenerateEvent(start, sessionId: "12345678"); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - Assert.False(context.IsCancelled); - Assert.True(context.IsProcessed); - - await RefreshDataAsync(); - var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(2, events.Total); - Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - - var sessionStartEvent = events.Documents.SingleOrDefault(e => e.IsSessionStart()); - Assert.NotNull(sessionStartEvent); - Assert.Equal(0, sessionStartEvent.Value); - Assert.False(sessionStartEvent.HasSessionEndTime()); - } + private async Task CreateManualSessionInternalAsync(DateTimeOffset start) { + var ev = GenerateEvent(start, sessionId: "12345678"); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + Assert.False(context.IsCancelled); + Assert.True(context.IsProcessed); + + await RefreshDataAsync(); + var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(2, events.Total); + Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + + var sessionStartEvent = events.Documents.SingleOrDefault(e => e.IsSessionStart()); + Assert.NotNull(sessionStartEvent); + Assert.Equal(0, sessionStartEvent.Value); + Assert.False(sessionStartEvent.HasSessionEndTime()); + } - [Fact] - public async Task CanUpdateExistingManualSessionAsync() { - var startDate = SystemClock.OffsetNow.SubtractMinutes(5); - await CreateManualSessionInternalAsync(startDate); + [Fact] + public async Task CanUpdateExistingManualSessionAsync() { + var startDate = SystemClock.OffsetNow.SubtractMinutes(5); + await CreateManualSessionInternalAsync(startDate); - var ev = GenerateEvent(startDate.AddMinutes(4), sessionId: "12345678"); + var ev = GenerateEvent(startDate.AddMinutes(4), sessionId: "12345678"); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - Assert.False(context.IsCancelled); - Assert.True(context.IsProcessed); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + Assert.False(context.IsCancelled); + Assert.True(context.IsProcessed); - await RefreshDataAsync(); - var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(3, events.Total); - Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + await RefreshDataAsync(); + var events = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(3, events.Total); + Assert.Single(events.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - var sessionStart = events.Documents.First(e => e.IsSessionStart()); - Assert.Equal(240, sessionStart.Value); - Assert.False(sessionStart.HasSessionEndTime()); - } + var sessionStart = events.Documents.First(e => e.IsSessionStart()); + Assert.Equal(240, sessionStart.Value); + Assert.False(sessionStart.HasSessionEndTime()); + } - [Fact] - public async Task CreateManualSingleSessionStartEventAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task CreateManualSingleSessionStartEventAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate, sessionId: "12345678"), GenerateEvent(firstEventDate.AddSeconds(10), type: Event.KnownTypes.Session, sessionId: "12345678"), GenerateEvent(firstEventDate.AddSeconds(20), type: Event.KnownTypes.SessionEnd, sessionId: "12345678"), GenerateEvent(firstEventDate.AddSeconds(30), sessionId: "12345678"), }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.DoesNotContain(contexts, c => c.IsCancelled); - Assert.Contains(contexts, c => c.IsProcessed); + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.DoesNotContain(contexts, c => c.IsCancelled); + Assert.Contains(contexts, c => c.IsProcessed); - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(4, results.Total); - Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(4, results.Total); + Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - var sessionStartEvent = results.Documents.SingleOrDefault(e => e.IsSessionStart()); - Assert.NotNull(sessionStartEvent); - Assert.Equal(30, sessionStartEvent.Value); - Assert.True(sessionStartEvent.HasSessionEndTime()); - } + var sessionStartEvent = results.Documents.SingleOrDefault(e => e.IsSessionStart()); + Assert.NotNull(sessionStartEvent); + Assert.Equal(30, sessionStartEvent.Value); + Assert.True(sessionStartEvent.HasSessionEndTime()); + } - [Fact] - public async Task CreateManualSessionStartEventAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task CreateManualSessionStartEventAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate, sessionId: "12345678"), // This event will be deduplicated as part of the manual session plugin. GenerateEvent(firstEventDate.AddSeconds(10), type: Event.KnownTypes.SessionEnd, sessionId: "12345678"), @@ -388,274 +383,274 @@ public async Task CreateManualSessionStartEventAsync() { GenerateEvent(firstEventDate.AddSeconds(30), type: Event.KnownTypes.SessionEnd, sessionId: "12345678"), }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled)); - Assert.Equal(3, contexts.Count(c => c.IsProcessed)); - - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(4, results.Total); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionStart())); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); - - var sessionStartEvent = results.Documents.First(e => e.IsSessionStart()); - Assert.NotNull(sessionStartEvent); - Assert.Equal(30, sessionStartEvent.Value); - Assert.True(sessionStartEvent.HasSessionEndTime()); - } + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled)); + Assert.Equal(3, contexts.Count(c => c.IsProcessed)); + + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(4, results.Total); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionStart())); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); + + var sessionStartEvent = results.Documents.First(e => e.IsSessionStart()); + Assert.NotNull(sessionStartEvent); + Assert.Equal(30, sessionStartEvent.Value); + Assert.True(sessionStartEvent.HasSessionEndTime()); + } - [Fact] - public async Task UpdateManualSessionLastActivityAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var lastEventDate = firstEventDate.Add(TimeSpan.FromMinutes(1)); + [Fact] + public async Task UpdateManualSessionLastActivityAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var lastEventDate = firstEventDate.Add(TimeSpan.FromMinutes(1)); - var events = new List { + var events = new List { GenerateEvent(firstEventDate, type: Event.KnownTypes.Session, sessionId: "12345678"), GenerateEvent(firstEventDate.AddSeconds(10), sessionId: "12345678"), GenerateEvent(lastEventDate, type: Event.KnownTypes.SessionEnd, sessionId: "12345678") }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.DoesNotContain(contexts, c => c.IsCancelled); - Assert.Contains(contexts, c => c.IsProcessed); - - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(3, results.Total); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionStart())); - Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); - Assert.Equal((decimal)(lastEventDate - firstEventDate).TotalSeconds, results.Documents.First(e => e.IsSessionStart()).Value); - } + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.DoesNotContain(contexts, c => c.IsCancelled); + Assert.Contains(contexts, c => c.IsProcessed); + + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(3, results.Total); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionStart())); + Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); + Assert.Equal((decimal)(lastEventDate - firstEventDate).TotalSeconds, results.Documents.First(e => e.IsSessionStart()).Value); + } - [Fact] - public async Task CloseExistingManualSessionAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task CloseExistingManualSessionAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate, sessionId: "12345678"), GenerateEvent(firstEventDate.AddSeconds(10), type: Event.KnownTypes.SessionHeartbeat, sessionId: "12345678") }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); - Assert.Contains(contexts, c => c.IsProcessed); + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); + Assert.Contains(contexts, c => c.IsProcessed); - events = new List { + events = new List { GenerateEvent(firstEventDate.AddSeconds(10), type: Event.KnownTypes.Session, sessionId: "12345678"), GenerateEvent(firstEventDate.AddSeconds(20), type: Event.KnownTypes.SessionEnd, sessionId: "12345678") }; - await RefreshDataAsync(); - contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled)); - Assert.Contains(contexts, c => c.IsProcessed); - - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(3, results.Total); - Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); - Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); - - var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Single(sessionStarts); - foreach (var sessionStart in sessionStarts) { - Assert.Equal(20, sessionStart.Value); - Assert.True(sessionStart.HasSessionEndTime()); - } + await RefreshDataAsync(); + contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled)); + Assert.Contains(contexts, c => c.IsProcessed); + + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(3, results.Total); + Assert.Single(results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct()); + Assert.Equal(1, results.Documents.Count(e => e.IsSessionEnd())); + + var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Single(sessionStarts); + foreach (var sessionStart in sessionStarts) { + Assert.Equal(20, sessionStart.Value); + Assert.True(sessionStart.HasSessionEndTime()); } + } - [Fact] - public async Task IgnoreDuplicateManualEndSessionsAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task IgnoreDuplicateManualEndSessionsAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate, type: Event.KnownTypes.SessionEnd, sessionId: "12345678"), GenerateEvent(firstEventDate.AddSeconds(10), type: Event.KnownTypes.SessionEnd, sessionId: "12345678") }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(2, contexts.Count(c => c.IsCancelled)); - Assert.False(contexts.All(c => c.IsProcessed)); + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(2, contexts.Count(c => c.IsCancelled)); + Assert.False(contexts.All(c => c.IsProcessed)); - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(0, results.Total); - } + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(0, results.Total); + } - [Fact] - public async Task WillMarkManualSessionHeartbeatStackHiddenAsync() { - var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); - var events = new List { + [Fact] + public async Task WillMarkManualSessionHeartbeatStackHiddenAsync() { + var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); + var events = new List { GenerateEvent(firstEventDate.AddSeconds(10), type: Event.KnownTypes.SessionHeartbeat, sessionId: "12345678") }; - var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.DoesNotContain(contexts, c => c.HasError); - Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); - Assert.Equal(0, contexts.Count(c => c.IsProcessed)); + var contexts = await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.DoesNotContain(contexts, c => c.HasError); + Assert.Equal(1, contexts.Count(c => c.IsCancelled && c.IsDiscarded)); + Assert.Equal(0, contexts.Count(c => c.IsProcessed)); - await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(1, results.Total); - var sessionStart = results.Documents.FirstOrDefault(e => e.IsSessionStart()); - Assert.NotNull(sessionStart); + await RefreshDataAsync(); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(1, results.Total); + var sessionStart = results.Documents.FirstOrDefault(e => e.IsSessionStart()); + Assert.NotNull(sessionStart); - var stack = await _stackRepository.GetByIdAsync(sessionStart.StackId); - Assert.NotNull(stack); - Assert.True(stack.Status == StackStatus.Ignored); - } + var stack = await _stackRepository.GetByIdAsync(sessionStart.StackId); + Assert.NotNull(stack); + Assert.True(stack.Status == StackStatus.Ignored); + } - [Fact] - public void CanIndexExtendedData() { - var ev = EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: SystemClock.UtcNow); - ev.Data.Add("First Name", "Eric"); // invalid field name - ev.Data.Add("IsVerified", true); - ev.Data.Add("IsVerified1", true.ToString()); - ev.Data.Add("Age", Int32.MaxValue); - ev.Data.Add("Age1", Int32.MaxValue.ToString(CultureInfo.InvariantCulture)); - ev.Data.Add("AgeDec", Decimal.MaxValue); - ev.Data.Add("AgeDec1", Decimal.MaxValue.ToString(CultureInfo.InvariantCulture)); - ev.Data.Add("AgeDbl", Double.MaxValue); - ev.Data.Add("AgeDbl1", Double.MaxValue.ToString("r", CultureInfo.InvariantCulture)); - ev.Data.Add(" Birthday ", DateTime.MinValue); - ev.Data.Add("BirthdayWithOffset", DateTimeOffset.MinValue); - ev.Data.Add("@excluded", DateTime.MinValue); - ev.Data.Add("Address", new { State = "Texas" }); - ev.SetSessionId("123456789"); - - ev.CopyDataToIndex(Array.Empty()); - - Assert.False(ev.Idx.ContainsKey("first-name-s")); - Assert.True(ev.Idx.ContainsKey("isverified-b")); - Assert.True(ev.Idx.ContainsKey("isverified1-b")); - Assert.True(ev.Idx.ContainsKey("age-n")); - Assert.True(ev.Idx.ContainsKey("age1-n")); - Assert.True(ev.Idx.ContainsKey("agedec-n")); - Assert.True(ev.Idx.ContainsKey("agedec1-n")); - Assert.True(ev.Idx.ContainsKey("agedbl-n")); - Assert.True(ev.Idx.ContainsKey("agedbl1-n")); - Assert.True(ev.Idx.ContainsKey("birthday-d")); - Assert.True(ev.Idx.ContainsKey("birthdaywithoffset-d")); - Assert.True(ev.Idx.ContainsKey("session-r")); - Assert.Equal(11, ev.Idx.Count); - } + [Fact] + public void CanIndexExtendedData() { + var ev = EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: SystemClock.UtcNow); + ev.Data.Add("First Name", "Eric"); // invalid field name + ev.Data.Add("IsVerified", true); + ev.Data.Add("IsVerified1", true.ToString()); + ev.Data.Add("Age", Int32.MaxValue); + ev.Data.Add("Age1", Int32.MaxValue.ToString(CultureInfo.InvariantCulture)); + ev.Data.Add("AgeDec", Decimal.MaxValue); + ev.Data.Add("AgeDec1", Decimal.MaxValue.ToString(CultureInfo.InvariantCulture)); + ev.Data.Add("AgeDbl", Double.MaxValue); + ev.Data.Add("AgeDbl1", Double.MaxValue.ToString("r", CultureInfo.InvariantCulture)); + ev.Data.Add(" Birthday ", DateTime.MinValue); + ev.Data.Add("BirthdayWithOffset", DateTimeOffset.MinValue); + ev.Data.Add("@excluded", DateTime.MinValue); + ev.Data.Add("Address", new { State = "Texas" }); + ev.SetSessionId("123456789"); + + ev.CopyDataToIndex(Array.Empty()); + + Assert.False(ev.Idx.ContainsKey("first-name-s")); + Assert.True(ev.Idx.ContainsKey("isverified-b")); + Assert.True(ev.Idx.ContainsKey("isverified1-b")); + Assert.True(ev.Idx.ContainsKey("age-n")); + Assert.True(ev.Idx.ContainsKey("age1-n")); + Assert.True(ev.Idx.ContainsKey("agedec-n")); + Assert.True(ev.Idx.ContainsKey("agedec1-n")); + Assert.True(ev.Idx.ContainsKey("agedbl-n")); + Assert.True(ev.Idx.ContainsKey("agedbl1-n")); + Assert.True(ev.Idx.ContainsKey("birthday-d")); + Assert.True(ev.Idx.ContainsKey("birthdaywithoffset-d")); + Assert.True(ev.Idx.ContainsKey("session-r")); + Assert.Equal(11, ev.Idx.Count); + } - [Fact] - public async Task SyncStackTagsAsync() { - const string Tag1 = "Tag One"; - const string Tag2 = "Tag Two"; - const string Tag2_Lowercase = "tag two"; + [Fact] + public async Task SyncStackTagsAsync() { + const string Tag1 = "Tag One"; + const string Tag2 = "Tag Two"; + const string Tag2_Lowercase = "tag two"; - var ev = GenerateEvent(SystemClock.UtcNow); - ev.Tags.Add(Tag1); + var ev = GenerateEvent(SystemClock.UtcNow); + ev.Tags.Add(Tag1); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); - ev = await _eventRepository.GetByIdAsync(ev.Id); - Assert.NotNull(ev); - Assert.NotNull(ev.StackId); + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); + Assert.NotNull(ev.StackId); - var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); - Assert.Equal(new [] { Tag1 }, stack.Tags.ToArray()); + var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.Equal(new[] { Tag1 }, stack.Tags.ToArray()); - ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); - ev.Tags.Add(Tag2); + ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); + ev.Tags.Add(Tag2); - await RefreshDataAsync(); - context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); - Assert.Equal(new [] { Tag1, Tag2 }, stack.Tags.ToArray()); + await RefreshDataAsync(); + context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); - ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); - ev.Tags.Add(Tag2_Lowercase); + ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); + ev.Tags.Add(Tag2_Lowercase); - await RefreshDataAsync(); - context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); - Assert.Equal(new [] { Tag1, Tag2}, stack.Tags.ToArray()); - } + await RefreshDataAsync(); + context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); + } - [Fact] - public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() { - string LargeRemovedTags = new string('x', 150); + [Fact] + public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() { + string LargeRemovedTags = new string('x', 150); - var ev = GenerateEvent(SystemClock.UtcNow); - ev.Tags.Add(LargeRemovedTags); + var ev = GenerateEvent(SystemClock.UtcNow); + ev.Tags.Add(LargeRemovedTags); - var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); + var context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); - ev = await _eventRepository.GetByIdAsync(ev.Id); - Assert.NotNull(ev); - Assert.NotNull(ev.StackId); - Assert.Empty(ev.Tags); + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); + Assert.NotNull(ev.StackId); + Assert.Empty(ev.Tags); - var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); - Assert.Empty(stack.Tags); + var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.Empty(stack.Tags); - ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); - ev.Tags.AddRange(Enumerable.Range(0, 100).Select(i => i.ToString())); + ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); + ev.Tags.AddRange(Enumerable.Range(0, 100).Select(i => i.ToString())); - await RefreshDataAsync(); - context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); + await RefreshDataAsync(); + context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); - ev = await _eventRepository.GetByIdAsync(ev.Id); - Assert.NotNull(ev); - Assert.NotNull(ev.StackId); - Assert.Equal(50, ev.Tags.Count); + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); + Assert.NotNull(ev.StackId); + Assert.Equal(50, ev.Tags.Count); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); - Assert.Equal(50, stack.Tags.Count); + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.Equal(50, stack.Tags.Count); - ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); - ev.Tags.Add(new string('x', 150)); - ev.Tags.AddRange(Enumerable.Range(100, 200).Select(i => i.ToString())); - ev.Tags.Add(Event.KnownTags.Critical); + ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: SystemClock.UtcNow); + ev.Tags.Add(new string('x', 150)); + ev.Tags.AddRange(Enumerable.Range(100, 200).Select(i => i.ToString())); + ev.Tags.Add(Event.KnownTags.Critical); - await RefreshDataAsync(); - context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - Assert.False(context.HasError, context.ErrorMessage); + await RefreshDataAsync(); + context = await _pipeline.RunAsync(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.False(context.HasError, context.ErrorMessage); - ev = await _eventRepository.GetByIdAsync(ev.Id); - Assert.NotNull(ev); - Assert.NotNull(ev.StackId); - Assert.Equal(50, ev.Tags.Count); - Assert.DoesNotContain(new string('x', 150), ev.Tags); - Assert.Contains(Event.KnownTags.Critical, ev.Tags); + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); + Assert.NotNull(ev.StackId); + Assert.Equal(50, ev.Tags.Count); + Assert.DoesNotContain(new string('x', 150), ev.Tags); + Assert.Contains(Event.KnownTags.Critical, ev.Tags); - stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); - Assert.Equal(50, stack.Tags.Count); - Assert.DoesNotContain(new string('x', 150), stack.Tags); - Assert.Contains(Event.KnownTags.Critical, stack.Tags); - } + stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.Equal(50, stack.Tags.Count); + Assert.DoesNotContain(new string('x', 150), stack.Tags); + Assert.Contains(Event.KnownTags.Critical, stack.Tags); + } - [Fact] - public async Task EnsureSingleNewStackAsync() { - string source = Guid.NewGuid().ToString(); - var contexts = new List { + [Fact] + public async Task EnsureSingleNewStackAsync() { + string source = Guid.NewGuid().ToString(); + var contexts = new List { new EventContext(new PersistentEvent { ProjectId = TestConstants.ProjectId, OrganizationId = TestConstants.OrganizationId, Message = "Test Sample", Source = source, Date = SystemClock.UtcNow, Type = Event.KnownTypes.Log }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(new PersistentEvent { ProjectId = TestConstants.ProjectId, OrganizationId = TestConstants.OrganizationId, Message = "Test Sample", Source = source, Date = SystemClock.UtcNow, Type = Event.KnownTypes.Log }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), }; - await _pipeline.RunAsync(contexts); - Assert.True(contexts.All(c => !c.HasError)); - Assert.True(contexts.All(c => c.Stack.Id == contexts.First().Stack.Id)); - Assert.Equal(1, contexts.Count(c => c.IsNew)); - Assert.Equal(1, contexts.Count(c => !c.IsNew)); - Assert.Equal(2, contexts.Count(c => !c.IsRegression)); - } + await _pipeline.RunAsync(contexts); + Assert.True(contexts.All(c => !c.HasError)); + Assert.True(contexts.All(c => c.Stack.Id == contexts.First().Stack.Id)); + Assert.Equal(1, contexts.Count(c => c.IsNew)); + Assert.Equal(1, contexts.Count(c => !c.IsNew)); + Assert.Equal(2, contexts.Count(c => !c.IsRegression)); + } - [Fact] - public async Task EnsureSingleGlobalErrorStackAsync() { - var contexts = new List { + [Fact] + public async Task EnsureSingleGlobalErrorStackAsync() { + var contexts = new List { new EventContext(new PersistentEvent { ProjectId = TestConstants.ProjectId, OrganizationId = TestConstants.OrganizationId, @@ -674,124 +669,124 @@ public async Task EnsureSingleGlobalErrorStackAsync() { }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), }; - await _pipeline.RunAsync(contexts); - Assert.True(contexts.All(c => !c.HasError)); - Assert.True(contexts.All(c => c.Stack.Id == contexts.First().Stack.Id)); - Assert.Equal(1, contexts.Count(c => c.IsNew)); - Assert.Equal(1, contexts.Count(c => !c.IsNew)); - Assert.Equal(2, contexts.Count(c => !c.IsRegression)); - } + await _pipeline.RunAsync(contexts); + Assert.True(contexts.All(c => !c.HasError)); + Assert.True(contexts.All(c => c.Stack.Id == contexts.First().Stack.Id)); + Assert.Equal(1, contexts.Count(c => c.IsNew)); + Assert.Equal(1, contexts.Count(c => !c.IsNew)); + Assert.Equal(2, contexts.Count(c => !c.IsRegression)); + } - [Fact] - public async Task EnsureSingleRegressionAsync() { - var utcNow = SystemClock.UtcNow; - var ev = EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow); - var context = new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - await _pipeline.RunAsync(context); + [Fact] + public async Task EnsureSingleRegressionAsync() { + var utcNow = SystemClock.UtcNow; + var ev = EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow); + var context = new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + await _pipeline.RunAsync(context); - Assert.False(context.HasError, context.ErrorMessage); - Assert.True(context.IsProcessed); - Assert.False(context.IsRegression); + Assert.False(context.HasError, context.ErrorMessage); + Assert.True(context.IsProcessed); + Assert.False(context.IsRegression); - ev = await _eventRepository.GetByIdAsync(ev.Id); - Assert.NotNull(ev); + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); - var stack = await _stackRepository.GetByIdAsync(ev.StackId); - stack.MarkFixed(); - await _stackRepository.SaveAsync(stack, o => o.Cache()); + var stack = await _stackRepository.GetByIdAsync(ev.StackId); + stack.MarkFixed(); + await _stackRepository.SaveAsync(stack, o => o.Cache()); - var contexts = new List { + var contexts = new List { new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(-1)), OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(-1)), OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }; - await RefreshDataAsync(); - await _pipeline.RunAsync(contexts); - Assert.True(contexts.All(c => !c.HasError)); - Assert.Equal(0, contexts.Count(c => c.IsRegression)); - Assert.Equal(2, contexts.Count(c => !c.IsRegression)); + await RefreshDataAsync(); + await _pipeline.RunAsync(contexts); + Assert.True(contexts.All(c => !c.HasError)); + Assert.Equal(0, contexts.Count(c => c.IsRegression)); + Assert.Equal(2, contexts.Count(c => !c.IsRegression)); - contexts = new List { + contexts = new List { new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1)), OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1)), OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }; - await RefreshDataAsync(); - await _pipeline.RunAsync(contexts); - Assert.True(contexts.All(c => !c.HasError)); - Assert.Equal(1, contexts.Count(c => c.IsRegression)); - Assert.Equal(1, contexts.Count(c => !c.IsRegression)); + await RefreshDataAsync(); + await _pipeline.RunAsync(contexts); + Assert.True(contexts.All(c => !c.HasError)); + Assert.Equal(1, contexts.Count(c => c.IsRegression)); + Assert.Equal(1, contexts.Count(c => !c.IsRegression)); - contexts = new List { + contexts = new List { new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1)), OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1)), OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }; - await RefreshDataAsync(); - await _pipeline.RunAsync(contexts); - Assert.True(contexts.All(c => !c.HasError)); - Assert.Equal(2, contexts.Count(c => !c.IsRegression)); - } + await RefreshDataAsync(); + await _pipeline.RunAsync(contexts); + Assert.True(contexts.All(c => !c.HasError)); + Assert.Equal(2, contexts.Count(c => !c.IsRegression)); + } - [Fact] - public async Task EnsureVersionedRegressionAsync() { - var utcNow = SystemClock.UtcNow; - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - var project = ProjectData.GenerateSampleProject(); - var ev = EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow); - var context = new EventContext(ev, organization, project); - await _pipeline.RunAsync(context); - await RefreshDataAsync(); + [Fact] + public async Task EnsureVersionedRegressionAsync() { + var utcNow = SystemClock.UtcNow; + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + var project = ProjectData.GenerateSampleProject(); + var ev = EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow); + var context = new EventContext(ev, organization, project); + await _pipeline.RunAsync(context); + await RefreshDataAsync(); - Assert.False(context.HasError, context.ErrorMessage); - Assert.True(context.IsProcessed); - Assert.False(context.IsRegression); + Assert.False(context.HasError, context.ErrorMessage); + Assert.True(context.IsProcessed); + Assert.False(context.IsRegression); - ev = await _eventRepository.GetByIdAsync(ev.Id); - Assert.NotNull(ev); + ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); - var stack = await _stackRepository.GetByIdAsync(ev.StackId); - stack.MarkFixed(new SemanticVersion(1, 0, 1, new []{ "rc2" })); - await _stackRepository.SaveAsync(stack, o => o.Cache()); + var stack = await _stackRepository.GetByIdAsync(ev.StackId); + stack.MarkFixed(new SemanticVersion(1, 0, 1, new[] { "rc2" })); + await _stackRepository.SaveAsync(stack, o => o.Cache()); - var contexts = new List { + var contexts = new List { new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1)), organization, project), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1), semver: "1.0.0"), organization, project), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1), semver: "1.0.0-beta2"), organization, project) }; - await RefreshDataAsync(); - await _pipeline.RunAsync(contexts); - Assert.True(contexts.All(c => !c.HasError)); - Assert.Equal(0, contexts.Count(c => c.IsRegression)); - Assert.Equal(3, contexts.Count(c => !c.IsRegression)); + await RefreshDataAsync(); + await _pipeline.RunAsync(contexts); + Assert.True(contexts.All(c => !c.HasError)); + Assert.Equal(0, contexts.Count(c => c.IsRegression)); + Assert.Equal(3, contexts.Count(c => !c.IsRegression)); - contexts = new List { + contexts = new List { new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1), semver: "1.0.0"), organization, project), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1), semver: "1.0.1-rc1"), organization, project), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(1), semver: "1.0.1-rc3"), organization, project), new EventContext(EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(-1), semver: "1.0.1-rc3"), organization, project) }; - await RefreshDataAsync(); - await _pipeline.RunAsync(contexts); - Assert.True(contexts.All(c => !c.HasError)); - Assert.Equal(1, contexts.Count(c => c.IsRegression)); - Assert.Equal(3, contexts.Count(c => !c.IsRegression)); + await RefreshDataAsync(); + await _pipeline.RunAsync(contexts); + Assert.True(contexts.All(c => !c.HasError)); + Assert.Equal(1, contexts.Count(c => c.IsRegression)); + Assert.Equal(3, contexts.Count(c => !c.IsRegression)); - var regressedEvent = contexts.First(c => c.IsRegression).Event; - Assert.Equal(utcNow.AddMinutes(-1), regressedEvent.Date); - Assert.Equal("1.0.1-rc3", regressedEvent.GetVersion()); - } + var regressedEvent = contexts.First(c => c.IsRegression).Event; + Assert.Equal(utcNow.AddMinutes(-1), regressedEvent.Date); + Assert.Equal("1.0.1-rc3", regressedEvent.GetVersion()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task EnsureIncludePrivateInformationIsRespectedAsync(bool includePrivateInformation) { - var project = ProjectData.GenerateSampleProject(); - project.Configuration.Settings.Add(SettingsDictionary.KnownKeys.IncludePrivateInformation, includePrivateInformation.ToString()); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task EnsureIncludePrivateInformationIsRespectedAsync(bool includePrivateInformation) { + var project = ProjectData.GenerateSampleProject(); + project.Configuration.Settings.Add(SettingsDictionary.KnownKeys.IncludePrivateInformation, includePrivateInformation.ToString()); - var contexts = new List { + var contexts = new List { new EventContext(new PersistentEvent { ProjectId = TestConstants.ProjectId, OrganizationId = TestConstants.OrganizationId, @@ -808,279 +803,279 @@ public async Task EnsureIncludePrivateInformationIsRespectedAsync(bool includePr }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), project) }; - await _pipeline.RunAsync(contexts); - var context = contexts.Single(); - Assert.False(context.HasError); - - var requestInfo = context.Event.GetRequestInfo(); - var environmentInfo = context.Event.GetEnvironmentInfo(); - var userInfo = context.Event.GetUserIdentity(); - var userDescription = context.Event.GetUserDescription(); - - Assert.Equal("/test", requestInfo?.Path); - Assert.Equal("Windows", environmentInfo?.OSName); - Assert.Equal("test", userDescription?.Description); - if (includePrivateInformation) { - Assert.NotNull(requestInfo?.ClientIpAddress); - Assert.Single(requestInfo.Cookies); - Assert.NotNull(requestInfo.PostData); - Assert.Single(requestInfo.QueryString); - - Assert.NotNull(environmentInfo?.IpAddress); - Assert.NotNull(environmentInfo.MachineName); - - Assert.NotNull(userInfo?.Identity); - Assert.NotNull(userDescription?.EmailAddress); - } else { - Assert.Null(requestInfo?.ClientIpAddress); - Assert.Empty(requestInfo?.Cookies); - Assert.Null(requestInfo?.PostData); - Assert.Empty(requestInfo?.QueryString); - - Assert.Null(environmentInfo?.IpAddress); - Assert.Null(environmentInfo?.MachineName); - - Assert.Null(userInfo); - Assert.Null(userDescription?.EmailAddress); - } + await _pipeline.RunAsync(contexts); + var context = contexts.Single(); + Assert.False(context.HasError); + + var requestInfo = context.Event.GetRequestInfo(); + var environmentInfo = context.Event.GetEnvironmentInfo(); + var userInfo = context.Event.GetUserIdentity(); + var userDescription = context.Event.GetUserDescription(); + + Assert.Equal("/test", requestInfo?.Path); + Assert.Equal("Windows", environmentInfo?.OSName); + Assert.Equal("test", userDescription?.Description); + if (includePrivateInformation) { + Assert.NotNull(requestInfo?.ClientIpAddress); + Assert.Single(requestInfo.Cookies); + Assert.NotNull(requestInfo.PostData); + Assert.Single(requestInfo.QueryString); + + Assert.NotNull(environmentInfo?.IpAddress); + Assert.NotNull(environmentInfo.MachineName); + + Assert.NotNull(userInfo?.Identity); + Assert.NotNull(userDescription?.EmailAddress); } - - [Fact] - public async Task WillHandleDiscardedStack() { - var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); - var project = ProjectData.GenerateSampleProject(); - - var ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow); - var context = await _pipeline.RunAsync(ev, organization, project); - Assert.True(context.IsProcessed); - Assert.False(context.HasError); - Assert.False(context.IsCancelled); - Assert.False(context.IsDiscarded); - await RefreshDataAsync(); - - var stack = context.Stack; - Assert.Equal(StackStatus.Open, stack.Status); - - stack.Status = StackStatus.Discarded; - stack = await _stackRepository.SaveAsync(stack, o => o.ImmediateConsistency()); - - - ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, stackId: ev.StackId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow); - context = await _pipeline.RunAsync(ev, organization, project); - Assert.False(context.IsProcessed); - Assert.False(context.HasError); - Assert.True(context.IsCancelled); - Assert.True(context.IsDiscarded); - await RefreshDataAsync(); - - ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow); - context = await _pipeline.RunAsync(ev, organization, project); - Assert.False(context.IsProcessed); - Assert.False(context.HasError); - Assert.True(context.IsCancelled); - Assert.True(context.IsDiscarded); - await RefreshDataAsync(); + else { + Assert.Null(requestInfo?.ClientIpAddress); + Assert.Empty(requestInfo?.Cookies); + Assert.Null(requestInfo?.PostData); + Assert.Empty(requestInfo?.QueryString); + + Assert.Null(environmentInfo?.IpAddress); + Assert.Null(environmentInfo?.MachineName); + + Assert.Null(userInfo); + Assert.Null(userDescription?.EmailAddress); } + } + + [Fact] + public async Task WillHandleDiscardedStack() { + var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); + var project = ProjectData.GenerateSampleProject(); + + var ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow); + var context = await _pipeline.RunAsync(ev, organization, project); + Assert.True(context.IsProcessed); + Assert.False(context.HasError); + Assert.False(context.IsCancelled); + Assert.False(context.IsDiscarded); + await RefreshDataAsync(); + + var stack = context.Stack; + Assert.Equal(StackStatus.Open, stack.Status); + + stack.Status = StackStatus.Discarded; + stack = await _stackRepository.SaveAsync(stack, o => o.ImmediateConsistency()); + + + ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, stackId: ev.StackId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow); + context = await _pipeline.RunAsync(ev, organization, project); + Assert.False(context.IsProcessed); + Assert.False(context.HasError); + Assert.True(context.IsCancelled); + Assert.True(context.IsDiscarded); + await RefreshDataAsync(); + + ev = EventData.GenerateEvent(organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: Event.KnownTypes.Log, source: "test", occurrenceDate: SystemClock.OffsetNow); + context = await _pipeline.RunAsync(ev, organization, project); + Assert.False(context.IsProcessed); + Assert.False(context.HasError); + Assert.True(context.IsCancelled); + Assert.True(context.IsDiscarded); + await RefreshDataAsync(); + } - [Theory] - [MemberData(nameof(Events))] - public async Task ProcessEventsAsync(string errorFilePath) { - var pipeline = GetService(); - var parserPluginManager = GetService(); - var events = parserPluginManager.ParseEvents(File.ReadAllText(errorFilePath), 2, "exceptionless/2.0.0.0"); + [Theory] + [MemberData(nameof(Events))] + public async Task ProcessEventsAsync(string errorFilePath) { + var pipeline = GetService(); + var parserPluginManager = GetService(); + var events = parserPluginManager.ParseEvents(File.ReadAllText(errorFilePath), 2, "exceptionless/2.0.0.0"); + Assert.NotNull(events); + Assert.True(events.Count > 0); + + foreach (var ev in events) { + ev.Date = SystemClock.UtcNow; + ev.ProjectId = TestConstants.ProjectId; + ev.OrganizationId = TestConstants.OrganizationId; + } + + var contexts = await pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + Assert.True(contexts.All(c => c.IsProcessed)); + Assert.True(contexts.All(c => !c.IsCancelled)); + Assert.True(contexts.All(c => !c.HasError)); + } + + [Fact] + public async Task PipelinePerformanceAsync() { + var parserPluginManager = GetService(); + var pipeline = GetService(); + var startDate = SystemClock.OffsetNow.SubtractHours(1); + int totalBatches = 0; + int totalEvents = 0; + + var sw = new Stopwatch(); + + string path = Path.Combine("..", "..", "..", "Pipeline", "Data"); + foreach (string file in Directory.GetFiles(path, "*.json", SearchOption.AllDirectories)) { + var events = parserPluginManager.ParseEvents(File.ReadAllText(file), 2, "exceptionless/2.0.0.0"); Assert.NotNull(events); Assert.True(events.Count > 0); foreach (var ev in events) { - ev.Date = SystemClock.UtcNow; + ev.Date = startDate; ev.ProjectId = TestConstants.ProjectId; ev.OrganizationId = TestConstants.OrganizationId; } + sw.Start(); var contexts = await pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + sw.Stop(); + Assert.True(contexts.All(c => c.IsProcessed)); Assert.True(contexts.All(c => !c.IsCancelled)); Assert.True(contexts.All(c => !c.HasError)); + + startDate = startDate.AddSeconds(5); + totalBatches++; + totalEvents += events.Count; } - [Fact] - public async Task PipelinePerformanceAsync() { - var parserPluginManager = GetService(); - var pipeline = GetService(); - var startDate = SystemClock.OffsetNow.SubtractHours(1); - int totalBatches = 0; - int totalEvents = 0; - - var sw = new Stopwatch(); - - string path = Path.Combine("..", "..", "..", "Pipeline", "Data"); - foreach (string file in Directory.GetFiles(path, "*.json", SearchOption.AllDirectories)) { - var events = parserPluginManager.ParseEvents(File.ReadAllText(file), 2, "exceptionless/2.0.0.0"); - Assert.NotNull(events); - Assert.True(events.Count > 0); - - foreach (var ev in events) { - ev.Date = startDate; - ev.ProjectId = TestConstants.ProjectId; - ev.OrganizationId = TestConstants.OrganizationId; - } + _logger.LogInformation("Took {Duration:g} to process {EventCount} with an average post size of {AveragePostSize}", sw.Elapsed, totalEvents, Math.Round(totalEvents * 1.0 / totalBatches, 4)); + } - sw.Start(); - var contexts = await pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - sw.Stop(); + [Fact(Skip = "Used to create performance data from the queue directory")] + public async Task GeneratePerformanceDataAsync() { + int currentBatchCount = 0; + var parserPluginManager = GetService(); + string dataDirectory = Path.GetFullPath(Path.Combine("..", "..", "..", "Pipeline", "Data")); - Assert.True(contexts.All(c => c.IsProcessed)); - Assert.True(contexts.All(c => !c.IsCancelled)); - Assert.True(contexts.All(c => !c.HasError)); + foreach (string file in Directory.GetFiles(dataDirectory)) + File.Delete(file); - startDate = startDate.AddSeconds(5); - totalBatches++; - totalEvents += events.Count; - } + var mappedUsers = new Dictionary(); + var mappedIPs = new Dictionary(); + var storage = new FolderFileStorage(new FolderFileStorageOptions { + Folder = Path.GetFullPath(Path.Combine("..", "..", "..", "src")), + LoggerFactory = Log + }); - _logger.LogInformation("Took {Duration:g} to process {EventCount} with an average post size of {AveragePostSize}", sw.Elapsed, totalEvents, Math.Round(totalEvents * 1.0 / totalBatches, 4)); - } + foreach (var file in await storage.GetFileListAsync(Path.Combine("Exceptionless.Web", "storage", "q", "*"))) { + byte[] data = await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")); + var eventPostInfo = await storage.GetObjectAsync(file.Path); + if (!String.IsNullOrEmpty(eventPostInfo.ContentEncoding)) + data = data.Decompress(eventPostInfo.ContentEncoding); - [Fact(Skip = "Used to create performance data from the queue directory")] - public async Task GeneratePerformanceDataAsync() { - int currentBatchCount = 0; - var parserPluginManager = GetService(); - string dataDirectory = Path.GetFullPath(Path.Combine("..", "..", "..", "Pipeline", "Data")); - - foreach (string file in Directory.GetFiles(dataDirectory)) - File.Delete(file); - - var mappedUsers = new Dictionary(); - var mappedIPs = new Dictionary(); - var storage = new FolderFileStorage(new FolderFileStorageOptions { - Folder = Path.GetFullPath(Path.Combine("..", "..", "..", "src")), - LoggerFactory = Log - }); - - foreach (var file in await storage.GetFileListAsync(Path.Combine("Exceptionless.Web", "storage", "q", "*"))) { - byte[] data = await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")); - var eventPostInfo = await storage.GetObjectAsync(file.Path); - if (!String.IsNullOrEmpty(eventPostInfo.ContentEncoding)) - data = data.Decompress(eventPostInfo.ContentEncoding); - - var encoding = Encoding.UTF8; - if (!String.IsNullOrEmpty(eventPostInfo.CharSet)) - encoding = Encoding.GetEncoding(eventPostInfo.CharSet); - - string input = encoding.GetString(data); - var events = parserPluginManager.ParseEvents(input, eventPostInfo.ApiVersion, eventPostInfo.UserAgent); - - foreach (var ev in events) { - ev.Date = new DateTimeOffset(new DateTime(2020, 1, 1)); - ev.ProjectId = null; - ev.OrganizationId = null; - ev.StackId = null; - - if (ev.Message != null) - ev.Message = RandomData.GetSentence(); - - var keysToRemove = ev.Data.Keys.Where(k => !k.StartsWith("@") && k != "MachineName" && k != "job" && k != "host" && k != "process").ToList(); - foreach (string key in keysToRemove) - ev.Data.Remove(key); - - ev.Data.Remove(Event.KnownDataKeys.UserDescription); - var identity = ev.GetUserIdentity(); - if (identity != null) { - if (!mappedUsers.ContainsKey(identity.Identity)) - mappedUsers.Add(identity.Identity, new UserInfo(Guid.NewGuid().ToString(), currentBatchCount.ToString())); - - ev.SetUserIdentity(mappedUsers[identity.Identity]); - } + var encoding = Encoding.UTF8; + if (!String.IsNullOrEmpty(eventPostInfo.CharSet)) + encoding = Encoding.GetEncoding(eventPostInfo.CharSet); - var request = ev.GetRequestInfo(); - if (request != null) { - request.Cookies?.Clear(); - request.PostData = null; - request.QueryString?.Clear(); - request.Referrer = null; - request.Host = RandomData.GetIp4Address(); - request.Path = $"/{RandomData.GetWord(false)}/{RandomData.GetWord(false)}"; - request.Data.Clear(); - - if (request.ClientIpAddress != null) { - if (!mappedIPs.ContainsKey(request.ClientIpAddress)) - mappedIPs.Add(request.ClientIpAddress, RandomData.GetIp4Address()); - - request.ClientIpAddress = mappedIPs[request.ClientIpAddress]; - } - } + string input = encoding.GetString(data); + var events = parserPluginManager.ParseEvents(input, eventPostInfo.ApiVersion, eventPostInfo.UserAgent); - InnerError error = ev.GetError(); - while (error != null) { - error.Message = RandomData.GetSentence(); - error.Data.Clear(); - (error as Error)?.Modules.Clear(); + foreach (var ev in events) { + ev.Date = new DateTimeOffset(new DateTime(2020, 1, 1)); + ev.ProjectId = null; + ev.OrganizationId = null; + ev.StackId = null; - error = error.Inner; - } + if (ev.Message != null) + ev.Message = RandomData.GetSentence(); - var environment = ev.GetEnvironmentInfo(); - environment?.Data.Clear(); - } + var keysToRemove = ev.Data.Keys.Where(k => !k.StartsWith("@") && k != "MachineName" && k != "job" && k != "host" && k != "process").ToList(); + foreach (string key in keysToRemove) + ev.Data.Remove(key); - // inject random session start events. - if (currentBatchCount % 10 == 0) - events.Insert(0, events[0].ToSessionStartEvent()); + ev.Data.Remove(Event.KnownDataKeys.UserDescription); + var identity = ev.GetUserIdentity(); + if (identity != null) { + if (!mappedUsers.ContainsKey(identity.Identity)) + mappedUsers.Add(identity.Identity, new UserInfo(Guid.NewGuid().ToString(), currentBatchCount.ToString())); - await storage.SaveObjectAsync(Path.Combine(dataDirectory, $"{currentBatchCount++}.json"), events); - } - } + ev.SetUserIdentity(mappedUsers[identity.Identity]); + } - public static IEnumerable Events { - get { - var result = new List(); - foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "ErrorData"), "*.expected.json", SearchOption.AllDirectories)) - result.Add(new object[] { file }); + var request = ev.GetRequestInfo(); + if (request != null) { + request.Cookies?.Clear(); + request.PostData = null; + request.QueryString?.Clear(); + request.Referrer = null; + request.Host = RandomData.GetIp4Address(); + request.Path = $"/{RandomData.GetWord(false)}/{RandomData.GetWord(false)}"; + request.Data.Clear(); + + if (request.ClientIpAddress != null) { + if (!mappedIPs.ContainsKey(request.ClientIpAddress)) + mappedIPs.Add(request.ClientIpAddress, RandomData.GetIp4Address()); + + request.ClientIpAddress = mappedIPs[request.ClientIpAddress]; + } + } - return result.ToArray(); - } - } + InnerError error = ev.GetError(); + while (error != null) { + error.Message = RandomData.GetSentence(); + error.Data.Clear(); + (error as Error)?.Modules.Clear(); - private async Task CreateProjectDataAsync() { - foreach (var organization in OrganizationData.GenerateSampleOrganizations(_billingManager, _plans)) { - if (organization.Id == TestConstants.OrganizationId3) - _billingManager.ApplyBillingPlan(organization, _plans.FreePlan, UserData.GenerateSampleUser()); - else - _billingManager.ApplyBillingPlan(organization, _plans.SmallPlan, UserData.GenerateSampleUser()); - - organization.StripeCustomerId = Guid.NewGuid().ToString("N"); - organization.CardLast4 = "1234"; - organization.SubscribeDate = SystemClock.UtcNow; - - if (organization.IsSuspended) { - organization.SuspendedByUserId = TestConstants.UserId; - organization.SuspensionCode = SuspensionCode.Billing; - organization.SuspensionDate = SystemClock.UtcNow; + error = error.Inner; } - await _organizationRepository.AddAsync(organization, o => o.Cache()); + var environment = ev.GetEnvironmentInfo(); + environment?.Data.Clear(); } - await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.Cache()); + // inject random session start events. + if (currentBatchCount % 10 == 0) + events.Insert(0, events[0].ToSessionStartEvent()); - foreach (var user in UserData.GenerateSampleUsers()) { - if (user.Id == TestConstants.UserId) { - user.OrganizationIds.Add(TestConstants.OrganizationId2); - user.OrganizationIds.Add(TestConstants.OrganizationId3); - } + await storage.SaveObjectAsync(Path.Combine(dataDirectory, $"{currentBatchCount++}.json"), events); + } + } + + public static IEnumerable Events { + get { + var result = new List(); + foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "ErrorData"), "*.expected.json", SearchOption.AllDirectories)) + result.Add(new object[] { file }); - if (!user.IsEmailAddressVerified) - user.CreateVerifyEmailAddressToken(); + return result.ToArray(); + } + } - await _userRepository.AddAsync(user, o => o.Cache()); + private async Task CreateProjectDataAsync() { + foreach (var organization in OrganizationData.GenerateSampleOrganizations(_billingManager, _plans)) { + if (organization.Id == TestConstants.OrganizationId3) + _billingManager.ApplyBillingPlan(organization, _plans.FreePlan, UserData.GenerateSampleUser()); + else + _billingManager.ApplyBillingPlan(organization, _plans.SmallPlan, UserData.GenerateSampleUser()); + + organization.StripeCustomerId = Guid.NewGuid().ToString("N"); + organization.CardLast4 = "1234"; + organization.SubscribeDate = SystemClock.UtcNow; + + if (organization.IsSuspended) { + organization.SuspendedByUserId = TestConstants.UserId; + organization.SuspensionCode = SuspensionCode.Billing; + organization.SuspensionDate = SystemClock.UtcNow; } - await RefreshDataAsync(); + await _organizationRepository.AddAsync(organization, o => o.Cache()); } - private PersistentEvent GenerateEvent(DateTimeOffset? occurrenceDate = null, string userIdentity = null, string type = null, string sessionId = null) { - occurrenceDate ??= SystemClock.OffsetNow; - return EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: occurrenceDate, userIdentity: userIdentity, type: type, sessionId: sessionId); + await _projectRepository.AddAsync(ProjectData.GenerateSampleProjects(), o => o.Cache()); + + foreach (var user in UserData.GenerateSampleUsers()) { + if (user.Id == TestConstants.UserId) { + user.OrganizationIds.Add(TestConstants.OrganizationId2); + user.OrganizationIds.Add(TestConstants.OrganizationId3); + } + + if (!user.IsEmailAddressVerified) + user.CreateVerifyEmailAddressToken(); + + await _userRepository.AddAsync(user, o => o.Cache()); } + + await RefreshDataAsync(); + } + + private PersistentEvent GenerateEvent(DateTimeOffset? occurrenceDate = null, string userIdentity = null, string type = null, string sessionId = null) { + occurrenceDate ??= SystemClock.OffsetNow; + return EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, generateData: false, occurrenceDate: occurrenceDate, userIdentity: userIdentity, type: type, sessionId: sessionId); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Plugins/EventParserTests.cs b/tests/Exceptionless.Tests/Plugins/EventParserTests.cs index 7d5dccfac7..e69d44c8aa 100644 --- a/tests/Exceptionless.Tests/Plugins/EventParserTests.cs +++ b/tests/Exceptionless.Tests/Plugins/EventParserTests.cs @@ -1,26 +1,23 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.EventParser; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Plugins { - public sealed class EventParserTests : TestWithServices { - private readonly EventParserPluginManager _parser; +namespace Exceptionless.Tests.Plugins; - public EventParserTests(ITestOutputHelper output) : base(output) { - _parser = GetService(); - } +public sealed class EventParserTests : TestWithServices { + private readonly EventParserPluginManager _parser; + + public EventParserTests(ITestOutputHelper output) : base(output) { + _parser = GetService(); + } - public static IEnumerable EventData => new[] { - new object[] { " \t", 0, null, Event.KnownTypes.Log }, - new object[] { "simple string", 1, new [] { "simple string" }, Event.KnownTypes.Log }, - new object[] { " \r\nsimple string", 1, new [] { "simple string" }, Event.KnownTypes.Log }, + public static IEnumerable EventData => new[] { + new object[] { " \t", 0, null, Event.KnownTypes.Log }, + new object[] { "simple string", 1, new [] { "simple string" }, Event.KnownTypes.Log }, + new object[] { " \r\nsimple string", 1, new [] { "simple string" }, Event.KnownTypes.Log }, new object[] { "{simple string", 1, new [] { "{simple string" }, Event.KnownTypes.Log }, new object[] { "{simple string,simple string}", 1, new [] { "{simple string,simple string}" }, Event.KnownTypes.Log }, new object[] { "{ \"name\": \"value\" }", 1, new string[] { null }, Event.KnownTypes.Log }, @@ -31,49 +28,48 @@ public EventParserTests(ITestOutputHelper output) : base(output) { new object[] { "simple string\r\nsimple string", 2, new [] { "simple string", "simple string" }, Event.KnownTypes.Log } }; - [Theory] - [MemberData(nameof(EventData))] - public void ParseEvents(string input, int expectedEvents, string[] expectedMessage, string expectedType) { - var events = _parser.ParseEvents(input, 2, "exceptionless/2.0.0.0"); - Assert.Equal(expectedEvents, events.Count); - for (int index = 0; index < events.Count; index++) { - var ev = events[index]; - Assert.Equal(expectedMessage[index], ev.Message); - Assert.Equal(expectedType, ev.Type); - Assert.NotEqual(DateTimeOffset.MinValue, ev.Date); - } + [Theory] + [MemberData(nameof(EventData))] + public void ParseEvents(string input, int expectedEvents, string[] expectedMessage, string expectedType) { + var events = _parser.ParseEvents(input, 2, "exceptionless/2.0.0.0"); + Assert.Equal(expectedEvents, events.Count); + for (int index = 0; index < events.Count; index++) { + var ev = events[index]; + Assert.Equal(expectedMessage[index], ev.Message); + Assert.Equal(expectedType, ev.Type); + Assert.NotEqual(DateTimeOffset.MinValue, ev.Date); } + } - [Theory] - [MemberData(nameof(Events))] - public void VerifyEventParserSerialization(string eventsFilePath) { - string json = File.ReadAllText(eventsFilePath); + [Theory] + [MemberData(nameof(Events))] + public void VerifyEventParserSerialization(string eventsFilePath) { + string json = File.ReadAllText(eventsFilePath); - var events = _parser.ParseEvents(json, 2, "exceptionless/2.0.0.0"); - Assert.Single(events); + var events = _parser.ParseEvents(json, 2, "exceptionless/2.0.0.0"); + Assert.Single(events); - string expectedContent = File.ReadAllText(eventsFilePath); - Assert.Equal(expectedContent, events.First().ToJson(Formatting.Indented, GetService())); - } + string expectedContent = File.ReadAllText(eventsFilePath); + Assert.Equal(expectedContent, events.First().ToJson(Formatting.Indented, GetService())); + } - [Theory] - [MemberData(nameof(Events))] - public void CanDeserializeEvents(string eventsFilePath) { - string json = File.ReadAllText(eventsFilePath); + [Theory] + [MemberData(nameof(Events))] + public void CanDeserializeEvents(string eventsFilePath) { + string json = File.ReadAllText(eventsFilePath); - var ev = json.FromJson(GetService()); - Assert.NotNull(ev); - } + var ev = json.FromJson(GetService()); + Assert.NotNull(ev); + } - public static IEnumerable Events { - get { - var result = new List(); - foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "Search", "Data"), "event*.json", SearchOption.AllDirectories)) - if (!file.EndsWith("summary.json")) - result.Add(new object[] { Path.GetFullPath(file) }); + public static IEnumerable Events { + get { + var result = new List(); + foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "Search", "Data"), "event*.json", SearchOption.AllDirectories)) + if (!file.EndsWith("summary.json")) + result.Add(new object[] { Path.GetFullPath(file) }); - return result.ToArray(); - } + return result.ToArray(); } } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Plugins/EventUpgraderTests.cs b/tests/Exceptionless.Tests/Plugins/EventUpgraderTests.cs index 92f2682a30..d8ca6601b5 100644 --- a/tests/Exceptionless.Tests/Plugins/EventUpgraderTests.cs +++ b/tests/Exceptionless.Tests/Plugins/EventUpgraderTests.cs @@ -1,43 +1,40 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Exceptionless.Core.Plugins.EventParser; +using Exceptionless.Core.Plugins.EventParser; using Exceptionless.Core.Plugins.EventUpgrader; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Plugins { - public sealed class EventUpgraderTests : TestWithServices { - private readonly EventUpgraderPluginManager _upgrader; - private readonly EventParserPluginManager _parser; +namespace Exceptionless.Tests.Plugins; - public EventUpgraderTests(ITestOutputHelper output) : base(output) { - _upgrader = GetService(); - _parser = GetService(); - } +public sealed class EventUpgraderTests : TestWithServices { + private readonly EventUpgraderPluginManager _upgrader; + private readonly EventParserPluginManager _parser; - [Theory] - [MemberData(nameof(Errors))] - public void ParseErrors(string errorFilePath) { - string json = File.ReadAllText(errorFilePath); - var ctx = new EventUpgraderContext(json); + public EventUpgraderTests(ITestOutputHelper output) : base(output) { + _upgrader = GetService(); + _parser = GetService(); + } - _upgrader.Upgrade(ctx); - string expectedContent = File.ReadAllText(Path.ChangeExtension(errorFilePath, ".expected.json")); - Assert.Equal(expectedContent, ctx.Documents.First.ToString()); + [Theory] + [MemberData(nameof(Errors))] + public void ParseErrors(string errorFilePath) { + string json = File.ReadAllText(errorFilePath); + var ctx = new EventUpgraderContext(json); - var events = _parser.ParseEvents(ctx.Documents.ToString(), 2, "exceptionless/2.0.0.0"); - Assert.Single(events); - } + _upgrader.Upgrade(ctx); + string expectedContent = File.ReadAllText(Path.ChangeExtension(errorFilePath, ".expected.json")); + Assert.Equal(expectedContent, ctx.Documents.First.ToString()); + + var events = _parser.ParseEvents(ctx.Documents.ToString(), 2, "exceptionless/2.0.0.0"); + Assert.Single(events); + } - public static IEnumerable Errors { - get { - var result = new List(); - foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "ErrorData"), "*.json", SearchOption.AllDirectories).Where(f => !f.EndsWith(".expected.json"))) - result.Add(new object[] { Path.GetFullPath(file) }); + public static IEnumerable Errors { + get { + var result = new List(); + foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "ErrorData"), "*.json", SearchOption.AllDirectories).Where(f => !f.EndsWith(".expected.json"))) + result.Add(new object[] { Path.GetFullPath(file) }); - return result.ToArray(); - } + return result.ToArray(); } } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Plugins/GeoTests.cs b/tests/Exceptionless.Tests/Plugins/GeoTests.cs index da69c55912..e6947fbea6 100644 --- a/tests/Exceptionless.Tests/Plugins/GeoTests.cs +++ b/tests/Exceptionless.Tests/Plugins/GeoTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Threading.Tasks; using Exceptionless.Core; using Exceptionless.Core.Billing; using Exceptionless.Core.Geo; @@ -15,289 +12,289 @@ using Exceptionless.Tests.Utility; using Foundatio.Caching; using Foundatio.Storage; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Plugins { - public sealed class GeoTests : TestWithServices { - private const string GREEN_BAY_COORDINATES = "44.5458,-88.1019"; - private const string GREEN_BAY_IP = "24.208.86.80"; - private const string IRVING_COORDINATES = "32.8489,-96.9667"; - private const string IRVING_IP = "192.91.253.248"; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - private readonly AppOptions _options; - - public GeoTests(ITestOutputHelper output) : base(output) { - _billingManager = GetService(); - _plans = GetService(); - _options = GetService(); - } - - private async Task GetResolverAsync(ILoggerFactory loggerFactory) { - if (String.IsNullOrEmpty(_options.MaxMindGeoIpKey)) { - _logger.LogInformation("Configure {SettingKey} to run geo tests.", nameof(AppOptions.MaxMindGeoIpKey)); - return new NullGeoIpService(); - } - - string dataDirectory = PathHelper.ExpandPath(".\\"); - var storage = new FolderFileStorage(new FolderFileStorageOptions { - Folder = dataDirectory, - LoggerFactory = loggerFactory - }); - - if (!await storage.ExistsAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH)) { - var job = new DownloadGeoIPDatabaseJob(_options, GetService(), storage, loggerFactory); - var result = await job.RunAsync(); - Assert.NotNull(result); - Assert.True(result.IsSuccess); - } +namespace Exceptionless.Tests.Plugins; + +public sealed class GeoTests : TestWithServices { + private const string GREEN_BAY_COORDINATES = "44.5458,-88.1019"; + private const string GREEN_BAY_IP = "24.208.86.80"; + private const string IRVING_COORDINATES = "32.8489,-96.9667"; + private const string IRVING_IP = "192.91.253.248"; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + private readonly AppOptions _options; + + public GeoTests(ITestOutputHelper output) : base(output) { + _billingManager = GetService(); + _plans = GetService(); + _options = GetService(); + } - return new MaxMindGeoIpService(storage, loggerFactory); + private async Task GetResolverAsync(ILoggerFactory loggerFactory) { + if (String.IsNullOrEmpty(_options.MaxMindGeoIpKey)) { + _logger.LogInformation("Configure {SettingKey} to run geo tests.", nameof(AppOptions.MaxMindGeoIpKey)); + return new NullGeoIpService(); } - [Fact] - public async Task WillNotSetLocation() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); - var ev = new PersistentEvent { Geo = GREEN_BAY_COORDINATES }; - await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); - - Assert.Equal(GREEN_BAY_COORDINATES, ev.Geo); - Assert.Null(ev.GetLocation()); + string dataDirectory = PathHelper.ExpandPath(".\\"); + var storage = new FolderFileStorage(new FolderFileStorageOptions { + Folder = dataDirectory, + LoggerFactory = loggerFactory + }); + + if (!await storage.ExistsAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH)) { + var job = new DownloadGeoIPDatabaseJob(_options, GetService(), storage, loggerFactory); + var result = await job.RunAsync(); + Assert.NotNull(result); + Assert.True(result.IsSuccess); } - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("Invalid")] - [InlineData("x,y")] - [InlineData("190,180")] - public async Task WillResetLocation(string geo) { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); - var ev = new PersistentEvent { Geo = geo }; - await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); - - Assert.Null(ev.Geo); - Assert.Null(ev.GetLocation()); - } + return new MaxMindGeoIpService(storage, loggerFactory); + } - [Fact] - public async Task WillSetLocationFromGeo() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); - var ev = new PersistentEvent { Geo = GREEN_BAY_IP }; - await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); + [Fact] + public async Task WillNotSetLocation() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; - Assert.NotNull(ev.Geo); - Assert.NotEqual(GREEN_BAY_IP, ev.Geo); + var plugin = new GeoPlugin(resolver, _options); + var ev = new PersistentEvent { Geo = GREEN_BAY_COORDINATES }; + await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); - var location = ev.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("WI", location?.Level1); - Assert.Equal("Green Bay", location?.Locality); - } + Assert.Equal(GREEN_BAY_COORDINATES, ev.Geo); + Assert.Null(ev.GetLocation()); + } - [Fact] - public async Task WillSetLocationFromRequestInfo() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); - var ev = new PersistentEvent(); - ev.AddRequestInfo(new RequestInfo { ClientIpAddress = GREEN_BAY_IP }); - await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("Invalid")] + [InlineData("x,y")] + [InlineData("190,180")] + public async Task WillResetLocation(string geo) { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; + + var plugin = new GeoPlugin(resolver, _options); + var ev = new PersistentEvent { Geo = geo }; + await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); + + Assert.Null(ev.Geo); + Assert.Null(ev.GetLocation()); + } - Assert.NotNull(ev.Geo); + [Fact] + public async Task WillSetLocationFromGeo() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; - var location = ev.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("WI", location?.Level1); - Assert.Equal("Green Bay", location?.Locality); - } + var plugin = new GeoPlugin(resolver, _options); + var ev = new PersistentEvent { Geo = GREEN_BAY_IP }; + await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); - [Fact] - public async Task WillSetLocationFromEnvironmentInfoInfo() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); - var ev = new PersistentEvent(); - ev.SetEnvironmentInfo(new EnvironmentInfo { IpAddress = $"127.0.0.1,{GREEN_BAY_IP}" }); - await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); + Assert.NotNull(ev.Geo); + Assert.NotEqual(GREEN_BAY_IP, ev.Geo); - Assert.NotNull(ev.Geo); + var location = ev.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("WI", location?.Level1); + Assert.Equal("Green Bay", location?.Locality); + } - var location = ev.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("WI", location?.Level1); - Assert.Equal("Green Bay", location?.Locality); - } + [Fact] + public async Task WillSetLocationFromRequestInfo() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; + + var plugin = new GeoPlugin(resolver, _options); + var ev = new PersistentEvent(); + ev.AddRequestInfo(new RequestInfo { ClientIpAddress = GREEN_BAY_IP }); + await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); + + Assert.NotNull(ev.Geo); + + var location = ev.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("WI", location?.Level1); + Assert.Equal("Green Bay", location?.Locality); + } + + [Fact] + public async Task WillSetLocationFromEnvironmentInfoInfo() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; + + var plugin = new GeoPlugin(resolver, _options); + var ev = new PersistentEvent(); + ev.SetEnvironmentInfo(new EnvironmentInfo { IpAddress = $"127.0.0.1,{GREEN_BAY_IP}" }); + await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); + + Assert.NotNull(ev.Geo); + + var location = ev.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("WI", location?.Level1); + Assert.Equal("Green Bay", location?.Locality); + } - [Fact] - public async Task WillSetFromSingleGeo() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); + [Fact] + public async Task WillSetFromSingleGeo() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; - var contexts = new List { + var plugin = new GeoPlugin(resolver, _options); + + var contexts = new List { new EventContext(new PersistentEvent { Geo = GREEN_BAY_IP }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(new PersistentEvent { Geo = GREEN_BAY_IP }, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }; - await plugin.EventBatchProcessingAsync(contexts); + await plugin.EventBatchProcessingAsync(contexts); - foreach (var context in contexts) { - AssertCoordinatesAreEqual(GREEN_BAY_COORDINATES, context.Event.Geo); + foreach (var context in contexts) { + AssertCoordinatesAreEqual(GREEN_BAY_COORDINATES, context.Event.Geo); - var location = context.Event.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("WI", location?.Level1); - Assert.Equal("Green Bay", location?.Locality); - } + var location = context.Event.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("WI", location?.Level1); + Assert.Equal("Green Bay", location?.Locality); } + } + + [Fact] + public async Task WillNotSetFromMultipleGeo() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; - [Fact] - public async Task WillNotSetFromMultipleGeo() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); - - var ev = new PersistentEvent(); - var greenBayEvent = new PersistentEvent { Geo = GREEN_BAY_IP }; - var irvingEvent = new PersistentEvent { Geo = IRVING_IP }; - await plugin.EventBatchProcessingAsync(new List { + var plugin = new GeoPlugin(resolver, _options); + + var ev = new PersistentEvent(); + var greenBayEvent = new PersistentEvent { Geo = GREEN_BAY_IP }; + var irvingEvent = new PersistentEvent { Geo = IRVING_IP }; + await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(greenBayEvent, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(irvingEvent, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); - AssertCoordinatesAreEqual(GREEN_BAY_COORDINATES, greenBayEvent.Geo); - var location = greenBayEvent.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("WI", location?.Level1); - Assert.Equal("Green Bay", location?.Locality); + AssertCoordinatesAreEqual(GREEN_BAY_COORDINATES, greenBayEvent.Geo); + var location = greenBayEvent.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("WI", location?.Level1); + Assert.Equal("Green Bay", location?.Locality); + + AssertCoordinatesAreEqual(IRVING_COORDINATES, irvingEvent.Geo); + location = irvingEvent.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("TX", location?.Level1); + Assert.Equal("Irving", location?.Locality); + } - AssertCoordinatesAreEqual(IRVING_COORDINATES, irvingEvent.Geo); - location = irvingEvent.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("TX", location?.Level1); - Assert.Equal("Irving", location?.Locality); - } + [Fact] + public async Task ReverseGeocodeLookup() { + var service = GetService(); + if (service is NullGeocodeService) + return; + + Assert.True(GeoResult.TryParse(GREEN_BAY_COORDINATES, out var coordinates)); + var location = await service.ReverseGeocodeAsync(coordinates.Latitude.GetValueOrDefault(), coordinates.Longitude.GetValueOrDefault()); + Assert.Equal("US", location?.Country); + Assert.Equal("WI", location?.Level1); + Assert.Equal("Brown County", location?.Level2); + Assert.Equal("Green Bay", location?.Locality); + } - [Fact] - public async Task ReverseGeocodeLookup() { - var service = GetService(); - if (service is NullGeocodeService) - return; + [Fact] + public async Task WillSetMultipleFromEmptyGeo() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; - Assert.True(GeoResult.TryParse(GREEN_BAY_COORDINATES, out var coordinates)); - var location = await service.ReverseGeocodeAsync(coordinates.Latitude.GetValueOrDefault(), coordinates.Longitude.GetValueOrDefault()); - Assert.Equal("US", location?.Country); - Assert.Equal("WI", location?.Level1); - Assert.Equal("Brown County", location?.Level2); - Assert.Equal("Green Bay", location?.Locality); - } + var plugin = new GeoPlugin(resolver, _options); - [Fact] - public async Task WillSetMultipleFromEmptyGeo() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var plugin = new GeoPlugin(resolver, _options); - - var ev = new PersistentEvent(); - var greenBayEvent = new PersistentEvent(); - greenBayEvent.SetEnvironmentInfo(new EnvironmentInfo { IpAddress = GREEN_BAY_IP }); - var irvingEvent = new PersistentEvent(); - irvingEvent.SetEnvironmentInfo(new EnvironmentInfo { IpAddress = IRVING_IP }); - await plugin.EventBatchProcessingAsync(new List { + var ev = new PersistentEvent(); + var greenBayEvent = new PersistentEvent(); + greenBayEvent.SetEnvironmentInfo(new EnvironmentInfo { IpAddress = GREEN_BAY_IP }); + var irvingEvent = new PersistentEvent(); + irvingEvent.SetEnvironmentInfo(new EnvironmentInfo { IpAddress = IRVING_IP }); + await plugin.EventBatchProcessingAsync(new List { new EventContext(ev, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(greenBayEvent, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()), new EventContext(irvingEvent, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()) }); - AssertCoordinatesAreEqual(GREEN_BAY_COORDINATES, greenBayEvent.Geo); - var location = greenBayEvent.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("WI", location?.Level1); - Assert.Equal("Green Bay", location?.Locality); + AssertCoordinatesAreEqual(GREEN_BAY_COORDINATES, greenBayEvent.Geo); + var location = greenBayEvent.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("WI", location?.Level1); + Assert.Equal("Green Bay", location?.Locality); + + AssertCoordinatesAreEqual(IRVING_COORDINATES, irvingEvent.Geo); + location = irvingEvent.GetLocation(); + Assert.Equal("US", location?.Country); + Assert.Equal("TX", location?.Level1); + Assert.Equal("Irving", location?.Locality); + } - AssertCoordinatesAreEqual(IRVING_COORDINATES, irvingEvent.Geo); - location = irvingEvent.GetLocation(); - Assert.Equal("US", location?.Country); - Assert.Equal("TX", location?.Level1); - Assert.Equal("Irving", location?.Locality); - } + [Theory] + [MemberData(nameof(IPData))] + public async Task CanResolveIpAsync(string ip, bool canResolve) { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; + + var result = await resolver.ResolveIpAsync(ip); + if (canResolve) + Assert.NotNull(result); + else + Assert.Null(result); + } - [Theory] - [MemberData(nameof(IPData))] - public async Task CanResolveIpAsync(string ip, bool canResolve) { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; - - var result = await resolver.ResolveIpAsync(ip); - if (canResolve) - Assert.NotNull(result); - else - Assert.Null(result); - } + [Fact] + public async Task CanResolveIpFromCacheAsync() { + var resolver = await GetResolverAsync(Log); + if (resolver is NullGeoIpService) + return; - [Fact] - public async Task CanResolveIpFromCacheAsync() { - var resolver = await GetResolverAsync(Log); - if (resolver is NullGeoIpService) - return; + // Load the database + await resolver.ResolveIpAsync("0.0.0.0"); - // Load the database - await resolver.ResolveIpAsync("0.0.0.0"); + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 1000; i++) + Assert.NotNull(await resolver.ResolveIpAsync("8.8.4.4")); - var sw = Stopwatch.StartNew(); - for (int i = 0; i < 1000; i++) - Assert.NotNull(await resolver.ResolveIpAsync("8.8.4.4")); + sw.Stop(); + Assert.InRange(sw.ElapsedMilliseconds, 0, 65); + } - sw.Stop(); - Assert.InRange(sw.ElapsedMilliseconds, 0, 65); + /// + /// Takes in 32.8489,-96.9667 and only checks to one decimal place. + /// + private void AssertCoordinatesAreEqual(string expected, string actual) { + if (String.Equals(actual, expected)) + return; + + string[] actualParts = actual.Split(','); + string[] expectedParts = expected.Split(','); + if (actualParts.Length != expectedParts.Length || actualParts.Length != 2) { + Assert.Equal(expected, actual); + return; } - /// - /// Takes in 32.8489,-96.9667 and only checks to one decimal place. - /// - private void AssertCoordinatesAreEqual(string expected, string actual) { - if (String.Equals(actual, expected)) - return; - - string[] actualParts = actual.Split(','); - string[] expectedParts = expected.Split(','); - if (actualParts.Length != expectedParts.Length || actualParts.Length != 2) { - Assert.Equal(expected, actual); - return; - } - - Assert.Equal(Math.Round(Double.Parse(expectedParts[0]), 1), Math.Round(Double.Parse(actualParts[0]), 1)); - Assert.Equal(Math.Round(Double.Parse(expectedParts[1]), 1), Math.Round(Double.Parse(actualParts[1]), 1)); - } + Assert.Equal(Math.Round(Double.Parse(expectedParts[0]), 1), Math.Round(Double.Parse(actualParts[0]), 1)); + Assert.Equal(Math.Round(Double.Parse(expectedParts[1]), 1), Math.Round(Double.Parse(actualParts[1]), 1)); + } - public static IEnumerable IPData => new List { + public static IEnumerable IPData => new List { new object[] { null, false }, new object[] { "::1", false }, new object[] { "127.0.0.1", false }, @@ -308,5 +305,4 @@ private void AssertCoordinatesAreEqual(string expected, string actual) { new object[] { "8.8.4.4", true }, new object[] { "2001:4860:4860::8844", true } }.ToArray(); - } } diff --git a/tests/Exceptionless.Tests/Plugins/ManualStackingTests.cs b/tests/Exceptionless.Tests/Plugins/ManualStackingTests.cs index fdd58d8c8f..0b472354d1 100644 --- a/tests/Exceptionless.Tests/Plugins/ManualStackingTests.cs +++ b/tests/Exceptionless.Tests/Plugins/ManualStackingTests.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Exceptionless.Core.Billing; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.EventProcessor; @@ -8,26 +5,26 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Plugins { - public class ManualStackingTests : TestWithServices { - public ManualStackingTests(ITestOutputHelper output) : base(output) {} +namespace Exceptionless.Tests.Plugins; - [Theory] - [MemberData(nameof(StackingData))] - public async Task AddManualStackSignatureData(string stackingKey, bool willAddManualStackSignature) { - var ev = new PersistentEvent(); - ev.SetManualStackingKey(stackingKey); +public class ManualStackingTests : TestWithServices { + public ManualStackingTests(ITestOutputHelper output) : base(output) { } - var context = new EventContext(ev, OrganizationData.GenerateSampleOrganization(GetService(), GetService()), ProjectData.GenerateSampleProject()); - var plugin = GetService(); - await plugin.EventBatchProcessingAsync(new List { context }); - Assert.Equal(willAddManualStackSignature, context.StackSignatureData.Count > 0); - } + [Theory] + [MemberData(nameof(StackingData))] + public async Task AddManualStackSignatureData(string stackingKey, bool willAddManualStackSignature) { + var ev = new PersistentEvent(); + ev.SetManualStackingKey(stackingKey); - public static IEnumerable StackingData => new List { + var context = new EventContext(ev, OrganizationData.GenerateSampleOrganization(GetService(), GetService()), ProjectData.GenerateSampleProject()); + var plugin = GetService(); + await plugin.EventBatchProcessingAsync(new List { context }); + Assert.Equal(willAddManualStackSignature, context.StackSignatureData.Count > 0); + } + + public static IEnumerable StackingData => new List { new object[] { "ManualStackData", true }, new object[] { null, false }, new object[] { String.Empty, false } }.ToArray(); - } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Plugins/SummaryDataTests.cs b/tests/Exceptionless.Tests/Plugins/SummaryDataTests.cs index b37f9eadc0..0c2c2b58f7 100644 --- a/tests/Exceptionless.Tests/Plugins/SummaryDataTests.cs +++ b/tests/Exceptionless.Tests/Plugins/SummaryDataTests.cs @@ -1,83 +1,81 @@ -using System.Collections.Generic; -using System.IO; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.Formatting; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Plugins { - public class SummaryDataTests : TestWithServices { - public SummaryDataTests(ITestOutputHelper output) : base(output) {} +namespace Exceptionless.Tests.Plugins; - [Theory] - [MemberData(nameof(Events))] - public void EventSummaryData(string path) { - var settings = GetService(); - settings.Formatting = Formatting.Indented; +public class SummaryDataTests : TestWithServices { + public SummaryDataTests(ITestOutputHelper output) : base(output) { } - string json = File.ReadAllText(path); + [Theory] + [MemberData(nameof(Events))] + public void EventSummaryData(string path) { + var settings = GetService(); + settings.Formatting = Formatting.Indented; - var ev = json.FromJson(settings); - Assert.NotNull(ev); + string json = File.ReadAllText(path); - var data = GetService().GetEventSummaryData(ev); - var summary = new EventSummaryModel { - TemplateKey = data.TemplateKey, - Id = ev.Id, - Date = ev.Date, - Data = data.Data - }; + var ev = json.FromJson(settings); + Assert.NotNull(ev); - string expectedContent = File.ReadAllText(Path.ChangeExtension(path, "summary.json")); - Assert.Equal(expectedContent, JsonConvert.SerializeObject(summary, settings)); - } + var data = GetService().GetEventSummaryData(ev); + var summary = new EventSummaryModel { + TemplateKey = data.TemplateKey, + Id = ev.Id, + Date = ev.Date, + Data = data.Data + }; - [Theory] - [MemberData(nameof(Stacks))] - public void StackSummaryData(string path) { - var settings = GetService(); - settings.Formatting = Formatting.Indented; + string expectedContent = File.ReadAllText(Path.ChangeExtension(path, "summary.json")); + Assert.Equal(expectedContent, JsonConvert.SerializeObject(summary, settings)); + } - string json = File.ReadAllText(path); - var stack = json.FromJson(settings); - Assert.NotNull(stack); + [Theory] + [MemberData(nameof(Stacks))] + public void StackSummaryData(string path) { + var settings = GetService(); + settings.Formatting = Formatting.Indented; - var data = GetService().GetStackSummaryData(stack); - var summary = new StackSummaryModel { - TemplateKey = data.TemplateKey, - Data = data.Data, - Id = stack.Id, - Title = stack.Title, - Status = stack.Status, - Total = 1 - }; + string json = File.ReadAllText(path); + var stack = json.FromJson(settings); + Assert.NotNull(stack); - string expectedContent = File.ReadAllText(Path.ChangeExtension(path, "summary.json")); - Assert.Equal(expectedContent, JsonConvert.SerializeObject(summary, settings)); - } + var data = GetService().GetStackSummaryData(stack); + var summary = new StackSummaryModel { + TemplateKey = data.TemplateKey, + Data = data.Data, + Id = stack.Id, + Title = stack.Title, + Status = stack.Status, + Total = 1 + }; - public static IEnumerable Events { - get { - var result = new List(); - foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "Search", "Data"), "event*.json", SearchOption.AllDirectories)) - if (!file.EndsWith("summary.json")) - result.Add(new object[] { Path.GetFullPath(file) }); + string expectedContent = File.ReadAllText(Path.ChangeExtension(path, "summary.json")); + Assert.Equal(expectedContent, JsonConvert.SerializeObject(summary, settings)); + } + + public static IEnumerable Events { + get { + var result = new List(); + foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "Search", "Data"), "event*.json", SearchOption.AllDirectories)) + if (!file.EndsWith("summary.json")) + result.Add(new object[] { Path.GetFullPath(file) }); - return result.ToArray(); - } + return result.ToArray(); } + } - public static IEnumerable Stacks { - get { - var result = new List(); - foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "Search", "Data"), "stack*.json", SearchOption.AllDirectories)) - if (!file.EndsWith("summary.json")) - result.Add(new object[] { Path.GetFullPath(file) }); + public static IEnumerable Stacks { + get { + var result = new List(); + foreach (string file in Directory.GetFiles(Path.Combine("..", "..", "..", "Search", "Data"), "stack*.json", SearchOption.AllDirectories)) + if (!file.EndsWith("summary.json")) + result.Add(new object[] { Path.GetFullPath(file) }); - return result.ToArray(); - } + return result.ToArray(); } } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Plugins/WebHookDataTests.cs b/tests/Exceptionless.Tests/Plugins/WebHookDataTests.cs index a29d49ca5f..caf625e36b 100644 --- a/tests/Exceptionless.Tests/Plugins/WebHookDataTests.cs +++ b/tests/Exceptionless.Tests/Plugins/WebHookDataTests.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Tests.Utility; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.Formatting; @@ -10,74 +7,76 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Plugins { - public sealed class WebHookDataTests : TestWithServices { - private readonly WebHookDataPluginManager _webHookData; - private readonly FormattingPluginManager _formatter; +namespace Exceptionless.Tests.Plugins; - public WebHookDataTests(ITestOutputHelper output) : base(output) { - _webHookData = GetService(); - _formatter = GetService(); - } +public sealed class WebHookDataTests : TestWithServices { + private readonly WebHookDataPluginManager _webHookData; + private readonly FormattingPluginManager _formatter; + + public WebHookDataTests(ITestOutputHelper output) : base(output) { + _webHookData = GetService(); + _formatter = GetService(); + } - [Theory] - [MemberData(nameof(WebHookData))] - public async Task CreateFromEventAsync(string version, bool expectData) { - var settings = GetService(); - settings.Formatting = Formatting.Indented; - object data = await _webHookData.CreateFromEventAsync(GetWebHookDataContext(version)); - if (expectData) { - string filePath = Path.GetFullPath(Path.Combine("..", "..", "..", "Plugins", "WebHookData", $"{version}.event.expected.json")); - string expectedContent = File.ReadAllText(filePath); - string actualContent = JsonConvert.SerializeObject(data, settings); - Assert.Equal(expectedContent, actualContent); - } else { - Assert.Null(data); - } + [Theory] + [MemberData(nameof(WebHookData))] + public async Task CreateFromEventAsync(string version, bool expectData) { + var settings = GetService(); + settings.Formatting = Formatting.Indented; + object data = await _webHookData.CreateFromEventAsync(GetWebHookDataContext(version)); + if (expectData) { + string filePath = Path.GetFullPath(Path.Combine("..", "..", "..", "Plugins", "WebHookData", $"{version}.event.expected.json")); + string expectedContent = File.ReadAllText(filePath); + string actualContent = JsonConvert.SerializeObject(data, settings); + Assert.Equal(expectedContent, actualContent); + } + else { + Assert.Null(data); } + } - [Theory] - [MemberData(nameof(WebHookData))] - public async Task CanCreateFromStackAsync(string version, bool expectData) { - var settings = GetService(); - settings.Formatting = Formatting.Indented; - object data = await _webHookData.CreateFromStackAsync(GetWebHookDataContext(version)); - if (expectData) { - string filePath = Path.GetFullPath(Path.Combine("..", "..", "..", "Plugins", "WebHookData", $"{version}.stack.expected.json")); - string expectedContent = File.ReadAllText(filePath); - string actualContent = JsonConvert.SerializeObject(data, settings); - Assert.Equal(expectedContent, actualContent); - } else { - Assert.Null(data); - } + [Theory] + [MemberData(nameof(WebHookData))] + public async Task CanCreateFromStackAsync(string version, bool expectData) { + var settings = GetService(); + settings.Formatting = Formatting.Indented; + object data = await _webHookData.CreateFromStackAsync(GetWebHookDataContext(version)); + if (expectData) { + string filePath = Path.GetFullPath(Path.Combine("..", "..", "..", "Plugins", "WebHookData", $"{version}.stack.expected.json")); + string expectedContent = File.ReadAllText(filePath); + string actualContent = JsonConvert.SerializeObject(data, settings); + Assert.Equal(expectedContent, actualContent); + } + else { + Assert.Null(data); } + } - public static IEnumerable WebHookData => new List { + public static IEnumerable WebHookData => new List { new object[] { "v0", false }, new object[] { WebHook.KnownVersions.Version1, true }, new object[] { WebHook.KnownVersions.Version2, true }, new object[] { "v3", false } }.ToArray(); - private WebHookDataContext GetWebHookDataContext(string version) { - string json = File.ReadAllText(Path.GetFullPath(Path.Combine("..", "..", "..", "ErrorData", "1477.expected.json"))); + private WebHookDataContext GetWebHookDataContext(string version) { + string json = File.ReadAllText(Path.GetFullPath(Path.Combine("..", "..", "..", "ErrorData", "1477.expected.json"))); - var settings = GetService(); - settings.Formatting = Formatting.Indented; + var settings = GetService(); + settings.Formatting = Formatting.Indented; - var ev = JsonConvert.DeserializeObject(json, settings); - ev.OrganizationId = TestConstants.OrganizationId; - ev.ProjectId = TestConstants.ProjectId; - ev.StackId = TestConstants.StackId; - ev.Id = TestConstants.EventId; + var ev = JsonConvert.DeserializeObject(json, settings); + ev.OrganizationId = TestConstants.OrganizationId; + ev.ProjectId = TestConstants.ProjectId; + ev.StackId = TestConstants.StackId; + ev.Id = TestConstants.EventId; - var context = new WebHookDataContext(version, ev, OrganizationData.GenerateSampleOrganization(GetService(), GetService()), ProjectData.GenerateSampleProject()) { - Stack = StackData.GenerateStack(id: TestConstants.StackId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, title: _formatter.GetStackTitle(ev), signatureHash: "722e7afd4dca4a3c91f4d94fec89dfdc") - }; - context.Stack.Tags = new TagSet { "Test" }; - context.Stack.FirstOccurrence = context.Stack.LastOccurrence = ev.Date.UtcDateTime; + var context = new WebHookDataContext(version, ev, OrganizationData.GenerateSampleOrganization(GetService(), GetService()), ProjectData.GenerateSampleProject()) { + Stack = StackData.GenerateStack(id: TestConstants.StackId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, title: _formatter.GetStackTitle(ev), signatureHash: "722e7afd4dca4a3c91f4d94fec89dfdc") + }; + context.Stack.Tags = new TagSet { "Test" }; + context.Stack.FirstOccurrence = context.Stack.LastOccurrence = ev.Date.UtcDateTime; - return context; - } + return context; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs index 0a5fdb28ef..2c8ffa5fd9 100644 --- a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Repositories; using Exceptionless.DateTimeExtensions; using Exceptionless.Helpers; @@ -12,220 +8,220 @@ using Foundatio.Repositories; using Foundatio.Repositories.Utility; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Tests.Repositories { - public sealed class EventRepositoryTests : IntegrationTestsBase { - private readonly IEventRepository _repository; - private readonly IStackRepository _stackRepository; +namespace Exceptionless.Tests.Repositories; - public EventRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _repository = GetService(); - _stackRepository = GetService(); - } +public sealed class EventRepositoryTests : IntegrationTestsBase { + private readonly IEventRepository _repository; + private readonly IStackRepository _stackRepository; - [Fact (Skip = "https://github.com/elastic/elasticsearch-net/issues/2463")] - public async Task GetAsync() { - Log.SetLogLevel(LogLevel.Trace); - var ev = await _repository.AddAsync(new PersistentEvent { - CreatedUtc = SystemClock.UtcNow, - Date = new DateTimeOffset(SystemClock.UtcNow.Date, TimeSpan.Zero), - OrganizationId = TestConstants.OrganizationId, - ProjectId = TestConstants.ProjectId, - StackId = TestConstants.StackId, - Type = Event.KnownTypes.Log, - Count = Int32.MaxValue, - Value = Decimal.MaxValue, - Geo = "40,-70" - }); - - Assert.Equal(ev, await _repository.GetByIdAsync(ev.Id)); - } + public EventRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _repository = GetService(); + _stackRepository = GetService(); + } - [Fact(Skip="Performance Testing")] - public async Task GetAsyncPerformanceAsync() { - var ev = await _repository.AddAsync(new RandomEventGenerator().GeneratePersistent()); - await RefreshDataAsync(); - Assert.Equal(1, await _repository.CountAsync()); + [Fact(Skip = "https://github.com/elastic/elasticsearch-net/issues/2463")] + public async Task GetAsync() { + Log.SetLogLevel(LogLevel.Trace); + var ev = await _repository.AddAsync(new PersistentEvent { + CreatedUtc = SystemClock.UtcNow, + Date = new DateTimeOffset(SystemClock.UtcNow.Date, TimeSpan.Zero), + OrganizationId = TestConstants.OrganizationId, + ProjectId = TestConstants.ProjectId, + StackId = TestConstants.StackId, + Type = Event.KnownTypes.Log, + Count = Int32.MaxValue, + Value = Decimal.MaxValue, + Geo = "40,-70" + }); + + Assert.Equal(ev, await _repository.GetByIdAsync(ev.Id)); + } - var sw = Stopwatch.StartNew(); - const int MAX_ITERATIONS = 100; - for (int i = 0; i < MAX_ITERATIONS; i++) { - Assert.NotNull(await _repository.GetByIdAsync(ev.Id)); - } + [Fact(Skip = "Performance Testing")] + public async Task GetAsyncPerformanceAsync() { + var ev = await _repository.AddAsync(new RandomEventGenerator().GeneratePersistent()); + await RefreshDataAsync(); + Assert.Equal(1, await _repository.CountAsync()); - sw.Stop(); - _logger.LogInformation("{Duration:g}", sw.Elapsed); + var sw = Stopwatch.StartNew(); + const int MAX_ITERATIONS = 100; + for (int i = 0; i < MAX_ITERATIONS; i++) { + Assert.NotNull(await _repository.GetByIdAsync(ev.Id)); } - [Fact] - public async Task GetPagedAsync() { - var events = new List(); - for (int i = 0; i < 6; i++) - events.Add(EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, stackId: TestConstants.StackId, occurrenceDate: SystemClock.UtcNow.Subtract(TimeSpan.FromMinutes(i)))); - - await _repository.AddAsync(events); - await RefreshDataAsync(); - Assert.Equal(events.Count, await _repository.CountAsync()); - - var results = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(2).PageLimit(2)); - Assert.Equal(2, results.Documents.Count); - Assert.Equal(results.Documents.First().Id, events[2].Id); - Assert.Equal(results.Documents.Last().Id, events[3].Id); - - results = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(3).PageLimit(2)); - Assert.Equal(2, results.Documents.Count); - Assert.Equal(results.Documents.First().Id, events[4].Id); - Assert.Equal(results.Documents.Last().Id, events[5].Id); - } + sw.Stop(); + _logger.LogInformation("{Duration:g}", sw.Elapsed); + } + + [Fact] + public async Task GetPagedAsync() { + var events = new List(); + for (int i = 0; i < 6; i++) + events.Add(EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, stackId: TestConstants.StackId, occurrenceDate: SystemClock.UtcNow.Subtract(TimeSpan.FromMinutes(i)))); + + await _repository.AddAsync(events); + await RefreshDataAsync(); + Assert.Equal(events.Count, await _repository.CountAsync()); + + var results = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(2).PageLimit(2)); + Assert.Equal(2, results.Documents.Count); + Assert.Equal(results.Documents.First().Id, events[2].Id); + Assert.Equal(results.Documents.Last().Id, events[3].Id); + + results = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(3).PageLimit(2)); + Assert.Equal(2, results.Documents.Count); + Assert.Equal(results.Documents.First().Id, events[4].Id); + Assert.Equal(results.Documents.Last().Id, events[5].Id); + } - [Fact] - public async Task GetPreviousEventIdInStackTestAsync() { - await CreateDataAsync(); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - _logger.LogDebug("Actual order:"); - foreach (var t in _ids) - _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); - - _logger.LogDebug(""); - _logger.LogDebug("Sorted order:"); - var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); - foreach (var t in sortedIds) - _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); - - _logger.LogDebug(""); - _logger.LogDebug("Tests:"); - await RefreshDataAsync(); - Assert.Equal(_ids.Count, await _repository.CountAsync()); - for (int i = 0; i < sortedIds.Count; i++) { - _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); - if (i == 0) - Assert.Null((await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); - else - Assert.Equal(sortedIds[i - 1].Item1, (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); - } + [Fact] + public async Task GetPreviousEventIdInStackTestAsync() { + await CreateDataAsync(); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + _logger.LogDebug("Actual order:"); + foreach (var t in _ids) + _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); + + _logger.LogDebug(""); + _logger.LogDebug("Sorted order:"); + var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); + foreach (var t in sortedIds) + _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); + + _logger.LogDebug(""); + _logger.LogDebug("Tests:"); + await RefreshDataAsync(); + Assert.Equal(_ids.Count, await _repository.CountAsync()); + for (int i = 0; i < sortedIds.Count; i++) { + _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); + if (i == 0) + Assert.Null((await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); + else + Assert.Equal(sortedIds[i - 1].Item1, (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); } + } - [Fact] - public async Task GetNextEventIdInStackTestAsync() { - await CreateDataAsync(); - - _logger.LogDebug("Actual order:"); - foreach (var t in _ids) - _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); - - _logger.LogDebug(""); - _logger.LogDebug("Sorted order:"); - var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); - foreach (var t in sortedIds) - _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); - - _logger.LogDebug(""); - _logger.LogDebug("Tests:"); - await RefreshDataAsync(); - Assert.Equal(_ids.Count, await _repository.CountAsync()); - for (int i = 0; i < sortedIds.Count; i++) { - _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); - string nextId = (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Next; - if (i == sortedIds.Count - 1) - Assert.Null(nextId); - else - Assert.Equal(sortedIds[i + 1].Item1, nextId); - } + [Fact] + public async Task GetNextEventIdInStackTestAsync() { + await CreateDataAsync(); + + _logger.LogDebug("Actual order:"); + foreach (var t in _ids) + _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); + + _logger.LogDebug(""); + _logger.LogDebug("Sorted order:"); + var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); + foreach (var t in sortedIds) + _logger.LogDebug("{Id}: {Date}", t.Item1, t.Item2.ToLongTimeString()); + + _logger.LogDebug(""); + _logger.LogDebug("Tests:"); + await RefreshDataAsync(); + Assert.Equal(_ids.Count, await _repository.CountAsync()); + for (int i = 0; i < sortedIds.Count; i++) { + _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); + string nextId = (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Next; + if (i == sortedIds.Count - 1) + Assert.Null(nextId); + else + Assert.Equal(sortedIds[i + 1].Item1, nextId); } + } - [Fact] - public async Task CanGetPreviousAndNExtEventIdWithFilterTestAsync() { - await CreateDataAsync(); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); + [Fact] + public async Task CanGetPreviousAndNExtEventIdWithFilterTestAsync() { + await CreateDataAsync(); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); - var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); - var result = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[1].Item1); - Assert.Equal(sortedIds[0].Item1, result.Previous); - Assert.Equal(sortedIds[2].Item1, result.Next); - } + var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); + var result = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[1].Item1); + Assert.Equal(sortedIds[0].Item1, result.Previous); + Assert.Equal(sortedIds[2].Item1, result.Next); + } - [Fact] - public async Task GetByReferenceIdAsync() { - string referenceId = ObjectId.GenerateNewId().ToString(); - await _repository.AddAsync(EventData.GenerateEvents(3, TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, referenceId: referenceId).ToList()); + [Fact] + public async Task GetByReferenceIdAsync() { + string referenceId = ObjectId.GenerateNewId().ToString(); + await _repository.AddAsync(EventData.GenerateEvents(3, TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, referenceId: referenceId).ToList()); - await RefreshDataAsync(); - var results = await _repository.GetByReferenceIdAsync(TestConstants.ProjectId, referenceId); - Assert.True(results.Total > 0); - Assert.NotNull(results.Documents.First()); - Assert.Equal(referenceId, results.Documents.First().ReferenceId); - } + await RefreshDataAsync(); + var results = await _repository.GetByReferenceIdAsync(TestConstants.ProjectId, referenceId); + Assert.True(results.Total > 0); + Assert.NotNull(results.Documents.First()); + Assert.Equal(referenceId, results.Documents.First().ReferenceId); + } - [Fact] - public async Task GetOpenSessionsAsync() { - var firstEvent = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(35)); + [Fact] + public async Task GetOpenSessionsAsync() { + var firstEvent = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(35)); - var sessionLastActive35MinAgo = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession", generateData: false); - var sessionLastActive34MinAgo = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession2", generateData: false); - sessionLastActive34MinAgo.UpdateSessionStart(firstEvent.UtcDateTime.AddMinutes(1)); - var sessionLastActive5MinAgo = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession3", generateData: false); - sessionLastActive5MinAgo.UpdateSessionStart(firstEvent.UtcDateTime.AddMinutes(30)); - var closedSession = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession", generateData: false); - closedSession.UpdateSessionStart(firstEvent.UtcDateTime.AddMinutes(5), true); + var sessionLastActive35MinAgo = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession", generateData: false); + var sessionLastActive34MinAgo = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession2", generateData: false); + sessionLastActive34MinAgo.UpdateSessionStart(firstEvent.UtcDateTime.AddMinutes(1)); + var sessionLastActive5MinAgo = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession3", generateData: false); + sessionLastActive5MinAgo.UpdateSessionStart(firstEvent.UtcDateTime.AddMinutes(30)); + var closedSession = EventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, occurrenceDate: firstEvent, type: Event.KnownTypes.Session, sessionId: "opensession", generateData: false); + closedSession.UpdateSessionStart(firstEvent.UtcDateTime.AddMinutes(5), true); - var events = new List { + var events = new List { sessionLastActive35MinAgo, sessionLastActive34MinAgo, sessionLastActive5MinAgo, closedSession }; - await _repository.AddAsync(events); + await _repository.AddAsync(events); - await RefreshDataAsync(); - var results = await _repository.GetOpenSessionsAsync(SystemClock.UtcNow.SubtractMinutes(30)); - Assert.Equal(3, results.Total); - } + await RefreshDataAsync(); + var results = await _repository.GetOpenSessionsAsync(SystemClock.UtcNow.SubtractMinutes(30)); + Assert.Equal(3, results.Total); + } - [Fact] - public async Task RemoveAllByClientIpAndDateAsync() { - const string _clientIpAddress = "123.123.12.255"; - - const int NUMBER_OF_EVENTS_TO_CREATE = 50; - var events = EventData.GenerateEvents(NUMBER_OF_EVENTS_TO_CREATE, TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, startDate: SystemClock.UtcNow.SubtractDays(2), endDate: SystemClock.UtcNow).ToList(); - events.ForEach(e => e.AddRequestInfo(new RequestInfo { ClientIpAddress = _clientIpAddress })); - await _repository.AddAsync(events); - - await RefreshDataAsync(); - events = (await _repository.GetByProjectIdAsync(TestConstants.ProjectId, o => o.PageLimit(NUMBER_OF_EVENTS_TO_CREATE))).Documents.ToList(); - Assert.Equal(NUMBER_OF_EVENTS_TO_CREATE, events.Count); - events.ForEach(e => { - var ri = e.GetRequestInfo(); - Assert.NotNull(ri); - Assert.Equal(_clientIpAddress, ri.ClientIpAddress); - }); - - await _repository.RemoveAllAsync(TestConstants.OrganizationId, _clientIpAddress, SystemClock.UtcNow.SubtractDays(3), SystemClock.UtcNow.AddDays(2)); - - await RefreshDataAsync(); - events = (await _repository.GetByProjectIdAsync(TestConstants.ProjectId, o => o.PageLimit(NUMBER_OF_EVENTS_TO_CREATE))).Documents.ToList(); - Assert.Empty(events); - } + [Fact] + public async Task RemoveAllByClientIpAndDateAsync() { + const string _clientIpAddress = "123.123.12.255"; + + const int NUMBER_OF_EVENTS_TO_CREATE = 50; + var events = EventData.GenerateEvents(NUMBER_OF_EVENTS_TO_CREATE, TestConstants.OrganizationId, TestConstants.ProjectId, TestConstants.StackId2, startDate: SystemClock.UtcNow.SubtractDays(2), endDate: SystemClock.UtcNow).ToList(); + events.ForEach(e => e.AddRequestInfo(new RequestInfo { ClientIpAddress = _clientIpAddress })); + await _repository.AddAsync(events); + + await RefreshDataAsync(); + events = (await _repository.GetByProjectIdAsync(TestConstants.ProjectId, o => o.PageLimit(NUMBER_OF_EVENTS_TO_CREATE))).Documents.ToList(); + Assert.Equal(NUMBER_OF_EVENTS_TO_CREATE, events.Count); + events.ForEach(e => { + var ri = e.GetRequestInfo(); + Assert.NotNull(ri); + Assert.Equal(_clientIpAddress, ri.ClientIpAddress); + }); + + await _repository.RemoveAllAsync(TestConstants.OrganizationId, _clientIpAddress, SystemClock.UtcNow.SubtractDays(3), SystemClock.UtcNow.AddDays(2)); + + await RefreshDataAsync(); + events = (await _repository.GetByProjectIdAsync(TestConstants.ProjectId, o => o.PageLimit(NUMBER_OF_EVENTS_TO_CREATE))).Documents.ToList(); + Assert.Empty(events); + } - private readonly List> _ids = new List>(); + private readonly List> _ids = new List>(); - private async Task CreateDataAsync() { - var baseDate = SystemClock.UtcNow.SubtractHours(1); - var occurrenceDateStart = baseDate.AddMinutes(-30); - var occurrenceDateMid = baseDate; - var occurrenceDateEnd = baseDate.AddMinutes(30); + private async Task CreateDataAsync() { + var baseDate = SystemClock.UtcNow.SubtractHours(1); + var occurrenceDateStart = baseDate.AddMinutes(-30); + var occurrenceDateMid = baseDate; + var occurrenceDateEnd = baseDate.AddMinutes(30); - await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId)); + await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId)); - var occurrenceDates = new List { + var occurrenceDates = new List { occurrenceDateStart, occurrenceDateEnd, baseDate.AddMinutes(-10), @@ -240,12 +236,11 @@ private async Task CreateDataAsync() { occurrenceDateStart }; - foreach (var date in occurrenceDates) { - var ev = await _repository.AddAsync(EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, stackId: TestConstants.StackId, occurrenceDate: date)); - _ids.Add(Tuple.Create(ev.Id, date)); - } - - await RefreshDataAsync(); + foreach (var date in occurrenceDates) { + var ev = await _repository.AddAsync(EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, stackId: TestConstants.StackId, occurrenceDate: date)); + _ids.Add(Tuple.Create(ev.Id, date)); } + + await RefreshDataAsync(); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Repositories/IndexTests.cs b/tests/Exceptionless.Tests/Repositories/IndexTests.cs index 4cbaa609b7..6c66e1211b 100644 --- a/tests/Exceptionless.Tests/Repositories/IndexTests.cs +++ b/tests/Exceptionless.Tests/Repositories/IndexTests.cs @@ -1,31 +1,30 @@ -using System.Threading.Tasks; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Utility; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Repositories { - public sealed class IndexTests : TestWithServices { - private readonly ExceptionlessElasticConfiguration _configuration; - public IndexTests(ITestOutputHelper output) : base(output) { - _configuration = GetService(); - _configuration.DeleteIndexesAsync().GetAwaiter().GetResult(); - } +namespace Exceptionless.Tests.Repositories; - [Fact] - public Task CanCreateOrganizationIndex() { - return _configuration.Organizations.ConfigureAsync(); - } +public sealed class IndexTests : TestWithServices { + private readonly ExceptionlessElasticConfiguration _configuration; + public IndexTests(ITestOutputHelper output) : base(output) { + _configuration = GetService(); + _configuration.DeleteIndexesAsync().GetAwaiter().GetResult(); + } + + [Fact] + public Task CanCreateOrganizationIndex() { + return _configuration.Organizations.ConfigureAsync(); + } - [Fact] - public Task CanCreateStackIndex() { - return _configuration.Stacks.ConfigureAsync(); - } + [Fact] + public Task CanCreateStackIndex() { + return _configuration.Stacks.ConfigureAsync(); + } - [Fact] - public async Task CanCreateEventIndex() { - await _configuration.Events.ConfigureAsync(); - await _configuration.Events.EnsureIndexAsync(SystemClock.UtcNow); - } + [Fact] + public async Task CanCreateEventIndex() { + await _configuration.Events.ConfigureAsync(); + await _configuration.Events.EnsureIndexAsync(SystemClock.UtcNow); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs index 133f8e5445..6aa95954e6 100644 --- a/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/OrganizationRepositoryTests.cs @@ -1,6 +1,4 @@ -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Core.Repositories; using Exceptionless.Core.Models; using Foundatio.Caching; @@ -9,59 +7,59 @@ using Xunit.Abstractions; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Tests.Repositories { - public sealed class OrganizationRepositoryTests : IntegrationTestsBase { - private readonly InMemoryCacheClient _cache; - private readonly IOrganizationRepository _repository; - private readonly BillingPlans _plans; +namespace Exceptionless.Tests.Repositories; - public OrganizationRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - Log.SetLogLevel(LogLevel.Trace); - _cache = GetService() as InMemoryCacheClient; - _repository = GetService(); - _plans = GetService(); - } +public sealed class OrganizationRepositoryTests : IntegrationTestsBase { + private readonly InMemoryCacheClient _cache; + private readonly IOrganizationRepository _repository; + private readonly BillingPlans _plans; - [Fact] - public async Task CanCreateUpdateRemoveAsync() { - Assert.Equal(0, await _repository.CountAsync()); + public OrganizationRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + Log.SetLogLevel(LogLevel.Trace); + _cache = GetService() as InMemoryCacheClient; + _repository = GetService(); + _plans = GetService(); + } + + [Fact] + public async Task CanCreateUpdateRemoveAsync() { + Assert.Equal(0, await _repository.CountAsync()); - var organization = new Organization { Name = "Test Organization", PlanId = _plans.FreePlan.Id }; - Assert.Null(organization.Id); + var organization = new Organization { Name = "Test Organization", PlanId = _plans.FreePlan.Id }; + Assert.Null(organization.Id); - await _repository.AddAsync(organization); - await RefreshDataAsync(); - Assert.NotNull(organization.Id); + await _repository.AddAsync(organization); + await RefreshDataAsync(); + Assert.NotNull(organization.Id); - organization = await _repository.GetByIdAsync(organization.Id); - Assert.NotNull(organization); + organization = await _repository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); - organization.Name = "New organization"; - await _repository.SaveAsync(organization); + organization.Name = "New organization"; + await _repository.SaveAsync(organization); - await _repository.RemoveAsync(organization.Id); - } + await _repository.RemoveAsync(organization.Id); + } - [Fact] - public async Task CanAddAndGetByCachedAsync() { - var organization = new Organization { Name = "Test Organization", PlanId = _plans.FreePlan.Id }; - Assert.Null(organization.Id); + [Fact] + public async Task CanAddAndGetByCachedAsync() { + var organization = new Organization { Name = "Test Organization", PlanId = _plans.FreePlan.Id }; + Assert.Null(organization.Id); - Assert.Equal(0, _cache.Count); - await _repository.AddAsync(organization, o => o.Cache()); - await RefreshDataAsync(); - Assert.NotNull(organization.Id); - Assert.Equal(1, _cache.Count); + Assert.Equal(0, _cache.Count); + await _repository.AddAsync(organization, o => o.Cache()); + await RefreshDataAsync(); + Assert.NotNull(organization.Id); + Assert.Equal(1, _cache.Count); - await _cache.RemoveAllAsync(); - Assert.Equal(0, _cache.Count); - await _repository.GetByIdAsync(organization.Id, o => o.Cache()); - Assert.NotNull(organization.Id); - Assert.Equal(1, _cache.Count); + await _cache.RemoveAllAsync(); + Assert.Equal(0, _cache.Count); + await _repository.GetByIdAsync(organization.Id, o => o.Cache()); + Assert.NotNull(organization.Id); + Assert.Equal(1, _cache.Count); - await _repository.RemoveAllAsync(); - await RefreshDataAsync(); - Assert.Equal(0, _cache.Count); - } + await _repository.RemoveAllAsync(); + await RefreshDataAsync(); + Assert.Equal(0, _cache.Count); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs index 2c1342ee88..b36bfc314f 100644 --- a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; @@ -14,127 +10,127 @@ using Foundatio.Repositories; using Foundatio.Repositories.Models; -namespace Exceptionless.Tests.Repositories { - public sealed class ProjectRepositoryTests : IntegrationTestsBase { - private readonly ICacheClient _cache; - private readonly IProjectRepository _repository; - - public ProjectRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _cache = GetService(); - _repository = GetService(); - } - - [Fact] - public async Task IncrementNextSummaryEndOfDayTicksAsync() { - Assert.Equal(0, await _repository.CountAsync()); - - var project = await _repository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); - Assert.NotNull(project.Id); - Assert.Equal(1, await _repository.CountAsync()); - Assert.Equal(1, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); - - await _repository.IncrementNextSummaryEndOfDayTicksAsync(new[] { project }); - await RefreshDataAsync(); - - var updatedProject = await _repository.GetByIdAsync(project.Id); - // TODO: Modified date isn't currently updated in the update scripts. - //Assert.NotEqual(project.ModifiedUtc, updatedProject.ModifiedUtc); - Assert.Equal(project.NextSummaryEndOfDayTicks + TimeSpan.TicksPerDay, updatedProject.NextSummaryEndOfDayTicks); - - Assert.Equal(1, await _repository.CountAsync()); - Assert.Equal(1, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); - - var project2 = await _repository.AddAsync(ProjectData.GenerateProject(organizationId: project.OrganizationId), o => o.ImmediateConsistency()); - Assert.NotNull(project2.Id); - - Assert.Equal(2, await _repository.CountAsync()); - Assert.Equal(2, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); - - await _repository.RemoveAsync(project2, o => o.Notifications(false).ImmediateConsistency()); - Assert.Equal(1, await _repository.CountAsync()); - Assert.Equal(1, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); - } - - [Fact] - public async Task GetByOrganizationIdsAsync() { - var project1 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, name: "One"), o => o.ImmediateConsistency()); - var project2 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.SuspendedProjectId, organizationId: TestConstants.OrganizationId, name: "Two"), o => o.ImmediateConsistency()); - - var results = await _repository.GetByOrganizationIdsAsync(new[] { project1.OrganizationId, TestConstants.OrganizationId2 }); - Assert.NotNull(results); - Assert.Equal(2, results.Documents.Count); - - results = await _repository.GetByOrganizationIdsAsync(new[] { project1.OrganizationId }); - Assert.NotNull(results); - Assert.Equal(2, results.Documents.Count); - - results = await _repository.GetByOrganizationIdsAsync(new[] { TestConstants.OrganizationId2 }); - Assert.NotNull(results); - Assert.Empty(results.Documents); - - await _repository.RemoveAsync(project2.Id, o => o.Notifications(false).ImmediateConsistency()); - results = await _repository.GetByOrganizationIdsAsync(new[] { project1.OrganizationId }); - Assert.NotNull(results); - Assert.Single(results.Documents); - await _repository.RemoveAllAsync(o => o.Notifications(false)); - } - - [Fact] - public async Task GetByFilterAsyncAsync() { - var organizations = OrganizationData.GenerateSampleOrganizations(GetService(), GetService()); - var organization1 = organizations.Single(o => String.Equals(o.Id, TestConstants.OrganizationId)); - var organization2 = organizations.Single(o => String.Equals(o.Id, TestConstants.OrganizationId2)); - - var project1 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.ProjectId, organizationId: organization1.Id, name: "One"), o => o.ImmediateConsistency()); - var project2 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.SuspendedProjectId, organizationId: organization1.Id, name: "Two"), o => o.ImmediateConsistency()); - - var results = await _repository.GetByFilterAsync(new AppFilter(organizations), null, null); - Assert.NotNull(results); - Assert.Equal(2, results.Documents.Count); - - results = await _repository.GetByFilterAsync(new AppFilter(organization1), null, null); - Assert.NotNull(results); - Assert.Equal(2, results.Documents.Count); - - results = await _repository.GetByFilterAsync(new AppFilter(organization2), null, null); - Assert.NotNull(results); - Assert.Empty(results.Documents); - - results = await _repository.GetByFilterAsync(new AppFilter(organization1), "name:one", null); - Assert.NotNull(results); - Assert.Single(results.Documents); - Assert.Equal(project1.Name, results.Documents.Single().Name); - - await _repository.RemoveAsync(project2.Id, o => o.Notifications(false).ImmediateConsistency()); - results = await _repository.GetByFilterAsync(new AppFilter(organization1), null, null); - Assert.NotNull(results); - Assert.Single(results.Documents); - await _repository.RemoveAllAsync(o => o.Notifications(false)); - } - - [Fact] - public async Task CanRoundTripWithCaching() { - var token = new SlackToken { - AccessToken = "MY KEY", - IncomingWebhook = new SlackToken.IncomingWebHook { - Url = "MY Url" - } - }; - - var project = ProjectData.GenerateSampleProject(); - project.Data[Project.KnownDataKeys.SlackToken] = token; - - await _repository.AddAsync(project, o => o.ImmediateConsistency()); - var actual = await _repository.GetByIdAsync(project.Id, o => o.Cache()); - Assert.Equal(project.Name, actual?.Name); - var actualToken = actual.GetSlackToken(); - Assert.Equal(token.AccessToken, actualToken?.AccessToken); - - var actualCache = await _cache.GetAsync>>("Project:" + project.Id); - Assert.True(actualCache.HasValue); - Assert.Equal(project.Name, actualCache.Value.Single().Document.Name); - var actualCacheToken = actual.GetSlackToken(); - Assert.Equal(token.AccessToken, actualCacheToken?.AccessToken); - } +namespace Exceptionless.Tests.Repositories; + +public sealed class ProjectRepositoryTests : IntegrationTestsBase { + private readonly ICacheClient _cache; + private readonly IProjectRepository _repository; + + public ProjectRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _cache = GetService(); + _repository = GetService(); + } + + [Fact] + public async Task IncrementNextSummaryEndOfDayTicksAsync() { + Assert.Equal(0, await _repository.CountAsync()); + + var project = await _repository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); + Assert.NotNull(project.Id); + Assert.Equal(1, await _repository.CountAsync()); + Assert.Equal(1, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); + + await _repository.IncrementNextSummaryEndOfDayTicksAsync(new[] { project }); + await RefreshDataAsync(); + + var updatedProject = await _repository.GetByIdAsync(project.Id); + // TODO: Modified date isn't currently updated in the update scripts. + //Assert.NotEqual(project.ModifiedUtc, updatedProject.ModifiedUtc); + Assert.Equal(project.NextSummaryEndOfDayTicks + TimeSpan.TicksPerDay, updatedProject.NextSummaryEndOfDayTicks); + + Assert.Equal(1, await _repository.CountAsync()); + Assert.Equal(1, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); + + var project2 = await _repository.AddAsync(ProjectData.GenerateProject(organizationId: project.OrganizationId), o => o.ImmediateConsistency()); + Assert.NotNull(project2.Id); + + Assert.Equal(2, await _repository.CountAsync()); + Assert.Equal(2, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); + + await _repository.RemoveAsync(project2, o => o.Notifications(false).ImmediateConsistency()); + Assert.Equal(1, await _repository.CountAsync()); + Assert.Equal(1, await _repository.GetCountByOrganizationIdAsync(project.OrganizationId)); + } + + [Fact] + public async Task GetByOrganizationIdsAsync() { + var project1 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, name: "One"), o => o.ImmediateConsistency()); + var project2 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.SuspendedProjectId, organizationId: TestConstants.OrganizationId, name: "Two"), o => o.ImmediateConsistency()); + + var results = await _repository.GetByOrganizationIdsAsync(new[] { project1.OrganizationId, TestConstants.OrganizationId2 }); + Assert.NotNull(results); + Assert.Equal(2, results.Documents.Count); + + results = await _repository.GetByOrganizationIdsAsync(new[] { project1.OrganizationId }); + Assert.NotNull(results); + Assert.Equal(2, results.Documents.Count); + + results = await _repository.GetByOrganizationIdsAsync(new[] { TestConstants.OrganizationId2 }); + Assert.NotNull(results); + Assert.Empty(results.Documents); + + await _repository.RemoveAsync(project2.Id, o => o.Notifications(false).ImmediateConsistency()); + results = await _repository.GetByOrganizationIdsAsync(new[] { project1.OrganizationId }); + Assert.NotNull(results); + Assert.Single(results.Documents); + await _repository.RemoveAllAsync(o => o.Notifications(false)); + } + + [Fact] + public async Task GetByFilterAsyncAsync() { + var organizations = OrganizationData.GenerateSampleOrganizations(GetService(), GetService()); + var organization1 = organizations.Single(o => String.Equals(o.Id, TestConstants.OrganizationId)); + var organization2 = organizations.Single(o => String.Equals(o.Id, TestConstants.OrganizationId2)); + + var project1 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.ProjectId, organizationId: organization1.Id, name: "One"), o => o.ImmediateConsistency()); + var project2 = await _repository.AddAsync(ProjectData.GenerateProject(id: TestConstants.SuspendedProjectId, organizationId: organization1.Id, name: "Two"), o => o.ImmediateConsistency()); + + var results = await _repository.GetByFilterAsync(new AppFilter(organizations), null, null); + Assert.NotNull(results); + Assert.Equal(2, results.Documents.Count); + + results = await _repository.GetByFilterAsync(new AppFilter(organization1), null, null); + Assert.NotNull(results); + Assert.Equal(2, results.Documents.Count); + + results = await _repository.GetByFilterAsync(new AppFilter(organization2), null, null); + Assert.NotNull(results); + Assert.Empty(results.Documents); + + results = await _repository.GetByFilterAsync(new AppFilter(organization1), "name:one", null); + Assert.NotNull(results); + Assert.Single(results.Documents); + Assert.Equal(project1.Name, results.Documents.Single().Name); + + await _repository.RemoveAsync(project2.Id, o => o.Notifications(false).ImmediateConsistency()); + results = await _repository.GetByFilterAsync(new AppFilter(organization1), null, null); + Assert.NotNull(results); + Assert.Single(results.Documents); + await _repository.RemoveAllAsync(o => o.Notifications(false)); + } + + [Fact] + public async Task CanRoundTripWithCaching() { + var token = new SlackToken { + AccessToken = "MY KEY", + IncomingWebhook = new SlackToken.IncomingWebHook { + Url = "MY Url" + } + }; + + var project = ProjectData.GenerateSampleProject(); + project.Data[Project.KnownDataKeys.SlackToken] = token; + + await _repository.AddAsync(project, o => o.ImmediateConsistency()); + var actual = await _repository.GetByIdAsync(project.Id, o => o.Cache()); + Assert.Equal(project.Name, actual?.Name); + var actualToken = actual.GetSlackToken(); + Assert.Equal(token.AccessToken, actualToken?.AccessToken); + + var actualCache = await _cache.GetAsync>>("Project:" + project.Id); + Assert.True(actualCache.HasValue); + Assert.Equal(project.Name, actualCache.Value.Single().Document.Name); + var actualCacheToken = actual.GetSlackToken(); + Assert.Equal(token.AccessToken, actualCacheToken?.AccessToken); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs index 097958f9e0..6e15ea0a53 100644 --- a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; @@ -17,182 +13,182 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Repositories { - public sealed class StackRepositoryTests : IntegrationTestsBase { - private readonly InMemoryCacheClient _cache; - private readonly IStackRepository _repository; - - public StackRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _cache = GetService() as InMemoryCacheClient; - _repository = GetService(); - } - - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - var service = GetService(); - await service.CreateDataAsync(); - } - - [Fact] - public async Task CanGetByStatus() { - Log.MinimumLevel = Microsoft.Extensions.Logging.LogLevel.Trace; - var organizationRepository = GetService(); - var organization = await organizationRepository.GetByIdAsync(TestConstants.OrganizationId); - Assert.NotNull(organization); - - await StackData.CreateSearchDataAsync(_repository, GetService(), true); - - var appFilter = new AppFilter(organization); - var stackIds = await _repository.GetIdsByQueryAsync(q => q.AppFilter(appFilter).FilterExpression("status:open OR status:regressed").DateRange(DateTime.UtcNow.AddDays(-5), DateTime.UtcNow), o => o.PageLimit(o.GetMaxLimit())); - Assert.Equal(2, stackIds.Total); - } - - [Fact] - public async Task CanGetByStackHashAsync() { - long count = _cache.Count; - long hits = _cache.Hits; - long misses = _cache.Misses; - - var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: SystemClock.UtcNow.SubtractMonths(1)), o => o.Cache()); - Assert.NotNull(stack?.Id); - Assert.Equal(count + 2, _cache.Count); - Assert.Equal(hits, _cache.Hits); - Assert.Equal(misses, _cache.Misses); - - var result = await _repository.GetStackBySignatureHashAsync(stack.ProjectId, stack.SignatureHash); - Assert.Equal(stack.ToJson(), result.ToJson()); - Assert.Equal(count + 2, _cache.Count); - Assert.Equal(hits + 1, _cache.Hits); - Assert.Equal(misses, _cache.Misses); - } - - [Fact] - public async Task CanGetByFixedAsync() { - var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId), o => o.ImmediateConsistency()); - - var results = await _repository.FindAsync(q => q.FilterExpression("fixed:true")); - Assert.NotNull(results); - Assert.Equal(0, results.Total); - - results = await _repository.FindAsync(q => q.FilterExpression("fixed:false")); - Assert.NotNull(results); - Assert.Equal(1, results.Total); - Assert.False(results.Documents.Single().Status == Core.Models.StackStatus.Regressed); - Assert.Null(results.Documents.Single().DateFixed); - - stack.MarkFixed(); - await _repository.SaveAsync(stack, o => o.ImmediateConsistency()); - - results = await _repository.FindAsync(q => q.FilterExpression("fixed:true")); - Assert.NotNull(results); - Assert.Equal(1, results.Total); - Assert.False(results.Documents.Single().Status == Core.Models.StackStatus.Regressed); - Assert.NotNull(results.Documents.Single().DateFixed); - - results = await _repository.FindAsync(q => q.FilterExpression("fixed:false")); - Assert.NotNull(results); - Assert.Equal(0, results.Total); - } - - [Fact] - public async Task CanMarkAsRegressedAsync() { - var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: SystemClock.UtcNow.SubtractMonths(1)), o => o.ImmediateConsistency()); - Assert.NotNull(stack); - Assert.False(stack.Status == Core.Models.StackStatus.Regressed); - Assert.NotNull(stack.DateFixed); - - await _repository.MarkAsRegressedAsync(stack.Id); - - stack = await _repository.GetByIdAsync(stack.Id); - Assert.NotNull(stack); - Assert.True(stack.Status == Core.Models.StackStatus.Regressed); - Assert.NotNull(stack.DateFixed); - } - - [Fact] - public async Task CanIncrementEventCounterAsync() { - var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId), o => o.ImmediateConsistency()); - Assert.NotNull(stack); - Assert.Equal(0, stack.TotalOccurrences); - Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); - Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); - Assert.NotEqual(DateTime.MinValue, stack.CreatedUtc); - Assert.NotEqual(DateTime.MinValue, stack.UpdatedUtc); - Assert.Equal(stack.CreatedUtc, stack.UpdatedUtc); - var updatedUtc = stack.UpdatedUtc; - - var utcNow = SystemClock.UtcNow; - await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow, utcNow, 1); - - stack = await _repository.GetByIdAsync(stack.Id); - Assert.Equal(1, stack.TotalOccurrences); - Assert.Equal(utcNow, stack.FirstOccurrence); - Assert.Equal(utcNow, stack.LastOccurrence); - Assert.Equal(updatedUtc, stack.CreatedUtc); - Assert.True(updatedUtc.IsBefore(stack.UpdatedUtc), $"Previous {updatedUtc}, Current: {stack.UpdatedUtc}"); - - await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.SubtractDays(1), utcNow.SubtractDays(1), 1); - - stack = await _repository.GetByIdAsync(stack.Id); - Assert.Equal(2, stack.TotalOccurrences); - Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); - Assert.Equal(utcNow, stack.LastOccurrence); - - await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.AddDays(1), utcNow.AddDays(1), 1); - - stack = await _repository.GetByIdAsync(stack.Id); - Assert.Equal(3, stack.TotalOccurrences); - Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); - Assert.Equal(utcNow.AddDays(1), stack.LastOccurrence); - } - - [Fact] - public async Task CanFindManyAsync() { - await _repository.AddAsync(StackData.GenerateSampleStacks(), o => o.ImmediateConsistency()); - - var stacks = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(1).PageLimit(1)); - Assert.NotNull(stacks); - Assert.Equal(3, stacks.Total); - Assert.Equal(1, stacks.Documents.Count); - - var stacks2 = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(2).PageLimit(1)); - Assert.NotNull(stacks); - Assert.Equal(1, stacks.Documents.Count); - - Assert.NotEqual(stacks.Documents.First().Id, stacks2.Documents.First().Id); - - stacks = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId); - Assert.NotNull(stacks); - Assert.Equal(3, stacks.Documents.Count); - - await _repository.RemoveAsync(stacks.Documents, o => o.ImmediateConsistency()); - Assert.Equal(0, await _repository.CountAsync()); - } - - [Fact] - public async Task GetStacksForCleanupAsync() { - var openStack10DaysOldWithReference = StackData.GenerateStack(id: TestConstants.StackId3, utcLastOccurrence: SystemClock.UtcNow.SubtractDays(10), status: StackStatus.Open); - openStack10DaysOldWithReference.References.Add("test"); - - await _repository.AddAsync(new List { +namespace Exceptionless.Tests.Repositories; + +public sealed class StackRepositoryTests : IntegrationTestsBase { + private readonly InMemoryCacheClient _cache; + private readonly IStackRepository _repository; + + public StackRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _cache = GetService() as InMemoryCacheClient; + _repository = GetService(); + } + + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + var service = GetService(); + await service.CreateDataAsync(); + } + + [Fact] + public async Task CanGetByStatus() { + Log.MinimumLevel = Microsoft.Extensions.Logging.LogLevel.Trace; + var organizationRepository = GetService(); + var organization = await organizationRepository.GetByIdAsync(TestConstants.OrganizationId); + Assert.NotNull(organization); + + await StackData.CreateSearchDataAsync(_repository, GetService(), true); + + var appFilter = new AppFilter(organization); + var stackIds = await _repository.GetIdsByQueryAsync(q => q.AppFilter(appFilter).FilterExpression("status:open OR status:regressed").DateRange(DateTime.UtcNow.AddDays(-5), DateTime.UtcNow), o => o.PageLimit(o.GetMaxLimit())); + Assert.Equal(2, stackIds.Total); + } + + [Fact] + public async Task CanGetByStackHashAsync() { + long count = _cache.Count; + long hits = _cache.Hits; + long misses = _cache.Misses; + + var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: SystemClock.UtcNow.SubtractMonths(1)), o => o.Cache()); + Assert.NotNull(stack?.Id); + Assert.Equal(count + 2, _cache.Count); + Assert.Equal(hits, _cache.Hits); + Assert.Equal(misses, _cache.Misses); + + var result = await _repository.GetStackBySignatureHashAsync(stack.ProjectId, stack.SignatureHash); + Assert.Equal(stack.ToJson(), result.ToJson()); + Assert.Equal(count + 2, _cache.Count); + Assert.Equal(hits + 1, _cache.Hits); + Assert.Equal(misses, _cache.Misses); + } + + [Fact] + public async Task CanGetByFixedAsync() { + var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId), o => o.ImmediateConsistency()); + + var results = await _repository.FindAsync(q => q.FilterExpression("fixed:true")); + Assert.NotNull(results); + Assert.Equal(0, results.Total); + + results = await _repository.FindAsync(q => q.FilterExpression("fixed:false")); + Assert.NotNull(results); + Assert.Equal(1, results.Total); + Assert.False(results.Documents.Single().Status == Core.Models.StackStatus.Regressed); + Assert.Null(results.Documents.Single().DateFixed); + + stack.MarkFixed(); + await _repository.SaveAsync(stack, o => o.ImmediateConsistency()); + + results = await _repository.FindAsync(q => q.FilterExpression("fixed:true")); + Assert.NotNull(results); + Assert.Equal(1, results.Total); + Assert.False(results.Documents.Single().Status == Core.Models.StackStatus.Regressed); + Assert.NotNull(results.Documents.Single().DateFixed); + + results = await _repository.FindAsync(q => q.FilterExpression("fixed:false")); + Assert.NotNull(results); + Assert.Equal(0, results.Total); + } + + [Fact] + public async Task CanMarkAsRegressedAsync() { + var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: SystemClock.UtcNow.SubtractMonths(1)), o => o.ImmediateConsistency()); + Assert.NotNull(stack); + Assert.False(stack.Status == Core.Models.StackStatus.Regressed); + Assert.NotNull(stack.DateFixed); + + await _repository.MarkAsRegressedAsync(stack.Id); + + stack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(stack); + Assert.True(stack.Status == Core.Models.StackStatus.Regressed); + Assert.NotNull(stack.DateFixed); + } + + [Fact] + public async Task CanIncrementEventCounterAsync() { + var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId), o => o.ImmediateConsistency()); + Assert.NotNull(stack); + Assert.Equal(0, stack.TotalOccurrences); + Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); + Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); + Assert.NotEqual(DateTime.MinValue, stack.CreatedUtc); + Assert.NotEqual(DateTime.MinValue, stack.UpdatedUtc); + Assert.Equal(stack.CreatedUtc, stack.UpdatedUtc); + var updatedUtc = stack.UpdatedUtc; + + var utcNow = SystemClock.UtcNow; + await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow, utcNow, 1); + + stack = await _repository.GetByIdAsync(stack.Id); + Assert.Equal(1, stack.TotalOccurrences); + Assert.Equal(utcNow, stack.FirstOccurrence); + Assert.Equal(utcNow, stack.LastOccurrence); + Assert.Equal(updatedUtc, stack.CreatedUtc); + Assert.True(updatedUtc.IsBefore(stack.UpdatedUtc), $"Previous {updatedUtc}, Current: {stack.UpdatedUtc}"); + + await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.SubtractDays(1), utcNow.SubtractDays(1), 1); + + stack = await _repository.GetByIdAsync(stack.Id); + Assert.Equal(2, stack.TotalOccurrences); + Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); + Assert.Equal(utcNow, stack.LastOccurrence); + + await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.AddDays(1), utcNow.AddDays(1), 1); + + stack = await _repository.GetByIdAsync(stack.Id); + Assert.Equal(3, stack.TotalOccurrences); + Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); + Assert.Equal(utcNow.AddDays(1), stack.LastOccurrence); + } + + [Fact] + public async Task CanFindManyAsync() { + await _repository.AddAsync(StackData.GenerateSampleStacks(), o => o.ImmediateConsistency()); + + var stacks = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(1).PageLimit(1)); + Assert.NotNull(stacks); + Assert.Equal(3, stacks.Total); + Assert.Equal(1, stacks.Documents.Count); + + var stacks2 = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageNumber(2).PageLimit(1)); + Assert.NotNull(stacks); + Assert.Equal(1, stacks.Documents.Count); + + Assert.NotEqual(stacks.Documents.First().Id, stacks2.Documents.First().Id); + + stacks = await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId); + Assert.NotNull(stacks); + Assert.Equal(3, stacks.Documents.Count); + + await _repository.RemoveAsync(stacks.Documents, o => o.ImmediateConsistency()); + Assert.Equal(0, await _repository.CountAsync()); + } + + [Fact] + public async Task GetStacksForCleanupAsync() { + var openStack10DaysOldWithReference = StackData.GenerateStack(id: TestConstants.StackId3, utcLastOccurrence: SystemClock.UtcNow.SubtractDays(10), status: StackStatus.Open); + openStack10DaysOldWithReference.References.Add("test"); + + await _repository.AddAsync(new List { StackData.GenerateStack(id: TestConstants.StackId, utcLastOccurrence: SystemClock.UtcNow.SubtractDays(5), status: StackStatus.Open), StackData.GenerateStack(id: TestConstants.StackId2, utcLastOccurrence: SystemClock.UtcNow.SubtractDays(10), status: StackStatus.Open), openStack10DaysOldWithReference, StackData.GenerateStack(id: TestConstants.StackId4, utcLastOccurrence: SystemClock.UtcNow.SubtractDays(10), status: StackStatus.Fixed) }, o => o.ImmediateConsistency()); - var stacks = await _repository.GetStacksForCleanupAsync(TestConstants.OrganizationId, SystemClock.UtcNow.SubtractDays(8)); - Assert.NotNull(stacks); - Assert.Equal(1, stacks.Total); - Assert.Equal(1, stacks.Documents.Count); - Assert.Equal(TestConstants.StackId2, stacks.Documents.Single().Id); - - stacks = await _repository.GetStacksForCleanupAsync(TestConstants.OrganizationId, SystemClock.UtcNow.SubtractDays(1)); - Assert.NotNull(stacks); - Assert.Equal(2, stacks.Total); - Assert.Equal(2, stacks.Documents.Count); - Assert.NotNull(stacks.Documents.SingleOrDefault(s => String.Equals(s.Id, TestConstants.StackId))); - Assert.NotNull(stacks.Documents.SingleOrDefault(s => String.Equals(s.Id, TestConstants.StackId2))); - } + var stacks = await _repository.GetStacksForCleanupAsync(TestConstants.OrganizationId, SystemClock.UtcNow.SubtractDays(8)); + Assert.NotNull(stacks); + Assert.Equal(1, stacks.Total); + Assert.Equal(1, stacks.Documents.Count); + Assert.Equal(TestConstants.StackId2, stacks.Documents.Single().Id); + + stacks = await _repository.GetStacksForCleanupAsync(TestConstants.OrganizationId, SystemClock.UtcNow.SubtractDays(1)); + Assert.NotNull(stacks); + Assert.Equal(2, stacks.Total); + Assert.Equal(2, stacks.Documents.Count); + Assert.NotNull(stacks.Documents.SingleOrDefault(s => String.Equals(s.Id, TestConstants.StackId))); + Assert.NotNull(stacks.Documents.SingleOrDefault(s => String.Equals(s.Id, TestConstants.StackId2))); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs index 8b08d987bc..ad086f761e 100644 --- a/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/TokenRepositoryTests.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; using Exceptionless.Tests.Utility; @@ -10,17 +8,18 @@ using Xunit.Abstractions; using Token = Exceptionless.Core.Models.Token; -namespace Exceptionless.Tests.Repositories { - public sealed class TokenRepositoryTests : IntegrationTestsBase { - private readonly ITokenRepository _repository; +namespace Exceptionless.Tests.Repositories; - public TokenRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _repository = GetService(); - } +public sealed class TokenRepositoryTests : IntegrationTestsBase { + private readonly ITokenRepository _repository; - [Fact] - public async Task GetAndRemoveByProjectIdOrDefaultProjectIdAsync() { - await _repository.AddAsync(new List { + public TokenRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _repository = GetService(); + } + + [Fact] + public async Task GetAndRemoveByProjectIdOrDefaultProjectIdAsync() { + await _repository.AddAsync(new List { new Token { OrganizationId = TestConstants.OrganizationId, CreatedUtc = SystemClock.UtcNow, UpdatedUtc = SystemClock.UtcNow, Id = StringExtensions.GetNewToken() }, new Token { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, CreatedUtc = SystemClock.UtcNow, UpdatedUtc = SystemClock.UtcNow, Id = StringExtensions.GetNewToken() }, new Token { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, CreatedUtc = SystemClock.UtcNow, UpdatedUtc = SystemClock.UtcNow, Id = StringExtensions.GetNewToken() }, @@ -29,47 +28,46 @@ await _repository.AddAsync(new List { new Token { DefaultProjectId = TestConstants.ProjectIdWithNoRoles, UserId = TestConstants.UserId, CreatedUtc = SystemClock.UtcNow, UpdatedUtc = SystemClock.UtcNow, Id = StringExtensions.GetNewToken() } }, o => o.ImmediateConsistency()); - Assert.Equal(5, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); - Assert.Equal(2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); - Assert.Equal(3, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); + Assert.Equal(5, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); + Assert.Equal(2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); + Assert.Equal(3, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); - await _repository.RemoveAllByProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectId); + await _repository.RemoveAllByProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectId); - await RefreshDataAsync(); - Assert.Equal(4, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); - Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); - Assert.Equal(3, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); + await RefreshDataAsync(); + Assert.Equal(4, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); + Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); + Assert.Equal(3, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); - await _repository.RemoveAllByProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectIdWithNoRoles); + await _repository.RemoveAllByProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectIdWithNoRoles); - await RefreshDataAsync(); - Assert.Equal(3, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); - Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); - Assert.Equal(2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); + await RefreshDataAsync(); + Assert.Equal(3, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); + Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); + Assert.Equal(2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); - await _repository.RemoveAllByOrganizationIdAsync(TestConstants.OrganizationId); + await _repository.RemoveAllByOrganizationIdAsync(TestConstants.OrganizationId); - await RefreshDataAsync(); - Assert.Equal(0, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); - Assert.Equal(0, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); - Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); - } + await RefreshDataAsync(); + Assert.Equal(0, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); + Assert.Equal(0, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); + Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); + } - [Fact] - public async Task GetAndRemoveByByUserIdAsync() { - await _repository.AddAsync(new List { + [Fact] + public async Task GetAndRemoveByByUserIdAsync() { + await _repository.AddAsync(new List { new Token { OrganizationId = TestConstants.OrganizationId, CreatedUtc = SystemClock.UtcNow, UpdatedUtc = SystemClock.UtcNow, Id = StringExtensions.GetNewToken(), Type = TokenType.Access }, new Token { OrganizationId = TestConstants.OrganizationId, UserId = TestConstants.UserId, CreatedUtc = SystemClock.UtcNow, UpdatedUtc = SystemClock.UtcNow, Id = StringExtensions.GetNewToken(), Type = TokenType.Access }, new Token { OrganizationId = TestConstants.OrganizationId, UserId = TestConstants.UserId, CreatedUtc = SystemClock.UtcNow, UpdatedUtc = SystemClock.UtcNow, Id = StringExtensions.GetNewToken(), Type = TokenType.Authentication } }, o => o.ImmediateConsistency()); - Assert.Equal(1, (await _repository.GetByTypeAndUserIdAsync(TokenType.Access, TestConstants.UserId)).Total); - Assert.Equal(1, (await _repository.GetByTypeAndUserIdAsync(TokenType.Authentication, TestConstants.UserId)).Total); + Assert.Equal(1, (await _repository.GetByTypeAndUserIdAsync(TokenType.Access, TestConstants.UserId)).Total); + Assert.Equal(1, (await _repository.GetByTypeAndUserIdAsync(TokenType.Authentication, TestConstants.UserId)).Total); - await _repository.RemoveAllByUserIdAsync(TestConstants.UserId); - await RefreshDataAsync(); - Assert.Equal(0, (await _repository.GetByTypeAndUserIdAsync(TokenType.Access, TestConstants.UserId)).Total); - Assert.Equal(0, (await _repository.GetByTypeAndUserIdAsync(TokenType.Authentication, TestConstants.UserId)).Total); - Assert.Equal(1, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); - } + await _repository.RemoveAllByUserIdAsync(TestConstants.UserId); + await RefreshDataAsync(); + Assert.Equal(0, (await _repository.GetByTypeAndUserIdAsync(TokenType.Access, TestConstants.UserId)).Total); + Assert.Equal(0, (await _repository.GetByTypeAndUserIdAsync(TokenType.Authentication, TestConstants.UserId)).Total); + Assert.Equal(1, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs index a1ae1e07b7..340dbaee7e 100644 --- a/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs @@ -1,40 +1,38 @@ -using System.Linq; -using System.Threading.Tasks; -using Exceptionless.Core.Repositories; +using Exceptionless.Core.Repositories; using Exceptionless.Core.Models; using Exceptionless.Tests.Utility; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Repositories { - public sealed class WebHookRepositoryTests : IntegrationTestsBase { - private readonly IWebHookRepository _repository; +namespace Exceptionless.Tests.Repositories; - public WebHookRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _repository = GetService(); - } +public sealed class WebHookRepositoryTests : IntegrationTestsBase { + private readonly IWebHookRepository _repository; - [Fact] - public async Task GetByOrganizationIdOrProjectIdAsync() { - await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); - await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); - await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); + public WebHookRepositoryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _repository = GetService(); + } + + [Fact] + public async Task GetByOrganizationIdOrProjectIdAsync() { + await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); + await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); + await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); - await RefreshDataAsync(); - Assert.Equal(3, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); - Assert.Equal(2, (await _repository.GetByOrganizationIdOrProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectId)).Total); - Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); - Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); - } + await RefreshDataAsync(); + Assert.Equal(3, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total); + Assert.Equal(2, (await _repository.GetByOrganizationIdOrProjectIdAsync(TestConstants.OrganizationId, TestConstants.ProjectId)).Total); + Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Total); + Assert.Equal(1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Total); + } - [Fact] - public async Task CanSaveWebHookVersionAsync() { - await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version1 }); - await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); + [Fact] + public async Task CanSaveWebHookVersionAsync() { + await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version1 }); + await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 }); - await RefreshDataAsync(); - Assert.Equal(WebHook.KnownVersions.Version1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Documents.First().Version); - Assert.Equal(WebHook.KnownVersions.Version2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Documents.First().Version); - } + await RefreshDataAsync(); + Assert.Equal(WebHook.KnownVersions.Version1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Documents.First().Version); + Assert.Equal(WebHook.KnownVersions.Version2, (await _repository.GetByProjectIdAsync(TestConstants.ProjectIdWithNoRoles)).Documents.First().Version); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Search/EventIndexTests.cs b/tests/Exceptionless.Tests/Search/EventIndexTests.cs index 99f3835329..995fbbe560 100644 --- a/tests/Exceptionless.Tests/Search/EventIndexTests.cs +++ b/tests/Exceptionless.Tests/Search/EventIndexTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Exceptionless.Core.Queries.Validation; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.EventParser; @@ -13,404 +11,404 @@ using Xunit.Abstractions; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Tests.Repositories { - public sealed class EventIndexTests : IntegrationTestsBase { - private readonly IEventRepository _repository; - private readonly PersistentEventQueryValidator _validator; - - public EventIndexTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - TestSystemClock.SetFrozenTime(new DateTime(2015, 2, 13, 0, 0, 0, DateTimeKind.Utc)); - _repository = GetService(); - _validator = GetService(); - } - - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await EventData.CreateSearchDataAsync(GetService(), _repository, GetService()); - } - - [Theory] - [InlineData("54dbc16ca0f5c61398427b00", 1)] // Id - [InlineData("\"GET /Print\"", 3)] // Source - [InlineData("\"Invalid hash. Parameter name: hash\"", 1)] // Message - [InlineData("\"Blake Niemyjski\"", 1)] // Tags - [InlineData("502", 1)] // Error.Code - [InlineData("NullReferenceException", 1)] // Error.Type - [InlineData("System.NullReferenceException", 1)] // Error.Type - [InlineData("Exception", 3)] // Error.TargetType - [InlineData("System.Web.ThreadContext.AssociateWithCurrentThread", 1)] // Error.TargetMethod - [InlineData("\"/apple-touch-icon.png\"", 1)] // RequestInfo.Path - [InlineData("my custom description", 1)] // UserDescription.Description - [InlineData("test@exceptionless.com", 1)] // UserDescription.EmailAddress - [InlineData("TEST@exceptionless.com", 1)] // UserDescription.EmailAddress wrong case - [InlineData("exceptionless.com", 2)] // UserDescription.EmailAddress partial - [InlineData("example@exceptionless.com", 2)] // UserInfo.Identity - [InlineData("test user", 1)] // UserInfo.Name - public async Task GetByAllFieldAsync(string search, int count) { - var result = await GetByFilterAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("000000000000000000000000", 0)] - [InlineData("54dbc16ca0f5c61398427b00", 1)] - [InlineData("54dbc16ca0f5c61398427b01", 1)] - public async Task GetAsync(string id, int count) { - var result = await GetByFilterAsync("id:" + id); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("000000000", 0)] - [InlineData("876554321", 1)] - public async Task GetByReferenceIdAsync(string id, int count) { - var result = await GetByFilterAsync("reference:" + id); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("_exists_:submission", 1)] - [InlineData("NOT _exists_:submission", 6)] - [InlineData("submission:UnobservedTaskException", 1)] - public async Task GetBySubmissionMethodAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("\"GET /Print\"", 3)] - public async Task GetBySourceAsync(string source, int count) { - var result = await GetByFilterAsync("source:" + source); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("Error", 1)] - public async Task GetByLevelAsync(string level, int count) { - var result = await GetByFilterAsync("level:" + level); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("\"2014-12-09T17:28:44.966\"", 1)] - [InlineData("\"2014-12-09T17:28:44.966+00:00\"", 1)] - [InlineData("\"2015-02-11T20:54:04.3457274+00:00\"", 1)] - public async Task GetByDateAsync(string date, int count) { - var result = await GetByFilterAsync("date:" + date); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData(false, 6)] - [InlineData(true, 1)] - public async Task GetByFirstAsync(bool first, int count) { - var result = await GetByFilterAsync("first:" + first.ToString().ToLowerInvariant()); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("message:\"Invalid hash. Parameter name: hash\"", 1)] - public async Task GetByMessageAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("NOT _exists_:value", 6)] - [InlineData("_exists_:value", 1)] - [InlineData("value:1", 1)] - [InlineData("value:>0", 1)] - [InlineData("value:0", 0)] - [InlineData("value:<0", 0)] - [InlineData("value:>0 AND value:<=10", 1)] - [InlineData("value:[1..10]", 1)] - public async Task GetByValueAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("1", 3)] - [InlineData("1.2", 2)] - [InlineData("1.2.3", 1)] - [InlineData("1.2.3.0", 1)] - [InlineData("0001.0002.0003.0000", 1)] - [InlineData("3", 1)] - [InlineData("3.2", 1)] - [InlineData("3.2.1", 1)] - [InlineData("3.2.1.0", 0)] - [InlineData("3.2.1-beta1", 1)] - // TODO: Add support for version ranges. - //[InlineData("<1", 0)] - //[InlineData(">1", 3)] - //[InlineData("<5", 3)] - //[InlineData("(>1 AND <4.0)", 2)] - public async Task GetByVersionAsync(string version, int count) { - var result = await GetByFilterAsync("version:" + version); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("machine:SERVER-01", 1)] - [InlineData("machine:\"SERVER-01\"", 1)] - public async Task GetByMachineAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("ip:\"2001:0:4137:9e76:cfd:33a0:5198:3a66\"", 1)] - [InlineData("ip:192.168.0.243", 1)] - [InlineData("ip:192.168.0.88", 1)] - [InlineData("ip:10.0.0.208", 1)] - [InlineData("ip:172.10.0.61", 1)] - public async Task GetByIPAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("x86", 0)] - [InlineData("x64", 1)] - public async Task GetByArchitectureAsync(string architecture, int count) { - var result = await GetByFilterAsync("architecture:" + architecture); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("Mozilla", 2)] - [InlineData("\"Mozilla/5.0\"", 2)] - [InlineData("5.0", 2)] - [InlineData("Macintosh", 1)] - public async Task GetByUserAgentAsync(string userAgent, int count) { - var result = await GetByFilterAsync("useragent:" + userAgent); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("\"/user.aspx\"", 1)] - [InlineData("path:\"/user.aspx\"", 1)] - public async Task GetByPathAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("browser:Chrome", 2)] - [InlineData("browser:\"Chrome Mobile\"", 1)] - public async Task GetByBrowserAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("browser.version:39.0.2171", 1)] - [InlineData("browser.version:26.0.1410", 1)] - [InlineData("browser.version:\"26.0.1410\"", 1)] - public async Task GetByBrowserVersionAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("39", 1)] - [InlineData("26", 1)] - public async Task GetByBrowserMajorVersionAsync(string browserMajorVersion, int count) { - var result = await GetByFilterAsync("browser.major:" + browserMajorVersion); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("device:Huawei", 1)] - [InlineData("device:\"Huawei U8686\"", 1)] - public async Task GetByDeviceAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("os:Android", 1)] - [InlineData("os:Mac", 1)] - [InlineData("os:\"Mac OS X\"", 1)] - [InlineData("os:\"Microsoft Windows Server\"", 1)] - [InlineData("os:\"Microsoft Windows Server 2012 R2 Standard\"", 1)] - public async Task GetByOSAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("os.version:4.1.1", 1)] - [InlineData("os.version:10.10.1", 1)] - [InlineData("os.version:\"10.10\"", 0)] - [InlineData("os.version:\"10.10.1\"", 1)] - public async Task GetByOSVersionAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("4", 1)] - [InlineData("10", 1)] - public async Task GetByOSMajorVersionAsync(string osMajorVersion, int count) { - var result = await GetByFilterAsync("os.major:" + osMajorVersion); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("bot:false", 1)] - [InlineData("-bot:true", 6)] - [InlineData("bot:true", 1)] - public async Task GetByBotAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("502", 1)] - [InlineData("error.code:\"-1\"", 1)] - [InlineData("error.code:502", 1)] - [InlineData("error.code:5000", 0)] - public async Task GetByErrorCodeAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("\"Invalid hash. Parameter name: hash\"", 1)] - [InlineData("error.message:\"Invalid hash. Parameter name: hash\"", 1)] - [InlineData("error.message:\"A Task's exception(s)\"", 1)] - public async Task GetByErrorMessageAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("AssociateWithCurrentThread", 1)] - [InlineData("System.Web.ThreadContext.AssociateWithCurrentThread", 1)] - [InlineData("error.targetmethod:System", 1)] - [InlineData("error.targetmethod:System.Web", 1)] - [InlineData("error.targetmethod:System.Web.ThreadContext", 1)] - [InlineData("error.targetmethod:ThreadContext", 1)] - [InlineData("error.targetmethod:AssociateWithCurrentThread", 1)] - [InlineData("error.targetmethod:System.Web.ThreadContext.AssociateWithCurrentThread", 1)] - [InlineData("error.targetmethod:\"System.Web.ThreadContext.AssociateWithCurrentThread()\"", 1)] - public async Task GetByErrorTargetMethodAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("Exception", 3)] - [InlineData("error.targettype:Exception", 1)] - [InlineData("error.targettype:\"System.Exception\"", 1)] - public async Task GetByErrorTargetTypeAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("NullReferenceException", 1)] - [InlineData("System.NullReferenceException", 1)] - [InlineData("error.type:NullReferenceException", 1)] - [InlineData("error.type:System.NullReferenceException", 1)] - [InlineData("error.type:System.Exception", 1)] - public async Task GetByErrorTypeAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("My-User-Identity", 2)] - [InlineData("user:My-User-Identity", 1)] - [InlineData("example@exceptionless.com", 2)] - [InlineData("user:example@exceptionless.com", 1)] - [InlineData("user:exceptionless.com", 2)] - [InlineData("example", 2)] - public async Task GetByUserAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("Blake", 1)] - [InlineData("user.name:Blake", 1)] - public async Task GetByUserNameAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("test@exceptionless.com", 1)] - [InlineData("user.email:test@exceptionless.com", 1)] - [InlineData("user.email:exceptionless.com", 2)] - public async Task GetByUserEmailAddressAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("\"my custom description\"", 1)] - [InlineData("user.description:\"my custom description\"", 1)] - public async Task GetByUserDescriptionAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("data.anumber:12", 1)] - [InlineData("data.anumber:>11", 1)] - [InlineData("data.anumber2:>11", 0)] - [InlineData("data.FriendlyErrorIdentifier:\"Foo-7967BB\"", 1)] - [InlineData("data.FriendlyErrorIdentifier:Foo-7967BB", 1)] - [InlineData("data.some-date:>2015-01-01", 1)] - [InlineData("data.some-date:<2015-01-01", 0)] - [InlineData("data.EntityId:88df3f71-c888-42a7-12c4-08d67c7d888", 1)] - [InlineData("data.UserId:\"3db4f3c2-88c7-4e37-b692-3a545036088\"", 1)] - public async Task GetByCustomDataAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - private async Task> GetByFilterAsync(string filter, string search = null) { - var result = await _validator.ValidateQueryAsync(filter); - Assert.True(result.IsValid); - Log.SetLogLevel(LogLevel.Trace); - return await _repository.FindAsync(q => q.FilterExpression(filter)); - } +namespace Exceptionless.Tests.Repositories; + +public sealed class EventIndexTests : IntegrationTestsBase { + private readonly IEventRepository _repository; + private readonly PersistentEventQueryValidator _validator; + + public EventIndexTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + TestSystemClock.SetFrozenTime(new DateTime(2015, 2, 13, 0, 0, 0, DateTimeKind.Utc)); + _repository = GetService(); + _validator = GetService(); + } + + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await EventData.CreateSearchDataAsync(GetService(), _repository, GetService()); + } + + [Theory] + [InlineData("54dbc16ca0f5c61398427b00", 1)] // Id + [InlineData("\"GET /Print\"", 3)] // Source + [InlineData("\"Invalid hash. Parameter name: hash\"", 1)] // Message + [InlineData("\"Blake Niemyjski\"", 1)] // Tags + [InlineData("502", 1)] // Error.Code + [InlineData("NullReferenceException", 1)] // Error.Type + [InlineData("System.NullReferenceException", 1)] // Error.Type + [InlineData("Exception", 3)] // Error.TargetType + [InlineData("System.Web.ThreadContext.AssociateWithCurrentThread", 1)] // Error.TargetMethod + [InlineData("\"/apple-touch-icon.png\"", 1)] // RequestInfo.Path + [InlineData("my custom description", 1)] // UserDescription.Description + [InlineData("test@exceptionless.com", 1)] // UserDescription.EmailAddress + [InlineData("TEST@exceptionless.com", 1)] // UserDescription.EmailAddress wrong case + [InlineData("exceptionless.com", 2)] // UserDescription.EmailAddress partial + [InlineData("example@exceptionless.com", 2)] // UserInfo.Identity + [InlineData("test user", 1)] // UserInfo.Name + public async Task GetByAllFieldAsync(string search, int count) { + var result = await GetByFilterAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("000000000000000000000000", 0)] + [InlineData("54dbc16ca0f5c61398427b00", 1)] + [InlineData("54dbc16ca0f5c61398427b01", 1)] + public async Task GetAsync(string id, int count) { + var result = await GetByFilterAsync("id:" + id); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("000000000", 0)] + [InlineData("876554321", 1)] + public async Task GetByReferenceIdAsync(string id, int count) { + var result = await GetByFilterAsync("reference:" + id); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("_exists_:submission", 1)] + [InlineData("NOT _exists_:submission", 6)] + [InlineData("submission:UnobservedTaskException", 1)] + public async Task GetBySubmissionMethodAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("\"GET /Print\"", 3)] + public async Task GetBySourceAsync(string source, int count) { + var result = await GetByFilterAsync("source:" + source); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("Error", 1)] + public async Task GetByLevelAsync(string level, int count) { + var result = await GetByFilterAsync("level:" + level); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("\"2014-12-09T17:28:44.966\"", 1)] + [InlineData("\"2014-12-09T17:28:44.966+00:00\"", 1)] + [InlineData("\"2015-02-11T20:54:04.3457274+00:00\"", 1)] + public async Task GetByDateAsync(string date, int count) { + var result = await GetByFilterAsync("date:" + date); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData(false, 6)] + [InlineData(true, 1)] + public async Task GetByFirstAsync(bool first, int count) { + var result = await GetByFilterAsync("first:" + first.ToString().ToLowerInvariant()); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("message:\"Invalid hash. Parameter name: hash\"", 1)] + public async Task GetByMessageAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("NOT _exists_:value", 6)] + [InlineData("_exists_:value", 1)] + [InlineData("value:1", 1)] + [InlineData("value:>0", 1)] + [InlineData("value:0", 0)] + [InlineData("value:<0", 0)] + [InlineData("value:>0 AND value:<=10", 1)] + [InlineData("value:[1..10]", 1)] + public async Task GetByValueAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("1", 3)] + [InlineData("1.2", 2)] + [InlineData("1.2.3", 1)] + [InlineData("1.2.3.0", 1)] + [InlineData("0001.0002.0003.0000", 1)] + [InlineData("3", 1)] + [InlineData("3.2", 1)] + [InlineData("3.2.1", 1)] + [InlineData("3.2.1.0", 0)] + [InlineData("3.2.1-beta1", 1)] + // TODO: Add support for version ranges. + //[InlineData("<1", 0)] + //[InlineData(">1", 3)] + //[InlineData("<5", 3)] + //[InlineData("(>1 AND <4.0)", 2)] + public async Task GetByVersionAsync(string version, int count) { + var result = await GetByFilterAsync("version:" + version); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("machine:SERVER-01", 1)] + [InlineData("machine:\"SERVER-01\"", 1)] + public async Task GetByMachineAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("ip:\"2001:0:4137:9e76:cfd:33a0:5198:3a66\"", 1)] + [InlineData("ip:192.168.0.243", 1)] + [InlineData("ip:192.168.0.88", 1)] + [InlineData("ip:10.0.0.208", 1)] + [InlineData("ip:172.10.0.61", 1)] + public async Task GetByIPAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("x86", 0)] + [InlineData("x64", 1)] + public async Task GetByArchitectureAsync(string architecture, int count) { + var result = await GetByFilterAsync("architecture:" + architecture); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("Mozilla", 2)] + [InlineData("\"Mozilla/5.0\"", 2)] + [InlineData("5.0", 2)] + [InlineData("Macintosh", 1)] + public async Task GetByUserAgentAsync(string userAgent, int count) { + var result = await GetByFilterAsync("useragent:" + userAgent); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("\"/user.aspx\"", 1)] + [InlineData("path:\"/user.aspx\"", 1)] + public async Task GetByPathAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("browser:Chrome", 2)] + [InlineData("browser:\"Chrome Mobile\"", 1)] + public async Task GetByBrowserAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("browser.version:39.0.2171", 1)] + [InlineData("browser.version:26.0.1410", 1)] + [InlineData("browser.version:\"26.0.1410\"", 1)] + public async Task GetByBrowserVersionAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("39", 1)] + [InlineData("26", 1)] + public async Task GetByBrowserMajorVersionAsync(string browserMajorVersion, int count) { + var result = await GetByFilterAsync("browser.major:" + browserMajorVersion); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("device:Huawei", 1)] + [InlineData("device:\"Huawei U8686\"", 1)] + public async Task GetByDeviceAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("os:Android", 1)] + [InlineData("os:Mac", 1)] + [InlineData("os:\"Mac OS X\"", 1)] + [InlineData("os:\"Microsoft Windows Server\"", 1)] + [InlineData("os:\"Microsoft Windows Server 2012 R2 Standard\"", 1)] + public async Task GetByOSAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("os.version:4.1.1", 1)] + [InlineData("os.version:10.10.1", 1)] + [InlineData("os.version:\"10.10\"", 0)] + [InlineData("os.version:\"10.10.1\"", 1)] + public async Task GetByOSVersionAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("4", 1)] + [InlineData("10", 1)] + public async Task GetByOSMajorVersionAsync(string osMajorVersion, int count) { + var result = await GetByFilterAsync("os.major:" + osMajorVersion); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("bot:false", 1)] + [InlineData("-bot:true", 6)] + [InlineData("bot:true", 1)] + public async Task GetByBotAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("502", 1)] + [InlineData("error.code:\"-1\"", 1)] + [InlineData("error.code:502", 1)] + [InlineData("error.code:5000", 0)] + public async Task GetByErrorCodeAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("\"Invalid hash. Parameter name: hash\"", 1)] + [InlineData("error.message:\"Invalid hash. Parameter name: hash\"", 1)] + [InlineData("error.message:\"A Task's exception(s)\"", 1)] + public async Task GetByErrorMessageAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("AssociateWithCurrentThread", 1)] + [InlineData("System.Web.ThreadContext.AssociateWithCurrentThread", 1)] + [InlineData("error.targetmethod:System", 1)] + [InlineData("error.targetmethod:System.Web", 1)] + [InlineData("error.targetmethod:System.Web.ThreadContext", 1)] + [InlineData("error.targetmethod:ThreadContext", 1)] + [InlineData("error.targetmethod:AssociateWithCurrentThread", 1)] + [InlineData("error.targetmethod:System.Web.ThreadContext.AssociateWithCurrentThread", 1)] + [InlineData("error.targetmethod:\"System.Web.ThreadContext.AssociateWithCurrentThread()\"", 1)] + public async Task GetByErrorTargetMethodAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("Exception", 3)] + [InlineData("error.targettype:Exception", 1)] + [InlineData("error.targettype:\"System.Exception\"", 1)] + public async Task GetByErrorTargetTypeAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("NullReferenceException", 1)] + [InlineData("System.NullReferenceException", 1)] + [InlineData("error.type:NullReferenceException", 1)] + [InlineData("error.type:System.NullReferenceException", 1)] + [InlineData("error.type:System.Exception", 1)] + public async Task GetByErrorTypeAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("My-User-Identity", 2)] + [InlineData("user:My-User-Identity", 1)] + [InlineData("example@exceptionless.com", 2)] + [InlineData("user:example@exceptionless.com", 1)] + [InlineData("user:exceptionless.com", 2)] + [InlineData("example", 2)] + public async Task GetByUserAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("Blake", 1)] + [InlineData("user.name:Blake", 1)] + public async Task GetByUserNameAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("test@exceptionless.com", 1)] + [InlineData("user.email:test@exceptionless.com", 1)] + [InlineData("user.email:exceptionless.com", 2)] + public async Task GetByUserEmailAddressAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("\"my custom description\"", 1)] + [InlineData("user.description:\"my custom description\"", 1)] + public async Task GetByUserDescriptionAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("data.anumber:12", 1)] + [InlineData("data.anumber:>11", 1)] + [InlineData("data.anumber2:>11", 0)] + [InlineData("data.FriendlyErrorIdentifier:\"Foo-7967BB\"", 1)] + [InlineData("data.FriendlyErrorIdentifier:Foo-7967BB", 1)] + [InlineData("data.some-date:>2015-01-01", 1)] + [InlineData("data.some-date:<2015-01-01", 0)] + [InlineData("data.EntityId:88df3f71-c888-42a7-12c4-08d67c7d888", 1)] + [InlineData("data.UserId:\"3db4f3c2-88c7-4e37-b692-3a545036088\"", 1)] + public async Task GetByCustomDataAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + private async Task> GetByFilterAsync(string filter, string search = null) { + var result = await _validator.ValidateQueryAsync(filter); + Assert.True(result.IsValid); + Log.SetLogLevel(LogLevel.Trace); + return await _repository.FindAsync(q => q.FilterExpression(filter)); } } diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs index dc967ccc78..bafec2d2f3 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; using Exceptionless.Core.Repositories.Queries; @@ -8,106 +5,105 @@ using Exceptionless.Tests.Utility; using Foundatio.Parsers.ElasticQueries.Visitors; using Foundatio.Repositories; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Search { - public class EventStackFilterQueryTests : IntegrationTestsBase { - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; - private static bool _isTestDataGenerated; - - public EventStackFilterQueryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _stackRepository = GetService(); - _eventRepository = GetService(); - } - - protected override async Task ResetDataAsync() { - if (_isTestDataGenerated) - return; - - await base.ResetDataAsync(); - await CreateDataAsync(d => { - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).Source("Namespace.ClassName").UserIdentity("test@test.com", "Test Test"); - d.Event().Type(Event.KnownTypes.Log).FreeProject().Status(StackStatus.Open); - d.Event().StackId(TestConstants.StackId).Type(Event.KnownTypes.Log).Status(StackStatus.Open); - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).Deleted(); - - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Fixed); - - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Ignored); - d.Event().Type(Event.KnownTypes.Session).Status(StackStatus.Ignored); - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).ReferenceId("referenceId"); - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).SessionId("sessionId"); - - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Discarded); - d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Regressed); - }); - - _isTestDataGenerated = true; - } - - [Theory] - [InlineData("status:open OR status:regressed", 7)] - [InlineData("NOT (status:open OR status:regressed)", 4)] - [InlineData("status:fixed", 1)] - [InlineData("NOT status:fixed", 10)] - [InlineData("stack:" + TestConstants.StackId, 1)] - [InlineData("stack_id:" + TestConstants.StackId, 1)] - [InlineData("-stack:" + TestConstants.StackId, 10)] - [InlineData("stack:" + TestConstants.StackId + " (status:open OR status:regressed)", 1)] - [InlineData("is_fixed:true", 1)] - [InlineData("is_regressed:true", 1)] - [InlineData("is_hidden:true", 4)] - [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed)", 6, 4)] - public async Task VerifyStackFilter(string filter, int expected, int? expectedInverted = null) { - Log.SetLogLevel(LogLevel.Trace); - - long totalStacks = await _stackRepository.CountAsync(o => o.IncludeSoftDeletes()); - - var ctx = new ElasticQueryVisitorContext(); - var stackFilter = await new EventStackFilter().GetStackFilterAsync(filter, ctx); - _logger.LogInformation("Finding Filter: {Filter}", stackFilter.Filter); - var stacks = await _stackRepository.FindAsync(q => q.FilterExpression(stackFilter.Filter), o => o.SoftDeleteMode(SoftDeleteQueryMode.All).PageLimit(1000)); - Assert.Equal(expected, stacks.Total); - - _logger.LogInformation("Finding Inverted Filter: {Filter}", stackFilter.InvertedFilter); - var invertedStacks = await _stackRepository.FindAsync(q => q.FilterExpression(stackFilter.InvertedFilter), o => o.SoftDeleteMode(SoftDeleteQueryMode.All).PageLimit(1000)); - long expectedInvert = expectedInverted ?? totalStacks - expected; - Assert.Equal(expectedInvert, invertedStacks.Total); - - var stackIds = new HashSet(stacks.Hits.Select(h => h.Id)); - var invertedStackIds = new HashSet(invertedStacks.Hits.Select(h => h.Id)); - - Assert.Empty(stackIds.Intersect(invertedStackIds)); - } - - [Theory] - [InlineData("status:open OR status:regressed", 6)] - [InlineData("NOT (status:open OR status:regressed)", 4)] - [InlineData("status:fixed", 1)] - [InlineData("NOT status:fixed", 9)] - [InlineData("stack:" + TestConstants.StackId, 1)] - [InlineData("stack_id:" + TestConstants.StackId, 1)] - [InlineData("-stack:" + TestConstants.StackId, 9)] - [InlineData("stack:" + TestConstants.StackId + " (status:open OR status:regressed)", 1)] - [InlineData("is_fixed:true", 1)] - [InlineData("is_regressed:true", 1)] - [InlineData("is_hidden:true", 4)] - [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed)", 5)] - [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed) ref.session:sessionId", 1)] - [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed) reference:referenceId", 1)] - public async Task VerifyEventFilter(string filter, int expected) { - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - - var events = await _eventRepository.FindAsync(q => q.FilterExpression(filter).EnforceEventStackFilter(), o => o.PageLimit(1000)); - Assert.Equal(expected, events.Total); - - var invertedEvents = await _eventRepository.FindAsync(q => q.FilterExpression("@!" + filter).EnforceEventStackFilter(), o => o.PageLimit(1000)); - Assert.Equal(expected, invertedEvents.Total); - } +namespace Exceptionless.Tests.Search; + +public class EventStackFilterQueryTests : IntegrationTestsBase { + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private static bool _isTestDataGenerated; + + public EventStackFilterQueryTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _stackRepository = GetService(); + _eventRepository = GetService(); + } + + protected override async Task ResetDataAsync() { + if (_isTestDataGenerated) + return; + + await base.ResetDataAsync(); + await CreateDataAsync(d => { + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).Source("Namespace.ClassName").UserIdentity("test@test.com", "Test Test"); + d.Event().Type(Event.KnownTypes.Log).FreeProject().Status(StackStatus.Open); + d.Event().StackId(TestConstants.StackId).Type(Event.KnownTypes.Log).Status(StackStatus.Open); + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).Deleted(); + + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Fixed); + + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Ignored); + d.Event().Type(Event.KnownTypes.Session).Status(StackStatus.Ignored); + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).ReferenceId("referenceId"); + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Open).SessionId("sessionId"); + + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Discarded); + d.Event().Type(Event.KnownTypes.Log).Status(StackStatus.Regressed); + }); + + _isTestDataGenerated = true; + } + + [Theory] + [InlineData("status:open OR status:regressed", 7)] + [InlineData("NOT (status:open OR status:regressed)", 4)] + [InlineData("status:fixed", 1)] + [InlineData("NOT status:fixed", 10)] + [InlineData("stack:" + TestConstants.StackId, 1)] + [InlineData("stack_id:" + TestConstants.StackId, 1)] + [InlineData("-stack:" + TestConstants.StackId, 10)] + [InlineData("stack:" + TestConstants.StackId + " (status:open OR status:regressed)", 1)] + [InlineData("is_fixed:true", 1)] + [InlineData("is_regressed:true", 1)] + [InlineData("is_hidden:true", 4)] + [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed)", 6, 4)] + public async Task VerifyStackFilter(string filter, int expected, int? expectedInverted = null) { + Log.SetLogLevel(LogLevel.Trace); + + long totalStacks = await _stackRepository.CountAsync(o => o.IncludeSoftDeletes()); + + var ctx = new ElasticQueryVisitorContext(); + var stackFilter = await new EventStackFilter().GetStackFilterAsync(filter, ctx); + _logger.LogInformation("Finding Filter: {Filter}", stackFilter.Filter); + var stacks = await _stackRepository.FindAsync(q => q.FilterExpression(stackFilter.Filter), o => o.SoftDeleteMode(SoftDeleteQueryMode.All).PageLimit(1000)); + Assert.Equal(expected, stacks.Total); + + _logger.LogInformation("Finding Inverted Filter: {Filter}", stackFilter.InvertedFilter); + var invertedStacks = await _stackRepository.FindAsync(q => q.FilterExpression(stackFilter.InvertedFilter), o => o.SoftDeleteMode(SoftDeleteQueryMode.All).PageLimit(1000)); + long expectedInvert = expectedInverted ?? totalStacks - expected; + Assert.Equal(expectedInvert, invertedStacks.Total); + + var stackIds = new HashSet(stacks.Hits.Select(h => h.Id)); + var invertedStackIds = new HashSet(invertedStacks.Hits.Select(h => h.Id)); + + Assert.Empty(stackIds.Intersect(invertedStackIds)); + } + + [Theory] + [InlineData("status:open OR status:regressed", 6)] + [InlineData("NOT (status:open OR status:regressed)", 4)] + [InlineData("status:fixed", 1)] + [InlineData("NOT status:fixed", 9)] + [InlineData("stack:" + TestConstants.StackId, 1)] + [InlineData("stack_id:" + TestConstants.StackId, 1)] + [InlineData("-stack:" + TestConstants.StackId, 9)] + [InlineData("stack:" + TestConstants.StackId + " (status:open OR status:regressed)", 1)] + [InlineData("is_fixed:true", 1)] + [InlineData("is_regressed:true", 1)] + [InlineData("is_hidden:true", 4)] + [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed)", 5)] + [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed) ref.session:sessionId", 1)] + [InlineData("project:" + SampleDataService.TEST_PROJECT_ID + " (status:open OR status:regressed) reference:referenceId", 1)] + public async Task VerifyEventFilter(string filter, int expected) { + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + + var events = await _eventRepository.FindAsync(q => q.FilterExpression(filter).EnforceEventStackFilter(), o => o.PageLimit(1000)); + Assert.Equal(expected, events.Total); + + var invertedEvents = await _eventRepository.FindAsync(q => q.FilterExpression("@!" + filter).EnforceEventStackFilter(), o => o.PageLimit(1000)); + Assert.Equal(expected, invertedEvents.Total); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs index d89ffb1bc0..6b6abad6f8 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs @@ -1,156 +1,153 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Repositories.Queries; +using Exceptionless.Core.Repositories.Queries; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Search { - public class EventStackFilterQueryVisitorTests : TestWithServices { - public EventStackFilterQueryVisitorTests(ITestOutputHelper output) : base(output) { } +namespace Exceptionless.Tests.Search; - [Theory] - [MemberData(nameof(FilterData.TestCases), MemberType = typeof(FilterData))] - public async Task CanBuildStackFilter(FilterScenario scenario) { - Log.SetLogLevel(Microsoft.Extensions.Logging.LogLevel.Trace); +public class EventStackFilterQueryVisitorTests : TestWithServices { + public EventStackFilterQueryVisitorTests(ITestOutputHelper output) : base(output) { } - var eventStackFilter = new EventStackFilter(); - var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); - Assert.Equal(scenario.Stack, stackFilter.Filter.Trim()); - } + [Theory] + [MemberData(nameof(FilterData.TestCases), MemberType = typeof(FilterData))] + public async Task CanBuildStackFilter(FilterScenario scenario) { + Log.SetLogLevel(Microsoft.Extensions.Logging.LogLevel.Trace); - [Theory] - [MemberData(nameof(FilterData.TestCases), MemberType = typeof(FilterData))] - public async Task CanBuildInvertedStackFilter(FilterScenario scenario) { - Log.SetLogLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + var eventStackFilter = new EventStackFilter(); + var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); + Assert.Equal(scenario.Stack, stackFilter.Filter.Trim()); + } + + [Theory] + [MemberData(nameof(FilterData.TestCases), MemberType = typeof(FilterData))] + public async Task CanBuildInvertedStackFilter(FilterScenario scenario) { + Log.SetLogLevel(Microsoft.Extensions.Logging.LogLevel.Trace); - var eventStackFilter = new EventStackFilter(); - var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); - Assert.Equal(scenario.InvertedStack, stackFilter.InvertedFilter.Trim()); - } + var eventStackFilter = new EventStackFilter(); + var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); + Assert.Equal(scenario.InvertedStack, stackFilter.InvertedFilter.Trim()); + } - [Theory] - [MemberData(nameof(FilterData.TestCases), MemberType = typeof(FilterData))] - public async Task CanBuildEventFilter(FilterScenario scenario) { - Log.SetLogLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + [Theory] + [MemberData(nameof(FilterData.TestCases), MemberType = typeof(FilterData))] + public async Task CanBuildEventFilter(FilterScenario scenario) { + Log.SetLogLevel(Microsoft.Extensions.Logging.LogLevel.Trace); - var eventStackFilter = new EventStackFilter(); - var stackFilter = await eventStackFilter.GetEventFilterAsync(scenario.Source); - Assert.Equal(scenario.Event, stackFilter.Trim()); - } + var eventStackFilter = new EventStackFilter(); + var stackFilter = await eventStackFilter.GetEventFilterAsync(scenario.Source); + Assert.Equal(scenario.Event, stackFilter.Trim()); } +} - public class FilterData { - public static IEnumerable TestCases() { - yield return new object[] { new FilterScenario { +public class FilterData { + public static IEnumerable TestCases() { + yield return new object[] { new FilterScenario { Source = "blah", Stack = "", InvertedStack = "", Event = "blah" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "status:fixed", Stack = "status:fixed", InvertedStack = "NOT status:fixed", Event = "" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "is_fixed:true", Stack = "status:fixed", InvertedStack = "NOT status:fixed", Event = "" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "is_regressed:true", Stack = "status:regressed", InvertedStack = "NOT status:regressed", Event = "" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "is_hidden:true", Stack = "NOT (status:open OR status:regressed)", InvertedStack = "(status:open OR status:regressed)", Event = "" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "is_hidden:false", Stack = "(status:open OR status:regressed)", InvertedStack = "NOT (status:open OR status:regressed)", Event = "" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "blah:true (status:fixed OR status:open)", Stack = "(status:fixed OR status:open)", InvertedStack = "NOT (status:fixed OR status:open)", Event = "blah:true" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "blah:true", Stack = "", InvertedStack = "", Event = "blah:true" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "type:session", Stack = "type:session", InvertedStack = "type:session", Event = "type:session" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "(organization:123 AND type:log) AND (blah:true (status:fixed OR status:open))", Stack = "(organization:123 AND type:log) AND (status:fixed OR status:open)", InvertedStack = "(organization:123 AND type:log) AND NOT (status:fixed OR status:open)", Event = "(organization:123 AND type:log) AND blah:true" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "project:123 (status:open OR status:regressed) (ref.session:5f3dce2668de920001466635)", Stack = "project:123 (status:open OR status:regressed)", InvertedStack = "project:123 NOT (status:open OR status:regressed)", Event = "project:123 ref.session:5f3dce2668de920001466635" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "project:123 (status:open OR status:regressed) (ref.session:5f3dce2668de920001466635 OR project:234)", Stack = "project:123 (status:open OR status:regressed) project:234", InvertedStack = "project:123 NOT (status:open OR status:regressed) project:234", Event = "project:123 (ref.session:5f3dce2668de920001466635 OR project:234)" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "first_occurrence:[1608854400000 TO 1609188757249] AND (status:open OR status:regressed)", Stack = "first_occurrence:[1608854400000 TO 1609188757249] AND (status:open OR status:regressed)", InvertedStack = "NOT (first_occurrence:[1608854400000 TO 1609188757249] AND (status:open OR status:regressed))", Event = "" }}; - yield return new object[] { new FilterScenario { + yield return new object[] { new FilterScenario { Source = "project:537650f3b77efe23a47914f4 first_occurrence:[1609459200000 TO 1609730450521] (status:open OR status:regressed)", Stack = "project:537650f3b77efe23a47914f4 first_occurrence:[1609459200000 TO 1609730450521] (status:open OR status:regressed)", InvertedStack = "project:537650f3b77efe23a47914f4 NOT (first_occurrence:[1609459200000 TO 1609730450521] (status:open OR status:regressed))", Event = "project:537650f3b77efe23a47914f4" }}; - } } +} - public class FilterScenario : IXunitSerializable { - public string Source { get; set; } = String.Empty; - public string Stack { get; set; } = String.Empty; - public string InvertedStack { get; set; } = String.Empty; - public string Event { get; set; } = String.Empty; +public class FilterScenario : IXunitSerializable { + public string Source { get; set; } = String.Empty; + public string Stack { get; set; } = String.Empty; + public string InvertedStack { get; set; } = String.Empty; + public string Event { get; set; } = String.Empty; - public override string ToString() { - return $"Source: \"{Source}\" Stack: \"{Stack}\" InvertedStack: \"{InvertedStack}\" Event: \"{Event}\""; - } + public override string ToString() { + return $"Source: \"{Source}\" Stack: \"{Stack}\" InvertedStack: \"{InvertedStack}\" Event: \"{Event}\""; + } - public void Deserialize(IXunitSerializationInfo info) { - var value = JsonConvert.DeserializeObject(info.GetValue("objValue")); - Source = value.Source; - Stack = value.Stack; - InvertedStack = value.InvertedStack; - Event = value.Event; - } + public void Deserialize(IXunitSerializationInfo info) { + var value = JsonConvert.DeserializeObject(info.GetValue("objValue")); + Source = value.Source; + Stack = value.Stack; + InvertedStack = value.InvertedStack; + Event = value.Event; + } - public void Serialize(IXunitSerializationInfo info) { - var json = JsonConvert.SerializeObject(this); - info.AddValue("objValue", json); - } + public void Serialize(IXunitSerializationInfo info) { + var json = JsonConvert.SerializeObject(this); + info.AddValue("objValue", json); } } diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs index 3d40fde8f5..ab71e4fe82 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Exceptionless.Core.Repositories; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.EventParser; @@ -8,71 +6,70 @@ using Foundatio.Repositories; using Foundatio.Repositories.Models; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Repositories { - public sealed class EventStackFilterTests : IntegrationTestsBase { - private readonly IStackRepository _stackRepository; - private readonly IEventRepository _eventRepository; +namespace Exceptionless.Tests.Repositories; - public EventStackFilterTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - TestSystemClock.SetFrozenTime(new DateTime(2015, 2, 13, 0, 0, 0, DateTimeKind.Utc)); - _stackRepository = GetService(); - _eventRepository = GetService(); +public sealed class EventStackFilterTests : IntegrationTestsBase { + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); - } + public EventStackFilterTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + TestSystemClock.SetFrozenTime(new DateTime(2015, 2, 13, 0, 0, 0, DateTimeKind.Utc)); + _stackRepository = GetService(); + _eventRepository = GetService(); - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); + } - var oldLoggingLevel = Log.MinimumLevel; - Log.MinimumLevel = LogLevel.Warning; + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); - await StackData.CreateSearchDataAsync(_stackRepository, GetService()); - await EventData.CreateSearchDataAsync(GetService(), _eventRepository, GetService()); + var oldLoggingLevel = Log.MinimumLevel; + Log.MinimumLevel = LogLevel.Warning; - Log.MinimumLevel = oldLoggingLevel; - } + await StackData.CreateSearchDataAsync(_stackRepository, GetService()); + await EventData.CreateSearchDataAsync(GetService(), _eventRepository, GetService()); - [Theory] - [InlineData("status:fixed", 2)] - [InlineData("status:regressed", 3)] - [InlineData("status:open", 1)] - public async Task GetByStatusAsync(string filter, int count) { - var result = await GetAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } + Log.MinimumLevel = oldLoggingLevel; + } - [Theory] - [InlineData("status:open", 1)] - [InlineData("status:regressed", 3)] - [InlineData("status:ignored", 1)] - [InlineData("(status:open OR status:regressed)", 4)] - [InlineData("is_fixed:true", 2)] - [InlineData("status:fixed", 2)] - [InlineData("status:discarded", 0)] - [InlineData("tags:old_tag", 0)] // Stack only tags won't be resolved - [InlineData("type:log status:fixed", 2)] - [InlineData("type:log version_fixed:1.2.3", 1)] - [InlineData("type:error is_hidden:false is_fixed:false is_regressed:true", 2)] - [InlineData("type:log status:fixed version_fixed:1.2.3", 1)] - [InlineData("54dbc16ca0f5c61398427b00", 1)] // Event Id - [InlineData("1ecd0826e447a44e78877ab1", 0)] // Stack Id - [InlineData("type:error", 2)] - public async Task GetByFilterAsync(string filter, int count) { - var result = await GetAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } + [Theory] + [InlineData("status:fixed", 2)] + [InlineData("status:regressed", 3)] + [InlineData("status:open", 1)] + public async Task GetByStatusAsync(string filter, int count) { + var result = await GetAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("status:open", 1)] + [InlineData("status:regressed", 3)] + [InlineData("status:ignored", 1)] + [InlineData("(status:open OR status:regressed)", 4)] + [InlineData("is_fixed:true", 2)] + [InlineData("status:fixed", 2)] + [InlineData("status:discarded", 0)] + [InlineData("tags:old_tag", 0)] // Stack only tags won't be resolved + [InlineData("type:log status:fixed", 2)] + [InlineData("type:log version_fixed:1.2.3", 1)] + [InlineData("type:error is_hidden:false is_fixed:false is_regressed:true", 2)] + [InlineData("type:log status:fixed version_fixed:1.2.3", 1)] + [InlineData("54dbc16ca0f5c61398427b00", 1)] // Event Id + [InlineData("1ecd0826e447a44e78877ab1", 0)] // Stack Id + [InlineData("type:error", 2)] + public async Task GetByFilterAsync(string filter, int count) { + var result = await GetAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } - private Task> GetAsync(string filter) { - return _eventRepository.FindAsync(q => q.FilterExpression(filter).EnforceEventStackFilter(), o => o.QueryLogLevel(LogLevel.Information)); - } + private Task> GetAsync(string filter) { + return _eventRepository.FindAsync(q => q.FilterExpression(filter).EnforceEventStackFilter(), o => o.QueryLogLevel(LogLevel.Information)); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Search/MoreEventIndexTests.cs b/tests/Exceptionless.Tests/Search/MoreEventIndexTests.cs index 79c656866c..2061d69a7a 100644 --- a/tests/Exceptionless.Tests/Search/MoreEventIndexTests.cs +++ b/tests/Exceptionless.Tests/Search/MoreEventIndexTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Exceptionless.Core.Queries.Validation; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; @@ -10,176 +8,175 @@ using Xunit.Abstractions; using LogLevel = Microsoft.Extensions.Logging.LogLevel; using Exceptionless.Core.Utility; -using Exceptionless.Tests.Utility; - -namespace Exceptionless.Tests.Repositories { - public sealed class MoreEventIndexTests : IntegrationTestsBase { - private readonly IEventRepository _repository; - private readonly PersistentEventQueryValidator _validator; - - public MoreEventIndexTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - TestSystemClock.SetFrozenTime(new DateTime(2015, 2, 13, 0, 0, 0, DateTimeKind.Utc)); - _repository = GetService(); - _validator = GetService(); - } - - [Theory] - [InlineData("source:\"GET /Print\"", 1)] - [InlineData("source:\"Gotham Bagle Company\"", 1)] - [InlineData("source:\"Exceptionless.Web.GeT.Print.SomeClass\"", 1)] - [InlineData("source:Exceptionless.Web.GET.Print.SomeClass", 1)] - [InlineData("source:exceptionless.web.gET.print.someClass", 1)] - [InlineData("source:some/web/path", 1)] - [InlineData("source:some\\\\/web*", 1)] - [InlineData("source:Exceptionless*", 2)] - [InlineData("source:exceptionless.web.gET.p*", 1)] - [InlineData("source:Exceptionless", 2)] - [InlineData("source:\"Exceptionless\"", 2)] - [InlineData("source:GET", 2)] - [InlineData("source:gEt", 2)] - [InlineData("source:Print", 2)] - [InlineData("source:\"/Print\"", 1)] - [InlineData("source:Bagle", 1)] - [InlineData("source:exceptionless.web*", 1)] - [InlineData("source:reason", 1)] - [InlineData("source:randomText", 1)] - [InlineData("source:getUrlV2", 1)] - [InlineData("source:namespace.controller.getUrlV2", 1)] - [InlineData("source:namespace.controller", 1)] - [InlineData("source:blake", 1)] - [InlineData("source:System.Text.StringBuilder", 1)] - [InlineData("source:System.Text", 1)] - [InlineData("source:System.Text.StringBuilder,System.Text", 1)] - public async Task GetBySourceAsync(string search, int count) { - Log.MinimumLevel = LogLevel.Trace; - - await CreateDataAsync(d => { - d.Event().Source("Exceptionless.Web.GET.Print.SomeClass"); - d.Event().Source("some/web/path"); - d.Event().Source("Exceptionless"); - d.Event().Source("GET /Print"); - d.Event().Source("Gotham Bagle Company"); - d.Event().Source("System.Text.StringBuilder,System.Text"); - d.Event().Source("randomText,namespace.controller.getUrlV2 (blake) reason https://10.0.1.1:1234/namespace/v2/controller/getUrl?mode=summary&message=test reason2"); - }); - - Log.SetLogLevel(LogLevel.Trace); - - var result = await GetEventsAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("exceptionless", 3)] - [InlineData("exceptionless.com", 3)] - [InlineData("test@exceptionless.com", 2)] - [InlineData("user.email:test@exceptionless.com", 2)] - [InlineData("user.email:exceptionless.com", 3)] - public async Task GetByUserEmailAddressAsync(string search, int count) { - await CreateDataAsync(d => { - d.Event().UserDescription("test@exceptionless.com", ""); - d.Event().UserIdentity("test@exceptionless.com"); - d.Event().UserIdentity("eric@exceptionless.com"); - d.Event().UserIdentity("eric@ericjsmith.net"); - d.Event().UserIdentity("blake.niemyjski@codesmithtools.com"); - }); - - var result = await GetEventsAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("type:log", 1)] - [InlineData("type:error", 1)] - [InlineData("type:custom", 1)] - public async Task GetByTypeAsync(string search, int count) { - await CreateDataAsync(d => { - d.Event().Type(Event.KnownTypes.Log); - d.Event().Type(Event.KnownTypes.Error); - d.Event().Type("custom"); - }); - - var result = await GetEventsAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("NOT _exists_:tag", 1)] - [InlineData("tag:test", 1)] - [InlineData("tag:Blake", 0)] - [InlineData("tag:Niemyjski", 0)] - [InlineData("tag:\"Blake Niemyjski\"", 1)] - public async Task GetByTagAsync(string search, int count) { - await CreateDataAsync(d => { - d.Event().Tag("Blake Niemyjski"); - d.Event().Tag("test"); - d.Event(); - }); - - var result = await GetEventsAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("project:000000000000000000000000", 0)] - [InlineData("project:" + SampleDataService.TEST_PROJECT_ID, 1)] - [InlineData("project:" + SampleDataService.FREE_PROJECT_ID, 1)] - [InlineData("project:123", 1)] - public async Task GetByProjectIdAsync(string search, int count) { - await CreateDataAsync(d => { - d.Event().TestProject(); - d.Event().FreeProject(); - d.Event().Project("123"); - }); - - var result = await GetEventsAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("organization:000000000000000000000000", 0)] - [InlineData("organization:" + SampleDataService.TEST_ORG_ID, 1)] - [InlineData("organization:" + SampleDataService.FREE_ORG_ID, 1)] - [InlineData("organization:123", 1)] - public async Task GetByOrganizationIdAsync(string search, int count) { - await CreateDataAsync(d => { - d.Event().TestProject(); - d.Event().FreeProject(); - d.Event().Organization("123"); - }); - - var result = await GetEventsAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("stack:000000000000000000000000", 0)] - [InlineData("stack:1ecd0826e447a44e78877ab1", 2)] - [InlineData("stack:2ecd0826e447a44e78877ab2", 1)] - public async Task GetByStackIdAsync(string search, int count) { - await CreateDataAsync(d => { - var stack1 = d.Event().StackId("1ecd0826e447a44e78877ab1"); - d.Event().Stack(stack1); - - d.Event().StackId("2ecd0826e447a44e78877ab2"); - }); - - var result = await GetEventsAsync(search); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - private async Task> GetEventsAsync(string search) { - var result = await _validator.ValidateQueryAsync(search); - Assert.True(result.IsValid); - Log.SetLogLevel(LogLevel.Trace); - return await _repository.FindAsync(q => q.SearchExpression(search)); - } + +namespace Exceptionless.Tests.Repositories; + +public sealed class MoreEventIndexTests : IntegrationTestsBase { + private readonly IEventRepository _repository; + private readonly PersistentEventQueryValidator _validator; + + public MoreEventIndexTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + TestSystemClock.SetFrozenTime(new DateTime(2015, 2, 13, 0, 0, 0, DateTimeKind.Utc)); + _repository = GetService(); + _validator = GetService(); + } + + [Theory] + [InlineData("source:\"GET /Print\"", 1)] + [InlineData("source:\"Gotham Bagle Company\"", 1)] + [InlineData("source:\"Exceptionless.Web.GeT.Print.SomeClass\"", 1)] + [InlineData("source:Exceptionless.Web.GET.Print.SomeClass", 1)] + [InlineData("source:exceptionless.web.gET.print.someClass", 1)] + [InlineData("source:some/web/path", 1)] + [InlineData("source:some\\\\/web*", 1)] + [InlineData("source:Exceptionless*", 2)] + [InlineData("source:exceptionless.web.gET.p*", 1)] + [InlineData("source:Exceptionless", 2)] + [InlineData("source:\"Exceptionless\"", 2)] + [InlineData("source:GET", 2)] + [InlineData("source:gEt", 2)] + [InlineData("source:Print", 2)] + [InlineData("source:\"/Print\"", 1)] + [InlineData("source:Bagle", 1)] + [InlineData("source:exceptionless.web*", 1)] + [InlineData("source:reason", 1)] + [InlineData("source:randomText", 1)] + [InlineData("source:getUrlV2", 1)] + [InlineData("source:namespace.controller.getUrlV2", 1)] + [InlineData("source:namespace.controller", 1)] + [InlineData("source:blake", 1)] + [InlineData("source:System.Text.StringBuilder", 1)] + [InlineData("source:System.Text", 1)] + [InlineData("source:System.Text.StringBuilder,System.Text", 1)] + public async Task GetBySourceAsync(string search, int count) { + Log.MinimumLevel = LogLevel.Trace; + + await CreateDataAsync(d => { + d.Event().Source("Exceptionless.Web.GET.Print.SomeClass"); + d.Event().Source("some/web/path"); + d.Event().Source("Exceptionless"); + d.Event().Source("GET /Print"); + d.Event().Source("Gotham Bagle Company"); + d.Event().Source("System.Text.StringBuilder,System.Text"); + d.Event().Source("randomText,namespace.controller.getUrlV2 (blake) reason https://10.0.1.1:1234/namespace/v2/controller/getUrl?mode=summary&message=test reason2"); + }); + + Log.SetLogLevel(LogLevel.Trace); + + var result = await GetEventsAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("exceptionless", 3)] + [InlineData("exceptionless.com", 3)] + [InlineData("test@exceptionless.com", 2)] + [InlineData("user.email:test@exceptionless.com", 2)] + [InlineData("user.email:exceptionless.com", 3)] + public async Task GetByUserEmailAddressAsync(string search, int count) { + await CreateDataAsync(d => { + d.Event().UserDescription("test@exceptionless.com", ""); + d.Event().UserIdentity("test@exceptionless.com"); + d.Event().UserIdentity("eric@exceptionless.com"); + d.Event().UserIdentity("eric@ericjsmith.net"); + d.Event().UserIdentity("blake.niemyjski@codesmithtools.com"); + }); + + var result = await GetEventsAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("type:log", 1)] + [InlineData("type:error", 1)] + [InlineData("type:custom", 1)] + public async Task GetByTypeAsync(string search, int count) { + await CreateDataAsync(d => { + d.Event().Type(Event.KnownTypes.Log); + d.Event().Type(Event.KnownTypes.Error); + d.Event().Type("custom"); + }); + + var result = await GetEventsAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("NOT _exists_:tag", 1)] + [InlineData("tag:test", 1)] + [InlineData("tag:Blake", 0)] + [InlineData("tag:Niemyjski", 0)] + [InlineData("tag:\"Blake Niemyjski\"", 1)] + public async Task GetByTagAsync(string search, int count) { + await CreateDataAsync(d => { + d.Event().Tag("Blake Niemyjski"); + d.Event().Tag("test"); + d.Event(); + }); + + var result = await GetEventsAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("project:000000000000000000000000", 0)] + [InlineData("project:" + SampleDataService.TEST_PROJECT_ID, 1)] + [InlineData("project:" + SampleDataService.FREE_PROJECT_ID, 1)] + [InlineData("project:123", 1)] + public async Task GetByProjectIdAsync(string search, int count) { + await CreateDataAsync(d => { + d.Event().TestProject(); + d.Event().FreeProject(); + d.Event().Project("123"); + }); + + var result = await GetEventsAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("organization:000000000000000000000000", 0)] + [InlineData("organization:" + SampleDataService.TEST_ORG_ID, 1)] + [InlineData("organization:" + SampleDataService.FREE_ORG_ID, 1)] + [InlineData("organization:123", 1)] + public async Task GetByOrganizationIdAsync(string search, int count) { + await CreateDataAsync(d => { + d.Event().TestProject(); + d.Event().FreeProject(); + d.Event().Organization("123"); + }); + + var result = await GetEventsAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("stack:000000000000000000000000", 0)] + [InlineData("stack:1ecd0826e447a44e78877ab1", 2)] + [InlineData("stack:2ecd0826e447a44e78877ab2", 1)] + public async Task GetByStackIdAsync(string search, int count) { + await CreateDataAsync(d => { + var stack1 = d.Event().StackId("1ecd0826e447a44e78877ab1"); + d.Event().Stack(stack1); + + d.Event().StackId("2ecd0826e447a44e78877ab2"); + }); + + var result = await GetEventsAsync(search); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + private async Task> GetEventsAsync(string search) { + var result = await _validator.ValidateQueryAsync(search); + Assert.True(result.IsValid); + Log.SetLogLevel(LogLevel.Trace); + return await _repository.FindAsync(q => q.SearchExpression(search)); } } diff --git a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs index 6531cae460..dbc8c09d18 100644 --- a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs +++ b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Exceptionless.Core.Extensions; using Exceptionless.Core.Queries.Validation; using Exceptionless.Core.Repositories.Configuration; @@ -8,120 +6,120 @@ using Foundatio.Parsers.ElasticQueries.Visitors; using Foundatio.Parsers.LuceneQueries.Nodes; using Foundatio.Parsers.LuceneQueries.Visitors; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Search { - public sealed class PersistentEventQueryValidatorTests : TestWithServices { - private readonly ElasticQueryParser _parser; - private readonly PersistentEventQueryValidator _validator; +namespace Exceptionless.Tests.Search; - public PersistentEventQueryValidatorTests(ITestOutputHelper output) : base(output){ - _parser = GetService().Events.QueryParser; - _validator = GetService(); - } - - [Theory] - [InlineData("data.@user.identity:blake", "data.@user.identity:blake", true, true)] - [InlineData("user:blake", "data.@user.identity:blake", true, true)] - [InlineData("NOT _exists_:data.sessionend", "NOT _exists_:idx.sessionend-d", true, true)] - [InlineData("data.SessionEnd:now", "idx.date-d:>now", true, true)] - [InlineData("data.date:[now/d-4d TO now/d+1d}", "idx.date-d:[now/d-4d TO now/d+1d}", true, true)] - [InlineData("data.date:[2012-01-01 TO 2012-12-31]", "idx.date-d:[2012-01-01 TO 2012-12-31]", true, true)] - [InlineData("data.date:[* TO 2012-12-31]", "idx.date-d:[* TO 2012-12-31]", true, true)] - [InlineData("data.date:[2012-01-01 TO *]", "idx.date-d:[2012-01-01 TO *]", true, true)] - [InlineData("(data.date:[now/d-4d TO now/d+1d})", "(idx.date-d:[now/d-4d TO now/d+1d})", true, true)] - [InlineData("data.count:[1..5}", "idx.count-n:[1..5}", true, true)] - [InlineData("data.Windows-identity:ejsmith", "idx.windows-identity-s:ejsmith", true, true)] - [InlineData("data.age:(>30 AND <=40)", "idx.age-n:(>30 AND <=40)", true, true)] - [InlineData("data.age:(+>=10 AND < 20)", "idx.age-n:(+>=10 AND <20)", true, true)] - [InlineData("data.age:(+>=10 +<20)", "idx.age-n:(+>=10 +<20)", true, true)] - [InlineData("data.age:(->=10 AND < 20)", "idx.age-n:(->=10 AND <20)", true, true)] - [InlineData("data.age:[10 TO *]", "idx.age-n:[10 TO *]", true, true)] - [InlineData("data.age:[* TO 10]", "idx.age-n:[* TO 10]", true, true)] - [InlineData("type:404 AND data.age:(>30 AND <=40)", "type:404 AND idx.age-n:(>30 AND <=40)", true, true)] - [InlineData("type:404", "type:404", true, false)] - [InlineData("reference:404", "reference:404", true, false)] - [InlineData("organization:404", "organization:404", true, false)] - [InlineData("project:404", "project:404", true, false)] - [InlineData("stack:404", "stack:404", true, false)] - [InlineData("ref.session:12345678", "idx.session-r:12345678", true, true)] - [InlineData("status:open", "status:open", true, false)] - public async Task CanProcessQueryAsync(string query, string expected, bool isValid, bool usesPremiumFeatures) { - var context = new ElasticQueryVisitorContext { QueryType = QueryType.Query }; - - IQueryNode result; - try { - result = await _parser.ParseAsync(query, context).AnyContext(); - } catch (Exception ex) { - _logger.LogError(ex, "Error parsing query: {Query}. Message: {Message}", query, ex.Message); - if (isValid) - throw; +public sealed class PersistentEventQueryValidatorTests : TestWithServices { + private readonly ElasticQueryParser _parser; + private readonly PersistentEventQueryValidator _validator; - return; - } + public PersistentEventQueryValidatorTests(ITestOutputHelper output) : base(output) { + _parser = GetService().Events.QueryParser; + _validator = GetService(); + } - // NOTE: we have to do this because we don't have access to the right query parser instance. - result = await EventFieldsQueryVisitor.RunAsync(result, context); - Assert.Equal(expected, await GenerateQueryVisitor.RunAsync(result, context)); + [Theory] + [InlineData("data.@user.identity:blake", "data.@user.identity:blake", true, true)] + [InlineData("user:blake", "data.@user.identity:blake", true, true)] + [InlineData("NOT _exists_:data.sessionend", "NOT _exists_:idx.sessionend-d", true, true)] + [InlineData("data.SessionEnd:now", "idx.date-d:>now", true, true)] + [InlineData("data.date:[now/d-4d TO now/d+1d}", "idx.date-d:[now/d-4d TO now/d+1d}", true, true)] + [InlineData("data.date:[2012-01-01 TO 2012-12-31]", "idx.date-d:[2012-01-01 TO 2012-12-31]", true, true)] + [InlineData("data.date:[* TO 2012-12-31]", "idx.date-d:[* TO 2012-12-31]", true, true)] + [InlineData("data.date:[2012-01-01 TO *]", "idx.date-d:[2012-01-01 TO *]", true, true)] + [InlineData("(data.date:[now/d-4d TO now/d+1d})", "(idx.date-d:[now/d-4d TO now/d+1d})", true, true)] + [InlineData("data.count:[1..5}", "idx.count-n:[1..5}", true, true)] + [InlineData("data.Windows-identity:ejsmith", "idx.windows-identity-s:ejsmith", true, true)] + [InlineData("data.age:(>30 AND <=40)", "idx.age-n:(>30 AND <=40)", true, true)] + [InlineData("data.age:(+>=10 AND < 20)", "idx.age-n:(+>=10 AND <20)", true, true)] + [InlineData("data.age:(+>=10 +<20)", "idx.age-n:(+>=10 +<20)", true, true)] + [InlineData("data.age:(->=10 AND < 20)", "idx.age-n:(->=10 AND <20)", true, true)] + [InlineData("data.age:[10 TO *]", "idx.age-n:[10 TO *]", true, true)] + [InlineData("data.age:[* TO 10]", "idx.age-n:[* TO 10]", true, true)] + [InlineData("type:404 AND data.age:(>30 AND <=40)", "type:404 AND idx.age-n:(>30 AND <=40)", true, true)] + [InlineData("type:404", "type:404", true, false)] + [InlineData("reference:404", "reference:404", true, false)] + [InlineData("organization:404", "organization:404", true, false)] + [InlineData("project:404", "project:404", true, false)] + [InlineData("stack:404", "stack:404", true, false)] + [InlineData("ref.session:12345678", "idx.session-r:12345678", true, true)] + [InlineData("status:open", "status:open", true, false)] + public async Task CanProcessQueryAsync(string query, string expected, bool isValid, bool usesPremiumFeatures) { + var context = new ElasticQueryVisitorContext { QueryType = QueryType.Query }; - var info = await _validator.ValidateQueryAsync(result); - _logger.LogInformation("UsesPremiumFeatures: {UsesPremiumFeatures} IsValid: {IsValid} Message: {Message}", info.UsesPremiumFeatures, info.IsValid, info.Message); - Assert.Equal(isValid, info.IsValid); - Assert.Equal(usesPremiumFeatures, info.UsesPremiumFeatures); + IQueryNode result; + try { + result = await _parser.ParseAsync(query, context).AnyContext(); } - - [Theory] - [InlineData(null, true, false)] - [InlineData("avg", false, false)] - [InlineData("avg:", false, false)] - [InlineData("avg:val", false, true)] - [InlineData("avg:value", true, false)] - [InlineData("max:date", true, false)] - [InlineData("avg:count", true, false)] - [InlineData("terms:(first @include:true)", true, false)] - [InlineData("cardinality:stack", true, false)] - [InlineData("cardinality:user", true, false)] - [InlineData("cardinality:type", true, false)] - [InlineData("cardinality:source", true, true)] - [InlineData("cardinality:tags", true, true)] - [InlineData("cardinality:geo", true, true)] - [InlineData("cardinality:organization", true, true)] - [InlineData("cardinality:project", true, true)] - [InlineData("cardinality:error.code", true, true)] - [InlineData("cardinality:error.type", true, true)] - [InlineData("cardinality:error.targettype", true, true)] - [InlineData("cardinality:error.targetmethod", true, true)] - [InlineData("cardinality:machine", true, true)] - [InlineData("cardinality:architecture", true, true)] - [InlineData("cardinality:country", true, true)] - [InlineData("cardinality:level1", true, true)] - [InlineData("cardinality:level2", true, true)] - [InlineData("cardinality:locality", true, true)] - [InlineData("cardinality:browser", true, true)] - [InlineData("cardinality:browser.major", true, true)] - [InlineData("cardinality:device", true, true)] - [InlineData("cardinality:os", true, true)] - [InlineData("cardinality:os.version", true, true)] - [InlineData("cardinality:os.major", true, true)] - [InlineData("cardinality:bot", true, true)] - [InlineData("cardinality:version", true, true)] - [InlineData("cardinality:level", true, true)] - [InlineData("terms:status", true, false)] - [InlineData("date:(date cardinality:stack sum:count~1) cardinality:stack terms:(first @include:true) sum:count~1", true, false)] // dashboards - [InlineData("date:(date cardinality:user sum:value avg:value sum:count~1) min:date max:date cardinality:user sum:count~1", true, false)] // stack dashboard - [InlineData("avg:value cardinality:user date:(date cardinality:user)", true, false)] // session dashboard - [InlineData("date:(date~month terms:(project cardinality:stack terms:(first @include:true)) cardinality:stack terms:(first @include:true))", true, true)] // Breakdown of total events, new events and unique events per month by project - public async Task CanProcessAggregationsAsync(string query, bool isValid, bool usesPremiumFeatures) { - var info = await _validator.ValidateAggregationsAsync(query); - _logger.LogInformation("UsesPremiumFeatures: {UsesPremiumFeatures} IsValid: {IsValid} Message: {Message}", info.UsesPremiumFeatures, info.IsValid, info.Message); - Assert.Equal(isValid, info.IsValid); + catch (Exception ex) { + _logger.LogError(ex, "Error parsing query: {Query}. Message: {Message}", query, ex.Message); if (isValid) - Assert.Equal(usesPremiumFeatures, info.UsesPremiumFeatures); + throw; + + return; } + + // NOTE: we have to do this because we don't have access to the right query parser instance. + result = await EventFieldsQueryVisitor.RunAsync(result, context); + Assert.Equal(expected, await GenerateQueryVisitor.RunAsync(result, context)); + + var info = await _validator.ValidateQueryAsync(result); + _logger.LogInformation("UsesPremiumFeatures: {UsesPremiumFeatures} IsValid: {IsValid} Message: {Message}", info.UsesPremiumFeatures, info.IsValid, info.Message); + Assert.Equal(isValid, info.IsValid); + Assert.Equal(usesPremiumFeatures, info.UsesPremiumFeatures); + } + + [Theory] + [InlineData(null, true, false)] + [InlineData("avg", false, false)] + [InlineData("avg:", false, false)] + [InlineData("avg:val", false, true)] + [InlineData("avg:value", true, false)] + [InlineData("max:date", true, false)] + [InlineData("avg:count", true, false)] + [InlineData("terms:(first @include:true)", true, false)] + [InlineData("cardinality:stack", true, false)] + [InlineData("cardinality:user", true, false)] + [InlineData("cardinality:type", true, false)] + [InlineData("cardinality:source", true, true)] + [InlineData("cardinality:tags", true, true)] + [InlineData("cardinality:geo", true, true)] + [InlineData("cardinality:organization", true, true)] + [InlineData("cardinality:project", true, true)] + [InlineData("cardinality:error.code", true, true)] + [InlineData("cardinality:error.type", true, true)] + [InlineData("cardinality:error.targettype", true, true)] + [InlineData("cardinality:error.targetmethod", true, true)] + [InlineData("cardinality:machine", true, true)] + [InlineData("cardinality:architecture", true, true)] + [InlineData("cardinality:country", true, true)] + [InlineData("cardinality:level1", true, true)] + [InlineData("cardinality:level2", true, true)] + [InlineData("cardinality:locality", true, true)] + [InlineData("cardinality:browser", true, true)] + [InlineData("cardinality:browser.major", true, true)] + [InlineData("cardinality:device", true, true)] + [InlineData("cardinality:os", true, true)] + [InlineData("cardinality:os.version", true, true)] + [InlineData("cardinality:os.major", true, true)] + [InlineData("cardinality:bot", true, true)] + [InlineData("cardinality:version", true, true)] + [InlineData("cardinality:level", true, true)] + [InlineData("terms:status", true, false)] + [InlineData("date:(date cardinality:stack sum:count~1) cardinality:stack terms:(first @include:true) sum:count~1", true, false)] // dashboards + [InlineData("date:(date cardinality:user sum:value avg:value sum:count~1) min:date max:date cardinality:user sum:count~1", true, false)] // stack dashboard + [InlineData("avg:value cardinality:user date:(date cardinality:user)", true, false)] // session dashboard + [InlineData("date:(date~month terms:(project cardinality:stack terms:(first @include:true)) cardinality:stack terms:(first @include:true))", true, true)] // Breakdown of total events, new events and unique events per month by project + public async Task CanProcessAggregationsAsync(string query, bool isValid, bool usesPremiumFeatures) { + var info = await _validator.ValidateAggregationsAsync(query); + _logger.LogInformation("UsesPremiumFeatures: {UsesPremiumFeatures} IsValid: {IsValid} Message: {Message}", info.UsesPremiumFeatures, info.IsValid, info.Message); + Assert.Equal(isValid, info.IsValid); + if (isValid) + Assert.Equal(usesPremiumFeatures, info.UsesPremiumFeatures); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Search/StackIndexTests.cs b/tests/Exceptionless.Tests/Search/StackIndexTests.cs index 2c6aed9a0e..12bfbc60e4 100644 --- a/tests/Exceptionless.Tests/Search/StackIndexTests.cs +++ b/tests/Exceptionless.Tests/Search/StackIndexTests.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Exceptionless.Core.Repositories; using Exceptionless.Core.Models; using Exceptionless.Tests.Utility; @@ -8,165 +7,165 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Repositories { - public sealed class StackIndexTests : IntegrationTestsBase { - private readonly IStackRepository _repository; - - public StackIndexTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _repository = GetService(); - } - - protected override async Task ResetDataAsync() { - await base.ResetDataAsync(); - await StackData.CreateSearchDataAsync(_repository, GetService()); - } - - [Theory] - [InlineData("\"GET /Print\"", 3)] // Title - [InlineData("\"my custom description\"", 1)] // Description - [InlineData("\"Blake Niemyjski\"", 1)] // Tags - [InlineData("\"http://exceptionless.io\"", 3)] // References - public async Task GetByAllFieldAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("000000000000000000000000", 0)] - [InlineData("1ecd0826e447a44e78877ab1", 1)] - [InlineData("2ecd0826e447a44e78877ab2", 1)] - public async Task GetAsync(string id, int count) { - var result = await GetByFilterAsync("id:" + id); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("000000000000000000000000", 0)] - [InlineData("537650f3b77efe23a47914f3", 5)] - public async Task GetByOrganizationIdAsync(string id, int count) { - var result = await GetByFilterAsync("organization:" + id); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("000000000000000000000000", 0)] - [InlineData("537650f3b77efe23a47914f4", 5)] - public async Task GetByProjectIdAsync(string id, int count) { - var result = await GetByFilterAsync("project:" + id); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("log", 4)] - [InlineData("error", 1)] - [InlineData("custom", 0)] - public async Task GetByTypeAsync(string type, int count) { - var result = await GetByFilterAsync("type:" + type); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("[2015-01-08 TO 2015-02-10]", 2)] - [InlineData("\"2015-01-08T18:29:01.428Z\"", 1)] - [InlineData("\"2015-02-10T01:05:54.399Z\"", 1)] - public async Task GetByFirstAsync(string first, int count) { - var result = await GetByFilterAsync("first:" + first); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("\"2015-02-03T16:52:41.982Z\"", 1)] - [InlineData("\"2015-02-11T20:54:04.3457274Z\"", 1)] - public async Task GetByLastAsync(string last, int count) { - var result = await GetByFilterAsync("last:" + last); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("{5 TO 50}", 1)] - [InlineData("5", 3)] - [InlineData("50", 1)] - public async Task GetByOccurrencesAsync(string occurrences, int count) { - var result = await GetByFilterAsync("occurrences:" + occurrences); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("title:\"GET /Print\"", 3)] - [InlineData("title:\"The provided anti-forgery token was meant\"", 1)] - [InlineData("title:\"test@exceptionless.com\"", 1)] - [InlineData("title:\"Row not found or changed.\"", 1)] - public async Task GetByTitleAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("tag:test", 3)] - [InlineData("tag:Blake", 0)] - [InlineData("tag:Niemyjski", 0)] - [InlineData("tag:\"Blake Niemyjski\"", 1)] - public async Task GetByTagAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("\"2015-02-11T20:54:04.3457274Z\"", 1)] - public async Task GetByFixedOnAsync(string fixedOn, int count) { - var result = await GetByFilterAsync("fixedon:" + fixedOn); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("open", 1)] - [InlineData("fixed", 2)] - [InlineData("regressed", 1)] - public async Task GetByStatusAsync(string status, int count) { - var result = await GetByFilterAsync("status:" + status); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData(false, 4)] - [InlineData(true, 1)] - public async Task GetByCriticalAsync(bool critical, int count) { - var result = await GetByFilterAsync("critical:" + critical.ToString().ToLowerInvariant()); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("links:\"http://exceptionless.io\"", 3)] - [InlineData("links:\"https://github.com/exceptionless/Exceptionless\"", 1)] - public async Task GetByLinksAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - [Theory] - [InlineData("description:\"my custom description\"", 1)] - public async Task GetByDescriptionAsync(string filter, int count) { - var result = await GetByFilterAsync(filter); - Assert.NotNull(result); - Assert.Equal(count, result.Total); - } - - private Task> GetByFilterAsync(string filter) { - return _repository.FindAsync(q => q.FilterExpression(filter)); - } - } -} \ No newline at end of file +namespace Exceptionless.Tests.Repositories; + +public sealed class StackIndexTests : IntegrationTestsBase { + private readonly IStackRepository _repository; + + public StackIndexTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _repository = GetService(); + } + + protected override async Task ResetDataAsync() { + await base.ResetDataAsync(); + await StackData.CreateSearchDataAsync(_repository, GetService()); + } + + [Theory] + [InlineData("\"GET /Print\"", 3)] // Title + [InlineData("\"my custom description\"", 1)] // Description + [InlineData("\"Blake Niemyjski\"", 1)] // Tags + [InlineData("\"http://exceptionless.io\"", 3)] // References + public async Task GetByAllFieldAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("000000000000000000000000", 0)] + [InlineData("1ecd0826e447a44e78877ab1", 1)] + [InlineData("2ecd0826e447a44e78877ab2", 1)] + public async Task GetAsync(string id, int count) { + var result = await GetByFilterAsync("id:" + id); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("000000000000000000000000", 0)] + [InlineData("537650f3b77efe23a47914f3", 5)] + public async Task GetByOrganizationIdAsync(string id, int count) { + var result = await GetByFilterAsync("organization:" + id); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("000000000000000000000000", 0)] + [InlineData("537650f3b77efe23a47914f4", 5)] + public async Task GetByProjectIdAsync(string id, int count) { + var result = await GetByFilterAsync("project:" + id); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("log", 4)] + [InlineData("error", 1)] + [InlineData("custom", 0)] + public async Task GetByTypeAsync(string type, int count) { + var result = await GetByFilterAsync("type:" + type); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("[2015-01-08 TO 2015-02-10]", 2)] + [InlineData("\"2015-01-08T18:29:01.428Z\"", 1)] + [InlineData("\"2015-02-10T01:05:54.399Z\"", 1)] + public async Task GetByFirstAsync(string first, int count) { + var result = await GetByFilterAsync("first:" + first); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("\"2015-02-03T16:52:41.982Z\"", 1)] + [InlineData("\"2015-02-11T20:54:04.3457274Z\"", 1)] + public async Task GetByLastAsync(string last, int count) { + var result = await GetByFilterAsync("last:" + last); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("{5 TO 50}", 1)] + [InlineData("5", 3)] + [InlineData("50", 1)] + public async Task GetByOccurrencesAsync(string occurrences, int count) { + var result = await GetByFilterAsync("occurrences:" + occurrences); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("title:\"GET /Print\"", 3)] + [InlineData("title:\"The provided anti-forgery token was meant\"", 1)] + [InlineData("title:\"test@exceptionless.com\"", 1)] + [InlineData("title:\"Row not found or changed.\"", 1)] + public async Task GetByTitleAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("tag:test", 3)] + [InlineData("tag:Blake", 0)] + [InlineData("tag:Niemyjski", 0)] + [InlineData("tag:\"Blake Niemyjski\"", 1)] + public async Task GetByTagAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("\"2015-02-11T20:54:04.3457274Z\"", 1)] + public async Task GetByFixedOnAsync(string fixedOn, int count) { + var result = await GetByFilterAsync("fixedon:" + fixedOn); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("open", 1)] + [InlineData("fixed", 2)] + [InlineData("regressed", 1)] + public async Task GetByStatusAsync(string status, int count) { + var result = await GetByFilterAsync("status:" + status); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData(false, 4)] + [InlineData(true, 1)] + public async Task GetByCriticalAsync(bool critical, int count) { + var result = await GetByFilterAsync("critical:" + critical.ToString().ToLowerInvariant()); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("links:\"http://exceptionless.io\"", 3)] + [InlineData("links:\"https://github.com/exceptionless/Exceptionless\"", 1)] + public async Task GetByLinksAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + [Theory] + [InlineData("description:\"my custom description\"", 1)] + public async Task GetByDescriptionAsync(string filter, int count) { + var result = await GetByFilterAsync(filter); + Assert.NotNull(result); + Assert.Equal(count, result.Total); + } + + private Task> GetByFilterAsync(string filter) { + return _repository.FindAsync(q => q.FilterExpression(filter)); + } +} diff --git a/tests/Exceptionless.Tests/SemanticVersionTests.cs b/tests/Exceptionless.Tests/SemanticVersionTests.cs index 34d0cfd9a5..7d29d721f9 100644 --- a/tests/Exceptionless.Tests/SemanticVersionTests.cs +++ b/tests/Exceptionless.Tests/SemanticVersionTests.cs @@ -1,56 +1,55 @@ -using System.Threading.Tasks; -using Exceptionless.Core.Utility; +using Exceptionless.Core.Utility; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests { - public class SemanticVersionTests : TestWithServices { - private readonly SemanticVersionParser _parser; - - public SemanticVersionTests(ITestOutputHelper output) : base(output) { - _parser = new SemanticVersionParser(Log); - } - - [Theory] - [InlineData(null, null)] - [InlineData("a.b.c.d", null)] - [InlineData("1.b", null)] - [InlineData("test", null)] - [InlineData("1", "1.0.0")] - [InlineData(" 1 ", "1.0.0")] - [InlineData("1.2", "1.2.0")] - [InlineData("1.2 7ab3b4da18", "1.2.0")] - [InlineData("1.2.3", "1.2.3")] - [InlineData("1.2.3 7ab3b4da18", "1.2.3")] - [InlineData("1.2.3-beta2", "1.2.3-beta2")] - [InlineData("1.2.3.*", "1.2.3")] - [InlineData("1.2.3.0", "1.2.3-0")] - [InlineData("1.2.3.0*", "1.2.3-0")] - [InlineData("1.2.3*.0", "1.2.3-0")] - [InlineData("1.2.*.0", "1.2.0")] - [InlineData("1.2.*", "1.2.0")] - [InlineData("1.2.3.4", "1.2.3-4")] - [InlineData("1.2.3.4 7ab3b4da18", "1.2.3-4")] - [InlineData("4.1.0034", "4.1.34")] - public async Task CanParseSemanticVersion(string input, string expected) { - var actual = await _parser.ParseAsync(input); - Assert.Equal(expected, actual?.ToString()); - } - - [Theory] - [InlineData("4.1.0034", "4.1.34")] - public async Task VerifySameSemanticVersion(string version1, string version2) { - var parsedVersion1 = await _parser.ParseAsync(version1); - var parsedVersion2 = await _parser.ParseAsync(version2); - Assert.Equal(parsedVersion1, parsedVersion2); - } - - [Theory] - [InlineData("4.1.0034", "4.1.35")] - public async Task VerifySemanticVersionIsNewer(string oldVersion, string newVersion) { - var parsedOldVersion = await _parser.ParseAsync(oldVersion); - var parsedNewVersion = await _parser.ParseAsync(newVersion); - Assert.True(parsedOldVersion < parsedNewVersion); - } +namespace Exceptionless.Tests; + +public class SemanticVersionTests : TestWithServices { + private readonly SemanticVersionParser _parser; + + public SemanticVersionTests(ITestOutputHelper output) : base(output) { + _parser = new SemanticVersionParser(Log); + } + + [Theory] + [InlineData(null, null)] + [InlineData("a.b.c.d", null)] + [InlineData("1.b", null)] + [InlineData("test", null)] + [InlineData("1", "1.0.0")] + [InlineData(" 1 ", "1.0.0")] + [InlineData("1.2", "1.2.0")] + [InlineData("1.2 7ab3b4da18", "1.2.0")] + [InlineData("1.2.3", "1.2.3")] + [InlineData("1.2.3 7ab3b4da18", "1.2.3")] + [InlineData("1.2.3-beta2", "1.2.3-beta2")] + [InlineData("1.2.3.*", "1.2.3")] + [InlineData("1.2.3.0", "1.2.3-0")] + [InlineData("1.2.3.0*", "1.2.3-0")] + [InlineData("1.2.3*.0", "1.2.3-0")] + [InlineData("1.2.*.0", "1.2.0")] + [InlineData("1.2.*", "1.2.0")] + [InlineData("1.2.3.4", "1.2.3-4")] + [InlineData("1.2.3.4 7ab3b4da18", "1.2.3-4")] + [InlineData("4.1.0034", "4.1.34")] + public async Task CanParseSemanticVersion(string input, string expected) { + var actual = await _parser.ParseAsync(input); + Assert.Equal(expected, actual?.ToString()); + } + + [Theory] + [InlineData("4.1.0034", "4.1.34")] + public async Task VerifySameSemanticVersion(string version1, string version2) { + var parsedVersion1 = await _parser.ParseAsync(version1); + var parsedVersion2 = await _parser.ParseAsync(version2); + Assert.Equal(parsedVersion1, parsedVersion2); + } + + [Theory] + [InlineData("4.1.0034", "4.1.35")] + public async Task VerifySemanticVersionIsNewer(string oldVersion, string newVersion) { + var parsedOldVersion = await _parser.ParseAsync(oldVersion); + var parsedNewVersion = await _parser.ParseAsync(newVersion); + Assert.True(parsedOldVersion < parsedNewVersion); } } diff --git a/tests/Exceptionless.Tests/SerializerTests.cs b/tests/Exceptionless.Tests/SerializerTests.cs index be681627ab..1337182ee2 100644 --- a/tests/Exceptionless.Tests/SerializerTests.cs +++ b/tests/Exceptionless.Tests/SerializerTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; @@ -10,121 +8,121 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests { - public class SerializerTests : TestWithServices { - public SerializerTests(ITestOutputHelper output) : base(output) { } +namespace Exceptionless.Tests; - [Fact] - public void CanDeserializeEventWithUnknownNamesAndProperties() { - const string json = @"{""tags"":[""One"",""Two""],""reference_id"":""12"",""Message"":""Hello"",""SomeString"":""Hi"",""SomeBool"":false,""SomeNum"":1,""UnknownProp"":{""Blah"":""SomeVal""},""Some"":{""Blah"":""SomeVal""},""@error"":{""Message"":""SomeVal"",""SomeProp"":""SomeVal""},""Some2"":""{\""Blah\"":\""SomeVal\""}"",""UnknownSerializedProp"":""{\""Blah\"":\""SomeVal\""}""}"; - var settings = new JsonSerializerSettings(); - var knownDataTypes = new Dictionary { +public class SerializerTests : TestWithServices { + public SerializerTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void CanDeserializeEventWithUnknownNamesAndProperties() { + const string json = @"{""tags"":[""One"",""Two""],""reference_id"":""12"",""Message"":""Hello"",""SomeString"":""Hi"",""SomeBool"":false,""SomeNum"":1,""UnknownProp"":{""Blah"":""SomeVal""},""Some"":{""Blah"":""SomeVal""},""@error"":{""Message"":""SomeVal"",""SomeProp"":""SomeVal""},""Some2"":""{\""Blah\"":\""SomeVal\""}"",""UnknownSerializedProp"":""{\""Blah\"":\""SomeVal\""}""}"; + var settings = new JsonSerializerSettings(); + var knownDataTypes = new Dictionary { { "Some", typeof(SomeModel) }, { "Some2", typeof(SomeModel) }, { Event.KnownDataKeys.Error, typeof(Error) } }; - settings.Converters.Add(new DataObjectConverter(_logger, knownDataTypes)); - settings.Converters.Add(new DataObjectConverter(_logger)); - - var ev = json.FromJson(settings); - Assert.Equal(8, ev.Data.Count); - Assert.Equal("Hi", ev.Data["SomeString"]); - Assert.False((bool)ev.Data["SomeBool"]); - Assert.Equal(1L, ev.Data["SomeNum"]); - Assert.Equal(typeof(JObject), ev.Data["UnknownProp"].GetType()); - Assert.Equal(typeof(JObject), ev.Data["UnknownSerializedProp"].GetType()); - Assert.Equal("SomeVal", (string)((dynamic)ev.Data["UnknownProp"]).Blah); - Assert.Equal(typeof(SomeModel), ev.Data["Some"].GetType()); - Assert.Equal(typeof(SomeModel), ev.Data["Some2"].GetType()); - Assert.Equal("SomeVal", ((SomeModel)ev.Data["Some"]).Blah); - Assert.Equal(typeof(Error), ev.Data[Event.KnownDataKeys.Error].GetType()); - Assert.Equal("SomeVal", ((Error)ev.Data[Event.KnownDataKeys.Error]).Message); - Assert.Single(((Error)ev.Data[Event.KnownDataKeys.Error]).Data); - Assert.Equal("SomeVal", ((Error)ev.Data[Event.KnownDataKeys.Error]).Data["SomeProp"]); - Assert.Equal("Hello", ev.Message); - Assert.Equal(2, ev.Tags.Count); - Assert.Contains("One", ev.Tags); - Assert.Contains("Two", ev.Tags); - Assert.Equal("12", ev.ReferenceId); - - const string expectedjson = @"{""Tags"":[""One"",""Two""],""Message"":""Hello"",""Data"":{""SomeString"":""Hi"",""SomeBool"":false,""SomeNum"":1,""UnknownProp"":{""Blah"":""SomeVal""},""Some"":{""Blah"":""SomeVal""},""@error"":{""Modules"":[],""Message"":""SomeVal"",""Data"":{""SomeProp"":""SomeVal""},""StackTrace"":[]},""Some2"":{""Blah"":""SomeVal""},""UnknownSerializedProp"":{""Blah"":""SomeVal""}},""ReferenceId"":""12""}"; - string newjson = ev.ToJson(Formatting.None, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }); - Assert.Equal(expectedjson, newjson); - } - - [Fact] - public void CanDeserializeEventWithInvalidKnownDataTypes() { - const string json = @"{""Message"":""Hello"",""Some"":""{\""Blah\"":\""SomeVal\""}"",""@Some"":""{\""Blah\"":\""SomeVal\""}""}"; - const string jsonWithInvalidDataType = @"{""Message"":""Hello"",""@Some"":""Testing"",""@string"":""Testing""}"; - - var settings = new JsonSerializerSettings(); - var knownDataTypes = new Dictionary { + settings.Converters.Add(new DataObjectConverter(_logger, knownDataTypes)); + settings.Converters.Add(new DataObjectConverter(_logger)); + + var ev = json.FromJson(settings); + Assert.Equal(8, ev.Data.Count); + Assert.Equal("Hi", ev.Data["SomeString"]); + Assert.False((bool)ev.Data["SomeBool"]); + Assert.Equal(1L, ev.Data["SomeNum"]); + Assert.Equal(typeof(JObject), ev.Data["UnknownProp"].GetType()); + Assert.Equal(typeof(JObject), ev.Data["UnknownSerializedProp"].GetType()); + Assert.Equal("SomeVal", (string)((dynamic)ev.Data["UnknownProp"]).Blah); + Assert.Equal(typeof(SomeModel), ev.Data["Some"].GetType()); + Assert.Equal(typeof(SomeModel), ev.Data["Some2"].GetType()); + Assert.Equal("SomeVal", ((SomeModel)ev.Data["Some"]).Blah); + Assert.Equal(typeof(Error), ev.Data[Event.KnownDataKeys.Error].GetType()); + Assert.Equal("SomeVal", ((Error)ev.Data[Event.KnownDataKeys.Error]).Message); + Assert.Single(((Error)ev.Data[Event.KnownDataKeys.Error]).Data); + Assert.Equal("SomeVal", ((Error)ev.Data[Event.KnownDataKeys.Error]).Data["SomeProp"]); + Assert.Equal("Hello", ev.Message); + Assert.Equal(2, ev.Tags.Count); + Assert.Contains("One", ev.Tags); + Assert.Contains("Two", ev.Tags); + Assert.Equal("12", ev.ReferenceId); + + const string expectedjson = @"{""Tags"":[""One"",""Two""],""Message"":""Hello"",""Data"":{""SomeString"":""Hi"",""SomeBool"":false,""SomeNum"":1,""UnknownProp"":{""Blah"":""SomeVal""},""Some"":{""Blah"":""SomeVal""},""@error"":{""Modules"":[],""Message"":""SomeVal"",""Data"":{""SomeProp"":""SomeVal""},""StackTrace"":[]},""Some2"":{""Blah"":""SomeVal""},""UnknownSerializedProp"":{""Blah"":""SomeVal""}},""ReferenceId"":""12""}"; + string newjson = ev.ToJson(Formatting.None, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }); + Assert.Equal(expectedjson, newjson); + } + + [Fact] + public void CanDeserializeEventWithInvalidKnownDataTypes() { + const string json = @"{""Message"":""Hello"",""Some"":""{\""Blah\"":\""SomeVal\""}"",""@Some"":""{\""Blah\"":\""SomeVal\""}""}"; + const string jsonWithInvalidDataType = @"{""Message"":""Hello"",""@Some"":""Testing"",""@string"":""Testing""}"; + + var settings = new JsonSerializerSettings(); + var knownDataTypes = new Dictionary { { "Some", typeof(SomeModel) }, { "@Some", typeof(SomeModel) }, { "_@Some", typeof(SomeModel) }, { "@string", typeof(string) } }; - settings.Converters.Add(new DataObjectConverter(_logger, knownDataTypes)); - - var ev = json.FromJson(settings); - Assert.Equal(2, ev.Data.Count); - Assert.True(ev.Data.ContainsKey("Some")); - Assert.Equal("SomeVal", ((SomeModel)ev.Data["Some"]).Blah); - Assert.True(ev.Data.ContainsKey("@Some")); - Assert.Equal("SomeVal", ((SomeModel)ev.Data["@Some"]).Blah); - - ev = jsonWithInvalidDataType.FromJson(settings); - Assert.Equal(2, ev.Data.Count); - Assert.True(ev.Data.ContainsKey("_@Some1")); - Assert.Equal("Testing", ev.Data["_@Some1"] as string); - Assert.True(ev.Data.ContainsKey("@string")); - Assert.Equal("Testing", ev.Data["@string"] as string); - } - - [Fact] - public void CanDeserializeEventWithData() { - const string json = @"{""Message"":""Hello"",""Data"":{""Blah"":""SomeVal""}}"; - var settings = new JsonSerializerSettings(); - settings.Converters.Add(new DataObjectConverter(_logger)); - - var ev = json.FromJson(settings); - Assert.Single(ev.Data); - Assert.Equal("Hello", ev.Message); - Assert.Equal("SomeVal", ev.Data["Blah"]); - } - - [Fact] - public void CanDeserializeWebHook() { - var hook = new WebHook { - Id = "test", - EventTypes = new[] { "NewError" }, - Version = WebHook.KnownVersions.Version2 - }; + settings.Converters.Add(new DataObjectConverter(_logger, knownDataTypes)); - var serializer = GetService(); - string json = serializer.SerializeToString(hook); - Assert.Equal("{\"id\":\"test\",\"event_types\":[\"NewError\"],\"is_enabled\":true,\"version\":\"v2\",\"created_utc\":\"0001-01-01T00:00:00\"}", json); - - var model = serializer.Deserialize(json); - Assert.Equal(hook.Id, model.Id); - Assert.Equal(hook.EventTypes, model.EventTypes); - Assert.Equal(hook.Version, model.Version); - } - - [Fact] - public void CanDeserializeProject() { - string json = "{\"last_event_date_utc\":\"2020-10-18T20:54:04.3457274+01:00\", \"created_utc\":\"0001-01-01T00:00:00\",\"updated_utc\":\"2020-09-21T04:41:32.7458321Z\"}"; - - var serializer = GetService(); - var model = serializer.Deserialize(json); - Assert.NotNull(model?.LastEventDateUtc); - Assert.NotEqual(DateTime.MinValue, model.LastEventDateUtc); - Assert.Equal(DateTime.MinValue, model.CreatedUtc); - Assert.NotEqual(DateTime.MinValue, model.UpdatedUtc); - } + var ev = json.FromJson(settings); + Assert.Equal(2, ev.Data.Count); + Assert.True(ev.Data.ContainsKey("Some")); + Assert.Equal("SomeVal", ((SomeModel)ev.Data["Some"]).Blah); + Assert.True(ev.Data.ContainsKey("@Some")); + Assert.Equal("SomeVal", ((SomeModel)ev.Data["@Some"]).Blah); + + ev = jsonWithInvalidDataType.FromJson(settings); + Assert.Equal(2, ev.Data.Count); + Assert.True(ev.Data.ContainsKey("_@Some1")); + Assert.Equal("Testing", ev.Data["_@Some1"] as string); + Assert.True(ev.Data.ContainsKey("@string")); + Assert.Equal("Testing", ev.Data["@string"] as string); } - public class SomeModel { - public string Blah { get; set; } + [Fact] + public void CanDeserializeEventWithData() { + const string json = @"{""Message"":""Hello"",""Data"":{""Blah"":""SomeVal""}}"; + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new DataObjectConverter(_logger)); + + var ev = json.FromJson(settings); + Assert.Single(ev.Data); + Assert.Equal("Hello", ev.Message); + Assert.Equal("SomeVal", ev.Data["Blah"]); } -} \ No newline at end of file + + [Fact] + public void CanDeserializeWebHook() { + var hook = new WebHook { + Id = "test", + EventTypes = new[] { "NewError" }, + Version = WebHook.KnownVersions.Version2 + }; + + var serializer = GetService(); + string json = serializer.SerializeToString(hook); + Assert.Equal("{\"id\":\"test\",\"event_types\":[\"NewError\"],\"is_enabled\":true,\"version\":\"v2\",\"created_utc\":\"0001-01-01T00:00:00\"}", json); + + var model = serializer.Deserialize(json); + Assert.Equal(hook.Id, model.Id); + Assert.Equal(hook.EventTypes, model.EventTypes); + Assert.Equal(hook.Version, model.Version); + } + + [Fact] + public void CanDeserializeProject() { + string json = "{\"last_event_date_utc\":\"2020-10-18T20:54:04.3457274+01:00\", \"created_utc\":\"0001-01-01T00:00:00\",\"updated_utc\":\"2020-09-21T04:41:32.7458321Z\"}"; + + var serializer = GetService(); + var model = serializer.Deserialize(json); + Assert.NotNull(model?.LastEventDateUtc); + Assert.NotEqual(DateTime.MinValue, model.LastEventDateUtc); + Assert.Equal(DateTime.MinValue, model.CreatedUtc); + Assert.NotEqual(DateTime.MinValue, model.UpdatedUtc); + } +} + +public class SomeModel { + public string Blah { get; set; } +} diff --git a/tests/Exceptionless.Tests/Services/SlackServiceTests.cs b/tests/Exceptionless.Tests/Services/SlackServiceTests.cs index 17ee80156a..273c277112 100644 --- a/tests/Exceptionless.Tests/Services/SlackServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/SlackServiceTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Core.Jobs; +using Exceptionless.Core.Jobs; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Services; @@ -9,28 +7,29 @@ using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Services { - public sealed class SlackServiceTests : TestWithServices { - private readonly Project _project; - private readonly SlackService _slackService; - - public SlackServiceTests(ITestOutputHelper output) : base(output) { - _slackService = GetService(); - _project = ProjectData.GenerateSampleProject(); - _project.Data[Project.KnownDataKeys.SlackToken] = new SlackToken { - AccessToken = "MY KEY", - IncomingWebhook = new SlackToken.IncomingWebHook { - Url = "MY Url" - } - }; - } +namespace Exceptionless.Tests.Services; + +public sealed class SlackServiceTests : TestWithServices { + private readonly Project _project; + private readonly SlackService _slackService; + + public SlackServiceTests(ITestOutputHelper output) : base(output) { + _slackService = GetService(); + _project = ProjectData.GenerateSampleProject(); + _project.Data[Project.KnownDataKeys.SlackToken] = new SlackToken { + AccessToken = "MY KEY", + IncomingWebhook = new SlackToken.IncomingWebHook { + Url = "MY Url" + } + }; + } - [Fact] - public Task SendEventNoticeSimpleErrorAsync() { - var ex = GetException(); - return SendEventNoticeAsync(new PersistentEvent { - Type = Event.KnownTypes.Error, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeSimpleErrorAsync() { + var ex = GetException(); + return SendEventNoticeAsync(new PersistentEvent { + Type = Event.KnownTypes.Error, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.SimpleError, new SimpleError { Message = ex.Message, @@ -39,181 +38,178 @@ public Task SendEventNoticeSimpleErrorAsync() { } } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeErrorAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Type = Event.KnownTypes.Error, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeErrorAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Type = Event.KnownTypes.Error, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Error, EventData.GenerateError() } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeErrorWithDetailsAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Type = Event.KnownTypes.Error, - Geo = "44.5241,-87.9056", - ReferenceId = "ex_blake_dreams_of_cookies", - Tags = new TagSet(new[] { "Out", "Of", "Cookies", "Critical" }), - Count = 2, - Value = 500, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeErrorWithDetailsAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Type = Event.KnownTypes.Error, + Geo = "44.5241,-87.9056", + ReferenceId = "ex_blake_dreams_of_cookies", + Tags = new TagSet(new[] { "Out", "Of", "Cookies", "Critical" }), + Count = 2, + Value = 500, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Error, EventData.GenerateError() }, { Event.KnownDataKeys.Version, "1.2.3" }, { Event.KnownDataKeys.UserInfo, new UserInfo("niemyjski", "Blake Niemyjski") }, { Event.KnownDataKeys.UserDescription, new UserDescription("noreply@exceptionless.io", "Blake ate two boxes of cookies and needs help") } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeNotFoundAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "[GET] /not-found?page=20", - Type = Event.KnownTypes.NotFound - }); - } + [Fact] + public Task SendEventNoticeNotFoundAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "[GET] /not-found?page=20", + Type = Event.KnownTypes.NotFound + }); + } - [Fact] - public Task SendEventNoticeFeatureAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "My Feature Usage", - Value = 1, - Type = Event.KnownTypes.FeatureUsage - }); - } + [Fact] + public Task SendEventNoticeFeatureAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "My Feature Usage", + Value = 1, + Type = Event.KnownTypes.FeatureUsage + }); + } - [Fact] - public Task SendEventNoticeEmptyLogEventAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Value = 1, - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeEmptyLogEventAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Value = 1, + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogMessageAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "Only Message", - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeLogMessageAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "Only Message", + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogSourceAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "Only Source", - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeLogSourceAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "Only Source", + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogReallyLongSourceAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Source = "Soooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooorce", - Type = Event.KnownTypes.Log - }); - } + [Fact] + public Task SendEventNoticeLogReallyLongSourceAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Source = "Soooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooorce", + Type = Event.KnownTypes.Log + }); + } - [Fact] - public Task SendEventNoticeLogTraceMessageSourceLevelAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "My Trace Message", - Source = "My Source", - Type = Event.KnownTypes.Log, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeLogTraceMessageSourceLevelAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "My Trace Message", + Source = "My Source", + Type = Event.KnownTypes.Log, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Level, "Trace" } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeLogInfoMessageSourceLevelAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "My Info Message", - Source = "My Source", - Type = Event.KnownTypes.Log, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeLogInfoMessageSourceLevelAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "My Info Message", + Source = "My Source", + Type = Event.KnownTypes.Log, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Level, "Info" } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeLogWarnMessageSourceLevelAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "My Warn Message", - Source = "My Source", - Type = Event.KnownTypes.Log, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeLogWarnMessageSourceLevelAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "My Warn Message", + Source = "My Source", + Type = Event.KnownTypes.Log, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Level, "Warn" } } - }); - } + }); + } - [Fact] - public Task SendEventNoticeLogErrorMessageSourceLevelAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "My Error Message", - Source = "My Source", - Type = Event.KnownTypes.Log, - Data = new Core.Models.DataDictionary { + [Fact] + public Task SendEventNoticeLogErrorMessageSourceLevelAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "My Error Message", + Source = "My Source", + Type = Event.KnownTypes.Log, + Data = new Core.Models.DataDictionary { { Event.KnownDataKeys.Level, "Error" } } - }); - } - - [Fact] - public Task SendEventNoticeDefaultAsync() { - return SendEventNoticeAsync(new PersistentEvent { - Message = "Default Test Message", - Source = "Default Test Source" - }); - } + }); + } - private async Task SendEventNoticeAsync(PersistentEvent ev) { - ev.Id = TestConstants.EventId; - ev.OrganizationId = TestConstants.OrganizationId; - ev.ProjectId = TestConstants.ProjectId; - ev.StackId = TestConstants.StackId; - ev.Date = SystemClock.OffsetUtcNow; + [Fact] + public Task SendEventNoticeDefaultAsync() { + return SendEventNoticeAsync(new PersistentEvent { + Message = "Default Test Message", + Source = "Default Test Source" + }); + } - await _slackService.SendEventNoticeAsync(ev, _project, RandomData.GetBool(), RandomData.GetBool()); - await RunWebHookJobAsync(); - } + private async Task SendEventNoticeAsync(PersistentEvent ev) { + ev.Id = TestConstants.EventId; + ev.OrganizationId = TestConstants.OrganizationId; + ev.ProjectId = TestConstants.ProjectId; + ev.StackId = TestConstants.StackId; + ev.Date = SystemClock.OffsetUtcNow; - private Task RunWebHookJobAsync() { - //if (!Settings.Current.EnableSlack) - // return Task.CompletedTask; + await _slackService.SendEventNoticeAsync(ev, _project, RandomData.GetBool(), RandomData.GetBool()); + await RunWebHookJobAsync(); + } - var job = GetService(); - return job.RunAsync(); - } + private Task RunWebHookJobAsync() { + //if (!Settings.Current.EnableSlack) + // return Task.CompletedTask; - private Exception GetException() { - void TestInner() - { - void TestInnerInner() - { - throw new ApplicationException("Random Test Exception"); - } + var job = GetService(); + return job.RunAsync(); + } - TestInnerInner(); + private Exception GetException() { + void TestInner() { + void TestInnerInner() { + throw new ApplicationException("Random Test Exception"); } - try { - TestInner(); - } - catch (Exception ex) { - return ex; - } + TestInnerInner(); + } - return null; + try { + TestInner(); } + catch (Exception ex) { + return ex; + } + + return null; } } diff --git a/tests/Exceptionless.Tests/Services/StackServiceTests.cs b/tests/Exceptionless.Tests/Services/StackServiceTests.cs index 0676fa5d38..dc66be3f06 100644 --- a/tests/Exceptionless.Tests/Services/StackServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/StackServiceTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Exceptionless.Core.Repositories; +using Exceptionless.Core.Repositories; using Exceptionless.Core.Services; using Exceptionless.DateTimeExtensions; using Exceptionless.Tests.Utility; @@ -12,130 +9,130 @@ using Xunit.Abstractions; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Tests.Services { - public class StackServiceTests : IntegrationTestsBase { - private readonly ICacheClient _cache; - private readonly StackService _stackService; - private readonly IStackRepository _stackRepository; - - public StackServiceTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - Log.SetLogLevel(LogLevel.Trace); - _cache = GetService(); - _stackService = GetService(); - _stackRepository = GetService(); - } +namespace Exceptionless.Tests.Services; + +public class StackServiceTests : IntegrationTestsBase { + private readonly ICacheClient _cache; + private readonly StackService _stackService; + private readonly IStackRepository _stackRepository; + + public StackServiceTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + Log.SetLogLevel(LogLevel.Trace); + _cache = GetService(); + _stackService = GetService(); + _stackRepository = GetService(); + } + + [Fact] + public async Task IncrementUsage_OnlyChangeCache() { + var stack = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId), o => o.ImmediateConsistency()); + + // Assert stack state in elasticsearch before increment usage + Assert.Equal(0, stack.TotalOccurrences); + Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); + Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); + + // Assert state in cache before increment usage + Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); + Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); + Assert.Equal(0, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); + var occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); + Assert.True(occurrenceSet.IsNull || !occurrenceSet.HasValue || occurrenceSet.Value.Count == 0); + + var firstUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); + await RefreshDataAsync(); + await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, firstUtcNow, firstUtcNow, 1); + + // Assert stack state has no change after increment usage + stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.Equal(0, stack.TotalOccurrences); + Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); + Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); + + // Assert state in cache has been changed after increment usage + Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); + Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); + Assert.Equal(1, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); + occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); + Assert.Single(occurrenceSet.Value); + + var secondUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); + await RefreshDataAsync(); + await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, secondUtcNow, secondUtcNow, 2); + + // Assert state in cache has been changed after increment usage again + Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); + Assert.Equal(secondUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); + Assert.Equal(3, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); + occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); + Assert.Single(occurrenceSet.Value); + } + + [Fact] + public async Task IncrementUsageConcurrently() { + var stack = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId)); + var stack2 = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId2, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId)); - [Fact] - public async Task IncrementUsage_OnlyChangeCache() { - var stack = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId), o => o.ImmediateConsistency()); - - // Assert stack state in elasticsearch before increment usage - Assert.Equal(0, stack.TotalOccurrences); - Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); - Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); - - // Assert state in cache before increment usage - Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); - Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); - Assert.Equal(0, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - var occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); - Assert.True(occurrenceSet.IsNull || !occurrenceSet.HasValue || occurrenceSet.Value.Count == 0); - - var firstUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); - await RefreshDataAsync(); - await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, firstUtcNow, firstUtcNow, 1); - - // Assert stack state has no change after increment usage - stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); - Assert.Equal(0, stack.TotalOccurrences); - Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); - Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); - - // Assert state in cache has been changed after increment usage - Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); - Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); - Assert.Equal(1, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); - Assert.Single(occurrenceSet.Value); - - var secondUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); - await RefreshDataAsync(); - await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, secondUtcNow, secondUtcNow, 2); - - // Assert state in cache has been changed after increment usage again - Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); - Assert.Equal(secondUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); - Assert.Equal(3, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); - Assert.Single(occurrenceSet.Value); + DateTime? minOccurrenceDate = null, maxOccurrenceDate = null; + var tasks = new List(); + for (int i = 0; i < 10; i++) { + tasks.Add(IncrementUsageBatch()); } + await Task.WhenAll(tasks); - [Fact] - public async Task IncrementUsageConcurrently() { - var stack = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId)); - var stack2 = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId2, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId)); + // Assert stack state has no change after increment usage + stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.Equal(0, stack.TotalOccurrences); + Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); + Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); - DateTime? minOccurrenceDate = null, maxOccurrenceDate = null; - var tasks = new List(); + // Assert state in cache has been changed after increment usage + Assert.Equal(minOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); + Assert.Equal(maxOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); + Assert.Equal(100, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); + + stack2 = await _stackRepository.GetByIdAsync(TestConstants.StackId2); + Assert.Equal(0, stack2.TotalOccurrences); + Assert.True(stack2.FirstOccurrence <= SystemClock.UtcNow); + Assert.True(stack2.LastOccurrence <= SystemClock.UtcNow); + + // Assert state in cache has been changed after increment usage + Assert.Equal(minOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack2.Id))); + Assert.Equal(maxOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack2.Id))); + Assert.Equal(200, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack2.Id), 0)); + + var occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); + Assert.Equal(2, occurrenceSet.Value.Count); + + async Task IncrementUsageBatch() { for (int i = 0; i < 10; i++) { - tasks.Add(IncrementUsageBatch()); - } - await Task.WhenAll(tasks); - - // Assert stack state has no change after increment usage - stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); - Assert.Equal(0, stack.TotalOccurrences); - Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); - Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); - - // Assert state in cache has been changed after increment usage - Assert.Equal(minOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); - Assert.Equal(maxOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); - Assert.Equal(100, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - - stack2 = await _stackRepository.GetByIdAsync(TestConstants.StackId2); - Assert.Equal(0, stack2.TotalOccurrences); - Assert.True(stack2.FirstOccurrence <= SystemClock.UtcNow); - Assert.True(stack2.LastOccurrence <= SystemClock.UtcNow); - - // Assert state in cache has been changed after increment usage - Assert.Equal(minOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack2.Id))); - Assert.Equal(maxOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack2.Id))); - Assert.Equal(200, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack2.Id), 0)); - - var occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); - Assert.Equal(2, occurrenceSet.Value.Count); - - async Task IncrementUsageBatch() { - for (int i = 0; i < 10; i++) { - var utcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); - if (!minOccurrenceDate.HasValue) - minOccurrenceDate = utcNow; - maxOccurrenceDate = utcNow; - await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow, utcNow, 1); - await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack2.Id, utcNow, utcNow, 2); - } + var utcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); + if (!minOccurrenceDate.HasValue) + minOccurrenceDate = utcNow; + maxOccurrenceDate = utcNow; + await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow, utcNow, 1); + await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack2.Id, utcNow, utcNow, 2); } } + } - [Fact] - public async Task CanSaveStackUsage() { - var stack = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId)); + [Fact] + public async Task CanSaveStackUsage() { + var stack = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId)); - var utcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); - DateTime minOccurrenceDate = utcNow.AddMinutes(-1), maxOccurrenceDate = utcNow; - await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, minOccurrenceDate, maxOccurrenceDate, 10); + var utcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); + DateTime minOccurrenceDate = utcNow.AddMinutes(-1), maxOccurrenceDate = utcNow; + await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, minOccurrenceDate, maxOccurrenceDate, 10); - await _stackService.SaveStackUsagesAsync(false); + await _stackService.SaveStackUsagesAsync(false); - // Assert state in cache after save stack usage - Assert.Equal(0, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); + // Assert state in cache after save stack usage + Assert.Equal(0, await _cache.GetAsync(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - // Assert stack state after save stack usage - stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); - Assert.Equal(10, stack.TotalOccurrences); - Assert.Equal(minOccurrenceDate, stack.FirstOccurrence); - Assert.Equal(maxOccurrenceDate, stack.LastOccurrence); - } + // Assert stack state after save stack usage + stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.Equal(10, stack.TotalOccurrences); + Assert.Equal(minOccurrenceDate, stack.FirstOccurrence); + Assert.Equal(maxOccurrenceDate, stack.LastOccurrence); } } diff --git a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs index 48fd3490c6..8c27bad639 100644 --- a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; +using System.Diagnostics; using Exceptionless.Tests.Extensions; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; @@ -14,236 +12,235 @@ using Foundatio.Messaging; using Foundatio.Repositories; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Tests.Services { - public sealed class UsageServiceTests : IntegrationTestsBase { - private readonly ICacheClient _cache; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly UsageService _usageService; - private readonly BillingPlans _plans; - - public UsageServiceTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - Log.SetLogLevel(LogLevel.Information); - _cache = GetService(); - _usageService = GetService(); - _organizationRepository = GetService(); - _projectRepository = GetService(); - _plans = GetService(); - } - - [Fact] - public async Task CanIncrementUsageAsync() { - var messageBus = GetService(); - - var countdown = new AsyncCountdownEvent(2); - await messageBus.SubscribeAsync(po => { - _logger.LogInformation("Plan Overage for {organization} (Hourly: {IsHourly})", po.OrganizationId, po.IsHourly); - countdown.Signal(); - }); - - var o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 750, PlanId = _plans.SmallPlan.Id }); - var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); - - await RefreshDataAsync(); - Assert.InRange(o.GetHourlyEventLimit(_plans), 1, 750); - - int totalToIncrement = o.GetHourlyEventLimit(_plans) - 1; - Assert.False(await _usageService.IncrementUsageAsync(o, project, false, totalToIncrement)); - await RefreshDataAsync(); - o = await _organizationRepository.GetByIdAsync(o.Id); - - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(2, countdown.CurrentCount); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - - Assert.True(await _usageService.IncrementUsageAsync(o, project, false, 2)); - await RefreshDataAsync(); - o = await _organizationRepository.GetByIdAsync(o.Id); - - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(1, countdown.CurrentCount); - Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(1, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(1, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(1, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(1, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - - o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 750, PlanId = _plans.SmallPlan.Id }); - project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); - await RefreshDataAsync(); - - await _cache.RemoveAllAsync(); - totalToIncrement = o.GetHourlyEventLimit(_plans) + 20; - Assert.True(await _usageService.IncrementUsageAsync(o, project, false, totalToIncrement)); - - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(0, countdown.CurrentCount); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(20, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(20, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(20, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(20, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - } - - - [Fact] - public async Task WillNotThrottleFreePlan() { - var messageBus = GetService(); - - var countdown = new AsyncCountdownEvent(2); - await messageBus.SubscribeAsync(po => { - _logger.LogInformation("Plan Overage for {organization} (Hourly: {IsHourly})", po.OrganizationId, po.IsHourly); - countdown.Signal(); - }); - - const int limit = 750; - var o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = limit, PlanId = _plans.FreePlan.Id }); - var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); - - await RefreshDataAsync(); - Assert.Equal(limit, o.GetHourlyEventLimit(_plans)); - - Assert.False(await _usageService.IncrementUsageAsync(o, project, false, limit)); - await RefreshDataAsync(); - o = await _organizationRepository.GetByIdAsync(o.Id); - - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(2, countdown.CurrentCount); - Assert.Equal(limit, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(limit, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(limit, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(limit, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - - Assert.True(await _usageService.IncrementUsageAsync(o, project, false, 2)); - await RefreshDataAsync(); - o = await _organizationRepository.GetByIdAsync(o.Id); - - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(1, countdown.CurrentCount); - Assert.Equal(limit + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(limit + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(limit + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(limit + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(2, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(2, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(2, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(2, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - } - - [Fact] - public async Task CanIncrementSuspendedOrganizationUsageAsync() { - var messageBus = GetService(); - - var countdown = new AsyncCountdownEvent(2); - await messageBus.SubscribeAsync(po => { - _logger.LogInformation("Plan Overage for {organization} (Hourly: {IsHourly})", po.OrganizationId, po.IsHourly); - countdown.Signal(); - }); - - var o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 750, PlanId = _plans.SmallPlan.Id }, opt => opt.Cache()); - var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); - Assert.False(await _usageService.IncrementUsageAsync(o, project, false, 5)); - - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(2, countdown.CurrentCount); - Assert.Equal(5, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(5, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(5, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(5, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - - o.IsSuspended = true; - o.SuspendedByUserId = TestConstants.UserId; - o.SuspensionDate = SystemClock.UtcNow; - o.SuspensionCode = SuspensionCode.Billing; - o = await _organizationRepository.SaveAsync(o, opt => opt.Cache()); - - Assert.True(await _usageService.IncrementUsageAsync(o, project, false, 4995)); - - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(1, countdown.CurrentCount); - Assert.Equal(5000, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(5000, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(5000, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(5000, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - - o.RemoveSuspension(); - o = await _organizationRepository.SaveAsync(o, opt => opt.Cache()); - - Assert.False(await _usageService.IncrementUsageAsync(o, project, false, 1)); - await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); - Assert.Equal(1, countdown.CurrentCount); - Assert.Equal(5001, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); - Assert.Equal(5001, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(5001, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); - Assert.Equal(5001, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); - Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); - } - - [Fact] - public async Task RunBenchmarkAsync() { - const int iterations = 10000; - var org = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 1000000, PlanId = _plans.ExtraLargePlan.Id}, opt => opt.Cache()); - var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = org.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); - - var sw = Stopwatch.StartNew(); - for (int i = 0; i < iterations; i++) - await _usageService.IncrementUsageAsync(org, project, false); - - sw.Stop(); - _logger.LogInformation("Time: {Duration:g}, Avg: ({AverageTickDuration:g}ticks | {AverageDuration}ms)", sw.Elapsed, sw.ElapsedTicks / iterations, sw.ElapsedMilliseconds / iterations); - } - - private string GetHourlyBlockedCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } - - private string GetHourlyTotalCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:total", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } - - private string GetMonthlyBlockedCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } - - private string GetMonthlyTotalCacheKey(string organizationId, string projectId = null) { - string key = String.Concat("usage:total", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); - return projectId == null ? key : String.Concat(key, ":", projectId); - } +namespace Exceptionless.Tests.Services; + +public sealed class UsageServiceTests : IntegrationTestsBase { + private readonly ICacheClient _cache; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly UsageService _usageService; + private readonly BillingPlans _plans; + + public UsageServiceTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + Log.SetLogLevel(LogLevel.Information); + _cache = GetService(); + _usageService = GetService(); + _organizationRepository = GetService(); + _projectRepository = GetService(); + _plans = GetService(); + } + + [Fact] + public async Task CanIncrementUsageAsync() { + var messageBus = GetService(); + + var countdown = new AsyncCountdownEvent(2); + await messageBus.SubscribeAsync(po => { + _logger.LogInformation("Plan Overage for {organization} (Hourly: {IsHourly})", po.OrganizationId, po.IsHourly); + countdown.Signal(); + }); + + var o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 750, PlanId = _plans.SmallPlan.Id }); + var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); + + await RefreshDataAsync(); + Assert.InRange(o.GetHourlyEventLimit(_plans), 1, 750); + + int totalToIncrement = o.GetHourlyEventLimit(_plans) - 1; + Assert.False(await _usageService.IncrementUsageAsync(o, project, false, totalToIncrement)); + await RefreshDataAsync(); + o = await _organizationRepository.GetByIdAsync(o.Id); + + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(2, countdown.CurrentCount); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + + Assert.True(await _usageService.IncrementUsageAsync(o, project, false, 2)); + await RefreshDataAsync(); + o = await _organizationRepository.GetByIdAsync(o.Id); + + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(1, countdown.CurrentCount); + Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(totalToIncrement + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(1, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(1, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(1, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(1, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + + o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 750, PlanId = _plans.SmallPlan.Id }); + project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); + await RefreshDataAsync(); + + await _cache.RemoveAllAsync(); + totalToIncrement = o.GetHourlyEventLimit(_plans) + 20; + Assert.True(await _usageService.IncrementUsageAsync(o, project, false, totalToIncrement)); + + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(0, countdown.CurrentCount); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(totalToIncrement, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(20, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(20, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(20, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(20, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + } + + + [Fact] + public async Task WillNotThrottleFreePlan() { + var messageBus = GetService(); + + var countdown = new AsyncCountdownEvent(2); + await messageBus.SubscribeAsync(po => { + _logger.LogInformation("Plan Overage for {organization} (Hourly: {IsHourly})", po.OrganizationId, po.IsHourly); + countdown.Signal(); + }); + + const int limit = 750; + var o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = limit, PlanId = _plans.FreePlan.Id }); + var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); + + await RefreshDataAsync(); + Assert.Equal(limit, o.GetHourlyEventLimit(_plans)); + + Assert.False(await _usageService.IncrementUsageAsync(o, project, false, limit)); + await RefreshDataAsync(); + o = await _organizationRepository.GetByIdAsync(o.Id); + + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(2, countdown.CurrentCount); + Assert.Equal(limit, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(limit, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(limit, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(limit, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + + Assert.True(await _usageService.IncrementUsageAsync(o, project, false, 2)); + await RefreshDataAsync(); + o = await _organizationRepository.GetByIdAsync(o.Id); + + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(1, countdown.CurrentCount); + Assert.Equal(limit + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(limit + 2, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(limit + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(limit + 2, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(2, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(2, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(2, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(2, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + } + + [Fact] + public async Task CanIncrementSuspendedOrganizationUsageAsync() { + var messageBus = GetService(); + + var countdown = new AsyncCountdownEvent(2); + await messageBus.SubscribeAsync(po => { + _logger.LogInformation("Plan Overage for {organization} (Hourly: {IsHourly})", po.OrganizationId, po.IsHourly); + countdown.Signal(); + }); + + var o = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 750, PlanId = _plans.SmallPlan.Id }, opt => opt.Cache()); + var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = o.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); + Assert.False(await _usageService.IncrementUsageAsync(o, project, false, 5)); + + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(2, countdown.CurrentCount); + Assert.Equal(5, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(5, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(5, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(5, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(0, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + + o.IsSuspended = true; + o.SuspendedByUserId = TestConstants.UserId; + o.SuspensionDate = SystemClock.UtcNow; + o.SuspensionCode = SuspensionCode.Billing; + o = await _organizationRepository.SaveAsync(o, opt => opt.Cache()); + + Assert.True(await _usageService.IncrementUsageAsync(o, project, false, 4995)); + + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(1, countdown.CurrentCount); + Assert.Equal(5000, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(5000, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(5000, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(5000, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + + o.RemoveSuspension(); + o = await _organizationRepository.SaveAsync(o, opt => opt.Cache()); + + Assert.False(await _usageService.IncrementUsageAsync(o, project, false, 1)); + await countdown.WaitAsync(TimeSpan.FromMilliseconds(150)); + Assert.Equal(1, countdown.CurrentCount); + Assert.Equal(5001, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id), 0)); + Assert.Equal(5001, await _cache.GetAsync(GetHourlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(5001, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id), 0)); + Assert.Equal(5001, await _cache.GetAsync(GetMonthlyTotalCacheKey(o.Id, project.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetHourlyBlockedCacheKey(o.Id, project.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id), 0)); + Assert.Equal(4995, await _cache.GetAsync(GetMonthlyBlockedCacheKey(o.Id, project.Id), 0)); + } + + [Fact] + public async Task RunBenchmarkAsync() { + const int iterations = 10000; + var org = await _organizationRepository.AddAsync(new Organization { Name = "Test", MaxEventsPerMonth = 1000000, PlanId = _plans.ExtraLargePlan.Id }, opt => opt.Cache()); + var project = await _projectRepository.AddAsync(new Project { Name = "Test", OrganizationId = org.Id, NextSummaryEndOfDayTicks = SystemClock.UtcNow.Ticks }, opt => opt.Cache()); + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + await _usageService.IncrementUsageAsync(org, project, false); + + sw.Stop(); + _logger.LogInformation("Time: {Duration:g}, Avg: ({AverageTickDuration:g}ticks | {AverageDuration}ms)", sw.Elapsed, sw.ElapsedTicks / iterations, sw.ElapsedMilliseconds / iterations); + } + + private string GetHourlyBlockedCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } + + private string GetHourlyTotalCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:total", ":", SystemClock.UtcNow.ToString("MMddHH"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } + + private string GetMonthlyBlockedCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:blocked", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); + } + + private string GetMonthlyTotalCacheKey(string organizationId, string projectId = null) { + string key = String.Concat("usage:total", ":", SystemClock.UtcNow.Date.ToString("MM"), ":", organizationId); + return projectId == null ? key : String.Concat(key, ":", projectId); } } diff --git a/tests/Exceptionless.Tests/Stats/AggregationTests.cs b/tests/Exceptionless.Tests/Stats/AggregationTests.cs index 1bf4d5d185..af22d72fad 100644 --- a/tests/Exceptionless.Tests/Stats/AggregationTests.cs +++ b/tests/Exceptionless.Tests/Stats/AggregationTests.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Billing; using Exceptionless.Core.Models; using Exceptionless.Core.Pipeline; @@ -17,207 +13,208 @@ using Xunit.Abstractions; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -namespace Exceptionless.Tests.Stats { - public sealed class AggregationTests : IntegrationTestsBase { - private readonly EventPipeline _pipeline; - private readonly IEventRepository _eventRepository; - private readonly IStackRepository _stackRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProjectRepository _projectRepository; - private readonly StackService _stackService; - private readonly BillingManager _billingManager; - private readonly BillingPlans _plans; - - public AggregationTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { - _pipeline = GetService(); - _eventRepository = GetService(); - _stackRepository = GetService(); - _organizationRepository = GetService(); - _projectRepository = GetService(); - _stackService = GetService(); - _billingManager = GetService(); - _plans = GetService(); - } +namespace Exceptionless.Tests.Stats; + +public sealed class AggregationTests : IntegrationTestsBase { + private readonly EventPipeline _pipeline; + private readonly IEventRepository _eventRepository; + private readonly IStackRepository _stackRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly StackService _stackService; + private readonly BillingManager _billingManager; + private readonly BillingPlans _plans; + + public AggregationTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { + _pipeline = GetService(); + _eventRepository = GetService(); + _stackRepository = GetService(); + _organizationRepository = GetService(); + _projectRepository = GetService(); + _stackService = GetService(); + _billingManager = GetService(); + _plans = GetService(); + } - [Fact] - public async Task CanGetCardinalityAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount, false); - Log.SetLogLevel(LogLevel.Trace); + [Fact] + public async Task CanGetCardinalityAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount, false); + Log.SetLogLevel(LogLevel.Trace); - var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("cardinality:stack_id cardinality:id")); - Assert.Equal(eventCount, result.Total); - Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); - Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Cardinality("cardinality_stack_id").Value.GetValueOrDefault()); - } + var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("cardinality:stack_id cardinality:id")); + Assert.Equal(eventCount, result.Total); + Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); + Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Cardinality("cardinality_stack_id").Value.GetValueOrDefault()); + } - [Fact] - public async Task CanGetDateHistogramWithCardinalityAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount, false); - Log.SetLogLevel(LogLevel.Trace); - - var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("date:(date cardinality:id) cardinality:id")); - Assert.Equal(eventCount, result.Total); - Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Total)); - Assert.Equal(1, result.Aggregations.DateHistogram("date_date").Buckets.First().Aggregations.Count); - Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); - Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault())); - - var stacks = await _stackRepository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageLimit(100)); - foreach (var stack in stacks.Documents) { - var stackResult = await _eventRepository.CountAsync(q => q.FilterExpression($"stack:{stack.Id}").AggregationsExpression("cardinality:id")); - Assert.Equal(stack.TotalOccurrences, stackResult.Total); - Assert.Equal(stack.TotalOccurrences, stackResult.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); - } + [Fact] + public async Task CanGetDateHistogramWithCardinalityAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount, false); + Log.SetLogLevel(LogLevel.Trace); + + var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("date:(date cardinality:id) cardinality:id")); + Assert.Equal(eventCount, result.Total); + Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Total)); + Assert.Equal(1, result.Aggregations.DateHistogram("date_date").Buckets.First().Aggregations.Count); + Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); + Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault())); + + var stacks = await _stackRepository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageLimit(100)); + foreach (var stack in stacks.Documents) { + var stackResult = await _eventRepository.CountAsync(q => q.FilterExpression($"stack:{stack.Id}").AggregationsExpression("cardinality:id")); + Assert.Equal(stack.TotalOccurrences, stackResult.Total); + Assert.Equal(stack.TotalOccurrences, stackResult.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); } + } - [Fact] - public async Task CanGetExcludedTermsAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount, false); - Log.SetLogLevel(LogLevel.Trace); + [Fact] + public async Task CanGetExcludedTermsAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount, false); + Log.SetLogLevel(LogLevel.Trace); - var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("terms:(is_first_occurrence @include:true)")); - Assert.Equal(eventCount, result.Total); - Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Terms("terms_is_first_occurrence").Buckets.First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); - } + var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("terms:(is_first_occurrence @include:true)")); + Assert.Equal(eventCount, result.Total); + Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Terms("terms_is_first_occurrence").Buckets.First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); + } - [Fact] - public async Task CanGetNumericAggregationsAsync() { - await CreateDataAsync(0, false); + [Fact] + public async Task CanGetNumericAggregationsAsync() { + await CreateDataAsync(0, false); - decimal?[] values = new decimal?[] { null, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; - foreach (decimal? value in values) - await CreateEventsAsync(1, null, value); + decimal?[] values = new decimal?[] { null, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; + foreach (decimal? value in values) + await CreateEventsAsync(1, null, value); - Log.SetLogLevel(LogLevel.Trace); - var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("avg:value~0 cardinality:value~0 sum:value~0 min:value~0 max:value~0")); + Log.SetLogLevel(LogLevel.Trace); + var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("avg:value~0 cardinality:value~0 sum:value~0 min:value~0 max:value~0")); - Assert.Equal(values.Length, result.Total); - Assert.Equal(5, result.Aggregations.Count); - Assert.Equal(50, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); - Assert.Equal(11, result.Aggregations.Cardinality("cardinality_value").Value.GetValueOrDefault()); - Assert.Equal(550, result.Aggregations.Sum("sum_value").Value.GetValueOrDefault()); - Assert.Equal(0, result.Aggregations.Min("min_value").Value.GetValueOrDefault()); - Assert.Equal(100, result.Aggregations.Max("max_value").Value.GetValueOrDefault()); - } + Assert.Equal(values.Length, result.Total); + Assert.Equal(5, result.Aggregations.Count); + Assert.Equal(50, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); + Assert.Equal(11, result.Aggregations.Cardinality("cardinality_value").Value.GetValueOrDefault()); + Assert.Equal(550, result.Aggregations.Sum("sum_value").Value.GetValueOrDefault()); + Assert.Equal(0, result.Aggregations.Min("min_value").Value.GetValueOrDefault()); + Assert.Equal(100, result.Aggregations.Max("max_value").Value.GetValueOrDefault()); + } - [Fact] - public async Task CanGetTagTermAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount, false); - Log.SetLogLevel(LogLevel.Trace); - - var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:tags")); - Assert.Equal(eventCount, result.Total); - // each event can be in multiple tag buckets since an event can have up to 3 sample tags - Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); - Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Count, 1, TestConstants.EventTags.Count); - foreach (var term in result.Aggregations.Terms("terms_tags").Buckets) - Assert.InRange(term.Total.GetValueOrDefault(), 1, eventCount); - } + [Fact] + public async Task CanGetTagTermAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount, false); + Log.SetLogLevel(LogLevel.Trace); + + var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:tags")); + Assert.Equal(eventCount, result.Total); + // each event can be in multiple tag buckets since an event can have up to 3 sample tags + Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); + Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Count, 1, TestConstants.EventTags.Count); + foreach (var term in result.Aggregations.Terms("terms_tags").Buckets) + Assert.InRange(term.Total.GetValueOrDefault(), 1, eventCount); + } - [Fact] - public async Task CanGetVersionTermAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount, false); - Log.SetLogLevel(LogLevel.Trace); + [Fact] + public async Task CanGetVersionTermAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount, false); + Log.SetLogLevel(LogLevel.Trace); - var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:version")); - Assert.Equal(eventCount, result.Total); - // NOTE: The events are created without a version. - Assert.Equal(0, result.Aggregations.Terms("terms_version").Buckets.Count); - } + var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:version")); + Assert.Equal(eventCount, result.Total); + // NOTE: The events are created without a version. + Assert.Equal(0, result.Aggregations.Terms("terms_version").Buckets.Count); + } + + [Fact] + public async Task CanGetStackIdTermAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount, false); + Log.SetLogLevel(LogLevel.Trace); - [Fact] - public async Task CanGetStackIdTermAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount, false); - Log.SetLogLevel(LogLevel.Trace); - - long stackSize = await _stackRepository.CountAsync(); - var result = await _eventRepository.CountAsync(q => q.AggregationsExpression($"terms:(stack_id terms:(is_first_occurrence~{stackSize} @include:true))")); - Assert.Equal(eventCount, result.Total); - - var termsAggregation = result.Aggregations.Terms("terms_stack_id"); - Assert.Equal(eventCount, termsAggregation.Buckets.Sum(b1 => b1.Total.GetValueOrDefault()) + (long)termsAggregation.Data["SumOtherDocCount"]); - foreach (var term in termsAggregation.Buckets) { - Assert.Equal(1, term.Aggregations.Terms("terms_is_first_occurrence").Buckets.Sum(b => b.Total.GetValueOrDefault())); - } + long stackSize = await _stackRepository.CountAsync(); + var result = await _eventRepository.CountAsync(q => q.AggregationsExpression($"terms:(stack_id terms:(is_first_occurrence~{stackSize} @include:true))")); + Assert.Equal(eventCount, result.Total); + + var termsAggregation = result.Aggregations.Terms("terms_stack_id"); + Assert.Equal(eventCount, termsAggregation.Buckets.Sum(b1 => b1.Total.GetValueOrDefault()) + (long)termsAggregation.Data["SumOtherDocCount"]); + foreach (var term in termsAggregation.Buckets) { + Assert.Equal(1, term.Aggregations.Terms("terms_is_first_occurrence").Buckets.Sum(b => b.Total.GetValueOrDefault())); } + } - [Fact] - public async Task CanGetStackIdTermMinMaxAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount, false); - Log.SetLogLevel(LogLevel.Trace); - Log.SetLogLevel(LogLevel.Trace); + [Fact] + public async Task CanGetStackIdTermMinMaxAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount, false); + Log.SetLogLevel(LogLevel.Trace); + Log.SetLogLevel(LogLevel.Trace); - long stackSize = await _stackRepository.CountAsync(); - var result = await _eventRepository.CountAsync(q => q.AggregationsExpression($"terms:(stack_id~{stackSize} min:date max:date)")); - Assert.Equal(eventCount, result.Total); + long stackSize = await _stackRepository.CountAsync(); + var result = await _eventRepository.CountAsync(q => q.AggregationsExpression($"terms:(stack_id~{stackSize} min:date max:date)")); + Assert.Equal(eventCount, result.Total); - var termsAggregation = result.Aggregations.Terms("terms_stack_id"); - var largestStackBucket = termsAggregation.Buckets.First(); + var termsAggregation = result.Aggregations.Terms("terms_stack_id"); + var largestStackBucket = termsAggregation.Buckets.First(); - var events = await _eventRepository.FindAsync(q => q.FilterExpression($"stack:{largestStackBucket.Key}"), o => o.PageLimit(eventCount)); - Assert.Equal(largestStackBucket.Total.GetValueOrDefault(), events.Total); + var events = await _eventRepository.FindAsync(q => q.FilterExpression($"stack:{largestStackBucket.Key}"), o => o.PageLimit(eventCount)); + Assert.Equal(largestStackBucket.Total.GetValueOrDefault(), events.Total); - var oldestEvent = events.Documents.OrderBy(e => e.Date).First(); - Assert.Equal(oldestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("min_date").Value.Floor(TimeSpan.FromMilliseconds(1))); + var oldestEvent = events.Documents.OrderBy(e => e.Date).First(); + Assert.Equal(oldestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("min_date").Value.Floor(TimeSpan.FromMilliseconds(1))); - var newestEvent= events.Documents.OrderByDescending(e => e.Date).First(); - Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("max_date").Value.Floor(TimeSpan.FromMilliseconds(1))); - } + var newestEvent = events.Documents.OrderByDescending(e => e.Date).First(); + Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("max_date").Value.Floor(TimeSpan.FromMilliseconds(1))); + } - [Fact] - public async Task CanGetProjectTermAggregationsAsync() { - const int eventCount = 100; - await CreateDataAsync(eventCount); - Log.SetLogLevel(LogLevel.Trace); + [Fact] + public async Task CanGetProjectTermAggregationsAsync() { + const int eventCount = 100; + await CreateDataAsync(eventCount); + Log.SetLogLevel(LogLevel.Trace); - var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:project_id")); - Assert.Equal(eventCount, result.Total); - Assert.InRange(result.Aggregations.Terms("terms_project_id").Buckets.Count, 1, 3); // 3 sample projects - Assert.Equal(eventCount, result.Aggregations.Terms("terms_project_id").Buckets.Sum(t => t.Total.GetValueOrDefault())); - } + var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:project_id")); + Assert.Equal(eventCount, result.Total); + Assert.InRange(result.Aggregations.Terms("terms_project_id").Buckets.Count, 1, 3); // 3 sample projects + Assert.Equal(eventCount, result.Aggregations.Terms("terms_project_id").Buckets.Sum(t => t.Total.GetValueOrDefault())); + } - [Fact] - public async Task CanGetSessionAggregationsAsync() { - await CreateDataAsync(); - await CreateSessionEventsAsync(); + [Fact] + public async Task CanGetSessionAggregationsAsync() { + await CreateDataAsync(); + await CreateSessionEventsAsync(); - var result = await _eventRepository.CountAsync(q => q.FilterExpression("type:session").AggregationsExpression("avg:value cardinality:user")); - Assert.Equal(3, result.Total); - Assert.Equal(3, result.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); - Assert.Equal(3600.0 / result.Total, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); - } + var result = await _eventRepository.CountAsync(q => q.FilterExpression("type:session").AggregationsExpression("avg:value cardinality:user")); + Assert.Equal(3, result.Total); + Assert.Equal(3, result.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); + Assert.Equal(3600.0 / result.Total, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); + } - private async Task CreateDataAsync(int eventCount = 0, bool multipleProjects = true) { - var orgs = OrganizationData.GenerateSampleOrganizations(_billingManager, _plans); - await _organizationRepository.AddAsync(orgs, o => o.Cache()); + private async Task CreateDataAsync(int eventCount = 0, bool multipleProjects = true) { + var orgs = OrganizationData.GenerateSampleOrganizations(_billingManager, _plans); + await _organizationRepository.AddAsync(orgs, o => o.Cache()); - var projects = ProjectData.GenerateSampleProjects(); - await _projectRepository.AddAsync(projects, o => o.Cache()); - await RefreshDataAsync(); + var projects = ProjectData.GenerateSampleProjects(); + await _projectRepository.AddAsync(projects, o => o.Cache()); + await RefreshDataAsync(); - if (eventCount > 0) - await CreateEventsAsync(eventCount, multipleProjects ? projects.Select(p => p.Id).ToArray() : new[] { TestConstants.ProjectId }); - } + if (eventCount > 0) + await CreateEventsAsync(eventCount, multipleProjects ? projects.Select(p => p.Id).ToArray() : new[] { TestConstants.ProjectId }); + } - private async Task CreateEventsAsync(int eventCount, string[] projectIds, decimal? value = -1) { - var events = EventData.GenerateEvents(eventCount, projectIds: projectIds, startDate: SystemClock.OffsetUtcNow.SubtractDays(3), endDate: SystemClock.OffsetUtcNow, value: value); - foreach (var eventGroup in events.GroupBy(ev => ev.ProjectId)) - await _pipeline.RunAsync(eventGroup, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - await _stackService.SaveStackUsagesAsync(); + private async Task CreateEventsAsync(int eventCount, string[] projectIds, decimal? value = -1) { + var events = EventData.GenerateEvents(eventCount, projectIds: projectIds, startDate: SystemClock.OffsetUtcNow.SubtractDays(3), endDate: SystemClock.OffsetUtcNow, value: value); + foreach (var eventGroup in events.GroupBy(ev => ev.ProjectId)) + await _pipeline.RunAsync(eventGroup, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + await _stackService.SaveStackUsagesAsync(); - await RefreshDataAsync(); - } + await RefreshDataAsync(); + } - private async Task> CreateSessionEventsAsync() { - var startDate = SystemClock.OffsetUtcNow.SubtractHours(1); - var events = new List { + private async Task> CreateSessionEventsAsync() { + var startDate = SystemClock.OffsetUtcNow.SubtractHours(1); + var events = new List { EventData.GenerateSessionStartEvent(occurrenceDate: startDate, userIdentity: "1"), EventData.GenerateSessionEndEvent(occurrenceDate: startDate.AddMinutes(10), userIdentity: "1"), EventData.GenerateSessionStartEvent(occurrenceDate: startDate.AddMinutes(10), userIdentity: "2"), @@ -226,17 +223,16 @@ private async Task> CreateSessionEventsAsync() { EventData.GenerateSessionEndEvent(occurrenceDate: startDate.AddMinutes(50), userIdentity: "3") }; - await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); - await RefreshDataAsync(); + await _pipeline.RunAsync(events, OrganizationData.GenerateSampleOrganization(_billingManager, _plans), ProjectData.GenerateSampleProject()); + await RefreshDataAsync(); - var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); - Assert.Equal(6, results.Total); - Assert.Equal(3, results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); + var results = await _eventRepository.FindAsync(q => q.SortExpression(EventIndex.Alias.Date)); + Assert.Equal(6, results.Total); + Assert.Equal(3, results.Documents.Where(e => !String.IsNullOrEmpty(e.GetSessionId())).Select(e => e.GetSessionId()).Distinct().Count()); - var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); - Assert.Equal(TimeSpan.FromMinutes(20).TotalSeconds, (int)(sessionStarts.Sum(e => e.Value.GetValueOrDefault()) / sessionStarts.Count)); + var sessionStarts = results.Documents.Where(e => e.IsSessionStart()).ToList(); + Assert.Equal(TimeSpan.FromMinutes(20).TotalSeconds, (int)(sessionStarts.Sum(e => e.Value.GetValueOrDefault()) / sessionStarts.Count)); - return events; - } + return events; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/TestWithServices.cs b/tests/Exceptionless.Tests/TestWithServices.cs index baf49a30ea..7cdbd6ff30 100644 --- a/tests/Exceptionless.Tests/TestWithServices.cs +++ b/tests/Exceptionless.Tests/TestWithServices.cs @@ -1,15 +1,10 @@ -using System; -using System.Threading.Tasks; -using Exceptionless.Tests.Authentication; +using Exceptionless.Tests.Authentication; using Exceptionless.Tests.Mail; using Exceptionless.Core.Authentication; using Exceptionless.Core.Mail; using Exceptionless.Insulation.Configuration; using Foundatio.Xunit; using Foundatio.Utility; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Xunit.Abstractions; using Exceptionless.Core; using Foundatio.Caching; @@ -18,60 +13,60 @@ using Foundatio.Metrics; using IAsyncLifetime = Foundatio.Utility.IAsyncLifetime; -namespace Exceptionless.Tests { - public class TestWithServices : TestWithLoggingBase, IAsyncLifetime { - private readonly IDisposable _testSystemClock = TestSystemClock.Install(); - private readonly IServiceProvider _container; +namespace Exceptionless.Tests; - public TestWithServices(ITestOutputHelper output) : base(output) { - Log.MinimumLevel = LogLevel.Information; - Log.SetLogLevel(LogLevel.Warning); - Log.SetLogLevel(LogLevel.Warning); - Log.SetLogLevel(LogLevel.Warning); - Log.SetLogLevel(LogLevel.Information); - - _container = CreateContainer(); - } +public class TestWithServices : TestWithLoggingBase, IAsyncLifetime { + private readonly IDisposable _testSystemClock = TestSystemClock.Install(); + private readonly IServiceProvider _container; - public virtual async Task InitializeAsync() { - var result = await _container.RunStartupActionsAsync(); - if (!result.Success) - throw new ApplicationException($"Startup action \"{result.FailedActionName}\" failed"); - } + public TestWithServices(ITestOutputHelper output) : base(output) { + Log.MinimumLevel = LogLevel.Information; + Log.SetLogLevel(LogLevel.Warning); + Log.SetLogLevel(LogLevel.Warning); + Log.SetLogLevel(LogLevel.Warning); + Log.SetLogLevel(LogLevel.Information); - protected TService GetService() where TService : class { - return _container.GetRequiredService(); - } - - protected virtual void RegisterServices(IServiceCollection services, AppOptions options) { - services.AddSingleton(Log); - services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); - Web.Bootstrapper.RegisterServices(services, options, Log); - Core.Bootstrapper.RegisterServices(services); - services.AddSingleton(); - services.AddSingleton(); - } + _container = CreateContainer(); + } + + public virtual async Task InitializeAsync() { + var result = await _container.RunStartupActionsAsync(); + if (!result.Success) + throw new ApplicationException($"Startup action \"{result.FailedActionName}\" failed"); + } - private IServiceProvider CreateContainer() { - var services = new ServiceCollection(); - - var config = new ConfigurationBuilder() - .SetBasePath(AppContext.BaseDirectory) - .AddYamlFile("appsettings.yml", optional: false, reloadOnChange: false) - .AddEnvironmentVariables() - .Build(); - - services.AddSingleton(config); - var appOptions = AppOptions.ReadFromConfiguration(config); - services.AddSingleton(appOptions); - RegisterServices(services, appOptions); + protected TService GetService() where TService : class { + return _container.GetRequiredService(); + } + + protected virtual void RegisterServices(IServiceCollection services, AppOptions options) { + services.AddSingleton(Log); + services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); + Web.Bootstrapper.RegisterServices(services, options, Log); + Core.Bootstrapper.RegisterServices(services); + services.AddSingleton(); + services.AddSingleton(); + } + + private IServiceProvider CreateContainer() { + var services = new ServiceCollection(); + + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddYamlFile("appsettings.yml", optional: false, reloadOnChange: false) + .AddEnvironmentVariables() + .Build(); + + services.AddSingleton(config); + var appOptions = AppOptions.ReadFromConfiguration(config); + services.AddSingleton(appOptions); + RegisterServices(services, appOptions); + + return services.BuildServiceProvider(); + } - return services.BuildServiceProvider(); - } - - public ValueTask DisposeAsync() { - _testSystemClock.Dispose(); - return new ValueTask(Task.CompletedTask); - } + public ValueTask DisposeAsync() { + _testSystemClock.Dispose(); + return new ValueTask(Task.CompletedTask); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Utility/AppSendBuilder.cs b/tests/Exceptionless.Tests/Utility/AppSendBuilder.cs index faf371b71c..c6bb97e553 100644 --- a/tests/Exceptionless.Tests/Utility/AppSendBuilder.cs +++ b/tests/Exceptionless.Tests/Utility/AppSendBuilder.cs @@ -1,89 +1,87 @@ -using System; -using System.Net; -using System.Net.Http; +using System.Net; using Exceptionless.Core.Utility; using FluentRest; -namespace Exceptionless.Tests.Utility { - public class AppSendBuilder : PostBuilder { - internal static readonly HttpMethod HttpPatch = new HttpMethod("PATCH"); - - public AppSendBuilder(HttpRequestMessage request) : base(request) { } - - /// - /// Sets HTTP request method. - /// - /// The header request method. - /// A fluent request builder. - /// is . - public AppSendBuilder Method(HttpMethod method) { - RequestMessage.Method = method ?? throw new ArgumentNullException(nameof(method)); - return this; - } - - /// - /// Sets HTTP request method to POST. - /// - /// A fluent request builder. - public AppSendBuilder Post() { - return Method(HttpMethod.Post); - } - - /// - /// Sets HTTP request method to PUT. - /// - /// A fluent request builder. - public AppSendBuilder Put() { - return Method(HttpMethod.Put); - } - - /// - /// Sets HTTP request method to PATCH. - /// - /// A fluent request builder. - public AppSendBuilder Patch() { - return Method(HttpPatch); - } - - /// - /// Sets HTTP request method to DELETE. - /// - /// A fluent request builder. - public AppSendBuilder Delete() { - return Method(HttpMethod.Delete); - } - - public AppSendBuilder AsGlobalAdminUser() { - return this.BasicAuthorization(SampleDataService.TEST_USER_EMAIL, SampleDataService.TEST_USER_PASSWORD); - } - - public AppSendBuilder AsTestOrganizationUser() { - return this.BasicAuthorization(SampleDataService.TEST_ORG_USER_EMAIL, SampleDataService.TEST_ORG_USER_PASSWORD); - } - - public AppSendBuilder AsFreeOrganizationUser() { - return this.BasicAuthorization(SampleDataService.FREE_USER_EMAIL, SampleDataService.FREE_USER_PASSWORD); - } - - public AppSendBuilder AsTestOrganizationClientUser() { - return this.BearerToken(SampleDataService.TEST_API_KEY); - } - - public AppSendBuilder AsFreeOrganizationClientUser() { - return this.BearerToken(SampleDataService.FREE_API_KEY); - } - - public bool IsAnonymous { get; private set; } - public AppSendBuilder AsAnonymousUser() { - IsAnonymous = true; - return this; - } - - public AppSendBuilder ExpectedStatus(HttpStatusCode statusCode) { - RequestMessage.Options.Set(ExpectedStatusKey, statusCode); - return this; - } - - public static readonly HttpRequestOptionsKey ExpectedStatusKey = new HttpRequestOptionsKey("ExpectedStatus"); +namespace Exceptionless.Tests.Utility; + +public class AppSendBuilder : PostBuilder { + internal static readonly HttpMethod HttpPatch = new HttpMethod("PATCH"); + + public AppSendBuilder(HttpRequestMessage request) : base(request) { } + + /// + /// Sets HTTP request method. + /// + /// The header request method. + /// A fluent request builder. + /// is . + public AppSendBuilder Method(HttpMethod method) { + RequestMessage.Method = method ?? throw new ArgumentNullException(nameof(method)); + return this; + } + + /// + /// Sets HTTP request method to POST. + /// + /// A fluent request builder. + public AppSendBuilder Post() { + return Method(HttpMethod.Post); + } + + /// + /// Sets HTTP request method to PUT. + /// + /// A fluent request builder. + public AppSendBuilder Put() { + return Method(HttpMethod.Put); + } + + /// + /// Sets HTTP request method to PATCH. + /// + /// A fluent request builder. + public AppSendBuilder Patch() { + return Method(HttpPatch); + } + + /// + /// Sets HTTP request method to DELETE. + /// + /// A fluent request builder. + public AppSendBuilder Delete() { + return Method(HttpMethod.Delete); + } + + public AppSendBuilder AsGlobalAdminUser() { + return this.BasicAuthorization(SampleDataService.TEST_USER_EMAIL, SampleDataService.TEST_USER_PASSWORD); } -} \ No newline at end of file + + public AppSendBuilder AsTestOrganizationUser() { + return this.BasicAuthorization(SampleDataService.TEST_ORG_USER_EMAIL, SampleDataService.TEST_ORG_USER_PASSWORD); + } + + public AppSendBuilder AsFreeOrganizationUser() { + return this.BasicAuthorization(SampleDataService.FREE_USER_EMAIL, SampleDataService.FREE_USER_PASSWORD); + } + + public AppSendBuilder AsTestOrganizationClientUser() { + return this.BearerToken(SampleDataService.TEST_API_KEY); + } + + public AppSendBuilder AsFreeOrganizationClientUser() { + return this.BearerToken(SampleDataService.FREE_API_KEY); + } + + public bool IsAnonymous { get; private set; } + public AppSendBuilder AsAnonymousUser() { + IsAnonymous = true; + return this; + } + + public AppSendBuilder ExpectedStatus(HttpStatusCode statusCode) { + RequestMessage.Options.Set(ExpectedStatusKey, statusCode); + return this; + } + + public static readonly HttpRequestOptionsKey ExpectedStatusKey = new HttpRequestOptionsKey("ExpectedStatus"); +} diff --git a/tests/Exceptionless.Tests/Utility/DataBuilder.cs b/tests/Exceptionless.Tests/Utility/DataBuilder.cs index dcdd5d4c95..0adb2a27d6 100644 --- a/tests/Exceptionless.Tests/Utility/DataBuilder.cs +++ b/tests/Exceptionless.Tests/Utility/DataBuilder.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Plugins.Formatting; @@ -13,495 +10,498 @@ using Foundatio.Serializer; using Foundatio.Utility; -namespace Exceptionless.Tests.Utility { - public class DataBuilder { - private readonly List _eventBuilders; - private readonly IServiceProvider _serviceProvider; +namespace Exceptionless.Tests.Utility; - public DataBuilder(List eventBuilders, IServiceProvider serviceProvider) { - _eventBuilders = eventBuilders; - _serviceProvider = serviceProvider; - } +public class DataBuilder { + private readonly List _eventBuilders; + private readonly IServiceProvider _serviceProvider; - public EventDataBuilder Event() { - var eventBuilder = _serviceProvider.GetService(); - _eventBuilders.Add(eventBuilder); - return eventBuilder; - } + public DataBuilder(List eventBuilders, IServiceProvider serviceProvider) { + _eventBuilders = eventBuilders; + _serviceProvider = serviceProvider; } - public class EventDataBuilder { - private readonly FormattingPluginManager _formattingPluginManager; - private readonly ISerializer _serializer; - private readonly ICollection> _stackMutations; - private int _additionalEventsToCreate = 0; - private readonly PersistentEvent _event = new PersistentEvent(); - private Stack _stack = null; - private EventDataBuilder _stackEventBuilder; - private bool _isFirstOccurrenceSet = false; - - public EventDataBuilder(FormattingPluginManager formattingPluginManager, ISerializer serializer) { - _stackMutations = new List>(); - _formattingPluginManager = formattingPluginManager; - _serializer = serializer; - } + public EventDataBuilder Event() { + var eventBuilder = _serviceProvider.GetService(); + _eventBuilders.Add(eventBuilder); + return eventBuilder; + } +} - public EventDataBuilder Mutate(Action mutation) { - mutation?.Invoke(_event); +public class EventDataBuilder { + private readonly FormattingPluginManager _formattingPluginManager; + private readonly ISerializer _serializer; + private readonly ICollection> _stackMutations; + private int _additionalEventsToCreate = 0; + private readonly PersistentEvent _event = new PersistentEvent(); + private Stack _stack = null; + private EventDataBuilder _stackEventBuilder; + private bool _isFirstOccurrenceSet = false; + + public EventDataBuilder(FormattingPluginManager formattingPluginManager, ISerializer serializer) { + _stackMutations = new List>(); + _formattingPluginManager = formattingPluginManager; + _serializer = serializer; + } - return this; - } + public EventDataBuilder Mutate(Action mutation) { + mutation?.Invoke(_event); - public EventDataBuilder MutateStack(Action mutation) { - _stackMutations.Add(mutation); + return this; + } - return this; - } + public EventDataBuilder MutateStack(Action mutation) { + _stackMutations.Add(mutation); - public EventDataBuilder Stack(EventDataBuilder stackEventBuilder) { - _stackEventBuilder = stackEventBuilder; + return this; + } - return this; - } + public EventDataBuilder Stack(EventDataBuilder stackEventBuilder) { + _stackEventBuilder = stackEventBuilder; - public EventDataBuilder Stack(Stack stack) { - _stack = stack; + return this; + } - return this; - } + public EventDataBuilder Stack(Stack stack) { + _stack = stack; - public EventDataBuilder StackId(string stackId) { - _event.StackId = stackId; - _stackMutations.Add(s => s.Id = stackId); + return this; + } - return this; - } + public EventDataBuilder StackId(string stackId) { + _event.StackId = stackId; + _stackMutations.Add(s => s.Id = stackId); - public EventDataBuilder Id(string id) { - _event.Id = id; - return this; - } + return this; + } - public EventDataBuilder TestProject() { - Organization(SampleDataService.TEST_ORG_ID); - Project(SampleDataService.TEST_PROJECT_ID); + public EventDataBuilder Id(string id) { + _event.Id = id; + return this; + } - return this; - } + public EventDataBuilder TestProject() { + Organization(SampleDataService.TEST_ORG_ID); + Project(SampleDataService.TEST_PROJECT_ID); - public EventDataBuilder FreeProject() { - Organization(SampleDataService.FREE_ORG_ID); - Project(SampleDataService.FREE_PROJECT_ID); + return this; + } - return this; - } + public EventDataBuilder FreeProject() { + Organization(SampleDataService.FREE_ORG_ID); + Project(SampleDataService.FREE_PROJECT_ID); - public EventDataBuilder Organization(string organizationId) { - _event.OrganizationId = organizationId; - return this; - } + return this; + } - public EventDataBuilder Project(string projectId) { - _event.ProjectId = projectId; - return this; - } + public EventDataBuilder Organization(string organizationId) { + _event.OrganizationId = organizationId; + return this; + } - public EventDataBuilder Type(string type) { - _event.Type = type; - return this; - } + public EventDataBuilder Project(string projectId) { + _event.ProjectId = projectId; + return this; + } - public EventDataBuilder Date(DateTimeOffset date) { - _event.Date = date; - return this; - } + public EventDataBuilder Type(string type) { + _event.Type = type; + return this; + } - public EventDataBuilder Date(DateTime date) { - _event.Date = date.ToUniversalTime(); - return this; - } + public EventDataBuilder Date(DateTimeOffset date) { + _event.Date = date; + return this; + } - public EventDataBuilder Date(string date) { - if (DateTimeOffset.TryParse(date, out var dt)) - return Date(dt); - - throw new ArgumentException("Invalid date specified", nameof(date)); - } + public EventDataBuilder Date(DateTime date) { + _event.Date = date.ToUniversalTime(); + return this; + } - public EventDataBuilder IsFirstOccurrence(bool isFirstOccurrence = true) { - _isFirstOccurrenceSet = true; - _event.IsFirstOccurrence = isFirstOccurrence; + public EventDataBuilder Date(string date) { + if (DateTimeOffset.TryParse(date, out var dt)) + return Date(dt); - return this; - } + throw new ArgumentException("Invalid date specified", nameof(date)); + } - public EventDataBuilder CreatedDate(DateTime createdUtc) { - _event.CreatedUtc = createdUtc; - return this; - } + public EventDataBuilder IsFirstOccurrence(bool isFirstOccurrence = true) { + _isFirstOccurrenceSet = true; + _event.IsFirstOccurrence = isFirstOccurrence; - public EventDataBuilder CreatedDate(string createdUtc) { - if (DateTime.TryParse(createdUtc, out var dt)) - return CreatedDate(dt); - - throw new ArgumentException("Invalid date specified", nameof(createdUtc)); - } + return this; + } - public EventDataBuilder Message(string message) { - _event.Message = message; - _stackMutations.Add(s => s.Title = message); - return this; - } + public EventDataBuilder CreatedDate(DateTime createdUtc) { + _event.CreatedUtc = createdUtc; + return this; + } - public EventDataBuilder Source(string source) { - _event.Source = source; - return this; - } + public EventDataBuilder CreatedDate(string createdUtc) { + if (DateTime.TryParse(createdUtc, out var dt)) + return CreatedDate(dt); - public EventDataBuilder Tag(params string[] tags) { - _event.Tags.AddRange(tags); - return this; - } + throw new ArgumentException("Invalid date specified", nameof(createdUtc)); + } - public EventDataBuilder Geo(string geo) { - _event.Geo = geo; - return this; - } + public EventDataBuilder Message(string message) { + _event.Message = message; + _stackMutations.Add(s => s.Title = message); + return this; + } - public EventDataBuilder Value(decimal? value) { - _event.Value = value; - return this; - } + public EventDataBuilder Source(string source) { + _event.Source = source; + return this; + } - public EventDataBuilder EnvironmentInfo(EnvironmentInfo environmentInfo) { - _event.SetEnvironmentInfo(environmentInfo); - return this; - } + public EventDataBuilder Tag(params string[] tags) { + _event.Tags.AddRange(tags); + return this; + } - public EventDataBuilder RequestInfo(RequestInfo requestInfo) { - _event.AddRequestInfo(requestInfo); - return this; - } + public EventDataBuilder Geo(string geo) { + _event.Geo = geo; + return this; + } - public EventDataBuilder RequestInfo(string json) { - _event.AddRequestInfo(_serializer.Deserialize(json)); - return this; - } + public EventDataBuilder Value(decimal? value) { + _event.Value = value; + return this; + } - public EventDataBuilder RequestInfoSample(Action requestMutator = null) { - var requestInfo = _serializer.Deserialize(_sampleRequestInfo); - requestMutator?.Invoke(requestInfo); - _event.AddRequestInfo(requestInfo); + public EventDataBuilder EnvironmentInfo(EnvironmentInfo environmentInfo) { + _event.SetEnvironmentInfo(environmentInfo); + return this; + } - return this; - } + public EventDataBuilder RequestInfo(RequestInfo requestInfo) { + _event.AddRequestInfo(requestInfo); + return this; + } - public EventDataBuilder ReferenceId(string id) { - _event.ReferenceId = id; - return this; - } + public EventDataBuilder RequestInfo(string json) { + _event.AddRequestInfo(_serializer.Deserialize(json)); + return this; + } - public EventDataBuilder Reference(string name, string id) { - _event.SetEventReference(name, id); - return this; - } + public EventDataBuilder RequestInfoSample(Action requestMutator = null) { + var requestInfo = _serializer.Deserialize(_sampleRequestInfo); + requestMutator?.Invoke(requestInfo); + _event.AddRequestInfo(requestInfo); - public EventDataBuilder UserDescription(string emailAddress, string description) { - _event.SetUserDescription(emailAddress, description); - return this; - } + return this; + } - public EventDataBuilder ManualStackingKey(string title, string manualStackingKey) { - _event.SetManualStackingKey(title, manualStackingKey); - return this; - } + public EventDataBuilder ReferenceId(string id) { + _event.ReferenceId = id; + return this; + } - public EventDataBuilder ManualStackingKey(string manualStackingKey) { - _event.SetManualStackingKey(manualStackingKey); - return this; - } + public EventDataBuilder Reference(string name, string id) { + _event.SetEventReference(name, id); + return this; + } - public EventDataBuilder SessionId(string sessionId) { - _event.SetSessionId(sessionId); - return this; - } + public EventDataBuilder UserDescription(string emailAddress, string description) { + _event.SetUserDescription(emailAddress, description); + return this; + } - public EventDataBuilder SubmissionClient(SubmissionClient submissionClient) { - _event.SetSubmissionClient(submissionClient); - return this; - } + public EventDataBuilder ManualStackingKey(string title, string manualStackingKey) { + _event.SetManualStackingKey(title, manualStackingKey); + return this; + } - public EventDataBuilder UserIdentity(string identity) { - _event.SetUserIdentity(identity); - return this; - } + public EventDataBuilder ManualStackingKey(string manualStackingKey) { + _event.SetManualStackingKey(manualStackingKey); + return this; + } - public EventDataBuilder UserIdentity(string identity, string name) { - _event.SetUserIdentity(identity, name); - return this; - } + public EventDataBuilder SessionId(string sessionId) { + _event.SetSessionId(sessionId); + return this; + } - public EventDataBuilder UserIdentity(UserInfo userInfo) { - _event.SetUserIdentity(userInfo); - return this; - } + public EventDataBuilder SubmissionClient(SubmissionClient submissionClient) { + _event.SetSubmissionClient(submissionClient); + return this; + } - public EventDataBuilder Level(string level) { - _event.SetLevel(level); - return this; - } + public EventDataBuilder UserIdentity(string identity) { + _event.SetUserIdentity(identity); + return this; + } - public EventDataBuilder Version(string version) { - _event.SetVersion(version); - return this; - } + public EventDataBuilder UserIdentity(string identity, string name) { + _event.SetUserIdentity(identity, name); + return this; + } - public EventDataBuilder Location(Location location) { - _event.SetLocation(location); - return this; - } + public EventDataBuilder UserIdentity(UserInfo userInfo) { + _event.SetUserIdentity(userInfo); + return this; + } - public EventDataBuilder Deleted() { - _stackMutations.Add(s => s.IsDeleted = true); + public EventDataBuilder Level(string level) { + _event.SetLevel(level); + return this; + } - return this; - } + public EventDataBuilder Version(string version) { + _event.SetVersion(version); + return this; + } - public EventDataBuilder Status(StackStatus status) { - _stackMutations.Add(s => s.Status = status); + public EventDataBuilder Location(Location location) { + _event.SetLocation(location); + return this; + } - return this; - } + public EventDataBuilder Deleted() { + _stackMutations.Add(s => s.IsDeleted = true); - public EventDataBuilder StackReference(string reference) { - _stackMutations.Add(s => s.References.Add(reference)); + return this; + } - return this; - } + public EventDataBuilder Status(StackStatus status) { + _stackMutations.Add(s => s.Status = status); - public EventDataBuilder OccurrencesAreCritical(bool occurrencesAreCritical = true) { - if (occurrencesAreCritical) - _event.MarkAsCritical(); + return this; + } - _stackMutations.Add(s => { - s.OccurrencesAreCritical = occurrencesAreCritical; - s.Tags.Add(Event.KnownTags.Critical); - }); - return this; - } + public EventDataBuilder StackReference(string reference) { + _stackMutations.Add(s => s.References.Add(reference)); - public EventDataBuilder TotalOccurrences(int totalOccurrences) { - _stackMutations.Add(s => s.TotalOccurrences = totalOccurrences); + return this; + } - return this; - } - - public EventDataBuilder Create(int additionalOccurrences) { - _additionalEventsToCreate = additionalOccurrences; - _stackMutations.Add(s => { - if (s.TotalOccurrences <= additionalOccurrences) - s.TotalOccurrences = additionalOccurrences + 1; - }); - - return this; - } - - public EventDataBuilder FirstOccurrence(DateTime firstOccurrenceUtc) { - _event.CreatedUtc = firstOccurrenceUtc; - _stackMutations.Add(s => s.FirstOccurrence = firstOccurrenceUtc); + public EventDataBuilder OccurrencesAreCritical(bool occurrencesAreCritical = true) { + if (occurrencesAreCritical) + _event.MarkAsCritical(); - return this; - } + _stackMutations.Add(s => { + s.OccurrencesAreCritical = occurrencesAreCritical; + s.Tags.Add(Event.KnownTags.Critical); + }); + return this; + } - public EventDataBuilder FirstOccurrence(string firstOccurrenceUtc) { - if (DateTime.TryParse(firstOccurrenceUtc, out var dt)) - return FirstOccurrence(dt); - - throw new ArgumentException("Invalid date specified", nameof(firstOccurrenceUtc)); - } + public EventDataBuilder TotalOccurrences(int totalOccurrences) { + _stackMutations.Add(s => s.TotalOccurrences = totalOccurrences); - public EventDataBuilder LastOccurrence(DateTime lastOccurrenceUtc) { - if (_event.CreatedUtc.IsAfter(lastOccurrenceUtc)) - _event.CreatedUtc = lastOccurrenceUtc; - - if (_event.Date.IsAfter(lastOccurrenceUtc)) - _event.Date = lastOccurrenceUtc; - - _stackMutations.Add(s => { - if (s.FirstOccurrence.IsAfter(lastOccurrenceUtc)) - s.FirstOccurrence = lastOccurrenceUtc; - - s.LastOccurrence = lastOccurrenceUtc; - }); - - return this; - } + return this; + } - public EventDataBuilder LastOccurrence(string lastOccurrenceUtc) { - if (DateTime.TryParse(lastOccurrenceUtc, out var dt)) - return LastOccurrence(dt); - - throw new ArgumentException("Invalid date specified", nameof(lastOccurrenceUtc)); - } + public EventDataBuilder Create(int additionalOccurrences) { + _additionalEventsToCreate = additionalOccurrences; + _stackMutations.Add(s => { + if (s.TotalOccurrences <= additionalOccurrences) + s.TotalOccurrences = additionalOccurrences + 1; + }); - public EventDataBuilder DateFixed(DateTime? dateFixed = null) { - Status(StackStatus.Fixed); - _stackMutations.Add(s => { - var fixedOn = dateFixed ?? SystemClock.UtcNow; - if (s.FirstOccurrence.IsAfter(fixedOn)) - throw new ArgumentException("Fixed on date is before first occurence"); - - s.DateFixed = fixedOn; - }); - - return this; - } + return this; + } - public EventDataBuilder DateFixed(string dateFixedUtc) { - if (DateTime.TryParse(dateFixedUtc, out var dt)) - return DateFixed(dt); - - throw new ArgumentException("Invalid date specified", nameof(dateFixedUtc)); - } + public EventDataBuilder FirstOccurrence(DateTime firstOccurrenceUtc) { + _event.CreatedUtc = firstOccurrenceUtc; + _stackMutations.Add(s => s.FirstOccurrence = firstOccurrenceUtc); + + return this; + } - public EventDataBuilder FixedInVersion(string version) { - Status(StackStatus.Fixed); - _stackMutations.Add(s => s.FixedInVersion = version); + public EventDataBuilder FirstOccurrence(string firstOccurrenceUtc) { + if (DateTime.TryParse(firstOccurrenceUtc, out var dt)) + return FirstOccurrence(dt); - return this; - } + throw new ArgumentException("Invalid date specified", nameof(firstOccurrenceUtc)); + } - public EventDataBuilder Snooze(DateTime? snoozeUntil = null) { - Status(StackStatus.Snoozed); - _stackMutations.Add(s => s.SnoozeUntilUtc = snoozeUntil ?? SystemClock.UtcNow.AddDays(1)); + public EventDataBuilder LastOccurrence(DateTime lastOccurrenceUtc) { + if (_event.CreatedUtc.IsAfter(lastOccurrenceUtc)) + _event.CreatedUtc = lastOccurrenceUtc; - return this; - } + if (_event.Date.IsAfter(lastOccurrenceUtc)) + _event.Date = lastOccurrenceUtc; - public Stack GetStack() { - Build(); - return _stack; - } + _stackMutations.Add(s => { + if (s.FirstOccurrence.IsAfter(lastOccurrenceUtc)) + s.FirstOccurrence = lastOccurrenceUtc; - private bool _isBuilt = false; - public (Stack Stack, PersistentEvent[] Events) Build() { - if (_isBuilt) - return (_stack, BuildEvents(_stack, _event)); - - if (String.IsNullOrEmpty(_event.OrganizationId)) - _event.OrganizationId = SampleDataService.TEST_ORG_ID; - if (String.IsNullOrEmpty(_event.ProjectId)) - _event.ProjectId = SampleDataService.TEST_PROJECT_ID; - if (String.IsNullOrEmpty(_event.Type)) - _event.Type = Event.KnownTypes.Log; - if (String.IsNullOrEmpty(_event.Source)) - _event.Source = "Test Event"; - if (_event.Date == DateTimeOffset.MinValue) - _event.Date = SystemClock.OffsetNow; - if (_event.CreatedUtc == DateTime.MinValue) - _event.CreatedUtc = _event.Date.UtcDateTime; - - _event.CopyDataToIndex(); - - if (_stackEventBuilder != null) { - _stack = _stackEventBuilder.GetStack(); - - _stack.TotalOccurrences++; - if (_event.Date.UtcDateTime < _stack.FirstOccurrence) { - if (!_isFirstOccurrenceSet) - _event.IsFirstOccurrence = true; - _stack.FirstOccurrence = _event.Date.UtcDateTime; - } - - if (_event.Date.UtcDateTime > _stack.LastOccurrence) - _stack.LastOccurrence = _event.Date.UtcDateTime; - - _stack.Tags.AddRange(_event.Tags ?? new TagSet()); - } else if (_stack == null) { - string title = _formattingPluginManager.GetStackTitle(_event); - _stack = new Stack { - OrganizationId = _event.OrganizationId, - ProjectId = _event.ProjectId, - Title = title?.Truncate(1000), - Tags = _event.Tags ?? new TagSet(), - Type = _event.Type, - TotalOccurrences = 1, - FirstOccurrence = _event.Date.UtcDateTime, - LastOccurrence = _event.Date.UtcDateTime - }; - - if (_event.Type == Event.KnownTypes.Session) - _stack.Status = StackStatus.Ignored; + s.LastOccurrence = lastOccurrenceUtc; + }); - if (!_isFirstOccurrenceSet) - _event.IsFirstOccurrence = true; - } else { - _stack.TotalOccurrences++; - if (_event.Date.UtcDateTime < _stack.FirstOccurrence) { - if (!_isFirstOccurrenceSet) - _event.IsFirstOccurrence = true; - _stack.FirstOccurrence = _event.Date.UtcDateTime; - } - - if (_event.Date.UtcDateTime > _stack.LastOccurrence) - _stack.LastOccurrence = _event.Date.UtcDateTime; - - _stack.Tags.AddRange(_event.Tags ?? new TagSet()); - } + return this; + } - foreach (var mutation in _stackMutations) - mutation?.Invoke(_stack); + public EventDataBuilder LastOccurrence(string lastOccurrenceUtc) { + if (DateTime.TryParse(lastOccurrenceUtc, out var dt)) + return LastOccurrence(dt); - if (_stack.FirstOccurrence < _stack.CreatedUtc) - _stack.CreatedUtc = _stack.FirstOccurrence; + throw new ArgumentException("Invalid date specified", nameof(lastOccurrenceUtc)); + } + + public EventDataBuilder DateFixed(DateTime? dateFixed = null) { + Status(StackStatus.Fixed); + _stackMutations.Add(s => { + var fixedOn = dateFixed ?? SystemClock.UtcNow; + if (s.FirstOccurrence.IsAfter(fixedOn)) + throw new ArgumentException("Fixed on date is before first occurence"); + + s.DateFixed = fixedOn; + }); + + return this; + } - if (_stack.FirstOccurrence < _event.Date) - _event.IsFirstOccurrence = false; + public EventDataBuilder DateFixed(string dateFixedUtc) { + if (DateTime.TryParse(dateFixedUtc, out var dt)) + return DateFixed(dt); + + throw new ArgumentException("Invalid date specified", nameof(dateFixedUtc)); + } + + public EventDataBuilder FixedInVersion(string version) { + Status(StackStatus.Fixed); + _stackMutations.Add(s => s.FixedInVersion = version); + + return this; + } - var msi = _event.GetManualStackingInfo(); - if (msi != null) { - _stack.Title = msi.Title; - _stack.SignatureInfo.Clear(); - _stack.SignatureInfo.AddRange(msi.SignatureData); + public EventDataBuilder Snooze(DateTime? snoozeUntil = null) { + Status(StackStatus.Snoozed); + _stackMutations.Add(s => s.SnoozeUntilUtc = snoozeUntil ?? SystemClock.UtcNow.AddDays(1)); + + return this; + } + + public Stack GetStack() { + Build(); + return _stack; + } + + private bool _isBuilt = false; + public (Stack Stack, PersistentEvent[] Events) Build() { + if (_isBuilt) + return (_stack, BuildEvents(_stack, _event)); + + if (String.IsNullOrEmpty(_event.OrganizationId)) + _event.OrganizationId = SampleDataService.TEST_ORG_ID; + if (String.IsNullOrEmpty(_event.ProjectId)) + _event.ProjectId = SampleDataService.TEST_PROJECT_ID; + if (String.IsNullOrEmpty(_event.Type)) + _event.Type = Event.KnownTypes.Log; + if (String.IsNullOrEmpty(_event.Source)) + _event.Source = "Test Event"; + if (_event.Date == DateTimeOffset.MinValue) + _event.Date = SystemClock.OffsetNow; + if (_event.CreatedUtc == DateTime.MinValue) + _event.CreatedUtc = _event.Date.UtcDateTime; + + _event.CopyDataToIndex(); + + if (_stackEventBuilder != null) { + _stack = _stackEventBuilder.GetStack(); + + _stack.TotalOccurrences++; + if (_event.Date.UtcDateTime < _stack.FirstOccurrence) { + if (!_isFirstOccurrenceSet) + _event.IsFirstOccurrence = true; + _stack.FirstOccurrence = _event.Date.UtcDateTime; } - if (_stack.SignatureInfo.Count == 0) { - _stack.SignatureInfo.AddItemIfNotEmpty("Type", _event.Type); - _stack.SignatureInfo.AddItemIfNotEmpty("Source", _event.Source); + if (_event.Date.UtcDateTime > _stack.LastOccurrence) + _stack.LastOccurrence = _event.Date.UtcDateTime; + + _stack.Tags.AddRange(_event.Tags ?? new TagSet()); + } + else if (_stack == null) { + string title = _formattingPluginManager.GetStackTitle(_event); + _stack = new Stack { + OrganizationId = _event.OrganizationId, + ProjectId = _event.ProjectId, + Title = title?.Truncate(1000), + Tags = _event.Tags ?? new TagSet(), + Type = _event.Type, + TotalOccurrences = 1, + FirstOccurrence = _event.Date.UtcDateTime, + LastOccurrence = _event.Date.UtcDateTime + }; + + if (_event.Type == Event.KnownTypes.Session) + _stack.Status = StackStatus.Ignored; + + if (!_isFirstOccurrenceSet) + _event.IsFirstOccurrence = true; + } + else { + _stack.TotalOccurrences++; + if (_event.Date.UtcDateTime < _stack.FirstOccurrence) { + if (!_isFirstOccurrenceSet) + _event.IsFirstOccurrence = true; + _stack.FirstOccurrence = _event.Date.UtcDateTime; } - string signatureHash = _stack.SignatureInfo.Values.ToSHA1(); - _stack.SignatureHash = signatureHash; - _stack.DuplicateSignature = _stack.ProjectId + ":" + signatureHash; + if (_event.Date.UtcDateTime > _stack.LastOccurrence) + _stack.LastOccurrence = _event.Date.UtcDateTime; - if (String.IsNullOrEmpty(_stack.Id)) - _stack.Id = ObjectId.GenerateNewId().ToString(); + _stack.Tags.AddRange(_event.Tags ?? new TagSet()); + } - _event.StackId = _stack.Id; + foreach (var mutation in _stackMutations) + mutation?.Invoke(_stack); - _isBuilt = true; - return (_stack, BuildEvents(_stack, _event)); + if (_stack.FirstOccurrence < _stack.CreatedUtc) + _stack.CreatedUtc = _stack.FirstOccurrence; + + if (_stack.FirstOccurrence < _event.Date) + _event.IsFirstOccurrence = false; + + var msi = _event.GetManualStackingInfo(); + if (msi != null) { + _stack.Title = msi.Title; + _stack.SignatureInfo.Clear(); + _stack.SignatureInfo.AddRange(msi.SignatureData); } - private PersistentEvent[] BuildEvents(Stack stack, PersistentEvent ev) { - var events = new List(_additionalEventsToCreate) { ev }; - if (_additionalEventsToCreate <= 0) - return events.ToArray(); - - int interval = (stack.LastOccurrence - stack.FirstOccurrence).Milliseconds / _additionalEventsToCreate; - for (int index = 0; index < stack.TotalOccurrences - 1; index++) { - var clone = ev.DeepClone(); - clone.Id = null; - if (interval > 0) - clone.Date = new DateTimeOffset(stack.FirstOccurrence.AddMilliseconds(interval * index), ev.Date.Offset); - - events.Add(clone); - } + if (_stack.SignatureInfo.Count == 0) { + _stack.SignatureInfo.AddItemIfNotEmpty("Type", _event.Type); + _stack.SignatureInfo.AddItemIfNotEmpty("Source", _event.Source); + } + + string signatureHash = _stack.SignatureInfo.Values.ToSHA1(); + _stack.SignatureHash = signatureHash; + _stack.DuplicateSignature = _stack.ProjectId + ":" + signatureHash; + if (String.IsNullOrEmpty(_stack.Id)) + _stack.Id = ObjectId.GenerateNewId().ToString(); + + _event.StackId = _stack.Id; + + _isBuilt = true; + return (_stack, BuildEvents(_stack, _event)); + } + + private PersistentEvent[] BuildEvents(Stack stack, PersistentEvent ev) { + var events = new List(_additionalEventsToCreate) { ev }; + if (_additionalEventsToCreate <= 0) return events.ToArray(); + + int interval = (stack.LastOccurrence - stack.FirstOccurrence).Milliseconds / _additionalEventsToCreate; + for (int index = 0; index < stack.TotalOccurrences - 1; index++) { + var clone = ev.DeepClone(); + clone.Id = null; + if (interval > 0) + clone.Date = new DateTimeOffset(stack.FirstOccurrence.AddMilliseconds(interval * index), ev.Date.Offset); + + events.Add(clone); } - private const string _sampleRequestInfo = @"{ + return events.ToArray(); + } + + private const string _sampleRequestInfo = @"{ ""user_agent"": ""Mozilla/5.0 (Linux; Android 4.1.1; Prism II Build/HuaweiU8686) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.58 Mobile Safari/537.31"", ""http_method"": ""GET"", ""is_secure"": true, @@ -520,5 +520,4 @@ private PersistentEvent[] BuildEvents(Stack stack, PersistentEvent ev) { ""@is_bot"": true } }"; - } } diff --git a/tests/Exceptionless.Tests/Utility/EventData.cs b/tests/Exceptionless.Tests/Utility/EventData.cs index 4063ab46f3..8b250e6ff2 100644 --- a/tests/Exceptionless.Tests/Utility/EventData.cs +++ b/tests/Exceptionless.Tests/Utility/EventData.cs @@ -1,190 +1,185 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Plugins.EventParser; using Exceptionless.Core.Repositories; using Exceptionless.Core.Repositories.Configuration; using Foundatio.Repositories; -using Foundatio.Repositories.Utility; using Foundatio.Utility; using Xunit; -namespace Exceptionless.Tests.Utility { - internal static class EventData { - public static IEnumerable GenerateEvents(int count = 10, string[] organizationIds = null, string[] projectIds = null, string[] stackIds = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, int maxErrorNestingLevel = 3, bool generateTags = true, bool generateData = true, string[] referenceIds = null, decimal? value = -1, string semver = null) { - for (int i = 0; i < count; i++) - yield return GenerateEvent(organizationIds, projectIds, stackIds, startDate, endDate, generateTags: generateTags, generateData: generateData, maxErrorNestingLevel: maxErrorNestingLevel, referenceIds: referenceIds, value: value, semver: semver); - } +namespace Exceptionless.Tests.Utility; - public static IEnumerable GenerateEvents(int count = 10, string organizationId = null, string projectId = null, string stackId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, int maxErrorNestingLevel = 3, bool generateTags = true, bool generateData = true, string referenceId = null, decimal? value = -1, string semver = null) { - for (int i = 0; i < count; i++) - yield return GenerateEvent(organizationId, projectId, stackId, startDate, endDate, generateTags: generateTags, generateData: generateData, maxErrorNestingLevel: maxErrorNestingLevel, referenceId: referenceId, value: value, semver: semver); - } +internal static class EventData { + public static IEnumerable GenerateEvents(int count = 10, string[] organizationIds = null, string[] projectIds = null, string[] stackIds = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, int maxErrorNestingLevel = 3, bool generateTags = true, bool generateData = true, string[] referenceIds = null, decimal? value = -1, string semver = null) { + for (int i = 0; i < count; i++) + yield return GenerateEvent(organizationIds, projectIds, stackIds, startDate, endDate, generateTags: generateTags, generateData: generateData, maxErrorNestingLevel: maxErrorNestingLevel, referenceIds: referenceIds, value: value, semver: semver); + } - public static PersistentEvent GenerateSampleEvent() { - return GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, maxErrorNestingLevel: 4); - } + public static IEnumerable GenerateEvents(int count = 10, string organizationId = null, string projectId = null, string stackId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, int maxErrorNestingLevel = 3, bool generateTags = true, bool generateData = true, string referenceId = null, decimal? value = -1, string semver = null) { + for (int i = 0; i < count; i++) + yield return GenerateEvent(organizationId, projectId, stackId, startDate, endDate, generateTags: generateTags, generateData: generateData, maxErrorNestingLevel: maxErrorNestingLevel, referenceId: referenceId, value: value, semver: semver); + } - public static PersistentEvent GenerateEvent(string organizationId = null, string projectId = null, string stackId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, DateTimeOffset? occurrenceDate = null, int maxErrorNestingLevel = 0, bool generateTags = true, bool generateData = true, string referenceId = null, string type = null, string sessionId = null, string userIdentity = null, decimal? value = -1, string semver = null, string source = null) { - return GenerateEvent( - organizationId != null ? new[] { organizationId } : null, - projectId != null ? new[] { projectId } : null, - stackId != null ? new[] { stackId } : null, - startDate, - endDate, - occurrenceDate, - maxErrorNestingLevel, - generateTags, - generateData, - referenceId != null ? new[] { referenceId } : null, - type, - sessionId, - userIdentity, - value, - semver, - source - ); - } + public static PersistentEvent GenerateSampleEvent() { + return GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, maxErrorNestingLevel: 4); + } - public static PersistentEvent GenerateSessionStartEvent(DateTimeOffset occurrenceDate, string sessionId = null, string userIdentity = null, decimal? value = -1) { - return GenerateEvent(projectIds: new string[0], type: Event.KnownTypes.Session, occurrenceDate: occurrenceDate, sessionId: sessionId, userIdentity: userIdentity, generateData: false, generateTags: false, value: value); - } + public static PersistentEvent GenerateEvent(string organizationId = null, string projectId = null, string stackId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, DateTimeOffset? occurrenceDate = null, int maxErrorNestingLevel = 0, bool generateTags = true, bool generateData = true, string referenceId = null, string type = null, string sessionId = null, string userIdentity = null, decimal? value = -1, string semver = null, string source = null) { + return GenerateEvent( + organizationId != null ? new[] { organizationId } : null, + projectId != null ? new[] { projectId } : null, + stackId != null ? new[] { stackId } : null, + startDate, + endDate, + occurrenceDate, + maxErrorNestingLevel, + generateTags, + generateData, + referenceId != null ? new[] { referenceId } : null, + type, + sessionId, + userIdentity, + value, + semver, + source + ); + } - public static PersistentEvent GenerateSessionEndEvent(DateTimeOffset occurrenceDate, string sessionId = null, string userIdentity = null) { - return GenerateEvent(projectIds: new string[0], type: Event.KnownTypes.SessionEnd, occurrenceDate: occurrenceDate, sessionId: sessionId, userIdentity: userIdentity, generateData: false, generateTags: false); - } + public static PersistentEvent GenerateSessionStartEvent(DateTimeOffset occurrenceDate, string sessionId = null, string userIdentity = null, decimal? value = -1) { + return GenerateEvent(projectIds: new string[0], type: Event.KnownTypes.Session, occurrenceDate: occurrenceDate, sessionId: sessionId, userIdentity: userIdentity, generateData: false, generateTags: false, value: value); + } - public static PersistentEvent GenerateEvent(string[] organizationIds = null, string[] projectIds = null, string[] stackIds = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, DateTimeOffset? occurrenceDate = null, int maxErrorNestingLevel = 0, bool generateTags = true, bool generateData = true, string[] referenceIds = null, string type = null, string sessionId = null, string userIdentity = null, decimal? value = -1, string semver = null, string source = null) { - if (!startDate.HasValue || startDate > SystemClock.OffsetNow.AddHours(1)) - startDate = SystemClock.OffsetNow.AddDays(-30); - if (!endDate.HasValue || endDate > SystemClock.OffsetNow.AddHours(1)) - endDate = SystemClock.OffsetNow; - - var ev = new PersistentEvent { - OrganizationId = organizationIds.Random(TestConstants.OrganizationId), - ProjectId = projectIds.Random(TestConstants.ProjectId), - ReferenceId = referenceIds.Random(), - Date = occurrenceDate ?? RandomData.GetDateTimeOffset(startDate, endDate), - Value = value.GetValueOrDefault() >= 0 ? value : RandomData.GetDecimal(0, Int32.MaxValue), - StackId = stackIds.Random(), - Source = source - }; - - if (!String.IsNullOrEmpty(userIdentity)) - ev.SetUserIdentity(userIdentity); - - if (generateData) { - for (int i = 0; i < RandomData.GetInt(1, 5); i++) { - string key = RandomData.GetWord(); - while (ev.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) - key = RandomData.GetWord(); - - ev.Data.Add(key, RandomData.GetWord()); - } + public static PersistentEvent GenerateSessionEndEvent(DateTimeOffset occurrenceDate, string sessionId = null, string userIdentity = null) { + return GenerateEvent(projectIds: new string[0], type: Event.KnownTypes.SessionEnd, occurrenceDate: occurrenceDate, sessionId: sessionId, userIdentity: userIdentity, generateData: false, generateTags: false); + } + + public static PersistentEvent GenerateEvent(string[] organizationIds = null, string[] projectIds = null, string[] stackIds = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null, DateTimeOffset? occurrenceDate = null, int maxErrorNestingLevel = 0, bool generateTags = true, bool generateData = true, string[] referenceIds = null, string type = null, string sessionId = null, string userIdentity = null, decimal? value = -1, string semver = null, string source = null) { + if (!startDate.HasValue || startDate > SystemClock.OffsetNow.AddHours(1)) + startDate = SystemClock.OffsetNow.AddDays(-30); + if (!endDate.HasValue || endDate > SystemClock.OffsetNow.AddHours(1)) + endDate = SystemClock.OffsetNow; + + var ev = new PersistentEvent { + OrganizationId = organizationIds.Random(TestConstants.OrganizationId), + ProjectId = projectIds.Random(TestConstants.ProjectId), + ReferenceId = referenceIds.Random(), + Date = occurrenceDate ?? RandomData.GetDateTimeOffset(startDate, endDate), + Value = value.GetValueOrDefault() >= 0 ? value : RandomData.GetDecimal(0, Int32.MaxValue), + StackId = stackIds.Random(), + Source = source + }; + + if (!String.IsNullOrEmpty(userIdentity)) + ev.SetUserIdentity(userIdentity); + + if (generateData) { + for (int i = 0; i < RandomData.GetInt(1, 5); i++) { + string key = RandomData.GetWord(); + while (ev.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) + key = RandomData.GetWord(); + + ev.Data.Add(key, RandomData.GetWord()); } + } - if (generateTags) { - for (int i = 0; i < RandomData.GetInt(1, 3); i++) { - string tag = TestConstants.EventTags.Random(); - if (!ev.Tags.Contains(tag)) - ev.Tags.Add(tag); - } + if (generateTags) { + for (int i = 0; i < RandomData.GetInt(1, 3); i++) { + string tag = TestConstants.EventTags.Random(); + if (!ev.Tags.Contains(tag)) + ev.Tags.Add(tag); } + } - if (String.IsNullOrEmpty(type) || String.Equals(type, Event.KnownTypes.Error, StringComparison.OrdinalIgnoreCase)) { - ev.Type = Event.KnownTypes.Error; + if (String.IsNullOrEmpty(type) || String.Equals(type, Event.KnownTypes.Error, StringComparison.OrdinalIgnoreCase)) { + ev.Type = Event.KnownTypes.Error; - // limit error variation so that stacking will occur - if (_randomErrors == null) - _randomErrors = new List(Enumerable.Range(1, 25).Select(i => GenerateError(maxErrorNestingLevel))); + // limit error variation so that stacking will occur + if (_randomErrors == null) + _randomErrors = new List(Enumerable.Range(1, 25).Select(i => GenerateError(maxErrorNestingLevel))); - ev.Data[Event.KnownDataKeys.Error] = _randomErrors.Random(); - } else { - ev.Type = type.ToLowerInvariant(); - } + ev.Data[Event.KnownDataKeys.Error] = _randomErrors.Random(); + } + else { + ev.Type = type.ToLowerInvariant(); + } - if (!String.IsNullOrEmpty(sessionId)) - ev.SetSessionId(sessionId); + if (!String.IsNullOrEmpty(sessionId)) + ev.SetSessionId(sessionId); - if (ev.IsSessionStart()) - ev.Value = null; + if (ev.IsSessionStart()) + ev.Value = null; - ev.SetVersion(semver); - return ev; - } + ev.SetVersion(semver); + return ev; + } - private static List _randomErrors; + private static List _randomErrors; - internal static Error GenerateError(int maxErrorNestingLevel = 3, bool generateData = true, int currentNestingLevel = 0) { - var error = new Error { - Message = "Generated exception message.", - Type = TestConstants.ExceptionTypes.Random() - }; + internal static Error GenerateError(int maxErrorNestingLevel = 3, bool generateData = true, int currentNestingLevel = 0) { + var error = new Error { + Message = "Generated exception message.", + Type = TestConstants.ExceptionTypes.Random() + }; - if (RandomData.GetBool()) - error.Code = RandomData.GetInt(-234523453, 98690899).ToString(); + if (RandomData.GetBool()) + error.Code = RandomData.GetInt(-234523453, 98690899).ToString(); - if (generateData) { - for (int i = 0; i < RandomData.GetInt(1, 5); i++) { - string key = RandomData.GetWord(); - while (error.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) - key = RandomData.GetWord(); + if (generateData) { + for (int i = 0; i < RandomData.GetInt(1, 5); i++) { + string key = RandomData.GetWord(); + while (error.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) + key = RandomData.GetWord(); - error.Data.Add(key, RandomData.GetWord()); - } + error.Data.Add(key, RandomData.GetWord()); } + } - var stack = new StackFrameCollection(); - for (int i = 0; i < RandomData.GetInt(1, 10); i++) - stack.Add(GenerateStackFrame()); - error.StackTrace = stack; + var stack = new StackFrameCollection(); + for (int i = 0; i < RandomData.GetInt(1, 10); i++) + stack.Add(GenerateStackFrame()); + error.StackTrace = stack; - if (currentNestingLevel < maxErrorNestingLevel && RandomData.GetBool()) - error.Inner = GenerateError(maxErrorNestingLevel, generateData, currentNestingLevel + 1); + if (currentNestingLevel < maxErrorNestingLevel && RandomData.GetBool()) + error.Inner = GenerateError(maxErrorNestingLevel, generateData, currentNestingLevel + 1); - return error; - } + return error; + } - private static StackFrame GenerateStackFrame() { - return new StackFrame { - DeclaringNamespace = TestConstants.Namespaces.Random(), - DeclaringType = TestConstants.TypeNames.Random(), - Name = TestConstants.MethodNames.Random(), - Parameters = new ParameterCollection { + private static StackFrame GenerateStackFrame() { + return new StackFrame { + DeclaringNamespace = TestConstants.Namespaces.Random(), + DeclaringType = TestConstants.TypeNames.Random(), + Name = TestConstants.MethodNames.Random(), + Parameters = new ParameterCollection { new Parameter { Type = "String", Name = "path" } } - }; - } - - public static async Task CreateSearchDataAsync(ExceptionlessElasticConfiguration configuration, IEventRepository eventRepository, EventParserPluginManager parserPluginManager, bool updateDates = false) { - string path = Path.Combine("..", "..", "..", "Search", "Data"); - foreach (string file in Directory.GetFiles(path, "event*.json", SearchOption.AllDirectories)) { - if (file.EndsWith("summary.json")) - continue; - - var events = parserPluginManager.ParseEvents(await File.ReadAllTextAsync(file), 2, "exceptionless/2.0.0.0"); - Assert.NotNull(events); - Assert.True(events.Count > 0); - foreach (var ev in events) { - if (updateDates) { - ev.Date = SystemClock.OffsetNow; - ev.CreatedUtc = SystemClock.UtcNow; - } + }; + } - ev.CopyDataToIndex(Array.Empty()); + public static async Task CreateSearchDataAsync(ExceptionlessElasticConfiguration configuration, IEventRepository eventRepository, EventParserPluginManager parserPluginManager, bool updateDates = false) { + string path = Path.Combine("..", "..", "..", "Search", "Data"); + foreach (string file in Directory.GetFiles(path, "event*.json", SearchOption.AllDirectories)) { + if (file.EndsWith("summary.json")) + continue; + + var events = parserPluginManager.ParseEvents(await File.ReadAllTextAsync(file), 2, "exceptionless/2.0.0.0"); + Assert.NotNull(events); + Assert.True(events.Count > 0); + foreach (var ev in events) { + if (updateDates) { + ev.Date = SystemClock.OffsetNow; + ev.CreatedUtc = SystemClock.UtcNow; } - await eventRepository.AddAsync(events, o => o.ImmediateConsistency()); + ev.CopyDataToIndex(Array.Empty()); } - configuration.Events.QueryParser.Configuration.MappingResolver.RefreshMapping(); + await eventRepository.AddAsync(events, o => o.ImmediateConsistency()); } + + configuration.Events.QueryParser.Configuration.MappingResolver.RefreshMapping(); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Utility/OrganizationData.cs b/tests/Exceptionless.Tests/Utility/OrganizationData.cs index 9696d91b3f..b640120f5b 100644 --- a/tests/Exceptionless.Tests/Utility/OrganizationData.cs +++ b/tests/Exceptionless.Tests/Utility/OrganizationData.cs @@ -1,55 +1,53 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Billing; +using Exceptionless.Core.Billing; using Exceptionless.Core.Models; using Exceptionless.Core.Extensions; using Foundatio.Repositories.Utility; using Foundatio.Utility; -namespace Exceptionless.Tests.Utility { - internal static class OrganizationData { - public static IEnumerable GenerateOrganizations(BillingManager billingManager, BillingPlans plans, int count = 10, bool generateId = false, string id = null) { - for (int i = 0; i < count; i++) - yield return GenerateOrganization(billingManager, plans, generateId, id); - } +namespace Exceptionless.Tests.Utility; + +internal static class OrganizationData { + public static IEnumerable GenerateOrganizations(BillingManager billingManager, BillingPlans plans, int count = 10, bool generateId = false, string id = null) { + for (int i = 0; i < count; i++) + yield return GenerateOrganization(billingManager, plans, generateId, id); + } - public static List GenerateSampleOrganizations(BillingManager billingManager, BillingPlans plans) { - return new List { + public static List GenerateSampleOrganizations(BillingManager billingManager, BillingPlans plans) { + return new List { GenerateSampleOrganization(billingManager, plans), GenerateOrganization(billingManager, plans, id: TestConstants.OrganizationId2, inviteEmail: TestConstants.InvitedOrganizationUserEmail), GenerateOrganization(billingManager, plans, id: TestConstants.OrganizationId3, inviteEmail: TestConstants.InvitedOrganizationUserEmail), GenerateOrganization(billingManager, plans, id: TestConstants.OrganizationId4, inviteEmail: TestConstants.InvitedOrganizationUserEmail), GenerateOrganization(billingManager, plans, id: TestConstants.SuspendedOrganizationId, inviteEmail: TestConstants.InvitedOrganizationUserEmail, isSuspended: true), }; - } - - public static Organization GenerateSampleOrganization(BillingManager billingManager, BillingPlans plans) { - return GenerateOrganization(billingManager, plans, id: TestConstants.OrganizationId, name: "Acme", inviteEmail: TestConstants.InvitedOrganizationUserEmail); - } + } - public static Organization GenerateOrganization(BillingManager billingManager, BillingPlans plans, bool generateId = false, string name = null, string id = null, string inviteEmail = null, bool isSuspended = false) { - var organization = new Organization { - Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : TestConstants.OrganizationId : id, - Name = name ?? $"Organization{id}" - }; + public static Organization GenerateSampleOrganization(BillingManager billingManager, BillingPlans plans) { + return GenerateOrganization(billingManager, plans, id: TestConstants.OrganizationId, name: "Acme", inviteEmail: TestConstants.InvitedOrganizationUserEmail); + } - billingManager.ApplyBillingPlan(organization, plans.UnlimitedPlan); + public static Organization GenerateOrganization(BillingManager billingManager, BillingPlans plans, bool generateId = false, string name = null, string id = null, string inviteEmail = null, bool isSuspended = false) { + var organization = new Organization { + Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : TestConstants.OrganizationId : id, + Name = name ?? $"Organization{id}" + }; - if (!String.IsNullOrEmpty(inviteEmail)) { - organization.Invites.Add(new Invite { - EmailAddress = inviteEmail, - Token = Guid.NewGuid().ToString() - }); - } + billingManager.ApplyBillingPlan(organization, plans.UnlimitedPlan); - if (isSuspended) { - organization.IsSuspended = true; - organization.SuspensionCode = SuspensionCode.Abuse; - organization.SuspendedByUserId = TestConstants.UserId; - organization.SuspensionDate = SystemClock.UtcNow; - } + if (!String.IsNullOrEmpty(inviteEmail)) { + organization.Invites.Add(new Invite { + EmailAddress = inviteEmail, + Token = Guid.NewGuid().ToString() + }); + } - return organization; + if (isSuspended) { + organization.IsSuspended = true; + organization.SuspensionCode = SuspensionCode.Abuse; + organization.SuspendedByUserId = TestConstants.UserId; + organization.SuspensionDate = SystemClock.UtcNow; } + + return organization; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Utility/ProjectData.cs b/tests/Exceptionless.Tests/Utility/ProjectData.cs index c66863c5d1..27913a19a0 100644 --- a/tests/Exceptionless.Tests/Utility/ProjectData.cs +++ b/tests/Exceptionless.Tests/Utility/ProjectData.cs @@ -1,50 +1,49 @@ -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Foundatio.Repositories.Utility; using Foundatio.Utility; -namespace Exceptionless.Tests.Utility { - internal static class ProjectData { - public static IEnumerable GenerateProjects(int count = 10, bool generateId = false, string id = null, string organizationId = null, long? nextSummaryEndOfDayTicks = null) { - for (int i = 0; i < count; i++) - yield return GenerateProject(generateId, id, organizationId, nextSummaryEndOfDayTicks: nextSummaryEndOfDayTicks); - } +namespace Exceptionless.Tests.Utility; + +internal static class ProjectData { + public static IEnumerable GenerateProjects(int count = 10, bool generateId = false, string id = null, string organizationId = null, long? nextSummaryEndOfDayTicks = null) { + for (int i = 0; i < count; i++) + yield return GenerateProject(generateId, id, organizationId, nextSummaryEndOfDayTicks: nextSummaryEndOfDayTicks); + } - public static List GenerateSampleProjects() { - return new List { + public static List GenerateSampleProjects() { + return new List { GenerateSampleProject(), GenerateProject(generateId: true, organizationId: TestConstants.OrganizationId2), GenerateProject(id: TestConstants.SuspendedProjectId, organizationId: TestConstants.SuspendedOrganizationId) }; - } - - public static Project GenerateSampleProject() { - return GenerateProject(id: TestConstants.ProjectId, name: "Disintegrating Pistol", organizationId: TestConstants.OrganizationId); - } - - public static Project GenerateProject(bool generateId = false, string id = null, string organizationId = null, string name = null, long? nextSummaryEndOfDayTicks = null) { - var project = new Project { - Id = !id.IsNullOrEmpty() ? id : generateId ? ObjectId.GenerateNewId().ToString() : null, - OrganizationId = organizationId.IsNullOrEmpty() ? TestConstants.OrganizationId : organizationId, - Name = name ?? $"Project{id}" - }; + } - if (nextSummaryEndOfDayTicks.HasValue) - project.NextSummaryEndOfDayTicks = nextSummaryEndOfDayTicks.Value; - else { - project.NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks; - } + public static Project GenerateSampleProject() { + return GenerateProject(id: TestConstants.ProjectId, name: "Disintegrating Pistol", organizationId: TestConstants.OrganizationId); + } - for (int i = 0; i < RandomData.GetInt(0, 5); i++) { - string key = RandomData.GetWord(); - while (project.Configuration.Settings.ContainsKey(key)) - key = RandomData.GetWord(); + public static Project GenerateProject(bool generateId = false, string id = null, string organizationId = null, string name = null, long? nextSummaryEndOfDayTicks = null) { + var project = new Project { + Id = !id.IsNullOrEmpty() ? id : generateId ? ObjectId.GenerateNewId().ToString() : null, + OrganizationId = organizationId.IsNullOrEmpty() ? TestConstants.OrganizationId : organizationId, + Name = name ?? $"Project{id}" + }; + + if (nextSummaryEndOfDayTicks.HasValue) + project.NextSummaryEndOfDayTicks = nextSummaryEndOfDayTicks.Value; + else { + project.NextSummaryEndOfDayTicks = SystemClock.UtcNow.Date.AddDays(1).AddHours(1).Ticks; + } - project.Configuration.Settings.Add(key, RandomData.GetWord()); - } + for (int i = 0; i < RandomData.GetInt(0, 5); i++) { + string key = RandomData.GetWord(); + while (project.Configuration.Settings.ContainsKey(key)) + key = RandomData.GetWord(); - return project; + project.Configuration.Settings.Add(key, RandomData.GetWord()); } + + return project; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Utility/RandomEventGenerator.cs b/tests/Exceptionless.Tests/Utility/RandomEventGenerator.cs index fb68c56827..4390f3e772 100644 --- a/tests/Exceptionless.Tests/Utility/RandomEventGenerator.cs +++ b/tests/Exceptionless.Tests/Utility/RandomEventGenerator.cs @@ -1,185 +1,184 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Foundatio.Utility; -namespace Exceptionless.Helpers { - public class RandomEventGenerator { - public DateTime? MinDate { get; set; } - public DateTime? MaxDate { get; set; } +namespace Exceptionless.Helpers; - public List Generate(int count, bool setUserIdentity = true) { - var events = new List(); - for (int i = 0; i < count; i++) - events.Add(Generate(setUserIdentity)); +public class RandomEventGenerator { + public DateTime? MinDate { get; set; } + public DateTime? MaxDate { get; set; } - return events; - } + public List Generate(int count, bool setUserIdentity = true) { + var events = new List(); + for (int i = 0; i < count; i++) + events.Add(Generate(setUserIdentity)); - public PersistentEvent GeneratePersistent(bool setUserIdentity = true) { - var ev = new PersistentEvent { - OrganizationId = "537650f3b77efe23a47914f3", - ProjectId = "537650f3b77efe23a47914f4", - StackId = "1ecd0826e447a44e78877ab1", - Date = SystemClock.UtcNow - }; + return events; + } - PopulateEvent(ev, setUserIdentity); - return ev; - } + public PersistentEvent GeneratePersistent(bool setUserIdentity = true) { + var ev = new PersistentEvent { + OrganizationId = "537650f3b77efe23a47914f3", + ProjectId = "537650f3b77efe23a47914f4", + StackId = "1ecd0826e447a44e78877ab1", + Date = SystemClock.UtcNow + }; - public Event Generate(bool setUserIdentity = true) { - var ev = new Event(); - PopulateEvent(ev, setUserIdentity); - return ev; - } + PopulateEvent(ev, setUserIdentity); + return ev; + } - public void PopulateEvent(Event ev, bool setUserIdentity = true) { - if (MinDate.HasValue || MaxDate.HasValue) - ev.Date = RandomData.GetDateTime(MinDate ?? DateTime.MinValue, MaxDate ?? DateTime.MaxValue); - - ev.Type = new [] { Event.KnownTypes.Error, Event.KnownTypes.FeatureUsage, Event.KnownTypes.Log, Event.KnownTypes.NotFound }.Random(); - if (ev.Type == Event.KnownTypes.FeatureUsage) - ev.Source = FeatureNames.Random(); - else if (ev.Type == Event.KnownTypes.NotFound) - ev.Source = PageNames.Random(); - else if (ev.Type == Event.KnownTypes.Log) { - ev.Source = LogSources.Random(); - ev.Message = RandomData.GetString(); - - string level = LogLevels.Random(); - if (!String.IsNullOrEmpty(level)) - ev.Data[Event.KnownDataKeys.Level] = level; - } + public Event Generate(bool setUserIdentity = true) { + var ev = new Event(); + PopulateEvent(ev, setUserIdentity); + return ev; + } + + public void PopulateEvent(Event ev, bool setUserIdentity = true) { + if (MinDate.HasValue || MaxDate.HasValue) + ev.Date = RandomData.GetDateTime(MinDate ?? DateTime.MinValue, MaxDate ?? DateTime.MaxValue); + + ev.Type = new[] { Event.KnownTypes.Error, Event.KnownTypes.FeatureUsage, Event.KnownTypes.Log, Event.KnownTypes.NotFound }.Random(); + if (ev.Type == Event.KnownTypes.FeatureUsage) + ev.Source = FeatureNames.Random(); + else if (ev.Type == Event.KnownTypes.NotFound) + ev.Source = PageNames.Random(); + else if (ev.Type == Event.KnownTypes.Log) { + ev.Source = LogSources.Random(); + ev.Message = RandomData.GetString(); + + string level = LogLevels.Random(); + if (!String.IsNullOrEmpty(level)) + ev.Data[Event.KnownDataKeys.Level] = level; + } - if (RandomData.GetBool(80)) - ev.Geo = RandomData.GetCoordinate(); + if (RandomData.GetBool(80)) + ev.Geo = RandomData.GetCoordinate(); - if (RandomData.GetBool(20)) - ev.Value = RandomData.GetInt(0, 10000); + if (RandomData.GetBool(20)) + ev.Value = RandomData.GetInt(0, 10000); - if (setUserIdentity) - ev.SetUserIdentity(Identities.Random()); + if (setUserIdentity) + ev.SetUserIdentity(Identities.Random()); - ev.SetVersion(RandomData.GetVersion("2.0", "4.0")); + ev.SetVersion(RandomData.GetVersion("2.0", "4.0")); - ev.AddRequestInfo(new RequestInfo { - //ClientIpAddress = ClientIpAddresses.Random(), - Path = PageNames.Random() - }); + ev.AddRequestInfo(new RequestInfo { + //ClientIpAddress = ClientIpAddresses.Random(), + Path = PageNames.Random() + }); - ev.Data.Add(Event.KnownDataKeys.EnvironmentInfo, new EnvironmentInfo { - IpAddress = MachineIpAddresses.Random() + ", " + MachineIpAddresses.Random(), - MachineName = MachineNames.Random() - }); + ev.Data.Add(Event.KnownDataKeys.EnvironmentInfo, new EnvironmentInfo { + IpAddress = MachineIpAddresses.Random() + ", " + MachineIpAddresses.Random(), + MachineName = MachineNames.Random() + }); - for (int i = 0; i < RandomData.GetInt(1, 3); i++) { - string key = RandomData.GetWord(); - while (ev.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) - key = RandomData.GetWord(); + for (int i = 0; i < RandomData.GetInt(1, 3); i++) { + string key = RandomData.GetWord(); + while (ev.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) + key = RandomData.GetWord(); - ev.Data.Add(key, RandomData.GetString()); - } + ev.Data.Add(key, RandomData.GetString()); + } - int tagCount = RandomData.GetInt(1, 3); - for (int i = 0; i < tagCount; i++) { - string tag = EventTags.Random(); - if (!ev.Tags.Contains(tag)) - ev.Tags.Add(tag); - } + int tagCount = RandomData.GetInt(1, 3); + for (int i = 0; i < tagCount; i++) { + string tag = EventTags.Random(); + if (!ev.Tags.Contains(tag)) + ev.Tags.Add(tag); + } - if (ev.Type == Event.KnownTypes.Error) { - if (RandomData.GetBool()) { - // limit error variation so that stacking will occur - if (_randomErrors == null) - _randomErrors = new List(Enumerable.Range(1, 25).Select(i => GenerateError())); + if (ev.Type == Event.KnownTypes.Error) { + if (RandomData.GetBool()) { + // limit error variation so that stacking will occur + if (_randomErrors == null) + _randomErrors = new List(Enumerable.Range(1, 25).Select(i => GenerateError())); - ev.Data[Event.KnownDataKeys.Error] = _randomErrors.Random(); - } else { - // limit error variation so that stacking will occur - if (_randomSimpleErrors == null) - _randomSimpleErrors = new List(Enumerable.Range(1, 25).Select(i => GenerateSimpleError())); + ev.Data[Event.KnownDataKeys.Error] = _randomErrors.Random(); + } + else { + // limit error variation so that stacking will occur + if (_randomSimpleErrors == null) + _randomSimpleErrors = new List(Enumerable.Range(1, 25).Select(i => GenerateSimpleError())); - ev.Data[Event.KnownDataKeys.SimpleError] = _randomSimpleErrors.Random(); - } + ev.Data[Event.KnownDataKeys.SimpleError] = _randomSimpleErrors.Random(); } } + } - private List _randomErrors; + private List _randomErrors; - public Error GenerateError(int maxErrorNestingLevel = 3, bool generateData = true, int currentNestingLevel = 0) { - var error = new Error { Message = @"Generated exception message.", Type = ExceptionTypes.Random() }; - if (RandomData.GetBool()) - error.Code = RandomData.GetInt(-234523453, 98690899).ToString(); + public Error GenerateError(int maxErrorNestingLevel = 3, bool generateData = true, int currentNestingLevel = 0) { + var error = new Error { Message = @"Generated exception message.", Type = ExceptionTypes.Random() }; + if (RandomData.GetBool()) + error.Code = RandomData.GetInt(-234523453, 98690899).ToString(); - if (generateData) { - for (int i = 0; i < RandomData.GetInt(1, 5); i++) { - string key = RandomData.GetWord(); - while (error.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) - key = RandomData.GetWord(); + if (generateData) { + for (int i = 0; i < RandomData.GetInt(1, 5); i++) { + string key = RandomData.GetWord(); + while (error.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) + key = RandomData.GetWord(); - error.Data.Add(key, RandomData.GetString()); - } + error.Data.Add(key, RandomData.GetString()); } + } - var stack = new StackFrameCollection(); - for (int i = 0; i < RandomData.GetInt(1, 10); i++) - stack.Add(GenerateStackFrame()); - error.StackTrace = stack; + var stack = new StackFrameCollection(); + for (int i = 0; i < RandomData.GetInt(1, 10); i++) + stack.Add(GenerateStackFrame()); + error.StackTrace = stack; - if (currentNestingLevel < maxErrorNestingLevel && RandomData.GetBool()) - error.Inner = GenerateError(maxErrorNestingLevel, generateData, currentNestingLevel + 1); + if (currentNestingLevel < maxErrorNestingLevel && RandomData.GetBool()) + error.Inner = GenerateError(maxErrorNestingLevel, generateData, currentNestingLevel + 1); - return error; - } + return error; + } - private List _randomSimpleErrors; + private List _randomSimpleErrors; - public SimpleError GenerateSimpleError(int maxErrorNestingLevel = 3, bool generateData = true, int currentNestingLevel = 0) { - var error = new SimpleError { Message = @"Generated exception message.", Type = ExceptionTypes.Random() }; - if (generateData) { - for (int i = 0; i < RandomData.GetInt(1, 5); i++) { - string key = RandomData.GetWord(); - while (error.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) - key = RandomData.GetWord(); + public SimpleError GenerateSimpleError(int maxErrorNestingLevel = 3, bool generateData = true, int currentNestingLevel = 0) { + var error = new SimpleError { Message = @"Generated exception message.", Type = ExceptionTypes.Random() }; + if (generateData) { + for (int i = 0; i < RandomData.GetInt(1, 5); i++) { + string key = RandomData.GetWord(); + while (error.Data.ContainsKey(key) || key == Event.KnownDataKeys.Error) + key = RandomData.GetWord(); - error.Data.Add(key, RandomData.GetString()); - } + error.Data.Add(key, RandomData.GetString()); } + } - error.StackTrace = RandomData.GetString(); + error.StackTrace = RandomData.GetString(); - if (currentNestingLevel < maxErrorNestingLevel && RandomData.GetBool()) - error.Inner = GenerateSimpleError(maxErrorNestingLevel, generateData, currentNestingLevel + 1); + if (currentNestingLevel < maxErrorNestingLevel && RandomData.GetBool()) + error.Inner = GenerateSimpleError(maxErrorNestingLevel, generateData, currentNestingLevel + 1); - return error; - } + return error; + } - public StackFrame GenerateStackFrame() { - return new StackFrame { - DeclaringNamespace = Namespaces.Random(), - DeclaringType = TypeNames.Random(), - Name = MethodNames.Random(), - Parameters = new ParameterCollection { + public StackFrame GenerateStackFrame() { + return new StackFrame { + DeclaringNamespace = Namespaces.Random(), + DeclaringType = TypeNames.Random(), + Name = MethodNames.Random(), + Parameters = new ParameterCollection { new Parameter { Type = "String", Name = "path" } } - }; - } + }; + } - #region Sample Data + #region Sample Data - public readonly List Identities = new List { + public readonly List Identities = new List { "eric@exceptionless.io", "blake@exceptionless.io", "marylou@exceptionless.io" }; - public readonly List MachineIpAddresses = new List { + public readonly List MachineIpAddresses = new List { "127.34.36.89", "45.66.89.98", "10.12.18.193", @@ -187,7 +186,7 @@ public StackFrame GenerateStackFrame() { "43.10.99.234" }; - public readonly List ClientIpAddresses = new List { + public readonly List ClientIpAddresses = new List { "77.23.23.78", "45.66.89.98", "10.12.18.193", @@ -195,14 +194,14 @@ public StackFrame GenerateStackFrame() { "231.23.34.1" }; - public readonly List LogSources = new List { + public readonly List LogSources = new List { "Some.Class", "MyClass", "CodeGenerator", "Exceptionless.Core.Parser.SomeClass" }; - public readonly List LogLevels = new List { + public readonly List LogLevels = new List { "Trace", "Info", "Debug", @@ -211,28 +210,28 @@ public StackFrame GenerateStackFrame() { "Custom" }; - public readonly List FeatureNames = new List { + public readonly List FeatureNames = new List { "Feature1", "Feature2", "Feature3", "Feature4" }; - public readonly List MachineNames = new List { + public readonly List MachineNames = new List { "machine1", "machine2", "machine3", "machine4" }; - public readonly List PageNames = new List { + public readonly List PageNames = new List { "/page1", "/page2", "/page3", "/page4" }; - public readonly List EventTypes = new List { + public readonly List EventTypes = new List { Event.KnownTypes.Error, Event.KnownTypes.FeatureUsage, Event.KnownTypes.Log, @@ -241,7 +240,7 @@ public StackFrame GenerateStackFrame() { Event.KnownTypes.SessionEnd }; - public readonly List ExceptionTypes = new List { + public readonly List ExceptionTypes = new List { "System.NullReferenceException", "System.ApplicationException", "System.AggregateException", @@ -249,7 +248,7 @@ public StackFrame GenerateStackFrame() { "System.InvalidOperationException" }; - public readonly List EventTags = new List { + public readonly List EventTags = new List { "Tag1", "Tag2", "Tag3", @@ -262,7 +261,7 @@ public StackFrame GenerateStackFrame() { "Tag10" }; - public readonly List Namespaces = new List { + public readonly List Namespaces = new List { "System", "System.IO", "CodeSmith", @@ -270,17 +269,16 @@ public StackFrame GenerateStackFrame() { "SomeOther.Blah" }; - public readonly List TypeNames = new List { + public readonly List TypeNames = new List { "DateTime", "SomeType", "ProjectGenerator" }; - public readonly List MethodNames = new List { + public readonly List MethodNames = new List { "SomeMethod", "GenerateCode" }; - #endregion - } + #endregion } diff --git a/tests/Exceptionless.Tests/Utility/Run.cs b/tests/Exceptionless.Tests/Utility/Run.cs index 1df0b22bda..7a59ad27ff 100644 --- a/tests/Exceptionless.Tests/Utility/Run.cs +++ b/tests/Exceptionless.Tests/Utility/Run.cs @@ -1,11 +1,7 @@ -using System; -using System.Linq; -using System.Threading.Tasks; +namespace Exceptionless.Tests.Utility; -namespace Exceptionless.Tests.Utility { - public static class Run { - public static Task InParallelAsync(int iterations, Func work) { - return Task.WhenAll(Enumerable.Range(1, iterations).Select(i => Task.Run(() => work(i)))); - } +public static class Run { + public static Task InParallelAsync(int iterations, Func work) { + return Task.WhenAll(Enumerable.Range(1, iterations).Select(i => Task.Run(() => work(i)))); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Utility/StackData.cs b/tests/Exceptionless.Tests/Utility/StackData.cs index 99aba08f5f..d2fb4a32c4 100644 --- a/tests/Exceptionless.Tests/Utility/StackData.cs +++ b/tests/Exceptionless.Tests/Utility/StackData.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Exceptionless.Core.Models; +using Exceptionless.Core.Models; using Exceptionless.Core.Extensions; using Exceptionless.Core.Repositories; using Exceptionless.DateTimeExtensions; @@ -12,79 +8,79 @@ using Newtonsoft.Json; using Xunit; -namespace Exceptionless.Tests.Utility { - internal static class StackData { - public static IEnumerable GenerateStacks(int count = 10, bool generateId = false, string id = null, string organizationId = null, string projectId = null, string type = null) { - for (int i = 0; i < count; i++) - yield return GenerateStack(generateId, id, organizationId, projectId, type: type); - } +namespace Exceptionless.Tests.Utility; + +internal static class StackData { + public static IEnumerable GenerateStacks(int count = 10, bool generateId = false, string id = null, string organizationId = null, string projectId = null, string type = null) { + for (int i = 0; i < count; i++) + yield return GenerateStack(generateId, id, organizationId, projectId, type: type); + } - public static List GenerateSampleStacks() { - return new List { + public static List GenerateSampleStacks() { + return new List { GenerateSampleStack(), GenerateStack(id: TestConstants.StackId2, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectIdWithNoRoles), GenerateStack(generateId: true, organizationId: TestConstants.OrganizationId) }; - } - - public static Stack GenerateSampleStack(string id = TestConstants.StackId) { - return GenerateStack(id: id, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId); - } + } - public static Stack GenerateStack(bool generateId = false, string id = null, string organizationId = null, string projectId = null, string type = null, string title = null, DateTime? dateFixed = null, DateTime? utcFirstOccurrence = null, DateTime? utcLastOccurrence = null, int totalOccurrences = 0, StackStatus status = StackStatus.Open, string signatureHash = null) { - var utcNow = SystemClock.UtcNow; - var stack = new Stack { - Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : null : id, - OrganizationId = organizationId.IsNullOrEmpty() ? TestConstants.OrganizationId : organizationId, - ProjectId = projectId.IsNullOrEmpty() ? TestConstants.ProjectIds.Random() : projectId, - Title = title ?? RandomData.GetTitleWords(), - Type = type ?? Stack.KnownTypes.Error, - DateFixed = dateFixed, - FirstOccurrence = utcFirstOccurrence ?? utcNow, - LastOccurrence = utcLastOccurrence ?? utcNow, - TotalOccurrences = totalOccurrences, - Status = status, - SignatureHash = signatureHash ?? RandomData.GetAlphaNumericString(10, 10), - SignatureInfo = new SettingsDictionary() - }; + public static Stack GenerateSampleStack(string id = TestConstants.StackId) { + return GenerateStack(id: id, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId); + } - stack.DuplicateSignature = String.Concat(stack.ProjectId, ":", stack.SignatureHash); + public static Stack GenerateStack(bool generateId = false, string id = null, string organizationId = null, string projectId = null, string type = null, string title = null, DateTime? dateFixed = null, DateTime? utcFirstOccurrence = null, DateTime? utcLastOccurrence = null, int totalOccurrences = 0, StackStatus status = StackStatus.Open, string signatureHash = null) { + var utcNow = SystemClock.UtcNow; + var stack = new Stack { + Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : null : id, + OrganizationId = organizationId.IsNullOrEmpty() ? TestConstants.OrganizationId : organizationId, + ProjectId = projectId.IsNullOrEmpty() ? TestConstants.ProjectIds.Random() : projectId, + Title = title ?? RandomData.GetTitleWords(), + Type = type ?? Stack.KnownTypes.Error, + DateFixed = dateFixed, + FirstOccurrence = utcFirstOccurrence ?? utcNow, + LastOccurrence = utcLastOccurrence ?? utcNow, + TotalOccurrences = totalOccurrences, + Status = status, + SignatureHash = signatureHash ?? RandomData.GetAlphaNumericString(10, 10), + SignatureInfo = new SettingsDictionary() + }; - if (type == Event.KnownTypes.Error) - stack.SignatureInfo.Add("ExceptionType", TestConstants.ExceptionTypes.Random()); + stack.DuplicateSignature = String.Concat(stack.ProjectId, ":", stack.SignatureHash); - for (int i = 0; i < RandomData.GetInt(0, 5); i++) { - string tag = RandomData.GetWord(); - while (stack.Tags.Contains(tag)) - tag = RandomData.GetWord(); + if (type == Event.KnownTypes.Error) + stack.SignatureInfo.Add("ExceptionType", TestConstants.ExceptionTypes.Random()); - stack.Tags.Add(tag); - } + for (int i = 0; i < RandomData.GetInt(0, 5); i++) { + string tag = RandomData.GetWord(); + while (stack.Tags.Contains(tag)) + tag = RandomData.GetWord(); - return stack; + stack.Tags.Add(tag); } - - public static async Task CreateSearchDataAsync(IStackRepository stackRepository, JsonSerializer serializer, bool updateDates = false) { - string path = Path.Combine("..", "..", "..", "Search", "Data"); - foreach (string file in Directory.GetFiles(path, "stack*.json", SearchOption.AllDirectories)) { - if (file.EndsWith("summary.json")) - continue; - using (var stream = new FileStream(file, FileMode.Open)) { - using (var streamReader = new StreamReader(stream)) { - var stack = serializer.Deserialize(streamReader, typeof(Stack)) as Stack; - Assert.NotNull(stack); - - if (updateDates) { - stack.CreatedUtc = stack.FirstOccurrence = SystemClock.UtcNow.SubtractDays(1); - stack.LastOccurrence = SystemClock.UtcNow; - } + return stack; + } + + public static async Task CreateSearchDataAsync(IStackRepository stackRepository, JsonSerializer serializer, bool updateDates = false) { + string path = Path.Combine("..", "..", "..", "Search", "Data"); + foreach (string file in Directory.GetFiles(path, "stack*.json", SearchOption.AllDirectories)) { + if (file.EndsWith("summary.json")) + continue; - await stackRepository.AddAsync(stack, o => o.ImmediateConsistency()); + using (var stream = new FileStream(file, FileMode.Open)) { + using (var streamReader = new StreamReader(stream)) { + var stack = serializer.Deserialize(streamReader, typeof(Stack)) as Stack; + Assert.NotNull(stack); + + if (updateDates) { + stack.CreatedUtc = stack.FirstOccurrence = SystemClock.UtcNow.SubtractDays(1); + stack.LastOccurrence = SystemClock.UtcNow; } + + await stackRepository.AddAsync(stack, o => o.ImmediateConsistency()); } } } - } -} \ No newline at end of file + +} diff --git a/tests/Exceptionless.Tests/Utility/TestConstants.cs b/tests/Exceptionless.Tests/Utility/TestConstants.cs index b054245534..eb4c695ba7 100644 --- a/tests/Exceptionless.Tests/Utility/TestConstants.cs +++ b/tests/Exceptionless.Tests/Utility/TestConstants.cs @@ -1,50 +1,50 @@ -using System.Collections.Generic; -using Exceptionless.Core.Extensions; +using Exceptionless.Core.Extensions; using Exceptionless.Core.Utility; -namespace Exceptionless.Tests.Utility { - public static class TestConstants { - public const string ProjectId = SampleDataService.TEST_PROJECT_ID; - public const string ProjectIdWithNoRoles = "1ecd0826e447ad1e78877a66"; - public const string SuspendedProjectId = "1ecd0826e446dd1e78877ab3"; - public const string InvalidProjectId = "0ecd0826e447ad1e78877ab0"; +namespace Exceptionless.Tests.Utility; - public const string OrganizationId = SampleDataService.TEST_ORG_ID; - public const string OrganizationId2 = "1ecd0826e447ad1e78877666"; - public const string OrganizationId3 = "1ecd0826e447ad1e78877777"; - public const string OrganizationId4 = "1ecd0826e447ad1e78877888"; - public const string SuspendedOrganizationId = "1ecd0826e447ad1e78877999"; - public const string InvalidOrganizationId = "0ecd0446e447ad1e78877ab0"; - public const string InvitedOrganizationUserEmail = "invited@exceptionless.io"; - public const string InvitedOrganizationUserEmail2 = "invited2@exceptionless.io"; - public const string InvalidInvitedOrganizationUserEmail = "invalid-invite@exceptionless.io"; +public static class TestConstants { + public const string ProjectId = SampleDataService.TEST_PROJECT_ID; + public const string ProjectIdWithNoRoles = "1ecd0826e447ad1e78877a66"; + public const string SuspendedProjectId = "1ecd0826e446dd1e78877ab3"; + public const string InvalidProjectId = "0ecd0826e447ad1e78877ab0"; - public const string UserId = "1ecd0826e447ad1e78822555"; - public const string UserId2 = "1ecd0826e447ad1e78822666"; - public const string UserEmail = SampleDataService.TEST_USER_EMAIL; - public const string UserEmail2 = "user2@exceptionless.io"; - public const string UserPassword = SampleDataService.TEST_USER_PASSWORD; - public static readonly string UserPasswordHash = UserPassword.ToSHA256(); - public const string UserIdWithNoRoles = "1ecd0826e447ad1e78822556"; - public const string UserEmailWithNoRoles = "user.noroles@exceptionless.io"; - public const string InvalidUserId = "0ec44826e447ad1e78444ab0"; - public const string InvalidUserEmail = "invalid@exceptionless.io"; + public const string OrganizationId = SampleDataService.TEST_ORG_ID; + public const string OrganizationId2 = "1ecd0826e447ad1e78877666"; + public const string OrganizationId3 = "1ecd0826e447ad1e78877777"; + public const string OrganizationId4 = "1ecd0826e447ad1e78877888"; + public const string SuspendedOrganizationId = "1ecd0826e447ad1e78877999"; + public const string InvalidOrganizationId = "0ecd0446e447ad1e78877ab0"; + public const string InvitedOrganizationUserEmail = "invited@exceptionless.io"; + public const string InvitedOrganizationUserEmail2 = "invited2@exceptionless.io"; + public const string InvalidInvitedOrganizationUserEmail = "invalid-invite@exceptionless.io"; - public const string EventId = "22cd0826e447a44e78877a22"; + public const string UserId = "1ecd0826e447ad1e78822555"; + public const string UserId2 = "1ecd0826e447ad1e78822666"; + public const string UserEmail = SampleDataService.TEST_USER_EMAIL; + public const string UserEmail2 = "user2@exceptionless.io"; + public const string UserPassword = SampleDataService.TEST_USER_PASSWORD; + public static readonly string UserPasswordHash = UserPassword.ToSHA256(); + public const string UserIdWithNoRoles = "1ecd0826e447ad1e78822556"; + public const string UserEmailWithNoRoles = "user.noroles@exceptionless.io"; + public const string InvalidUserId = "0ec44826e447ad1e78444ab0"; + public const string InvalidUserEmail = "invalid@exceptionless.io"; - public const string StackId = "1ecd0826e447a44e78877ab1"; - public const string StackId2 = "2ecd0826e447a44e78877ab2"; - public const string StackId3 = "2ecd0826e447a44e78877ab3"; - public const string StackId4 = "2ecd0826e447a44e78877ab4"; - public const string InvalidStackId = "0ecd0826e447ad1e78877ab0"; - public const string TokenId = "88cd0826e447a44e78877ab1"; + public const string EventId = "22cd0826e447a44e78877a22"; - public const string ApiKey = SampleDataService.TEST_API_KEY; - public const string UserApiKey = SampleDataService.TEST_USER_API_KEY; - public const string SuspendedApiKey = "5ccd0826e447ad1e78877ab4"; - public const string InvalidApiKey = "1dddddd6e447ad1e78877ab1"; + public const string StackId = "1ecd0826e447a44e78877ab1"; + public const string StackId2 = "2ecd0826e447a44e78877ab2"; + public const string StackId3 = "2ecd0826e447a44e78877ab3"; + public const string StackId4 = "2ecd0826e447a44e78877ab4"; + public const string InvalidStackId = "0ecd0826e447ad1e78877ab0"; + public const string TokenId = "88cd0826e447a44e78877ab1"; - public static readonly List ExceptionTypes = new List { + public const string ApiKey = SampleDataService.TEST_API_KEY; + public const string UserApiKey = SampleDataService.TEST_USER_API_KEY; + public const string SuspendedApiKey = "5ccd0826e447ad1e78877ab4"; + public const string InvalidApiKey = "1dddddd6e447ad1e78877ab1"; + + public static readonly List ExceptionTypes = new List { "System.NullReferenceException", "System.ApplicationException", "System.AggregateException", @@ -54,7 +54,7 @@ public static class TestConstants { "System.InvalidOperationException" }; - public static readonly List EventTags = new List { + public static readonly List EventTags = new List { "Tag1", "Tag2", "Tag3", @@ -62,13 +62,13 @@ public static class TestConstants { "Tag5" }; - public static readonly List ProjectIds = new List { + public static readonly List ProjectIds = new List { ProjectId, InvalidProjectId, ProjectIdWithNoRoles }; - public static readonly List Namespaces = new List { + public static readonly List Namespaces = new List { "System", "System.IO", "CodeSmith", @@ -76,15 +76,14 @@ public static class TestConstants { "SomeOther.Blah" }; - public static readonly List TypeNames = new List { + public static readonly List TypeNames = new List { "DateTime", "SomeType", "ProjectGenerator" }; - public static readonly List MethodNames = new List { + public static readonly List MethodNames = new List { "SomeMethod", "GenerateCode" }; - } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Utility/TokenData.cs b/tests/Exceptionless.Tests/Utility/TokenData.cs index 5e61aab31f..65eb6aef78 100644 --- a/tests/Exceptionless.Tests/Utility/TokenData.cs +++ b/tests/Exceptionless.Tests/Utility/TokenData.cs @@ -3,30 +3,30 @@ using Foundatio.Repositories.Utility; using Foundatio.Utility; -namespace Exceptionless.Tests.Utility { - internal static class TokenData { - public static Token GenerateSampleApiKeyToken() { - return GenerateToken(id: TestConstants.ApiKey, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId); - } +namespace Exceptionless.Tests.Utility; - public static Token GenerateSampleUserToken() { - return GenerateToken(id: TestConstants.UserApiKey, userId: TestConstants.UserId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: TokenType.Authentication); - } +internal static class TokenData { + public static Token GenerateSampleApiKeyToken() { + return GenerateToken(id: TestConstants.ApiKey, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId); + } + + public static Token GenerateSampleUserToken() { + return GenerateToken(id: TestConstants.UserApiKey, userId: TestConstants.UserId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId, type: TokenType.Authentication); + } - public static Token GenerateToken(bool generateId = false, string id = null, string userId = null, string organizationId = null, string projectId = null, TokenType type = TokenType.Access, string notes = null) { -; var token = new Token { - Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : TestConstants.ApiKey : id, - UserId = userId, - OrganizationId = organizationId, - ProjectId = projectId, - CreatedUtc = SystemClock.UtcNow, - UpdatedUtc = SystemClock.UtcNow, - CreatedBy = userId, - Type = type, - Notes = notes - }; + public static Token GenerateToken(bool generateId = false, string id = null, string userId = null, string organizationId = null, string projectId = null, TokenType type = TokenType.Access, string notes = null) { + ; var token = new Token { + Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : TestConstants.ApiKey : id, + UserId = userId, + OrganizationId = organizationId, + ProjectId = projectId, + CreatedUtc = SystemClock.UtcNow, + UpdatedUtc = SystemClock.UtcNow, + CreatedBy = userId, + Type = type, + Notes = notes + }; - return token; - } + return token; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Utility/UserData.cs b/tests/Exceptionless.Tests/Utility/UserData.cs index 0b45bb52ea..bfcc4bb61e 100644 --- a/tests/Exceptionless.Tests/Utility/UserData.cs +++ b/tests/Exceptionless.Tests/Utility/UserData.cs @@ -1,20 +1,19 @@ -using System; -using System.Collections.Generic; -using Exceptionless.Core.Authorization; +using Exceptionless.Core.Authorization; using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Foundatio.Repositories.Extensions; using Foundatio.Repositories.Utility; -namespace Exceptionless.Tests.Utility { - internal static class UserData { - public static IEnumerable GenerateUsers(int count = 10, bool generateId = false, string id = null, string organizationId = null, string emailAddress = null, List roles = null) { - for (int i = 0; i < count; i++) - yield return GenerateUser(generateId, id, organizationId, emailAddress, roles); - } +namespace Exceptionless.Tests.Utility; - public static IEnumerable GenerateSampleUsers() { - return new List { +internal static class UserData { + public static IEnumerable GenerateUsers(int count = 10, bool generateId = false, string id = null, string organizationId = null, string emailAddress = null, List roles = null) { + for (int i = 0; i < count; i++) + yield return GenerateUser(generateId, id, organizationId, emailAddress, roles); + } + + public static IEnumerable GenerateSampleUsers() { + return new List { GenerateSampleUser(), GenerateSampleUserWithNoRoles(), GenerateUser(id: TestConstants.UserId2, organizationId: TestConstants.OrganizationId2, emailAddress: TestConstants.UserEmail2, roles: new List { @@ -23,35 +22,34 @@ public static IEnumerable GenerateSampleUsers() { AuthorizationRoles.Client }) }; - } + } - public static User GenerateSampleUser() { - return GenerateUser(id: TestConstants.UserId, organizationId: TestConstants.OrganizationId, emailAddress: TestConstants.UserEmail, roles: new List { + public static User GenerateSampleUser() { + return GenerateUser(id: TestConstants.UserId, organizationId: TestConstants.OrganizationId, emailAddress: TestConstants.UserEmail, roles: new List { AuthorizationRoles.GlobalAdmin, AuthorizationRoles.User, AuthorizationRoles.Client }); - } - - public static User GenerateSampleUserWithNoRoles() { - return GenerateUser(id: TestConstants.UserIdWithNoRoles, organizationId: TestConstants.OrganizationId, emailAddress: TestConstants.UserEmailWithNoRoles); - } - - public static User GenerateUser(bool generateId = false, string id = null, string organizationId = null, string emailAddress = null, IEnumerable roles = null) { - var user = new User { - Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : TestConstants.UserId : id, - EmailAddress = emailAddress.IsNullOrEmpty() ? String.Concat(RandomData.GetWord(false), "@", RandomData.GetWord(false), ".com") : emailAddress, - Password = TestConstants.UserPassword, - FullName = "Eric Smith", - PasswordResetToken = Guid.NewGuid().ToString() - }; + } + + public static User GenerateSampleUserWithNoRoles() { + return GenerateUser(id: TestConstants.UserIdWithNoRoles, organizationId: TestConstants.OrganizationId, emailAddress: TestConstants.UserEmailWithNoRoles); + } + + public static User GenerateUser(bool generateId = false, string id = null, string organizationId = null, string emailAddress = null, IEnumerable roles = null) { + var user = new User { + Id = id.IsNullOrEmpty() ? generateId ? ObjectId.GenerateNewId().ToString() : TestConstants.UserId : id, + EmailAddress = emailAddress.IsNullOrEmpty() ? String.Concat(RandomData.GetWord(false), "@", RandomData.GetWord(false), ".com") : emailAddress, + Password = TestConstants.UserPassword, + FullName = "Eric Smith", + PasswordResetToken = Guid.NewGuid().ToString() + }; - user.OrganizationIds.Add(organizationId.IsNullOrEmpty() ? TestConstants.OrganizationId : organizationId); + user.OrganizationIds.Add(organizationId.IsNullOrEmpty() ? TestConstants.OrganizationId : organizationId); - if (roles != null) - user.Roles.AddRange(roles); + if (roles != null) + user.Roles.AddRange(roles); - return user; - } + return user; } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Validation/EventValidatorTests.cs b/tests/Exceptionless.Tests/Validation/EventValidatorTests.cs index 8b522fa62c..cb6b35a400 100644 --- a/tests/Exceptionless.Tests/Validation/EventValidatorTests.cs +++ b/tests/Exceptionless.Tests/Validation/EventValidatorTests.cs @@ -1,104 +1,99 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; +using System.Diagnostics; using Exceptionless.Core.Validation; using Exceptionless.Core.Models; using Exceptionless.Core.Plugins.EventParser; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Validation { - public sealed class EventValidatorTests : TestWithServices { - private readonly PersistentEvent _benchmarkEvent; - private readonly PersistentEventValidator _validator; +namespace Exceptionless.Tests.Validation; - public EventValidatorTests(ITestOutputHelper output) : base(output) { - _validator = new PersistentEventValidator(); +public sealed class EventValidatorTests : TestWithServices { + private readonly PersistentEvent _benchmarkEvent; + private readonly PersistentEventValidator _validator; - string path = Path.Combine("..", "..", "..", "Search", "Data", "event1.json"); - var parserPluginManager = GetService(); - var events = parserPluginManager.ParseEvents(File.ReadAllText(path), 2, "exceptionless/2.0.0.0"); - _benchmarkEvent = events.First(); - } + public EventValidatorTests(ITestOutputHelper output) : base(output) { + _validator = new PersistentEventValidator(); - [Fact] - public void RunBenchmark() { - const int iterations = 10000; + string path = Path.Combine("..", "..", "..", "Search", "Data", "event1.json"); + var parserPluginManager = GetService(); + var events = parserPluginManager.ParseEvents(File.ReadAllText(path), 2, "exceptionless/2.0.0.0"); + _benchmarkEvent = events.First(); + } - var sw = Stopwatch.StartNew(); - for (int i = 0; i < iterations; i++) { - var result = _validator.Validate(_benchmarkEvent); - Assert.True(result.IsValid); - } + [Fact] + public void RunBenchmark() { + const int iterations = 10000; - sw.Stop(); - _logger.LogInformation("Time: {Duration:g}, Avg: ({AverageTickDuration:g}ticks | {AverageDuration}ms)", sw.Elapsed, sw.ElapsedTicks / iterations, sw.ElapsedMilliseconds / iterations); + var sw = Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) { + var result = _validator.Validate(_benchmarkEvent); + Assert.True(result.IsValid); } - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("1", true)] - [InlineData("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456", false)] - public void ValidateTag(string tag, bool isValid) { - var ev = new PersistentEvent { Type = Event.KnownTypes.Error, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }; - ev.Tags.Add(tag); + sw.Stop(); + _logger.LogInformation("Time: {Duration:g}, Avg: ({AverageTickDuration:g}ticks | {AverageDuration}ms)", sw.Elapsed, sw.ElapsedTicks / iterations, sw.ElapsedMilliseconds / iterations); + } - var result = _validator.Validate(ev); - Assert.Equal(isValid, result.IsValid); - } + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("1", true)] + [InlineData("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456", false)] + public void ValidateTag(string tag, bool isValid) { + var ev = new PersistentEvent { Type = Event.KnownTypes.Error, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }; + ev.Tags.Add(tag); - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("1", true)] - [InlineData("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456", false)] - public async Task ValidateTagAsync(string tag, bool isValid) { - var ev = new PersistentEvent { Type = Event.KnownTypes.Error, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }; - ev.Tags.Add(tag); + var result = _validator.Validate(ev); + Assert.Equal(isValid, result.IsValid); + } - var result = await _validator.ValidateAsync(ev); - Assert.Equal(isValid, result.IsValid); - } - [Theory] - [InlineData(null, true)] - [InlineData("1234567", false)] - [InlineData("12345678", true)] - [InlineData("1234567890123456", true)] - [InlineData("123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123", false)] - public void ValidateReferenceId(string referenceId, bool isValid) { - var result = _validator.Validate(new PersistentEvent { Type = Event.KnownTypes.Error, ReferenceId = referenceId, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }); - Assert.Equal(isValid, result.IsValid); - } + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("1", true)] + [InlineData("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456", false)] + public async Task ValidateTagAsync(string tag, bool isValid) { + var ev = new PersistentEvent { Type = Event.KnownTypes.Error, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }; + ev.Tags.Add(tag); - [Theory] - [InlineData(null, false)] - [InlineData(-60d, true)] - [InlineData(0d, true)] - [InlineData(60d, true)] - [InlineData(61d, false)] - public void ValidateDate(double? minutes, bool isValid) { - var date = minutes.HasValue ? SystemClock.OffsetNow.AddMinutes(minutes.Value) : DateTimeOffset.MinValue; - var result = _validator.Validate(new PersistentEvent { Type = Event.KnownTypes.Error, Date = date, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }); - _logger.LogInformation(date + " " + result.IsValid + " " + String.Join(" ", result.Errors.Select(e => e.ErrorMessage))); - Assert.Equal(isValid, result.IsValid); - } + var result = await _validator.ValidateAsync(ev); + Assert.Equal(isValid, result.IsValid); + } + [Theory] + [InlineData(null, true)] + [InlineData("1234567", false)] + [InlineData("12345678", true)] + [InlineData("1234567890123456", true)] + [InlineData("123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123123456789012345678901234567890123", false)] + public void ValidateReferenceId(string referenceId, bool isValid) { + var result = _validator.Validate(new PersistentEvent { Type = Event.KnownTypes.Error, ReferenceId = referenceId, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }); + Assert.Equal(isValid, result.IsValid); + } - [Theory] - [InlineData(Event.KnownTypes.Error, true)] - [InlineData(Event.KnownTypes.FeatureUsage, true)] - [InlineData(Event.KnownTypes.Log, true)] - [InlineData(Event.KnownTypes.NotFound, true)] - [InlineData(Event.KnownTypes.SessionEnd, true)] - [InlineData(Event.KnownTypes.Session, true)] - [InlineData("12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901", false)] - public void ValidateType(string type, bool isValid) { - var result = _validator.Validate(new PersistentEvent { Type = type, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }); - Assert.Equal(isValid, result.IsValid); - } + [Theory] + [InlineData(null, false)] + [InlineData(-60d, true)] + [InlineData(0d, true)] + [InlineData(60d, true)] + [InlineData(61d, false)] + public void ValidateDate(double? minutes, bool isValid) { + var date = minutes.HasValue ? SystemClock.OffsetNow.AddMinutes(minutes.Value) : DateTimeOffset.MinValue; + var result = _validator.Validate(new PersistentEvent { Type = Event.KnownTypes.Error, Date = date, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }); + _logger.LogInformation(date + " " + result.IsValid + " " + String.Join(" ", result.Errors.Select(e => e.ErrorMessage))); + Assert.Equal(isValid, result.IsValid); + } + + [Theory] + [InlineData(Event.KnownTypes.Error, true)] + [InlineData(Event.KnownTypes.FeatureUsage, true)] + [InlineData(Event.KnownTypes.Log, true)] + [InlineData(Event.KnownTypes.NotFound, true)] + [InlineData(Event.KnownTypes.SessionEnd, true)] + [InlineData(Event.KnownTypes.Session, true)] + [InlineData("12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901", false)] + public void ValidateType(string type, bool isValid) { + var result = _validator.Validate(new PersistentEvent { Type = type, Date = SystemClock.OffsetNow, Id = "123456789012345678901234", OrganizationId = "123456789012345678901234", ProjectId = "123456789012345678901234", StackId = "123456789012345678901234" }); + Assert.Equal(isValid, result.IsValid); } -} \ No newline at end of file +} diff --git a/tests/Exceptionless.Tests/Validation/TokenValidatorTests.cs b/tests/Exceptionless.Tests/Validation/TokenValidatorTests.cs index 5f02aa92a1..a9b25c2dc1 100644 --- a/tests/Exceptionless.Tests/Validation/TokenValidatorTests.cs +++ b/tests/Exceptionless.Tests/Validation/TokenValidatorTests.cs @@ -2,38 +2,37 @@ using Exceptionless.Core.Models; using Exceptionless.Core.Utility; using Foundatio.Utility; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; -namespace Exceptionless.Tests.Validation { - public sealed class TokenValidatorTests : TestWithServices { - private readonly TokenValidator _validator; +namespace Exceptionless.Tests.Validation; - public TokenValidatorTests(ITestOutputHelper output) : base(output) { - _validator = new TokenValidator(); - } +public sealed class TokenValidatorTests : TestWithServices { + private readonly TokenValidator _validator; - [Theory] - [InlineData(TokenType.Access, false, true)] - [InlineData(TokenType.Access, true, true)] - [InlineData(TokenType.Authentication, false, true)] - [InlineData(TokenType.Authentication, true, false)] - public void VerifyIsDisabled(TokenType type, bool isDisabled, bool isValid) { - var token = new Token { - Id = SampleDataService.TEST_API_KEY, - OrganizationId = SampleDataService.TEST_ORG_ID, - Type = type, - IsDisabled = isDisabled, - CreatedUtc = SystemClock.UtcNow, - UpdatedUtc = SystemClock.UtcNow - }; - - var result = _validator.Validate(token); - if (!result.IsValid) - _logger.LogInformation(result.ToString()); - - Assert.Equal(isValid, result.IsValid); - } + public TokenValidatorTests(ITestOutputHelper output) : base(output) { + _validator = new TokenValidator(); } -} \ No newline at end of file + + [Theory] + [InlineData(TokenType.Access, false, true)] + [InlineData(TokenType.Access, true, true)] + [InlineData(TokenType.Authentication, false, true)] + [InlineData(TokenType.Authentication, true, false)] + public void VerifyIsDisabled(TokenType type, bool isDisabled, bool isValid) { + var token = new Token { + Id = SampleDataService.TEST_API_KEY, + OrganizationId = SampleDataService.TEST_ORG_ID, + Type = type, + IsDisabled = isDisabled, + CreatedUtc = SystemClock.UtcNow, + UpdatedUtc = SystemClock.UtcNow + }; + + var result = _validator.Validate(token); + if (!result.IsValid) + _logger.LogInformation(result.ToString()); + + Assert.Equal(isValid, result.IsValid); + } +}