Adding upstream version 2.52.6.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
a960158181
commit
6d002e9543
441 changed files with 95392 additions and 0 deletions
132
middleware/monitor/config.go
Normal file
132
middleware/monitor/config.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package monitor
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// Config defines the config for middleware.
|
||||
type Config struct {
|
||||
// Metrics page title
|
||||
//
|
||||
// Optional. Default: "Fiber Monitor"
|
||||
Title string
|
||||
|
||||
// Refresh period
|
||||
//
|
||||
// Optional. Default: 3 seconds
|
||||
Refresh time.Duration
|
||||
|
||||
// Whether the service should expose only the monitoring API.
|
||||
//
|
||||
// Optional. Default: false
|
||||
APIOnly bool
|
||||
|
||||
// Next defines a function to skip this middleware when returned true.
|
||||
//
|
||||
// Optional. Default: nil
|
||||
Next func(c *fiber.Ctx) bool
|
||||
|
||||
// Custom HTML Code to Head Section(Before End)
|
||||
//
|
||||
// Optional. Default: empty
|
||||
CustomHead string
|
||||
|
||||
// FontURL for specify font resource path or URL . also you can use relative path
|
||||
//
|
||||
// Optional. Default: https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap
|
||||
FontURL string
|
||||
|
||||
// ChartJsURL for specify ChartJS library path or URL . also you can use relative path
|
||||
//
|
||||
// Optional. Default: https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js
|
||||
ChartJsURL string // TODO: Rename to "ChartJSURL" in v3
|
||||
|
||||
index string
|
||||
}
|
||||
|
||||
var ConfigDefault = Config{
|
||||
Title: defaultTitle,
|
||||
Refresh: defaultRefresh,
|
||||
FontURL: defaultFontURL,
|
||||
ChartJsURL: defaultChartJSURL,
|
||||
CustomHead: defaultCustomHead,
|
||||
APIOnly: false,
|
||||
Next: nil,
|
||||
index: newIndex(viewBag{
|
||||
defaultTitle,
|
||||
defaultRefresh,
|
||||
defaultFontURL,
|
||||
defaultChartJSURL,
|
||||
defaultCustomHead,
|
||||
}),
|
||||
}
|
||||
|
||||
func configDefault(config ...Config) Config {
|
||||
// Users can change ConfigDefault.Title/Refresh which then
|
||||
// become incompatible with ConfigDefault.index
|
||||
if ConfigDefault.Title != defaultTitle ||
|
||||
ConfigDefault.Refresh != defaultRefresh ||
|
||||
ConfigDefault.FontURL != defaultFontURL ||
|
||||
ConfigDefault.ChartJsURL != defaultChartJSURL ||
|
||||
ConfigDefault.CustomHead != defaultCustomHead {
|
||||
if ConfigDefault.Refresh < minRefresh {
|
||||
ConfigDefault.Refresh = minRefresh
|
||||
}
|
||||
// update default index with new default title/refresh
|
||||
ConfigDefault.index = newIndex(viewBag{
|
||||
ConfigDefault.Title,
|
||||
ConfigDefault.Refresh,
|
||||
ConfigDefault.FontURL,
|
||||
ConfigDefault.ChartJsURL,
|
||||
ConfigDefault.CustomHead,
|
||||
})
|
||||
}
|
||||
|
||||
// Return default config if nothing provided
|
||||
if len(config) < 1 {
|
||||
return ConfigDefault
|
||||
}
|
||||
|
||||
// Override default config
|
||||
cfg := config[0]
|
||||
|
||||
// Set default values
|
||||
if cfg.Title == "" {
|
||||
cfg.Title = ConfigDefault.Title
|
||||
}
|
||||
|
||||
if cfg.Refresh == 0 {
|
||||
cfg.Refresh = ConfigDefault.Refresh
|
||||
}
|
||||
if cfg.FontURL == "" {
|
||||
cfg.FontURL = defaultFontURL
|
||||
}
|
||||
|
||||
if cfg.ChartJsURL == "" {
|
||||
cfg.ChartJsURL = defaultChartJSURL
|
||||
}
|
||||
if cfg.Refresh < minRefresh {
|
||||
cfg.Refresh = minRefresh
|
||||
}
|
||||
|
||||
if cfg.Next == nil {
|
||||
cfg.Next = ConfigDefault.Next
|
||||
}
|
||||
|
||||
if !cfg.APIOnly {
|
||||
cfg.APIOnly = ConfigDefault.APIOnly
|
||||
}
|
||||
|
||||
// update cfg.index with custom title/refresh
|
||||
cfg.index = newIndex(viewBag{
|
||||
title: cfg.Title,
|
||||
refresh: cfg.Refresh,
|
||||
fontURL: cfg.FontURL,
|
||||
chartJSURL: cfg.ChartJsURL,
|
||||
customHead: cfg.CustomHead,
|
||||
})
|
||||
|
||||
return cfg
|
||||
}
|
163
middleware/monitor/config_test.go
Normal file
163
middleware/monitor/config_test.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package monitor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
)
|
||||
|
||||
func Test_Config_Default(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("use default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := configDefault()
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, defaultRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set title", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
title := "title"
|
||||
cfg := configDefault(Config{
|
||||
Title: title,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, title, cfg.Title)
|
||||
utils.AssertEqual(t, defaultRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{title, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set refresh less than default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := configDefault(Config{
|
||||
Refresh: 100 * time.Millisecond,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, minRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, minRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set refresh", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
refresh := time.Second
|
||||
cfg := configDefault(Config{
|
||||
Refresh: refresh,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, refresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, refresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set font url", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fontURL := "https://example.com"
|
||||
cfg := configDefault(Config{
|
||||
FontURL: fontURL,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, defaultRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, fontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, fontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set chart js url", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
chartURL := "http://example.com"
|
||||
cfg := configDefault(Config{
|
||||
ChartJsURL: chartURL,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, defaultRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, chartURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, chartURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set custom head", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
head := "head"
|
||||
cfg := configDefault(Config{
|
||||
CustomHead: head,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, defaultRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, head, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, head}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set api only", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := configDefault(Config{
|
||||
APIOnly: true,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, defaultRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, true, cfg.APIOnly)
|
||||
utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
|
||||
t.Run("set next", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := func(c *fiber.Ctx) bool {
|
||||
return true
|
||||
}
|
||||
cfg := configDefault(Config{
|
||||
Next: f,
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, defaultTitle, cfg.Title)
|
||||
utils.AssertEqual(t, defaultRefresh, cfg.Refresh)
|
||||
utils.AssertEqual(t, defaultFontURL, cfg.FontURL)
|
||||
utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL)
|
||||
utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead)
|
||||
utils.AssertEqual(t, false, cfg.APIOnly)
|
||||
utils.AssertEqual(t, f(nil), cfg.Next(nil))
|
||||
utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
|
||||
})
|
||||
}
|
271
middleware/monitor/index.go
Normal file
271
middleware/monitor/index.go
Normal file
|
@ -0,0 +1,271 @@
|
|||
package monitor
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type viewBag struct {
|
||||
title string
|
||||
refresh time.Duration
|
||||
fontURL string
|
||||
chartJSURL string
|
||||
customHead string
|
||||
}
|
||||
|
||||
// returns index with new title/refresh
|
||||
func newIndex(dat viewBag) string {
|
||||
timeout := dat.refresh.Milliseconds() - timeoutDiff
|
||||
if timeout < timeoutDiff {
|
||||
timeout = timeoutDiff
|
||||
}
|
||||
ts := strconv.FormatInt(timeout, 10)
|
||||
replacer := strings.NewReplacer("$TITLE", dat.title, "$TIMEOUT", ts,
|
||||
"$FONT_URL", dat.fontURL, "$CHART_JS_URL", dat.chartJSURL, "$CUSTOM_HEAD", dat.customHead,
|
||||
)
|
||||
return replacer.Replace(indexHTML)
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTitle = "Fiber Monitor"
|
||||
|
||||
defaultRefresh = 3 * time.Second
|
||||
timeoutDiff = 200 // timeout will be Refresh (in milliseconds) - timeoutDiff
|
||||
minRefresh = timeoutDiff * time.Millisecond
|
||||
defaultFontURL = `https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap`
|
||||
defaultChartJSURL = `https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js`
|
||||
defaultCustomHead = ``
|
||||
|
||||
// parametrized by $TITLE and $TIMEOUT
|
||||
indexHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="$FONT_URL" rel="stylesheet">
|
||||
<script src="$CHART_JS_URL"></script>
|
||||
|
||||
<title>$TITLE</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font: 16px / 1.6 'Roboto', sans-serif;
|
||||
}
|
||||
.wrapper {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.title h1 {
|
||||
font-size: 1.8em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.row .column:first-child { width: 35%; }
|
||||
.row .column:last-child { width: 65%; }
|
||||
.metric {
|
||||
color: #777;
|
||||
font-weight: 900;
|
||||
}
|
||||
h2 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 2.2em;
|
||||
}
|
||||
h2 span {
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
}
|
||||
h2 span.ram_os { color: rgba(255, 150, 0, .8); }
|
||||
h2 span.ram_total { color: rgba(0, 200, 0, .8); }
|
||||
canvas {
|
||||
width: 200px;
|
||||
height: 180px;
|
||||
}
|
||||
$CUSTOM_HEAD
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section class="wrapper">
|
||||
<div class="title"><h1>$TITLE</h1></div>
|
||||
<section class="charts">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="metric">CPU Usage</div>
|
||||
<h2 id="cpuMetric">0.00%</h2>
|
||||
</div>
|
||||
<div class="column">
|
||||
<canvas id="cpuChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="metric">Memory Usage</div>
|
||||
<h2 id="ramMetric" title="PID used / OS used / OS total">0.00 MB</h2>
|
||||
</div>
|
||||
<div class="column">
|
||||
<canvas id="ramChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="metric">Response Time</div>
|
||||
<h2 id="rtimeMetric">0ms</h2>
|
||||
</div>
|
||||
<div class="column">
|
||||
<canvas id="rtimeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="metric">Open Connections</div>
|
||||
<h2 id="connsMetric">0</h2>
|
||||
</div>
|
||||
<div class="column">
|
||||
<canvas id="connsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<script>
|
||||
function formatBytes(bytes, decimals = 1) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
Chart.defaults.global.legend.display = false;
|
||||
Chart.defaults.global.defaultFontSize = 8;
|
||||
Chart.defaults.global.animation.duration = 1000;
|
||||
Chart.defaults.global.animation.easing = 'easeOutQuart';
|
||||
Chart.defaults.global.elements.line.backgroundColor = 'rgba(0, 172, 215, 0.25)';
|
||||
Chart.defaults.global.elements.line.borderColor = 'rgba(0, 172, 215, 1)';
|
||||
Chart.defaults.global.elements.line.borderWidth = 2;
|
||||
|
||||
const options = {
|
||||
scales: {
|
||||
yAxes: [{ ticks: { beginAtZero: true }}],
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
time: {
|
||||
unitStepSize: 30,
|
||||
unit: 'second'
|
||||
},
|
||||
gridlines: { display: false }
|
||||
}]
|
||||
},
|
||||
tooltips: { enabled: false },
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false
|
||||
};
|
||||
const cpuMetric = document.querySelector('#cpuMetric');
|
||||
const ramMetric = document.querySelector('#ramMetric');
|
||||
const rtimeMetric = document.querySelector('#rtimeMetric');
|
||||
const connsMetric = document.querySelector('#connsMetric');
|
||||
|
||||
const cpuChartCtx = document.querySelector('#cpuChart').getContext('2d');
|
||||
const ramChartCtx = document.querySelector('#ramChart').getContext('2d');
|
||||
const rtimeChartCtx = document.querySelector('#rtimeChart').getContext('2d');
|
||||
const connsChartCtx = document.querySelector('#connsChart').getContext('2d');
|
||||
|
||||
const cpuChart = createChart(cpuChartCtx);
|
||||
const ramChart = createChart(ramChartCtx);
|
||||
const rtimeChart = createChart(rtimeChartCtx);
|
||||
const connsChart = createChart(connsChartCtx);
|
||||
|
||||
const charts = [cpuChart, ramChart, rtimeChart, connsChart];
|
||||
|
||||
function createChart(ctx) {
|
||||
return new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: '',
|
||||
data: [],
|
||||
lineTension: 0.2,
|
||||
pointRadius: 0,
|
||||
}]
|
||||
},
|
||||
options
|
||||
});
|
||||
}
|
||||
ramChart.data.datasets.push({
|
||||
data: [],
|
||||
lineTension: 0.2,
|
||||
pointRadius: 0,
|
||||
backgroundColor: 'rgba(255, 200, 0, .6)',
|
||||
borderColor: 'rgba(255, 150, 0, .8)',
|
||||
})
|
||||
ramChart.data.datasets.push({
|
||||
data: [],
|
||||
lineTension: 0.2,
|
||||
pointRadius: 0,
|
||||
backgroundColor: 'rgba(0, 255, 0, .4)',
|
||||
borderColor: 'rgba(0, 200, 0, .8)',
|
||||
})
|
||||
function update(json, rtime) {
|
||||
cpu = json.pid.cpu.toFixed(1);
|
||||
cpuOS = json.os.cpu.toFixed(1);
|
||||
|
||||
cpuMetric.innerHTML = cpu + '% <span>' + cpuOS + '%</span>';
|
||||
ramMetric.innerHTML = formatBytes(json.pid.ram) + '<span> / </span><span class="ram_os">' + formatBytes(json.os.ram) +
|
||||
'<span><span> / </span><span class="ram_total">' + formatBytes(json.os.total_ram) + '</span>';
|
||||
rtimeMetric.innerHTML = rtime + 'ms <span>client</span>';
|
||||
connsMetric.innerHTML = json.pid.conns + ' <span>' + json.os.conns + '</span>';
|
||||
|
||||
cpuChart.data.datasets[0].data.push(cpu);
|
||||
ramChart.data.datasets[2].data.push((json.os.total_ram / 1e6).toFixed(2));
|
||||
ramChart.data.datasets[1].data.push((json.os.ram / 1e6).toFixed(2));
|
||||
ramChart.data.datasets[0].data.push((json.pid.ram / 1e6).toFixed(2));
|
||||
rtimeChart.data.datasets[0].data.push(rtime);
|
||||
connsChart.data.datasets[0].data.push(json.pid.conns);
|
||||
|
||||
const timestamp = new Date().getTime();
|
||||
|
||||
charts.forEach(chart => {
|
||||
if (chart.data.labels.length > 50) {
|
||||
chart.data.datasets.forEach(function (dataset) { dataset.data.shift(); });
|
||||
chart.data.labels.shift();
|
||||
}
|
||||
chart.data.labels.push(timestamp);
|
||||
chart.update();
|
||||
});
|
||||
setTimeout(fetchJSON, $TIMEOUT)
|
||||
}
|
||||
function fetchJSON() {
|
||||
var t1 = ''
|
||||
var t0 = performance.now()
|
||||
fetch(window.location.href, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(res => {
|
||||
t1 = performance.now()
|
||||
return res.json()
|
||||
})
|
||||
.then(res => { update(res, Math.round(t1 - t0)) })
|
||||
.catch(console.error);
|
||||
}
|
||||
fetchJSON()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
)
|
137
middleware/monitor/monitor.go
Normal file
137
middleware/monitor/monitor.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package monitor
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/internal/gopsutil/cpu"
|
||||
"github.com/gofiber/fiber/v2/internal/gopsutil/load"
|
||||
"github.com/gofiber/fiber/v2/internal/gopsutil/mem"
|
||||
"github.com/gofiber/fiber/v2/internal/gopsutil/net"
|
||||
"github.com/gofiber/fiber/v2/internal/gopsutil/process"
|
||||
)
|
||||
|
||||
type stats struct {
|
||||
PID statsPID `json:"pid"`
|
||||
OS statsOS `json:"os"`
|
||||
}
|
||||
|
||||
type statsPID struct {
|
||||
CPU float64 `json:"cpu"`
|
||||
RAM uint64 `json:"ram"`
|
||||
Conns int `json:"conns"`
|
||||
}
|
||||
|
||||
type statsOS struct {
|
||||
CPU float64 `json:"cpu"`
|
||||
RAM uint64 `json:"ram"`
|
||||
TotalRAM uint64 `json:"total_ram"`
|
||||
LoadAvg float64 `json:"load_avg"`
|
||||
Conns int `json:"conns"`
|
||||
}
|
||||
|
||||
var (
|
||||
monitPIDCPU atomic.Value
|
||||
monitPIDRAM atomic.Value
|
||||
monitPIDConns atomic.Value
|
||||
|
||||
monitOSCPU atomic.Value
|
||||
monitOSRAM atomic.Value
|
||||
monitOSTotalRAM atomic.Value
|
||||
monitOSLoadAvg atomic.Value
|
||||
monitOSConns atomic.Value
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.RWMutex
|
||||
once sync.Once
|
||||
data = &stats{}
|
||||
)
|
||||
|
||||
// New creates a new middleware handler
|
||||
func New(config ...Config) fiber.Handler {
|
||||
// Set default config
|
||||
cfg := configDefault(config...)
|
||||
|
||||
// Start routine to update statistics
|
||||
once.Do(func() {
|
||||
p, _ := process.NewProcess(int32(os.Getpid())) //nolint:errcheck // TODO: Handle error
|
||||
numcpu := runtime.NumCPU()
|
||||
updateStatistics(p, numcpu)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(cfg.Refresh)
|
||||
|
||||
updateStatistics(p, numcpu)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
// Return new handler
|
||||
//nolint:errcheck // Ignore the type-assertion errors
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Don't execute middleware if Next returns true
|
||||
if cfg.Next != nil && cfg.Next(c) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
if c.Method() != fiber.MethodGet {
|
||||
return fiber.ErrMethodNotAllowed
|
||||
}
|
||||
if c.Get(fiber.HeaderAccept) == fiber.MIMEApplicationJSON || cfg.APIOnly {
|
||||
mutex.Lock()
|
||||
data.PID.CPU, _ = monitPIDCPU.Load().(float64)
|
||||
data.PID.RAM, _ = monitPIDRAM.Load().(uint64)
|
||||
data.PID.Conns, _ = monitPIDConns.Load().(int)
|
||||
|
||||
data.OS.CPU, _ = monitOSCPU.Load().(float64)
|
||||
data.OS.RAM, _ = monitOSRAM.Load().(uint64)
|
||||
data.OS.TotalRAM, _ = monitOSTotalRAM.Load().(uint64)
|
||||
data.OS.LoadAvg, _ = monitOSLoadAvg.Load().(float64)
|
||||
data.OS.Conns, _ = monitOSConns.Load().(int)
|
||||
mutex.Unlock()
|
||||
return c.Status(fiber.StatusOK).JSON(data)
|
||||
}
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
|
||||
return c.Status(fiber.StatusOK).SendString(cfg.index)
|
||||
}
|
||||
}
|
||||
|
||||
func updateStatistics(p *process.Process, numcpu int) {
|
||||
pidCPU, err := p.Percent(0)
|
||||
if err == nil {
|
||||
monitPIDCPU.Store(pidCPU / float64(numcpu))
|
||||
}
|
||||
|
||||
if osCPU, err := cpu.Percent(0, false); err == nil && len(osCPU) > 0 {
|
||||
monitOSCPU.Store(osCPU[0])
|
||||
}
|
||||
|
||||
if pidRAM, err := p.MemoryInfo(); err == nil && pidRAM != nil {
|
||||
monitPIDRAM.Store(pidRAM.RSS)
|
||||
}
|
||||
|
||||
if osRAM, err := mem.VirtualMemory(); err == nil && osRAM != nil {
|
||||
monitOSRAM.Store(osRAM.Used)
|
||||
monitOSTotalRAM.Store(osRAM.Total)
|
||||
}
|
||||
|
||||
if loadAvg, err := load.Avg(); err == nil && loadAvg != nil {
|
||||
monitOSLoadAvg.Store(loadAvg.Load1)
|
||||
}
|
||||
|
||||
pidConns, err := net.ConnectionsPid("tcp", p.Pid)
|
||||
if err == nil {
|
||||
monitPIDConns.Store(len(pidConns))
|
||||
}
|
||||
|
||||
osConns, err := net.Connections("tcp")
|
||||
if err == nil {
|
||||
monitOSConns.Store(len(osConns))
|
||||
}
|
||||
}
|
198
middleware/monitor/monitor_test.go
Normal file
198
middleware/monitor/monitor_test.go
Normal file
|
@ -0,0 +1,198 @@
|
|||
package monitor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Test_Monitor_405(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Use("/", New())
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/", nil))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 405, resp.StatusCode)
|
||||
}
|
||||
|
||||
func Test_Monitor_Html(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
// defaults
|
||||
app.Get("/", New())
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8,
|
||||
resp.Header.Get(fiber.HeaderContentType))
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte("<title>"+defaultTitle+"</title>")))
|
||||
timeoutLine := fmt.Sprintf("setTimeout(fetchJSON, %d)",
|
||||
defaultRefresh.Milliseconds()-timeoutDiff)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine)))
|
||||
|
||||
// custom config
|
||||
conf := Config{Title: "New " + defaultTitle, Refresh: defaultRefresh + time.Second}
|
||||
app.Get("/custom", New(conf))
|
||||
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/custom", nil))
|
||||
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8,
|
||||
resp.Header.Get(fiber.HeaderContentType))
|
||||
buf, err = io.ReadAll(resp.Body)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte("<title>"+conf.Title+"</title>")))
|
||||
timeoutLine = fmt.Sprintf("setTimeout(fetchJSON, %d)",
|
||||
conf.Refresh.Milliseconds()-timeoutDiff)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine)))
|
||||
}
|
||||
|
||||
func Test_Monitor_Html_CustomCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
// defaults
|
||||
app.Get("/", New())
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
|
||||
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8,
|
||||
resp.Header.Get(fiber.HeaderContentType))
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte("<title>"+defaultTitle+"</title>")))
|
||||
timeoutLine := fmt.Sprintf("setTimeout(fetchJSON, %d)",
|
||||
defaultRefresh.Milliseconds()-timeoutDiff)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine)))
|
||||
|
||||
// custom config
|
||||
conf := Config{
|
||||
Title: "New " + defaultTitle,
|
||||
Refresh: defaultRefresh + time.Second,
|
||||
ChartJsURL: "https://cdnjs.com/libraries/Chart.js",
|
||||
FontURL: "/public/my-font.css",
|
||||
CustomHead: `<style>body{background:#fff}</style>`,
|
||||
}
|
||||
app.Get("/custom", New(conf))
|
||||
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/custom", nil))
|
||||
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8,
|
||||
resp.Header.Get(fiber.HeaderContentType))
|
||||
buf, err = io.ReadAll(resp.Body)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte("<title>"+conf.Title+"</title>")))
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte("https://cdnjs.com/libraries/Chart.js")))
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte("/public/my-font.css")))
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte(conf.CustomHead)))
|
||||
|
||||
timeoutLine = fmt.Sprintf("setTimeout(fetchJSON, %d)",
|
||||
conf.Refresh.Milliseconds()-timeoutDiff)
|
||||
utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine)))
|
||||
}
|
||||
|
||||
// go test -run Test_Monitor_JSON -race
|
||||
func Test_Monitor_JSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Get("/", New())
|
||||
|
||||
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
|
||||
req.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON)
|
||||
resp, err := app.Test(req)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
utils.AssertEqual(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType))
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, bytes.Contains(b, []byte("pid")))
|
||||
utils.AssertEqual(t, true, bytes.Contains(b, []byte("os")))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Monitor -benchmem -count=4
|
||||
func Benchmark_Monitor(b *testing.B) {
|
||||
app := fiber.New()
|
||||
|
||||
app.Get("/", New())
|
||||
|
||||
h := app.Handler()
|
||||
|
||||
fctx := &fasthttp.RequestCtx{}
|
||||
fctx.Request.Header.SetMethod(fiber.MethodGet)
|
||||
fctx.Request.SetRequestURI("/")
|
||||
fctx.Request.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
h(fctx)
|
||||
}
|
||||
})
|
||||
|
||||
utils.AssertEqual(b, 200, fctx.Response.Header.StatusCode())
|
||||
utils.AssertEqual(b,
|
||||
fiber.MIMEApplicationJSON,
|
||||
string(fctx.Response.Header.Peek(fiber.HeaderContentType)))
|
||||
}
|
||||
|
||||
// go test -run Test_Monitor_Next
|
||||
func Test_Monitor_Next(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Use("/", New(Config{
|
||||
Next: func(_ *fiber.Ctx) bool {
|
||||
return true
|
||||
},
|
||||
}))
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/", nil))
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 404, resp.StatusCode)
|
||||
}
|
||||
|
||||
// go test -run Test_Monitor_APIOnly -race
|
||||
func Test_Monitor_APIOnly(t *testing.T) {
|
||||
app := fiber.New()
|
||||
|
||||
app.Get("/", New(Config{
|
||||
APIOnly: true,
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
|
||||
req.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON)
|
||||
resp, err := app.Test(req)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, 200, resp.StatusCode)
|
||||
utils.AssertEqual(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType))
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, bytes.Contains(b, []byte("pid")))
|
||||
utils.AssertEqual(t, true, bytes.Contains(b, []byte("os")))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue