import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.sone.web.page.TemplateRenderer;
-import net.pterodactylus.sone.web.pages.AboutPage;
-import net.pterodactylus.sone.web.pages.BookmarkPage;
-import net.pterodactylus.sone.web.pages.BookmarksPage;
-import net.pterodactylus.sone.web.pages.CreateAlbumPage;
-import net.pterodactylus.sone.web.pages.CreatePostPage;
-import net.pterodactylus.sone.web.pages.CreateReplyPage;
-import net.pterodactylus.sone.web.pages.CreateSonePage;
-import net.pterodactylus.sone.web.pages.DeleteAlbumPage;
-import net.pterodactylus.sone.web.pages.DeleteImagePage;
-import net.pterodactylus.sone.web.pages.DeletePostPage;
-import net.pterodactylus.sone.web.pages.DeleteProfileFieldPage;
-import net.pterodactylus.sone.web.pages.DeleteReplyPage;
-import net.pterodactylus.sone.web.pages.DeleteSonePage;
-import net.pterodactylus.sone.web.pages.DismissNotificationPage;
-import net.pterodactylus.sone.web.pages.DistrustPage;
-import net.pterodactylus.sone.web.pages.EditAlbumPage;
-import net.pterodactylus.sone.web.pages.EditImagePage;
-import net.pterodactylus.sone.web.pages.EditProfileFieldPage;
-import net.pterodactylus.sone.web.pages.EditProfilePage;
-import net.pterodactylus.sone.web.pages.EmptyAlbumTitlePage;
-import net.pterodactylus.sone.web.pages.EmptyImageTitlePage;
-import net.pterodactylus.sone.web.pages.FollowSonePage;
-import net.pterodactylus.sone.web.pages.GetImagePage;
-import net.pterodactylus.sone.web.pages.ImageBrowserPage;
-import net.pterodactylus.sone.web.pages.IndexPage;
-import net.pterodactylus.sone.web.pages.InvalidPage;
-import net.pterodactylus.sone.web.pages.KnownSonesPage;
-import net.pterodactylus.sone.web.pages.LikePage;
-import net.pterodactylus.sone.web.pages.LockSonePage;
-import net.pterodactylus.sone.web.pages.LoginPage;
-import net.pterodactylus.sone.web.pages.LogoutPage;
-import net.pterodactylus.sone.web.pages.MarkAsKnownPage;
-import net.pterodactylus.sone.web.pages.NewPage;
-import net.pterodactylus.sone.web.pages.NoPermissionPage;
-import net.pterodactylus.sone.web.pages.OptionsPage;
-import net.pterodactylus.sone.web.pages.RescuePage;
-import net.pterodactylus.sone.web.pages.SearchPage;
-import net.pterodactylus.sone.web.pages.SoneTemplatePage;
-import net.pterodactylus.sone.web.pages.TrustPage;
-import net.pterodactylus.sone.web.pages.UnbookmarkPage;
-import net.pterodactylus.sone.web.pages.UnfollowSonePage;
-import net.pterodactylus.sone.web.pages.UnlikePage;
-import net.pterodactylus.sone.web.pages.UnlockSonePage;
-import net.pterodactylus.sone.web.pages.UntrustPage;
-import net.pterodactylus.sone.web.pages.UploadImagePage;
-import net.pterodactylus.sone.web.pages.ViewPostPage;
-import net.pterodactylus.sone.web.pages.ViewSonePage;
+import net.pterodactylus.sone.web.pages.*;
import net.pterodactylus.util.notify.Notification;
import net.pterodactylus.util.notify.NotificationManager;
import net.pterodactylus.util.notify.TemplateNotification;
import freenet.clients.http.ToadletContext;
import freenet.l10n.BaseL10n;
+import com.codahale.metrics.*;
import com.google.common.base.Optional;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
private final L10nFilter l10nFilter;
private final PageToadletRegistry pageToadletRegistry;
+ private final MetricRegistry metricRegistry;
/** The “new Sone” notification. */
private final ListNotification<Sone> newSoneNotification;
ParserFilter parserFilter, ShortenFilter shortenFilter,
RenderFilter renderFilter,
LinkedElementRenderFilter linkedElementRenderFilter,
- PageToadletRegistry pageToadletRegistry) {
+ PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry) {
this.sonePlugin = sonePlugin;
this.loaders = loaders;
this.listNotificationFilter = listNotificationFilter;
this.renderFilter = renderFilter;
this.linkedElementRenderFilter = linkedElementRenderFilter;
this.pageToadletRegistry = pageToadletRegistry;
+ this.metricRegistry = metricRegistry;
formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
soneTextParser = new SoneTextParser(getCore(), getCore());
l10nFilter = new L10nFilter(getL10n());
pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
+ pageToadletRegistry.addPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
--- /dev/null
+package net.pterodactylus.sone.web.pages
+
+import com.codahale.metrics.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.page.*
+import net.pterodactylus.util.template.*
+import javax.inject.*
+
+@MenuName("Metrics")
+@TemplatePath("/templates/metrics.html")
+@ToadletPath("metrics.html")
+class MetricsPage @Inject constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer, private val metricsRegistry: MetricRegistry) : SoneTemplatePage(webInterface, loaders, templateRenderer, "Page.Metrics.Title") {
+
+ override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) {
+ metricsRegistry.histogram("sone.parsing.duration").snapshot.also { snapshot ->
+ templateContext["soneParsingDurationCount"] = snapshot.size()
+ templateContext["soneParsingDurationMin"] = snapshot.min
+ templateContext["soneParsingDurationMax"] = snapshot.max
+ templateContext["soneParsingDurationMedian"] = snapshot.median
+ templateContext["soneParsingDurationMean"] = snapshot.mean
+ templateContext["soneParsingDurationPercentile75"] = snapshot.get75thPercentile()
+ templateContext["soneParsingDurationPercentile95"] = snapshot.get95thPercentile()
+ templateContext["soneParsingDurationPercentile98"] = snapshot.get98thPercentile()
+ templateContext["soneParsingDurationPercentile99"] = snapshot.get99thPercentile()
+ templateContext["soneParsingDurationPercentile999"] = snapshot.get999thPercentile()
+ }
+ }
+
+}
Navigation.Menu.Sone.Item.Rescue.Tooltip=Rescue Sone
Navigation.Menu.Sone.Item.About.Name=About
Navigation.Menu.Sone.Item.About.Tooltip=Information about Sone
+Navigation.Menu.Sone.Item.Metrics.Name=Metrics
+Navigation.Menu.Sone.Item.Metrics.Tooltip=Metrics collected by Sone
Page.About.Title=About - Sone
Page.About.Page.Title=About
Page.Invalid.Page.Title=Invalid Action Performed
Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug.
+Page.Metrics.Title=Metrics
+Page.Metrics.Page.Title=Metrics
+Page.Metrics.SoneParsingDuration.Title=Sone Parsing Duration
+
View.Search.Button.Search=Search
View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
text-align: right;
}
+#sone td.numeric {
+ text-align: right;
+}
+
#sone .post {
padding: 1ex 0px;
border-bottom: solid 1px #ccc;
--- /dev/null
+<%include include/head.html>
+
+ <div class="page-id hidden">metrics</div>
+
+ <h1><%= Page.Metrics.Page.Title|l10n|html></h1>
+
+ <table>
+ <thead>
+ <tr>
+ <td>Metric</td>
+ <td>Count</td>
+ <td>Min</td>
+ <td>Max</td>
+ <td>Mean</td>
+ <td>Median</td>
+ <td>75%</td>
+ <td>95%</td>
+ <td>98%</td>
+ <td>99%</td>
+ <td>99.9%</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><%= Page.Metrics.SoneParsingDuration.Title|l10n|html></td>
+ <td class="numeric"><% soneParsingDurationCount|html></td>
+ <td class="numeric"><% soneParsingDurationMin|html>μs</td>
+ <td class="numeric"><% soneParsingDurationMax|html>μs</td>
+ <td class="numeric"><% soneParsingDurationMean|format format=='%.0f'|html>μs</td>
+ <td class="numeric"><% soneParsingDurationMedian|format format=='%.0f'|html>μs</td>
+ <td class="numeric"><% soneParsingDurationPercentile75|format format=='%.0f'|html>μs</td>
+ <td class="numeric"><% soneParsingDurationPercentile95|format format=='%.0f'|html>μs</td>
+ <td class="numeric"><% soneParsingDurationPercentile98|format format=='%.0f'|html>μs</td>
+ <td class="numeric"><% soneParsingDurationPercentile99|format format=='%.0f'|html>μs</td>
+ <td class="numeric"><% soneParsingDurationPercentile999|format format=='%.0f'|html>μs</td>
+ </tr>
+ </tbody>
+ </table>
+ <h2><%= Page.Metrics.SoneParsingDuration.Title|l10n|html></h2>
+
+<%include include/tail.html>
--- /dev/null
+/**
+ * Sone - MetricsPageTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.pages
+
+import com.codahale.metrics.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.page.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class MetricsPageTest : WebPageTest({ webInterface, loaders, templateRenderer -> MetricsPage(webInterface, loaders, templateRenderer, metricRegistry) }) {
+
+ companion object {
+ val metricRegistry = MetricRegistry()
+ }
+
+ @Test
+ fun `page returns correct path`() {
+ assertThat(page.path, equalTo("metrics.html"))
+ }
+
+ @Test
+ fun `page does not require login`() {
+ assertThat(page.requiresLogin(), equalTo(false))
+ }
+
+ @Test
+ fun `page returns correct title`() {
+ addTranslation("Page.Metrics.Title", "metrics page title")
+ assertThat(page.getPageTitle(soneRequest), equalTo("metrics page title"))
+ }
+
+ @Test
+ fun `page can be created by dependency injection`() {
+ assertThat(baseInjector.getInstance<MetricsPage>(), notNullValue())
+ }
+
+ @Test
+ fun `page is annotated with the correct menuname`() {
+ assertThat(page.menuName, equalTo("Metrics"))
+ }
+
+ @Test
+ fun `page is annotated with correct template path`() {
+ assertThat(page.templatePath, equalTo("/templates/metrics.html"))
+ }
+
+ @Test
+ fun `metrics page lists stats about sone parsing durations`() {
+ createHistogram("sone.parsing.duration")
+ page.handleRequest(soneRequest, templateContext)
+ verifyHistogram("soneParsingDuration")
+ }
+
+ private fun verifyHistogram(name: String) {
+ assertThat(templateContext["${name}Count"] as Int, equalTo(5))
+ assertThat(templateContext["${name}Min"] as Long, equalTo(1L))
+ assertThat(templateContext["${name}Max"] as Long, equalTo(10L))
+ assertThat(templateContext["${name}Median"] as Double, equalTo(8.0))
+ assertThat(templateContext["${name}Percentile75"] as Double, equalTo(9.0))
+ assertThat(templateContext["${name}Percentile95"] as Double, equalTo(10.0))
+ assertThat(templateContext["${name}Percentile98"] as Double, equalTo(10.0))
+ assertThat(templateContext["${name}Percentile99"] as Double, equalTo(10.0))
+ assertThat(templateContext["${name}Percentile999"] as Double, equalTo(10.0))
+ }
+
+ private fun createHistogram(name: String) = metricRegistry.histogram(name).run {
+ update(10)
+ update(9)
+ update(1)
+ update(1)
+ update(8)
+ }
+
+}