✨ Add metrics page
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 28 Jul 2019 10:15:49 +0000 (12:15 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 28 Jul 2019 11:13:55 +0000 (13:13 +0200)
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt [new file with mode: 0644]
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/templates/metrics.html [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt [new file with mode: 0644]

index cf67c0b..a6dc8ff 100644 (file)
@@ -113,53 +113,7 @@ import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
 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;
@@ -173,6 +127,7 @@ import freenet.clients.http.SessionManager.Session;
 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;
@@ -222,6 +177,7 @@ public class WebInterface implements SessionProvider {
        private final L10nFilter l10nFilter;
 
        private final PageToadletRegistry pageToadletRegistry;
+       private final MetricRegistry metricRegistry;
 
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
@@ -273,7 +229,7 @@ public class WebInterface implements SessionProvider {
                        ParserFilter parserFilter, ShortenFilter shortenFilter,
                        RenderFilter renderFilter,
                        LinkedElementRenderFilter linkedElementRenderFilter,
-                       PageToadletRegistry pageToadletRegistry) {
+                       PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
                this.listNotificationFilter = listNotificationFilter;
@@ -286,6 +242,7 @@ public class WebInterface implements SessionProvider {
                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());
@@ -659,6 +616,7 @@ public class WebInterface implements SessionProvider {
                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"));
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt
new file mode 100644 (file)
index 0000000..041e755
--- /dev/null
@@ -0,0 +1,30 @@
+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()
+               }
+       }
+
+}
index 7b62cf1..dedaf18 100644 (file)
@@ -26,6 +26,8 @@ Navigation.Menu.Sone.Item.Rescue.Name=Rescue
 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
@@ -331,6 +333,10 @@ Page.Invalid.Title=Invalid Action Performed - Sone
 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}.
index 5ea5f64..349ab5c 100644 (file)
@@ -254,6 +254,10 @@ textarea {
        text-align: right;
 }
 
+#sone td.numeric {
+       text-align: right;
+}
+
 #sone .post {
        padding: 1ex 0px;
        border-bottom: solid 1px #ccc;
diff --git a/src/main/resources/templates/metrics.html b/src/main/resources/templates/metrics.html
new file mode 100644 (file)
index 0000000..9c143eb
--- /dev/null
@@ -0,0 +1,41 @@
+<%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>
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt
new file mode 100644 (file)
index 0000000..ca69e66
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * 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)
+       }
+
+}