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 => "
").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: