From: David ‘Bombe’ Roden Date: Sun, 28 Jul 2019 10:15:49 +0000 (+0200) Subject: ✨ Add metrics page X-Git-Tag: v81^2~175 X-Git-Url: https://git.pterodactylus.net/?a=commitdiff_plain;h=6562eedae6d6b25ecfb2f662a827db85f7026d50;p=Sone.git ✨ Add metrics page --- diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index cf67c0b..a6dc8ff 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -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 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.loadStaticPage("css/", "/static/css/", "text/css")); pageToadletRegistry.addPage(loaders.loadStaticPage("javascript/", "/static/javascript/", "text/javascript")); pageToadletRegistry.addPage(loaders.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 index 0000000..041e755 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt @@ -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() + } + } + +} diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index 7b62cf1..dedaf18 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -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}. diff --git a/src/main/resources/static/css/sone.css b/src/main/resources/static/css/sone.css index 5ea5f64..349ab5c 100644 --- a/src/main/resources/static/css/sone.css +++ b/src/main/resources/static/css/sone.css @@ -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 index 0000000..9c143eb --- /dev/null +++ b/src/main/resources/templates/metrics.html @@ -0,0 +1,41 @@ +<%include include/head.html> + + + +

<%= Page.Metrics.Page.Title|l10n|html>

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricCountMinMaxMeanMedian75%95%98%99%99.9%
<%= Page.Metrics.SoneParsingDuration.Title|l10n|html><% soneParsingDurationCount|html><% soneParsingDurationMin|html>μs<% soneParsingDurationMax|html>μs<% soneParsingDurationMean|format format=='%.0f'|html>μs<% soneParsingDurationMedian|format format=='%.0f'|html>μs<% soneParsingDurationPercentile75|format format=='%.0f'|html>μs<% soneParsingDurationPercentile95|format format=='%.0f'|html>μs<% soneParsingDurationPercentile98|format format=='%.0f'|html>μs<% soneParsingDurationPercentile99|format format=='%.0f'|html>μs<% soneParsingDurationPercentile999|format format=='%.0f'|html>μs
+

<%= Page.Metrics.SoneParsingDuration.Title|l10n|html>

+ +<%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 index 0000000..ca69e66 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt @@ -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 . + */ + +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(), 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) + } + +}