diff --git a/.github/release-drafter-monitor.yml b/.github/release-drafter-monitor.yml
new file mode 100644
index 00000000..e6775071
--- /dev/null
+++ b/.github/release-drafter-monitor.yml
@@ -0,0 +1,50 @@
+name-template: 'Monitor - v$RESOLVED_VERSION'
+tag-template: 'monitor/v$RESOLVED_VERSION'
+tag-prefix: monitor/v
+include-paths:
+ - monitor
+categories:
+ - title: 'โ Breaking Changes'
+ labels:
+ - 'โ BreakingChange'
+ - title: '๐ New'
+ labels:
+ - 'โ๏ธ Feature'
+ - title: '๐งน Updates'
+ labels:
+ - '๐งน Updates'
+ - '๐ค Dependencies'
+ - title: '๐ Fixes'
+ labels:
+ - 'โข๏ธ Bug'
+ - title: '๐ Documentation'
+ labels:
+ - '๐ Documentation'
+change-template: '- $TITLE (#$NUMBER)'
+change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
+exclude-contributors:
+ - dependabot
+ - dependabot[bot]
+version-resolver:
+ major:
+ labels:
+ - 'major'
+ - 'โ BreakingChange'
+ minor:
+ labels:
+ - 'minor'
+ - 'โ๏ธ Feature'
+ patch:
+ labels:
+ - 'patch'
+ - '๐ Documentation'
+ - 'โข๏ธ Bug'
+ - '๐ค Dependencies'
+ - '๐งน Updates'
+ default: patch
+template: |
+ $CHANGES
+
+ **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...monitor/v$RESOLVED_VERSION
+
+ Thank you $CONTRIBUTORS for making this update possible.
diff --git a/.github/workflows/release-drafter-monitor.yml b/.github/workflows/release-drafter-monitor.yml
new file mode 100644
index 00000000..db8df581
--- /dev/null
+++ b/.github/workflows/release-drafter-monitor.yml
@@ -0,0 +1,19 @@
+name: Release Drafter Monitor
+on:
+ push:
+ # branches to consider in the event; optional, defaults to all
+ branches:
+ - master
+ - main
+ paths:
+ - 'monitor/**'
+jobs:
+ draft_release_casbin:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: release-drafter/release-drafter@v5
+ with:
+ config-name: release-drafter-monitor.yml
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/test-monitor.yml b/.github/workflows/test-monitor.yml
new file mode 100644
index 00000000..6a33d883
--- /dev/null
+++ b/.github/workflows/test-monitor.yml
@@ -0,0 +1,31 @@
+name: "Test Monitor"
+
+on:
+ push:
+ branches:
+ - master
+ - main
+ paths:
+ - 'monitor/**'
+ pull_request:
+ paths:
+ - 'monitor/**'
+
+jobs:
+ Tests:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ go-version:
+ - 1.22.x
+ - 1.23.x
+ steps:
+ - name: Fetch Repository
+ uses: actions/checkout@v4
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '${{ matrix.go-version }}'
+ - name: Run Test
+ working-directory: ./monitor
+ run: go test -v -race ./...
diff --git a/README.md b/README.md
index 96086331..264208e1 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ Repository for third party middlewares with dependencies.
* [JWT](./jwt/README.md)
* [Loadshed](./loadshed/README.md)
* [NewRelic](./fibernewrelic/README.md)
+* [Monitor](./monitor/README.md)
* [Open Policy Agent](./opafiber/README.md)
* [Otelfiber (OpenTelemetry)](./otelfiber/README.md)
* [Paseto](./paseto/README.md)
diff --git a/monitor/README.md b/monitor/README.md
new file mode 100644
index 00000000..0dd78fc7
--- /dev/null
+++ b/monitor/README.md
@@ -0,0 +1,84 @@
+---
+id: monitor
+---
+
+# Monitor
+
+
+
+
+
+
+
+Monitor middleware for [Fiber](https://github.com/gofiber/fiber) that reports server metrics, inspired by [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor)
+
+
+
+## Install
+
+This middleware supports Fiber v3.
+
+```
+go get -u github.com/gofiber/fiber/v3
+go get -u github.com/gofiber/contrib/monitor
+```
+
+### Signature
+
+```go
+monitor.New(config ...monitor.Config) fiber.Handler
+```
+
+### Config
+
+| Property | Type | Description | Default |
+| :--------- | :------------------------ | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- |
+| Title | `string` | Metrics page title. | `Fiber Monitor` |
+| Refresh | `time.Duration` | Refresh period. | `3 seconds` |
+| APIOnly | `bool` | Whether the service should expose only the montioring API. | `false` |
+| Next | `func(c *fiber.Ctx) bool` | Define a function to add custom fields. | `nil` |
+| CustomHead | `string` | Custom HTML code to Head Section(Before End). | `empty` |
+| FontURL | `string` | FontURL for specilt font resource path or URL. also you can use relative path. | `https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap` |
+| ChartJsURL | `string` | ChartJsURL for specilt chartjs library, path or URL, also you can use relative path. | `https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js` |
+
+### Example
+
+```go
+package main
+
+import (
+ "log"
+
+ "github.com/gofiber/fiber/v3"
+ "github.com/gofiber/contrib/monitor"
+)
+
+func main() {
+ app := fiber.New()
+
+ // Initialize default config (Assign the middleware to /metrics)
+ app.Get("/metrics", monitor.New())
+
+ // Or extend your config for customization
+ // Assign the middleware to /metrics
+ // and change the Title to `MyService Metrics Page`
+ app.Get("/metrics", monitor.New(monitor.Config{Title: "MyService Metrics Page"}))
+
+ log.Fatal(app.Listen(":3000"))
+}
+```
+
+
+## Default Config
+
+```go
+var ConfigDefault = Config{
+ Title: defaultTitle,
+ Refresh: defaultRefresh,
+ FontURL: defaultFontURL,
+ ChartJsURL: defaultChartJSURL,
+ CustomHead: defaultCustomHead,
+ APIOnly: false,
+ Next: nil,
+}
+```
diff --git a/monitor/config.go b/monitor/config.go
new file mode 100644
index 00000000..048563a4
--- /dev/null
+++ b/monitor/config.go
@@ -0,0 +1,132 @@
+package monitor
+
+import (
+ "time"
+
+ "github.com/gofiber/fiber/v3"
+)
+
+// 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 to specify font resource path or URL. You can also use a relative path.
+ //
+ // Optional. Default: https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap
+ FontURL string
+
+ // ChartJSURL to specify ChartJS library path or URL. You can also use a relative path.
+ //
+ // Optional. Default: https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js
+ ChartJSURL string
+
+ 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
+}
diff --git a/monitor/config_test.go b/monitor/config_test.go
new file mode 100644
index 00000000..9a4101c9
--- /dev/null
+++ b/monitor/config_test.go
@@ -0,0 +1,163 @@
+package monitor
+
+import (
+ "testing"
+ "time"
+
+ "github.com/gofiber/fiber/v3"
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_Config_Default(t *testing.T) {
+ t.Parallel()
+
+ t.Run("use default", func(t *testing.T) {
+ t.Parallel()
+ cfg := configDefault()
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, defaultRefresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, title, cfg.Title)
+ assert.Equal(t, defaultRefresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, minRefresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, refresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, defaultRefresh, cfg.Refresh)
+ assert.Equal(t, fontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, defaultRefresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, chartURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, defaultRefresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, head, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, defaultRefresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, true, cfg.APIOnly)
+ assert.IsType(t, (func(*fiber.Ctx) bool)(nil), cfg.Next)
+ assert.Equal(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,
+ })
+
+ assert.Equal(t, defaultTitle, cfg.Title)
+ assert.Equal(t, defaultRefresh, cfg.Refresh)
+ assert.Equal(t, defaultFontURL, cfg.FontURL)
+ assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)
+ assert.Equal(t, defaultCustomHead, cfg.CustomHead)
+ assert.Equal(t, false, cfg.APIOnly)
+ assert.Equal(t, f(nil), cfg.Next(nil))
+ assert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)
+ })
+}
\ No newline at end of file
diff --git a/monitor/go.mod b/monitor/go.mod
new file mode 100644
index 00000000..8a15c043
--- /dev/null
+++ b/monitor/go.mod
@@ -0,0 +1,32 @@
+module github.com/gofiber/contrib/monitor
+
+go 1.22
+
+require (
+ github.com/gofiber/fiber/v3 v3.0.0-beta.3
+ github.com/shirou/gopsutil/v4 v4.24.8
+ github.com/stretchr/testify v1.9.0
+ github.com/valyala/fasthttp v1.55.0
+)
+
+require (
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/shoenig/go-m1cpu v0.1.6 // indirect
+ github.com/tklauser/go-sysconf v0.3.14 // indirect
+ github.com/tklauser/numcpus v0.8.0 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/tcplisten v1.0.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/monitor/go.sum b/monitor/go.sum
new file mode 100644
index 00000000..ca0474e0
--- /dev/null
+++ b/monitor/go.sum
@@ -0,0 +1,59 @@
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg=
+github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY=
+github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
+github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
+github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
+github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
+github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
+github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
+github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
+github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/monitor/index.go b/monitor/index.go
new file mode 100644
index 00000000..c873290c
--- /dev/null
+++ b/monitor/index.go
@@ -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 = `
+
+