From: David ‘Bombe’ Roden Date: Tue, 19 Nov 2019 21:55:14 +0000 (+0100) Subject: ✨ Render histograms in a filter X-Git-Tag: v81^2~35 X-Git-Url: https://git.pterodactylus.net/?a=commitdiff_plain;h=ab598a7019581eca9981cf1101b315e13a3bcbaa;p=Sone.git ✨ Render histograms in a filter --- diff --git a/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt b/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt new file mode 100644 index 0000000..6816135 --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/sone/template/HistogramRenderer.kt @@ -0,0 +1,46 @@ +package net.pterodactylus.sone.template + +import com.codahale.metrics.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.util.template.* + +/** + * [Filter] that renders a [Histogram] as a table row. + */ +class HistogramRenderer : Filter { + + override fun format(templateContext: TemplateContext, data: Any?, parameters: Map?): Any? { + templateContext["metricName"] = (parameters?.get("name") as String?)?.dotToCamel()?.let { "Page.Metrics.$it.Title" } + (data as? Histogram)?.snapshot?.run { + templateContext["count"] = size() + templateContext["min"] = min + templateContext["max"] = max + templateContext["mean"] = mean + templateContext["median"] = median + templateContext["percentile75"] = get75thPercentile() + templateContext["percentile95"] = get95thPercentile() + templateContext["percentile98"] = get98thPercentile() + templateContext["percentile99"] = get99thPercentile() + templateContext["percentile999"] = get999thPercentile() + } + return template.render(templateContext) + } + +} + +private val template = """ + <% metricName|l10n|html> + <% count|html> + <% min|duration scale=='μs'|html> + <% max|duration scale=='μs'|html> + <% mean|duration scale=='μs'|html> + <% median|duration scale=='μs'|html> + <% percentile75|duration scale=='μs'|html> + <% percentile95|duration scale=='μs'|html> + <% percentile98|duration scale=='μs'|html> + <% percentile99|duration scale=='μs'|html> + <% percentile999|duration scale=='μs'|html> +""".asTemplate() + +private fun String.dotToCamel() = + split(".").joinToString("", transform = String::capitalize) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt b/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt index f79eff5..c996e30 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt @@ -72,6 +72,7 @@ class WebInterfaceModule : AbstractModule() { addFilter("unique", UniqueElementFilter()) addFilter("mod", ModFilter()) addFilter("paginate", PaginationFilter()) + addFilter("render-histogram", HistogramRenderer()) addProvider(TemplateProvider.TEMPLATE_CONTEXT_PROVIDER) addProvider(loaders.templateProvider) diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt index 655c912..837e2c8 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/MetricsPage.kt @@ -13,41 +13,7 @@ import javax.inject.* 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.histograms - .mapKeys { it.key to it.key.toI18n() } - .onEach { addHistogram(templateContext, it.key.first, it.key.second) } - .keys - .map(Pair<*, String>::second) - .let { templateContext["histogramKeys"] = it } - } - - private fun addHistogram(templateContext: TemplateContext, metricName: String, variablePrefix: String) { - metricsRegistry.histogram(metricName).also { histogram -> - templateContext["${variablePrefix}I18n"] = variablePrefix.capitalizeFirst() - templateContext["${variablePrefix}Count"] = histogram.count - histogram.snapshot.also { snapshot -> - templateContext["${variablePrefix}Min"] = snapshot.min - templateContext["${variablePrefix}Max"] = snapshot.max - templateContext["${variablePrefix}Median"] = snapshot.median - templateContext["${variablePrefix}Mean"] = snapshot.mean - templateContext["${variablePrefix}Percentile75"] = snapshot.get75thPercentile() - templateContext["${variablePrefix}Percentile95"] = snapshot.get95thPercentile() - templateContext["${variablePrefix}Percentile98"] = snapshot.get98thPercentile() - templateContext["${variablePrefix}Percentile99"] = snapshot.get99thPercentile() - templateContext["${variablePrefix}Percentile999"] = snapshot.get999thPercentile() - } - } + templateContext["histograms"] = metricsRegistry.histograms } } - -private fun String.toI18n() = split(".") - .mapIndexed { index, s -> if (index > 0) s.capitalizeFirst() else s } - .joinToString("") - -private fun String.capitalizeFirst() = - get(0).toUpperCase() + if (length > 0) { - substring(1) - } else { - "" - } diff --git a/src/main/resources/i18n/sone.de.properties b/src/main/resources/i18n/sone.de.properties index b4c68d7..32e73cf 100644 --- a/src/main/resources/i18n/sone.de.properties +++ b/src/main/resources/i18n/sone.de.properties @@ -331,6 +331,12 @@ Page.Invalid.Title=Ungültige Aktion ausgeführt - Sone Page.Invalid.Page.Title=Ungültige Aktion ausgeführt Page.Invalid.Text=Eine ungültige Aktion wurde ausgeführt, oder eine gültige Aktion hatte ungültige Parameter. Bitte kehren Sie zur {link}Hauptseite{/link} zurück und versuchen Sie Ihre Aktion erneut. Wenn der Fehler weiterhin besteht, haben Sie wahrscheinlich einen Programmierfehler gefunden. +Page.Metrics.Title=Metriken +Page.Metrics.Page.Title=Metriken +Page.Metrics.SoneInsertDuration.Title=Hochladedauer einer Sone +Page.Metrics.SoneParsingDuration.Title=Parsdauer einer Sone +Page.Metrics.ConfigurationSavingDuration.Title=Speicherdauer der Konfiguration + View.Search.Button.Search=Suchen View.CreateSone.Text.WotIdentityRequired=Um eine Sone anzulegen, brauchen Sie eine Identität aus dem {link}„Web of Trust“ Plugin{/link}. diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index c539fe2..0b7cbf8 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -337,6 +337,7 @@ Page.Metrics.Title=Metrics Page.Metrics.Page.Title=Metrics Page.Metrics.SoneInsertDuration.Title=Sone Insert Duration Page.Metrics.SoneParsingDuration.Title=Sone Parsing Duration +Page.Metrics.ConfigurationSavingDuration.Title=Configuration Saving Duration View.Search.Button.Search=Search diff --git a/src/main/resources/templates/metrics.html b/src/main/resources/templates/metrics.html index e69ea19..25052d5 100644 --- a/src/main/resources/templates/metrics.html +++ b/src/main/resources/templates/metrics.html @@ -21,20 +21,8 @@ - <%foreach histogramKeys histogramKey> - - <%= "Page.Metrics.<%= '<%histogramKey>I18n'|parse>.Title"|parse|l10n|html> - <% "<%histogramKey>Count"|parse|html> - <% "<%histogramKey>Min"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Max"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Mean"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Median"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Percentile75"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Percentile95"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Percentile98"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Percentile99"|parse|duration scale=="μs"|html> - <% "<%histogramKey>Percentile999"|parse|duration scale=="μs"|html> - + <%foreach histograms histogram> + <% histogram.value|render-histogram name=histogram.key> <%/foreach> diff --git a/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt new file mode 100644 index 0000000..592c3ef --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/template/HistogramRendererTest.kt @@ -0,0 +1,204 @@ +/** + * Sone - HistogramRendererTest.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.template + +import com.codahale.metrics.* +import net.pterodactylus.sone.freenet.* +import net.pterodactylus.util.template.* +import org.hamcrest.MatcherAssert.* +import org.hamcrest.Matchers.* +import org.jsoup.* +import org.jsoup.nodes.* +import org.junit.* +import java.util.* + +/** + * Unit test for [HistogramRenderer]. + */ +class HistogramRendererTest { + + private val translation = object : Translation { + override val currentLocale = Locale.ENGLISH + override fun translate(key: String) = "Metric Name".takeIf { key == "Page.Metrics.TestHistogram.Title" } ?: "" + } + private val metricRenderer = HistogramRenderer() + private val templateContext = TemplateContext().apply { + addFilter("html", HtmlFilter()) + addFilter("duration", DurationFormatFilter()) + addFilter("l10n", L10nFilter(translation)) + } + + @Test + fun `histogram is rendered as table row`() { + createAndVerifyTableRow { + assertThat(it.nodeName(), equalTo("tr")) + } + } + + @Test + fun `histogram has eleven columns`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td"), hasSize(11)) + } + } + + @Test + fun `first column contains translated metric name`() { + createAndVerifyTableRow(mapOf("name" to "test.histogram")) { + assertThat(it.getElementsByTag("td")[0].text(), equalTo("Metric Name")) + } + } + + @Test + fun `second column is numeric`() { + verifyColumnIsNumeric(1) + } + + @Test + fun `second column contains count`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[1].text(), equalTo("1001")) + } + } + + @Test + fun `third column is numeric`() { + verifyColumnIsNumeric(2) + } + + @Test + fun `third column contains min value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[2].text(), equalTo("3.4ms")) + } + } + + @Test + fun `fourth column is numeric`() { + verifyColumnIsNumeric(3) + } + + @Test + fun `fourth column contains max value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[3].text(), equalTo("998.9ms")) + } + } + + @Test + fun `fifth column is numeric`() { + verifyColumnIsNumeric(4) + } + + @Test + fun `fifth column contains mean value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[4].text(), equalTo("503.4ms")) + } + } + + @Test + fun `sixth column is numeric`() { + verifyColumnIsNumeric(5) + } + + @Test + fun `sixth column contains median value`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[5].text(), equalTo("501.0ms")) + } + } + + @Test + fun `seventh column is numeric`() { + verifyColumnIsNumeric(6) + } + + @Test + fun `seventh column contains 75th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[6].text(), equalTo("757.8ms")) + } + } + + @Test + fun `eighth column is numeric`() { + verifyColumnIsNumeric(7) + } + + @Test + fun `eighth column contains 95th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[7].text(), equalTo("948.6ms")) + } + } + + @Test + fun `ninth column is numeric`() { + verifyColumnIsNumeric(8) + } + + @Test + fun `ninth column contains 98th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[8].text(), equalTo("972.7ms")) + } + } + + @Test + fun `tenth column is numeric`() { + verifyColumnIsNumeric(9) + } + + @Test + fun `tenth column contains 99th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[9].text(), equalTo("986.4ms")) + } + } + + @Test + fun `eleventh column is numeric`() { + verifyColumnIsNumeric(10) + } + + @Test + fun `eleventh column contains 99,9th percentile`() { + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[10].text(), equalTo("998.5ms")) + } + } + + private fun createAndVerifyTableRow(parameters: Map? = null, verify: (Element) -> Unit) = + metricRenderer.format(templateContext, histogram, parameters) + .let { "$it
" } + .let(Jsoup::parseBodyFragment) + .getElementById("t").child(0).child(0) + .let(verify) + + private fun verifyColumnIsNumeric(column: Int) = + createAndVerifyTableRow { + assertThat(it.getElementsByTag("td")[column].classNames(), hasItem("numeric")) + } + +} + +private val random = Random(1) +private val histogram = MetricRegistry().histogram("test.histogram").apply { + (0..1000).map { random.nextInt(1_000_000) }.forEach(this::update) +} diff --git a/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt index 4d8fc83..14427ef 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt @@ -243,6 +243,11 @@ class WebInterfaceModuleTest { verifyFilter("paginate") } + @Test + fun `template context histogram renderer`() { + verifyFilter("render-histogram") + } + private inline fun verifyFilter(name: String) { assertThat(getFilter(name), instanceOf(F::class.java)) } diff --git a/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt index 5a5d588..9e2c492 100644 --- a/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt +++ b/src/test/kotlin/net/pterodactylus/sone/web/pages/MetricsPageTest.kt @@ -62,46 +62,16 @@ class MetricsPageTest : WebPageTest() { } @Test - fun `metrics page auto-converts histogram name`() { - createHistogram("sone.random.duration") - page.handleRequest(soneRequest, templateContext) - verifyHistogram("soneRandomDuration") - } - - @Test @Suppress("UNCHECKED_CAST") - fun `metrics page stores histogram keys in template`() { + fun `metrics page stores histograms in template context`() { createHistogram("sone.random.duration2") createHistogram("sone.random.duration1") page.handleRequest(soneRequest, templateContext) - assertThat(templateContext["histogramKeys"] as Iterable, contains("soneRandomDuration1", "soneRandomDuration2")) - } - - @Test - fun `metrics page stores i18n names for histogram keys`() { - createHistogram("sone.random.duration1") - page.handleRequest(soneRequest, templateContext) - assertThat(templateContext["soneRandomDuration1I18n"] as String, equalTo("SoneRandomDuration1")) - } - - @Test - fun `metrics page delivers correct histogram size`() { - val histogram = metricRegistry.histogram("sone.parsing.duration") - (0..4000).forEach(histogram::update) - page.handleRequest(soneRequest, templateContext) - assertThat(templateContext["soneParsingDurationCount"] as Long, equalTo(4001L)) - } - - private fun verifyHistogram(name: String) { - assertThat(templateContext["${name}Count"] as Long, equalTo(5L)) - 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)) + val histograms = templateContext["histograms"] as Map + assertThat(histograms.entries.map { it.key to it.value }, containsInAnyOrder( + "sone.random.duration1" to metricRegistry.histogram("sone.random.duration1"), + "sone.random.duration2" to metricRegistry.histogram("sone.random.duration2") + )) } private fun createHistogram(name: String) = metricRegistry.histogram(name).run {