diff --git a/app/assets/javascripts/discourse/components/global-notice.js.es6 b/app/assets/javascripts/discourse/components/global-notice.js.es6 index 080427d80f3..852f19ecd88 100644 --- a/app/assets/javascripts/discourse/components/global-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/global-notice.js.es6 @@ -1,4 +1,7 @@ +import { on } from 'ember-addons/ember-computed-decorators'; import StringBuffer from 'discourse/mixins/string-buffer'; +import { iconHTML } from 'discourse/helpers/fa-icon'; +import LogsNotice from 'discourse/services/logs-notice'; export default Ember.Component.extend(StringBuffer, { rerenderTriggers: ['site.isReadOnly'], @@ -18,8 +21,33 @@ export default Ember.Component.extend(StringBuffer, { notices.push([this.siteSettings.global_notice, 'alert-global-notice']); } - if (notices.length > 0) { - buffer.push(_.map(notices, n => "
" + n[0] + "
").join("")); + if (!LogsNotice.currentProp('hidden')) { + notices.push([LogsNotice.currentProp('message'), 'alert-logs-notice', `
${iconHTML('times')}
`]); } + + if (notices.length > 0) { + buffer.push(_.map(notices, n => { + var html = `
${n[0]}`; + if (n[2]) html += n[2]; + html += '
'; + return html; + }).join("")); + } + }, + + @on('didInsertElement') + _setupLogsNotice() { + LogsNotice.current().addObserver('hidden', () => { + this.rerenderString(); + }); + + this.$().on('click.global-notice', '.alert-logs-notice .close', () => { + LogsNotice.currentProp('text', ''); + }); + }, + + @on('willDestroyElement') + _teardownLogsNotice() { + this.$().off('click.global-notice'); } }); diff --git a/app/assets/javascripts/discourse/initializers/logs-notice.js.es6 b/app/assets/javascripts/discourse/initializers/logs-notice.js.es6 new file mode 100644 index 00000000000..424d84298d5 --- /dev/null +++ b/app/assets/javascripts/discourse/initializers/logs-notice.js.es6 @@ -0,0 +1,18 @@ +import LogsNotice from 'discourse/services/logs-notice'; +import Singleton from 'discourse/mixins/singleton'; + +export default { + name: "logs-notice", + after: "message-bus", + + initialize: function (container) { + const siteSettings = container.lookup('site-settings:main'); + const messageBus = container.lookup('message-bus:main'); + const keyValueStore = container.lookup('key-value-store:main'); + LogsNotice.reopenClass(Singleton, { + createCurrent() { + return this.create({ messageBus, keyValueStore, siteSettings}); + } + }); + } +}; diff --git a/app/assets/javascripts/discourse/services/logs-notice.js.es6 b/app/assets/javascripts/discourse/services/logs-notice.js.es6 new file mode 100644 index 00000000000..8948de3a162 --- /dev/null +++ b/app/assets/javascripts/discourse/services/logs-notice.js.es6 @@ -0,0 +1,69 @@ +import { on, observes } from 'ember-addons/ember-computed-decorators'; +import computed from 'ember-addons/ember-computed-decorators'; + +const LOGS_NOTICE_KEY = "logs-notice-text"; + +const LogsNotice = Ember.Object.extend({ + text: "", + + @on('init') + _setup() { + if (!this.get('isActivated')) return; + + const text = this.keyValueStore.getItem(LOGS_NOTICE_KEY); + if (text) this.set('text', text); + + this.messageBus.subscribe("/logs_error_rate_exceeded", data => { + const duration = data.duration; + var siteSettingLimit = 0; + + if (duration === 'minute') { + siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_minute; + } else if (duration === 'hour') { + siteSettingLimit = this.siteSettings.alert_admins_if_errors_per_hour; + } + + this.set('text', + I18n.t('logs_error_rate_exceeded_notice', { + timestamp: moment().format("YYYY-MM-DD H:mm:ss"), + siteSettingLimit: siteSettingLimit, + rate: data.rate, + duration: duration, + url: Discourse.getURL('/logs') + }) + ); + }); + }, + + @computed('text') + isEmpty(text) { + return Ember.isEmpty(text); + }, + + @computed('text') + message(text) { + return new Handlebars.SafeString(text); + }, + + @computed('currentUser') + isAdmin(currentUser) { + return currentUser && currentUser.admin; + }, + + @computed('isEmpty', 'isAdmin') + hidden(isEmpty, isAdmin) { + return !isAdmin || isEmpty; + }, + + @observes('text') + _updateKeyValueStore() { + this.keyValueStore.setItem(LOGS_NOTICE_KEY, this.get('text')); + }, + + @computed('siteSettings.alert_admins_if_errors_per_hour', 'siteSettings.alert_admins_if_errors_per_minute') + isActivated(errorsPerHour, errorsPerMinute) { + return errorsPerHour > 0 || errorsPerMinute > 0; + } +}); + +export default LogsNotice; diff --git a/config/initializers/100-logster.rb b/config/initializers/100-logster.rb index 90f3978e0e3..8315587eafa 100644 --- a/config/initializers/100-logster.rb +++ b/config/initializers/100-logster.rb @@ -56,3 +56,22 @@ Logster.config.current_context = lambda{|env,&blk| Logster.config.subdirectory = "#{GlobalSetting.relative_url_root}/logs" Logster.config.application_version = Discourse.git_version + +redis = Logster.store.redis +Logster.config.redis_prefix = "#{redis.namespace}" +Logster.config.redis_raw_connection = redis.without_namespace + +%w{minute hour}.each do |duration| + site_setting_error_rate = SiteSetting.public_send("alert_admins_if_errors_per_#{duration}") + + if site_setting_error_rate > 0 + Logster.store.public_send( + "register_rate_limit_per_#{duration}", + [Logger::WARN, Logger::ERROR, Logger::FATAL, Logger::UNKNOWN], + site_setting_error_rate + ) do |rate| + + MessageBus.publish("/logs_error_rate_exceeded", { rate: rate, duration: duration }) + end + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 47536fdab0e..fdb8467e11f 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -786,6 +786,7 @@ en: too_few_topics_and_posts_notice: "Let's get this discussion started! There are currently %{currentTopics} / %{requiredTopics} topics and %{currentPosts} / %{requiredPosts} posts. New visitors need some conversations to read and respond to." too_few_topics_notice: "Let's get this discussion started! There are currently %{currentTopics} / %{requiredTopics} topics. New visitors need some conversations to read and respond to." too_few_posts_notice: "Let's get this discussion started! There are currently %{currentPosts} / %{requiredPosts} posts. New visitors need some conversations to read and respond to." + logs_error_rate_exceeded_notice: "%{timestamp}: Current rate of %{rate} errors/%{duration} has exceeded site settings's limit of %{siteSettingLimit} errors/%{duration}." learn_more: "learn more..." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index dd707fe8ba2..eb4d3f2e96c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -981,6 +981,9 @@ en: max_invites_per_day: "Maximum number of invites a user can send per day." max_topic_invitations_per_day: "Maximum number of topic invitations a user can send per day." + alert_admins_if_errors_per_minute: "Number of errors per minute in order to trigger an admin alert. A value of 0 disables this feature. NOTE: requires restart." + alert_admins_if_errors_per_hour: "Number of errors per hour in order to trigger an admin alert. A value of 0 disables this feature. NOTE: requires restart." + suggested_topics: "Number of suggested topics shown at the bottom of a topic." limit_suggested_to_category: "Only show topics from the current category in suggested topics." diff --git a/config/site_settings.yml b/config/site_settings.yml index 14ecb6fce1e..7400a72ee6d 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -765,6 +765,14 @@ rate_limits: tl2_additional_likes_per_day_multiplier: 1.5 tl3_additional_likes_per_day_multiplier: 2 tl4_additional_likes_per_day_multiplier: 3 + alert_admins_if_errors_per_minute: + client: true + shadowed_by_global: true + default: 0 + alert_admins_if_errors_per_hour: + client: true + shadowed_by_global: true + default: 0 developer: force_hostname: