diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..1cd3b95 --- /dev/null +++ b/build.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/console/webapps/META-INF/MANIFEST.MF b/plugin/console/webapps/META-INF/MANIFEST.MF new file mode 100644 index 0000000..d8e4d24 --- /dev/null +++ b/plugin/console/webapps/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Ant-Version: Apache Ant 1.10.14 +Created-By: 1.8.0_442-8u442-b06~us1-0ubuntu1~24.10-b06 (Private Build) + diff --git a/plugin/console/webapps/WEB-INF/web.xml b/plugin/console/webapps/WEB-INF/web.xml new file mode 100644 index 0000000..93a393c --- /dev/null +++ b/plugin/console/webapps/WEB-INF/web.xml @@ -0,0 +1,17 @@ + + + + + io.prometheus.metrics.exporter.servlet.javax.PrometheusMetricsServlet + io.prometheus.metrics.exporter.servlet.javax.PrometheusMetricsServlet + 1 + + + + + + io.prometheus.metrics.exporter.servlet.javax.PrometheusMetricsServlet + / + + + diff --git a/resources/collapse.css b/resources/collapse.css new file mode 100644 index 0000000..bf5c058 --- /dev/null +++ b/resources/collapse.css @@ -0,0 +1,13 @@ +#configuration { + display: none !important; +} + +#expand { + display: inline-block !important; + z-index: 100 !important; +} + +#collapse { + display: none !important; + z-index: -1 !important; +} \ No newline at end of file diff --git a/resources/expand.css b/resources/expand.css new file mode 100644 index 0000000..3acf197 --- /dev/null +++ b/resources/expand.css @@ -0,0 +1,13 @@ +#configuration { + display: table !important; +} + +#expand { + display: none !important; + z-index: -1 !important; +} + +#collapse { + display: inline-block !important; + z-index: 100 !important; +} diff --git a/resources/images/collapse.png b/resources/images/collapse.png new file mode 100644 index 0000000..6ad65cc Binary files /dev/null and b/resources/images/collapse.png differ diff --git a/resources/images/configure.svg b/resources/images/configure.svg new file mode 100644 index 0000000..6c51046 --- /dev/null +++ b/resources/images/configure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/cross.svg b/resources/images/cross.svg new file mode 100644 index 0000000..bd6f23d --- /dev/null +++ b/resources/images/cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/expand.png b/resources/images/expand.png new file mode 100644 index 0000000..ecf1fa8 Binary files /dev/null and b/resources/images/expand.png differ diff --git a/resources/images/infohelp.svg b/resources/images/infohelp.svg new file mode 100644 index 0000000..397fb77 --- /dev/null +++ b/resources/images/infohelp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/prometheus.svg b/resources/images/prometheus.svg new file mode 100644 index 0000000..5c51f66 --- /dev/null +++ b/resources/images/prometheus.svg @@ -0,0 +1,50 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/resources/images/starting.svg b/resources/images/starting.svg new file mode 100644 index 0000000..fe184c7 --- /dev/null +++ b/resources/images/starting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/tick.svg b/resources/images/tick.svg new file mode 100644 index 0000000..fca668d --- /dev/null +++ b/resources/images/tick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/tile2.png b/resources/images/tile2.png new file mode 100644 index 0000000..5da7a46 Binary files /dev/null and b/resources/images/tile2.png differ diff --git a/resources/prometheus.css b/resources/prometheus.css new file mode 100644 index 0000000..0932230 --- /dev/null +++ b/resources/prometheus.css @@ -0,0 +1,557 @@ +/* I2P Socks Proxy Plugin Theme by dr|z3d 2022 */ + +:root { + --a: #3b6bbf; + --active: #f30; + --bevel: inset 0 0 0 1px #d0d0da, 0 0 1px 1px #97a2ce; + --bg: #a4a4cb; + --bg_status: var(--bg_table); + --bg_table: #f2f2ff; + --bodybg: var(--bg) url(images/tile2.png) fixed; + --bodyfont: 10.5pt Droid Sans, Open Sans, Noto Sans, Ubuntu, Segoe UI, Verdana, Helvetica Neue, sans-serif; + --border: 1px solid #6c6c93; + --border_inner: 1px solid #9898b3; + --btn_border: 1px solid #97a2ce; + --btn_hover: linear-gradient(180deg, #ddd, #fff); + --btn_active: var(--btn_hover); + --btn: linear-gradient(180deg, #fff, #e8e8ff); + --btn_shadow_active: var(--highlight), inset 4px 4px 4px rgba(0,0,0,.5); + --container: linear-gradient(90deg, #fff, #efefff, #fff); + --filter_shadow: drop-shadow(0 0 1px var(--hover)) drop-shadow(0 0 2px var(--hover)); + --highlight: inset 0 0 0 1px #fff; + --highlight_notice: var(--highlight), inset 0 0 1px 1px #900, 0 0 2px 2px rgba(0,0,0,.2); + --hover: #f60; + --ink: #4f4f63; + --ink_light: #5b5b71; + --ink_status_hover: #292d3d; + --input_text: var(--bg_table); + --input_text_focus: #fff; + --monospaced: Droid Sans Mono, Noto Mono, DejaVu Sans Mono, Lucida Console, monospace; + --outline: 0 0 0 1px rgba(255,96,0,.5); + --rowEven: repeating-linear-gradient(135deg, rgba(252,252,255,.5) 2px, rgba(240, 240, 255, .3) 3px, #fafaff 5px) #f0f0ff; + --rowOdd: repeating-linear-gradient(45deg, rgba(255,255,255,.5) 2px, rgba(220, 220, 255, .3) 3px, #fafaff 5px), #fafaff; + --scrollbar_hover: rgba(16,16,48,.4) var(--bg); + --scrollbar: rgba(16,16,48,.3) rgba(0,0,0,0); + --selected: #77f; + --shadow_table: 0 0 1px 0 #bbc; + --shadow_input: var(--highlight), inset 1px 1px 1px 1px rgba(0,0,0,.6); + --th: linear-gradient(180deg, #fcfcff 50%, #f2f2ff 50%, #efefff); + --th_sub: linear-gradient(180deg, #fdfdff, #f0f0ff); + --txtshadow_title: 0 1px 1px #fff, 0 -1px 1px #e2e2ff, 0 2px 1px #ddf; + --visited: #2c4e8f; + --configure: url(images/configure.svg); + --cross: url(images/cross.svg); + --infohelp: url(images/infohelp.svg); + --socks: url(images/prometheus.svg); + --starting: url(images/starting.svg); + --tick: url(images/tick.svg); +} + +@media (prefers-color-scheme: light) { + #status .subtitle + tr, #status .subtitle + tr td { + border-bottom: none !important; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --a: #3b6bbf; + --active: #f30; + --bevel: inset 0 0 0 1px #000, 0 0 1px 1px #111, 0 0 0 2px #000; + --bodyfont: 10pt Droid Sans, Open Sans, Noto Sans, Ubuntu, Segoe UI, Verdana, Helvetica Neue, sans-serif; + --bg: #111; + --bg_status: #504a4a; + --bg_table: #222; + --bodybg: var(--bg); + --border: 1px solid #444; + --border_inner: 1px solid #181818; + --btn_border: 1px solid #97a2ce; + --btn_active: linear-gradient(180deg, #333, #444); + --btn_hover: linear-gradient(180deg, #444, #333 50%, #111); + --btn: linear-gradient(180deg, #333, #181818); + --container: #222; + --highlight: inset 0 0 0 1px rgba(255,255,255,.1); + --highlight_notice: var(--highlight), inset 0 0 1px 1px #300, 0 0 2px 2px rgba(0,0,0,.5); + --hover: #f60; + --ink: #ddd; + --ink_light: #eee; + --ink_status_hover: #fff; + --input_text_focus: #333; + --rowEven: #555; + --rowOdd: #444; + --scrollbar_hover: rgba(16,16,48,.4) var(--bg); + --scrollbar: rgba(16,16,48,.3) rgba(0,0,0,0); + --selected: #d30; + --shadow_table: 0 0 0 1px #222, 0 0 0 8px #333; + --th: linear-gradient(180deg, #303030 50%, #222 50%, #111); + --th_sub: linear-gradient(180deg, #333, #222); + --txtshadow_title: 0 0 0 1px #000; + --visited: #2c4e8f; + } + #expand img, #collapse img { + filter: invert(1) opacity(.7); + } + #expand:hover img, #collapse:hover img { + filter: invert(1) opacity(.95) var(--filter_shadow) !important; + } + #status { + border-collapse: separate; + border-spacing: 0; + border: var(--border) !important; + } + thead th, #configtitle { + box-shadow: inset 0 0 0 1px #444; + } + #expand, #collapse { + border-left: var(--border) !important; + } + #container { + padding: 0 !important; + } +} + +html, body { + background: var(--bodybg); +} + +body { + margin: 10px; + padding: 0; + min-width: 600px; + color: var(--ink); +} + +body, table { + font: var(--bodyfont); +} + +html { + scrollbar-color: var(--scrollbar); +} + +html:hover { + scrollbar-color: var(--scrollbar_hover); +} + +::selection { + text-shadow: none; + color: #fff; + background: var(--selected); +} + +hr, .hidden { + display: none; +} + +a:link, .node a:visited { + color: var(--a); + text-decoration: none; + outline: none; +} + +a:visited { + color: var(--visited); + text-decoration: none; +} + +a:hover, a:focus { + color: var(--hover); +} + +a:active { + color: var(--active); +} + +#container { + margin: 15px 5px; + padding: 8px; + max-width: 1920px; + border: var(--border); + box-shadow: var(--bevel); + background: var(--container); +} + +code { + padding: 0 2px; + font-family: var(--monospaced); + color: #373; +} + +table { + width: 100%; + border-collapse: collapse; + border: var(--border_inner); + background: var(--bg_table); +} + +tr { + border: var(--border_inner); +} + +#configtitle { + font-size: 13pt; +} + +.subtitle th { + padding: 6px 8px; + letter-spacing: normal; + text-transform: none; + font-size: 10.5pt; + background: var(--th_sub); +} + +#status { + border: var(--border_inner); +} + +#status th, #status td { + text-align: center; +} + +#status th, #status td { + width: 20%; +} + +#status td { + padding: 9px 8px; + border: var(--border_inner); + background: var(--bg_status); +} + +#status td:first-child { + text-transform: lowercase; +} + +#status td:first-child::first-letter { + text-transform: uppercase; +} + +#status td::before { + display: none; +} + +#starting, #running, #registered, #notregistered { + vertical-align: middle; + font-size: 0; +} + +#starting::before, #running::before, #registered::before, #notregistered::before { + width: 20px; + height: 20px; + display: inline-block; + vertical-align: middle; + background: var(--starting) no-repeat center center / 22px; + content: ""; + animation: spin linear 3s forwards infinite; +} + +#running::before, #registered::before { + background: var(--tick) no-repeat center center / 16px; + animation: none; +} + +#notregistered::before { + background: var(--cross) no-repeat center center / 16px; + animation: none; +} + +input.accept { + padding: 5px 9px 2px 26px; + background: var(--tick) no-repeat 9px center / 16px; +} + +@keyframes spin { + from {transform: rotate(0)} + to {transform: rotate(360deg)} +} + +table table tr:not(.stream):nth-child(odd), #configuration tr:not(.stream):nth-child(odd) { + background: var(--rowOdd); +} + +table table tr:not(.stream):nth-child(even), #configuration tr:not(.stream):nth-child(even) { + background: var(--rowEven); +} + +td { + padding: 8px; +} + +table table td::before { + min-height: 20px; + display: inline-block; + vertical-align: middle; + content: ""; +} + +table table td, #configuration table td { + padding: 5px 8px; +} + +th, #configtitle { + padding: 6px 8px; + text-align: left; + letter-spacing: .08em; + text-transform: uppercase; + font-size: 11pt; + background: var(--th); +} + +#title { + padding: 10px 8px 10px 44px; + font-size: 16pt; + letter-spacing: .08em !important; + line-height: 1.1; + text-shadow: var(--txtshadow_title); + background: var(--socks) no-repeat 6px center / 32px, var(--th); +} + +#configsection, #configsection td { + border: none; +} + +#configsection td { + padding: 0; +} + +#configsection table td { + padding: 10px; + vertical-align: text-bottom; +} + +#configuration { + position: relative; + border: none; +} + +#configuration tr { + border-left: none !important; + border-right: none !important; +} + +#configuration td:first-child { + border-left: none !important; +} + +#configuration td:last-child { + border-right: none !important; +} + +#configuration tr:last-child, #configuration tr:last-child td { + border-bottom: none !important; +} + +#configuration th { + width: 25%; + letter-spacing: normal; + text-transform: none; +} + +#configuration td:first-child { + border-left: none !important; +} + +#configuration td:last-child { + border-right: none !important; + line-height: 1.1; +} + +#formaction td { + padding: 12px !important; + background: var(--th_sub); +} + +#configtitle { + margin: -1px 0; + padding: 8px 8px 8px 32px; + width: 100%; + display: inline-block; + position: relative; + z-index: 99; + box-sizing: border-box; + text-transform: uppercase; + letter-spacing: .05em; + font-size: 12.5pt; + font-weight: bold; + border: var(--border_inner); + border-left: none; + border-right: none; + background: var(--configure) no-repeat 8px center / 18px, var(--th_sub) !important; + contain: paint; +} + +#expand, #collapse { + display: inline-block !important; + position: absolute; + top: calc(50% - 18px); + right: -1px; + font-size: 0; + border-left: var(--border_inner); +} + +#collapse { + display: none !important; +} + +#expand img, #collapse img { + padding: 15px; + width: 8px; + height: 8px; + display: inline-block; + vertical-align: middle; + transform: scaleX(.6); +} + +#collapse img { + transform: scaleY(.6); +} + +#expand:hover img, #collapse:hover img { + filter: var(--filter_shadow); + transform: scaleY(1.2) scaleX(.8); +} + +#collapse:hover img { + transform: scaleX(1.2) scaleY(.8); +} + +#expand::before, #collapse::before { + content: ""; + display: inline-block; + height: 100%; + width: 100%; + background: #f00; +} + +#configuration td:nth-child(2) code { + line-height: 1.15; + color: #050; +} + +#configuration td:nth-child(2) code, #configuration td:nth-child(2) .nowrap { + display: inline-block; + vertical-align: middle; +} + +.nowrap { + display: inline-block; + vertical-align: middle; + white-space: nowrap; +} + +#configuration .nowrap { + font-style: italic; + color: var(--ink); +} + +#configuration i { + margin-right: 1px; +} + +#configuration tr { + border-top: var(--border_inner); + border-bottom: var(--border_inner); +} + +td, #configuration td { + border: var(--border_inner); + border-left-style: solid; + border-right-style: solid; + box-shadow: var(--highlight) !important; +} + +td:first-child { + border-left: var(--border_inner) !important; +} + +td:last-child { + border-right: var(--border_inner) !important; +} + +#configuration td:first-child { + text-align: right; +} + +#formaction { + background: var(--th); +} + +input[type=text], input[type=password], input[type=submit] { + font: var(--bodyfont); + border: var(--border_inner); +} + +input[type=text], input[type=password] { + padding: 7px 10px; + color: var(--ink_lighter); + background: var(--bg_table); + box-shadow: var(--shadow_input); +} + +input[type=text]:focus, input[type=password]:focus { + color: var(--ink); + background: var(--bg_table); + box-shadow: var(--btn_shadow_active), 0 0 0 1px rgba(255,96,0,.7); + outline: none; +} + +input[type=submit] { + padding: 7px 14px 7px 30px; + color: var(--ink); + border-radius: 4px; + box-shadow: var(--highlight); + background: var(--tick) no-repeat 12px center / 12px, var(--btn); + cursor: pointer; +} + +input[type=submit]:hover { + color: var(--ink_lighter); + background: var(--tick) no-repeat 12px center / 12px, var(--btn_hover); + box-shadow: var(--highlight), var(--outline); +} + +input[type=submit]:active { + box-shadow: var(--btn_shadow_active), var(--outline); + background: var(--tick) no-repeat 12px center / 12px, var(--btn_active); +} + +#message { + padding: 45px 15px 20px; + width: 400px; + display: inline-block; + position: absolute; + top: 0; + right: -1000px; + text-align: center; + border: var(--border); + box-shadow: var(--highlight_notice); + background: var(--infohelp) no-repeat center 22px / 24px, var(--th_sub); + animation: slideLeft .75s ease-in .1s both, toast 2s ease 4s both; + filter: opacity(0); +} + +#message td { + border: none !important; + box-shadow: none !important; + background: none; +} + +@keyframes toast{ + from {top: 0;} + to {top: -1000px} +} + +@keyframes slideLeft { + from {top: 0; right: -800px; filter: opacity(0)} + to {top: 0; right: 0; filter: opacity(1)} +} + +@media screen and (max-width: 1000px) { + body {margin: 3px} + body, table {font-size: 9.5pt} + #container {margin: 0} + #title {font-size: 14pt} + #configtitle {font-size: 12pt} + .subtitle th {font-size: 10pt} + #refresh {margin-top: 0} +} diff --git a/resources/toggleConfig.js b/resources/toggleConfig.js new file mode 100644 index 0000000..f528be8 --- /dev/null +++ b/resources/toggleConfig.js @@ -0,0 +1,60 @@ +var expandConfig = null; +var collapseConfig = null; +var config = null; + +function initToggleConfig() +{ + expandConfig = document.getElementById("expandConfig"); + collapseConfig = document.getElementById("collapseConfig"); + config = document.getElementById("configuration"); + hideConfig(); +} + +function hideConfig() { + if (!collapseConfig) + config.style.display = "none"; +} + +function showConfig() { + if (collapseConfig) + config.style.display = "block"; +} + +function clean() { + if (expandConfig) { + expandConfig.remove(); + } + if (collapseConfig) { + collapseConfig.remove(); + } +} + +function expand() { + clean(); + var x = document.createElement("link"); + x.type="text/css"; + x.rel="stylesheet"; + x.href="/prometheus/resources/expand.css"; + x.setAttribute("id", "expandConfig"); + document.head.appendChild(x); + showConfig(); +} + +function collapse() { + clean(); + var c = document.createElement("link"); + c.type="text/css"; + c.rel="stylesheet"; + c.href="/prometheus/resources/collapse.css"; + c.setAttribute("id", "collapseConfig"); + document.head.appendChild(c); + hideConfig(); +} + +function copyText() { + document.execCommand("copy"); +} + +document.addEventListener("DOMContentLoaded", function() { + initToggleConfig(); +}, true); diff --git a/scripts/makeplugin.sh b/scripts/makeplugin.sh new file mode 100755 index 0000000..79b4fc1 --- /dev/null +++ b/scripts/makeplugin.sh @@ -0,0 +1,130 @@ +#!/bin/sh +# +# basic packaging up of a plugin +# +# usage: makeplugin.sh plugindir +# +# zzz 2010-02 +# zzz 2014-08 added support for su3 files +# + +if [ -z "$I2P" -a -d "$PWD/../i2p.i2p/pkg-temp" ]; then + export I2P=../i2p.i2p/pkg-temp +fi + +if [ ! -d "$I2P" ]; then + echo "Can't locate your I2P installation. Please add a environment variable named I2P with the path to the folder as value" + echo "On OSX this solved with running: export I2P=/Applications/i2p if default install directory is used." + exit 1 +fi + +PUBKEYDIR=$HOME/.i2p-plugin-keys +PUBKEYFILE=$PUBKEYDIR/plugin-public-signing.key +PRIVKEYFILE=$PUBKEYDIR/plugin-private-signing.key +B64KEYFILE=$PUBKEYDIR/plugin-public-signing.txt +PUBKEYSTORE=$PUBKEYDIR/plugin-su3-public-signing.crt +PRIVKEYSTORE=$PUBKEYDIR/plugin-su3-keystore.ks +KEYTYPE=RSA_SHA512_4096 + +PLUGINDIR=${1:-plugin} + +PC=plugin.config +PCT=${PC}.tmp + +if [ ! -d $PLUGINDIR ] +then + echo "You must have a $PLUGINDIR directory" + exit 1 +fi + +if [ ! -f $PLUGINDIR/$PC ] +then + echo "You must have a $PLUGINDIR/$PC file" + exit 1 +fi + +SIGNER=`grep '^signer=' $PLUGINDIR/$PC` +if [ "$?" -ne "0" ] +then + echo "You must have a plugin name in $PC" + echo 'For example name=foo' + exit 1 +fi +SIGNER=`echo $SIGNER | cut -f 2 -d '='` + +if [ ! -f $PRIVKEYSTORE ] +then + echo "Creating new SU3 $KEYTYPE keys for $SIGNER" + java -cp $I2P/lib/i2p.jar net.i2p.crypto.SU3File keygen -t $KEYTYPE $PUBKEYSTORE $PRIVKEYSTORE $SIGNER || exit 1 + echo '*** Save your password in a safe place!!! ***' + rm -rf logs/ + # copy to the router dir so verify will work + CDIR=$I2P/certificates/plugin + mkdir -p $CDIR || exit 1 + CFILE=$CDIR/`echo $SIGNER | sed s/@/_at_/`.crt + cp $PUBKEYSTORE $CFILE + chmod 444 $PUBKEYSTORE + chmod 400 $PRIVKEYSTORE + chmod 644 $CFILE + echo "Created new SU3 keys: $PUBKEYSTORE $PRIVKEYSTORE" + echo "Copied public key to $CFILE for testing" +fi + +rm -f plugin.zip + +OPWD=$PWD +cd $PLUGINDIR + +grep -q '^name=' $PC +if [ "$?" -ne "0" ] +then + echo "You must have a plugin name in $PC" + echo 'For example name=foo' + exit 1 +fi + +grep -q '^version=' $PC +if [ "$?" -ne "0" ] +then + echo "You must have a version in $PC" + echo 'For example version=0.1.2' + exit 1 +fi + +# update the date +grep -v '^date=' $PC > $PCT +DATE=`date '+%s000'` +echo "date=$DATE" >> $PCT +mv $PCT $PC || exit 1 + +# add our Base64 key +grep -v '^key=' $PC > $PCT +B64KEY=`cat $B64KEYFILE` +echo "key=$B64KEY" >> $PCT || exit 1 +mv $PCT $PC || exit 1 + +# zip it +zip -r $OPWD/plugin.zip * || exit 1 + +# get the version and use it for the sud header +VERSION=`grep '^version=' $PC | cut -f 2 -d '='` +# get the name and use it for the file name +NAME=`grep '^name=' $PC | cut -f 2 -d '='` +SU3=${NAME}.su3 +cd $OPWD + +# sign it +echo 'Signing. ...' +java -cp $I2P/lib/i2p.jar net.i2p.crypto.SU3File sign -c PLUGIN -t $KEYTYPE plugin.zip $SU3 $PRIVKEYSTORE $VERSION $SIGNER || exit 1 +rm -f plugin.zip + +# verify +echo "Verifying with $PUBKEYSTORE ..." +java -cp $I2P/lib/i2p.jar net.i2p.crypto.SU3File showversion $SU3 || exit 1 +java -cp $I2P/lib/i2p.jar net.i2p.crypto.SU3File verifysig -k $PUBKEYSTORE $SU3 || exit 1 +rm -rf logs/ + +echo 'Plugin files created: ' +wc -c $SU3 + +exit 0 diff --git a/scripts/plugin.config b/scripts/plugin.config new file mode 100644 index 0000000..dc5ae84 --- /dev/null +++ b/scripts/plugin.config @@ -0,0 +1,10 @@ +name=prometheus +consoleLinkName=Prometheus Metrics +consoleLinkURL=/prometheus/status.html +console-icon=/resources/images/prometheus.svg +signer=zzz-plugin@mail.i2p +description=Prometheus Metrics Server +author=zzz +updateURL.su3=http://stats.i2p/i2p/plugins/prometheus-update.su3 +websiteURL=http://zzz.i2p/forums/16 +license=Apache 2.0 diff --git a/src/build.xml b/src/build.xml new file mode 100644 index 0000000..d3b0048 --- /dev/null +++ b/src/build.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/net/i2p/prometheus/PromManager.java b/src/java/net/i2p/prometheus/PromManager.java new file mode 100644 index 0000000..fd65190 --- /dev/null +++ b/src/java/net/i2p/prometheus/PromManager.java @@ -0,0 +1,175 @@ +package net.i2p.prometheus; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import java.net.Socket; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; + +import io.prometheus.metrics.core.metrics.GaugeWithCallback; +import io.prometheus.metrics.instrumentation.jvm.JvmMetrics; +import io.prometheus.metrics.model.registry.PrometheusRegistry; + +import net.i2p.I2PAppContext; +import net.i2p.app.*; +import static net.i2p.app.ClientAppState.*; +import net.i2p.data.DataHelper; +import net.i2p.stat.Rate; +import net.i2p.stat.RateStat; +import net.i2p.stat.StatManager; +import net.i2p.util.Log; + +/** + * + * @author zzz + */ +public class PromManager implements ClientApp { + private final I2PAppContext _context; + private final Log _log; + private final ClientAppManager _mgr; + private int i2pCount, jvmCount; + + private ClientAppState _state = UNINITIALIZED; + + public PromManager(I2PAppContext ctx, ClientAppManager mgr, String args[]) { + _context = ctx; + _log = ctx.logManager().getLog(PromManager.class); + _mgr = mgr; + _state = INITIALIZED; + } + + public int getJVMCount() { return jvmCount; } + public int getI2PCount() { return i2pCount; } + + /** + * Not supported + */ + public synchronized static void main(String args[]) { + throw new UnsupportedOperationException("Must use ClientApp interface"); + } + + /** + * This adds the stats present at plugin startup. + * TODO: add a monitor to add stats that appear later. + */ + private void addStats() { + StatManager sm = _context.statManager(); + Map> groups = sm.getStatsByGroup(); + int n = 0; + for (Map.Entry> e : groups.entrySet()) { + //String pfx = "i2p." + e.getKey() + '.'; + String pfx = "i2p."; + for (String s : e.getValue()) { + RateStat rs = sm.getRate(s); + if (rs == null) + continue; + String desc = rs.getDescription(); + if (desc == null) + desc = ""; + long[] pers = rs.getPeriods(); + final long per = pers[0]; + final Rate rate = rs.getRate(per); + if (rate == null) + continue; + + String name = pfx + s + '.' + DataHelper.formatDuration(per); + name = name.replace(".", "_"); + name = name.replace("-", "_"); + // https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + // Prevent IllegalArgumentExceptions + if (name.replaceAll("[a-zA-Z0-9_]", "").length() != 0) { + if (_log.shouldWarn()) + _log.warn("skipping stat with illegal chars: " + name); + continue; + } + + if (_log.shouldDebug()) + _log.debug("adding gauge " + name); + + GaugeWithCallback.builder() + .name(name) + .help(desc) + .labelNames("state") + .callback(callback -> { + callback.call(rate.getAvgOrLifetimeAvg(), "average"); + }) + .register(); + n++; + } + } + i2pCount = n; + if (_log.shouldDebug()) + _log.info(n + " PromManager I2P metrics registered"); + } + + + /////// ClientApp methods + + public synchronized void startup() throws Exception { + if (_state != STOPPED && _state != INITIALIZED && _state != START_FAILED) { + _log.error("Start while state = " + _state); + return; + } + _log.info("PromManager startup"); + JvmMetrics.builder().register(); + jvmCount = PrometheusRegistry.defaultRegistry.scrape().size(); + if (_log.shouldInfo()) + _log.info(jvmCount + " PromManager JVM metrics registered"); + + addStats(); + changeState(RUNNING); + _mgr.register(this); + } + + public synchronized void shutdown(String[] args) { + _log.warn("PromManager shutdown"); + if (_state == STOPPED) + return; + changeState(STOPPING); + // clear() supported as of v1.3.2 + PrometheusRegistry.defaultRegistry.clear(); + _mgr.unregister(this); + changeState(STOPPED); + } + + public ClientAppState getState() { + return _state; + } + + public String getName() { + return "prometheus"; + } + + public String getDisplayName() { + return "PromManager Metrics"; + } + + /////// end ClientApp methods + + private synchronized void changeState(ClientAppState state) { + if (state == _state) + return; + _state = state; + _mgr.notify(this, state, null, null); + } + + private synchronized void changeState(ClientAppState state, String msg, Exception e) { + if (state == _state) + return; + _state = state; + _mgr.notify(this, state, msg, e); + } +} diff --git a/src/java/net/i2p/prometheus/web/BasicServlet.java b/src/java/net/i2p/prometheus/web/BasicServlet.java new file mode 100644 index 0000000..55324d3 --- /dev/null +++ b/src/java/net/i2p/prometheus/web/BasicServlet.java @@ -0,0 +1,151 @@ +// ======================================================================== +// Copyright 199-2004 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ======================================================================== + +package net.i2p.prometheus.web; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Enumeration; +import java.util.List; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.i2p.I2PAppContext; +import net.i2p.data.ByteArray; +import net.i2p.data.DataHelper; +import net.i2p.util.ByteCache; +import net.i2p.util.Log; +import net.i2p.util.SystemVersion; + + +/* ------------------------------------------------------------ */ +/** + * Based on DefaultServlet from Jetty 6.1.26, heavily simplified + * and modified to remove all dependencies on Jetty libs. + * + * Supports HEAD and GET only, for resources from the .war and local files. + * Supports files and resource only. + * Supports MIME types with local overrides and additions. + * Supports Last-Modified. + * Supports single request ranges. + * + * Does not support directories or "welcome files". + * Does not support gzip. + * Does not support multiple request ranges. + * Does not cache. + * + * POST returns 405. + * Directories return 403. + * Jar resources are sent with a long cache directive. + * + * ------------------------------------------------------------ + * + * The default servlet. + * This servlet, normally mapped to /, provides the handling for static + * content, OPTION and TRACE methods for the context. + * The following initParameters are supported, these can be set + * on the servlet itself: + *
                                                                      
+ *
+ *  resourceBase      Set to replace the context resource base
+
+ *  warBase      Path allowed for resource in war
+ * 
+ * 
+ * + * + * @author Greg Wilkins (gregw) + * @author Nigel Canonizado + * + * @since Jetty 7 + */ +class BasicServlet extends HttpServlet +{ + protected final I2PAppContext _context; + protected final Log _log; + protected File _resourceBase; + private String _warBase; + + /** same as PeerState.PARTSIZE */ + private static final int BUFSIZE = 16*1024; + private ByteCache _cache = ByteCache.getInstance(16, BUFSIZE); + + private static final int WAR_CACHE_CONTROL_SECS = 24*60*60; + private static final int FILE_CACHE_CONTROL_SECS = 24*60*60; + + public BasicServlet() { + super(); + _context = I2PAppContext.getGlobalContext(); + _log = _context.logManager().getLog(getClass()); + } + + /* ------------------------------------------------------------ */ + public void init(ServletConfig cfg) throws ServletException { + super.init(cfg); + } + + /* ------------------------------------------------------------ */ + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + response.sendError(405); + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + protected void doTrace(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + response.sendError(405); + } + + protected void doOptions(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + response.sendError(405); + } + + protected void doDelete(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + response.sendError(405); + } + + /** + * Simple version of URIUtil.addPaths() + * @param path may be null + */ + protected static String addPaths(String base, String path) { + if (path == null) + return base; + String rv = (new File(base, path)).toString(); + if (SystemVersion.isWindows()) + rv = rv.replace("\\", "/"); + return rv; + } +} diff --git a/src/java/net/i2p/prometheus/web/PrometheusServlet.java b/src/java/net/i2p/prometheus/web/PrometheusServlet.java new file mode 100644 index 0000000..4c638aa --- /dev/null +++ b/src/java/net/i2p/prometheus/web/PrometheusServlet.java @@ -0,0 +1,254 @@ +package net.i2p.prometheus.web; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Properties; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.i2p.app.ClientAppManager; +import net.i2p.app.ClientAppState; +import net.i2p.data.DataHelper; +import net.i2p.prometheus.PromManager; +import net.i2p.util.I2PAppThread; +import net.i2p.util.PortMapper; +import net.i2p.util.Translate; + +import net.i2p.I2PAppContext; + +/** + * From socksoutproxy + */ +public class PrometheusServlet extends BasicServlet { + private String _contextPath; + private String _contextName; + private volatile PromManager _manager; + private volatile boolean _isRunning; + private static long _nonce; + + private static final String DEFAULT_NAME = "prometheus"; + private static final String DOCTYPE = "\n"; + private static final String FOOTER = "\n\n\n\n"; + // for now, use console bundle, hope to pick up a few translations for free + private static final String BUNDLE = "net.i2p.router.web.messages"; + private static final String RESOURCES = "/prometheus/resources/"; + private static final String VERSION = "0.1"; + + public PrometheusServlet() { + super(); + } + + @Override + public void init(ServletConfig cfg) throws ServletException { + super.init(cfg); + String cpath = getServletContext().getContextPath(); + _contextPath = cpath == "" ? "/" : cpath; + _contextName = cpath == "" ? DEFAULT_NAME : cpath.substring(1).replace("/", "_"); + _nonce = _context.random().nextLong(); + _isRunning = true; + (new Starter()).start(); + } + + /** + * Wait for the ClientAppManager + */ + private class Starter extends I2PAppThread { + public Starter() { + super("Prometheus Starter"); + } + + public void run() { + try { + run2(); + } catch (Throwable t) { + // class problems, old router version, ... + _log.error("Unable to start Prometheus", t); + _isRunning = false; + } + } + + private void run2() throws Exception { + File f = new File(_context.getConfigDir(), "plugins"); + f = new File(f, _contextName); + String[] args = new String[] { f.toString() }; + while (_isRunning) { + ClientAppManager cam = _context.clientAppManager(); + if (cam != null) { + _manager = new PromManager(_context, cam, args); + _manager.startup(); + break; + } else { + try { + Thread.sleep(10*1000); + } catch (InterruptedException ie) {} + } + } + } + } + + @Override + public void destroy() { + _isRunning = false; + if (_manager != null) + _manager.shutdown(null); + super.destroy(); + } + + /** + * Handle what we can here, calling super.doGet() for the rest. + */ + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + doGetAndPost(request, response); + } + + /** + * Handle what we can here, calling super.doPost() for the rest. + */ + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + doGetAndPost(request, response); + } + + /** + * Handle all here + */ + private void doGetAndPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + PromManager c = _manager; + String method = req.getMethod(); + String msg = null; + if (c != null) { + if (c.getState() != ClientAppState.RUNNING) { + try { + c.startup(); + msg = "Prometheus started"; + } catch (Exception e) { + msg = "Prometheus failure: " + e; + } + } + } + + // this is the part after /orchid + String path = req.getServletPath(); + resp.setHeader("X-Frame-Options", "SAMEORIGIN"); + + req.setCharacterEncoding("UTF-8"); + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/html; charset=UTF-8"); + + PrintWriter out = resp.getWriter(); + out.write(DOCTYPE + "\n\n"); + out.write(_t("Prometheus Metrics Client")); + out.write("\n"); + out.write("\n"); + out.write("\n"); + out.write("\n"); + out.write("\n"); + out.write("\n\n"); + out.write("\n
\n\n" + + "\n"); + out.write("\n
" + _t("Prometheus Metrics Client") + "
\n
\n\n" + + "" + + "" + + "" + + "" + + ""); + out.write("\n\n"); + if (c != null) + out.write("\n"); + else + out.write("\n"); + if (c != null) + out.write("\n"); + else + out.write("\n"); + if (msg != null) + out.write("\n"); + out.write("
" + _t("Status") + "" + _t("Registered with I2P") + "" + _t("Plugin Version") + "" + _t("I2P Metrics") + "" + _t("Java Metrics") + "
"); + if (c != null) { + ClientAppState status = c.getState(); + if (status == ClientAppState.RUNNING) + out.write("" + _t("Running") + ""); + else if (status == ClientAppState.STARTING) + out.write("" + _t("Starting") + "..."); + else if (status == ClientAppState.START_FAILED) + out.write("" + _t("Start failed") + ""); + else + out.write(status.toString()); + } else { + out.write(_t("Not initialized")); + } + out.write(""); + ClientAppManager cam = _context.clientAppManager(); + if (c != null && cam != null && cam.getRegisteredApp(DEFAULT_NAME) == c) { + out.write("" + _t("Yes") + ""); + } else { + out.write("" + _t("No") + ""); + } + out.write("" + VERSION + "" + c.getI2PCount() + "--" + c.getJVMCount() + "--
" + msg + "
\n"); + if (c != null) { + out.write("
\n
\n
Configuration \n" + + "" + + "\"Expand\"\n" + + "" + + "\"Collapse\"
\n"); + out.write(getHTMLConfig(c)); + } + out.write(FOOTER); + } + + private String getHTMLConfig(PromManager tc) { + StringBuilder buf = new StringBuilder(1024); + buf.append("
\n
\n"); + boolean full = _context.getBooleanProperty("stat.full"); + if (full) + buf.append("

Full stats are enabled.

\n"); + else + buf.append("

For more metrics, enable full stats and restart.

\n"); + buf.append("

Prometheus server configuration - add to /etc/prometheus/prometheus.yml:

\n"); + int port = _context.portMapper().getPort(PortMapper.SVC_CONSOLE); + if (port <= 0) + port = 7657; + buf.append("
" +
+                   "  - job_name: i2p\n" +
+                   "    scrape_interval: 60s\n" +
+                   "    metrics_path: /prometheus/metrics\n" +
+                   "    static_configs:\n" +
+                   "      - targets: ['localhost:").append(port).append("']\n" +
+                   "
\n"); + buf.append("
\n"); + return buf.toString(); + } + + /** translate */ + private String _t(String s) { + return Translate.getString(s, _context, BUNDLE); + } + + /** translate */ + private String _t(String s, Object o) { + return Translate.getString(s, o, _context, BUNDLE); + } + + /** translate */ + private String _t(String s, Object o, Object o2) { + return Translate.getString(s, o, o2, _context, BUNDLE); + } + + /** translate (ngettext) @since 0.7.14 */ + private String ngettext(String s, String p, int n) { + return Translate.getString(n, s, p, _context, BUNDLE); + } + + /** dummy for tagging */ + private static String ngettext(String s, String p) { + return null; + } + +} diff --git a/src/jsp/WEB-INF/web.xml b/src/jsp/WEB-INF/web.xml new file mode 100644 index 0000000..9f3283c --- /dev/null +++ b/src/jsp/WEB-INF/web.xml @@ -0,0 +1,60 @@ + + + + + PrometheusServlet + net.i2p.prometheus.web.PrometheusServlet + 1 + + + + PrometheusMetricsServlet + io.prometheus.metrics.exporter.servlet.javax.PrometheusMetricsServlet + 1 + + + + + + + + PrometheusServlet + /index.jsp + + + + PrometheusServlet + /index.html + + + + PrometheusServlet + /status + + + + PrometheusServlet + /status.html + + + + PrometheusServlet + /status.jsp + + + + PrometheusMetricsServlet + /metrics + + + + PrometheusMetricsServlet + /metrics/ + + +