--- /dev/null
+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<String, Any?>?): 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 = """<tr>
+ <td><% metricName|l10n|html></td>
+ <td class="numeric"><% count|html></td>
+ <td class="numeric"><% min|duration scale=='μs'|html></td>
+ <td class="numeric"><% max|duration scale=='μs'|html></td>
+ <td class="numeric"><% mean|duration scale=='μs'|html></td>
+ <td class="numeric"><% median|duration scale=='μs'|html></td>
+ <td class="numeric"><% percentile75|duration scale=='μs'|html></td>
+ <td class="numeric"><% percentile95|duration scale=='μs'|html></td>
+ <td class="numeric"><% percentile98|duration scale=='μs'|html></td>
+ <td class="numeric"><% percentile99|duration scale=='μs'|html></td>
+ <td class="numeric"><% percentile999|duration scale=='μs'|html></td>
+</tr>""".asTemplate()
+
+private fun String.dotToCamel() =
+ split(".").joinToString("", transform = String::capitalize)
addFilter("unique", UniqueElementFilter())
addFilter("mod", ModFilter())
addFilter("paginate", PaginationFilter())
+ addFilter("render-histogram", HistogramRenderer())
addProvider(TemplateProvider.TEMPLATE_CONTEXT_PROVIDER)
addProvider(loaders.templateProvider)
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 {
- ""
- }
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}.
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
</tr>
</thead>
<tbody>
- <%foreach histogramKeys histogramKey>
- <tr>
- <td><%= "Page.Metrics.<%= '<%histogramKey>I18n'|parse>.Title"|parse|l10n|html></td>
- <td class="numeric"><% "<%histogramKey>Count"|parse|html></td>
- <td class="numeric"><% "<%histogramKey>Min"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Max"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Mean"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Median"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Percentile75"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Percentile95"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Percentile98"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Percentile99"|parse|duration scale=="μs"|html></td>
- <td class="numeric"><% "<%histogramKey>Percentile999"|parse|duration scale=="μs"|html></td>
- </tr>
+ <%foreach histograms histogram>
+ <% histogram.value|render-histogram name=histogram.key>
<%/foreach>
</tbody>
</table>
--- /dev/null
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String, Any?>? = null, verify: (Element) -> Unit) =
+ metricRenderer.format(templateContext, histogram, parameters)
+ .let { "<table id='t'>$it</table>" }
+ .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)
+}
verifyFilter<PaginationFilter>("paginate")
}
+ @Test
+ fun `template context histogram renderer`() {
+ verifyFilter<HistogramRenderer>("render-histogram")
+ }
+
private inline fun <reified F : Filter> verifyFilter(name: String) {
assertThat(getFilter(name), instanceOf(F::class.java))
}
}
@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<String>, 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<String, Histogram>
+ 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 {