Added some monitoring widgets and scripts

main
Antonio De Lucreziis 2 years ago
parent d4b9dee6d9
commit 8e86943b83

@ -1,9 +1,13 @@
import { MessageWidget } from './MessageWidget.jsx' import { DiskUsage } from './widgets/DiskUsage.jsx'
import { PieChartWidget } from './PieChartWidget.jsx' import { MessageWidget } from './widgets/MessageWidget.jsx'
import { PieChartWidget } from './widgets/PieChartWidget.jsx'
import { ScriptStatus } from './widgets/ScriptStatus.jsx'
const WidgetTypes = { const WidgetTypes = {
pie: PieChartWidget, pie: PieChartWidget,
message: MessageWidget, message: MessageWidget,
'script-status': ScriptStatus,
'disk-usage': DiskUsage,
} }
export const Widget = ({ type, value }) => { export const Widget = ({ type, value }) => {

@ -1,13 +1,9 @@
import { useEffect, useRef } from 'preact/hooks' import { useEffect, useRef } from 'preact/hooks'
import { hashCode } from '../util.jsx' import { hashCode } from '../../util.jsx'
const CANVAS_SIZE = 350
export const PieChartWidget = ({ title, parts, labels, total }) => {
title = title || 'Grafico a torta'
export const PieChart = ({ parts, labels, total }) => {
parts = parts || [1] parts = parts || [1]
labels = labels || [] labels = labels || parts
const canvasRef = useRef() const canvasRef = useRef()
@ -33,7 +29,7 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
g.scale(2, 2) g.scale(2, 2)
g.translate(width / 2, height / 2) g.translate(width / 2, height / 2)
g.font = `18px 'Open Sans'` g.font = `16px 'Open Sans'`
g.textAlign = 'center' g.textAlign = 'center'
g.textBaseline = 'middle' g.textBaseline = 'middle'
@ -62,13 +58,14 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
g.beginPath() g.beginPath()
g.moveTo(0, 0) g.moveTo(0, 0)
g.arc(0, 0, width * 0.5 * 0.8, acc - 0.5 * Math.PI, acc + angle - 0.5 * Math.PI) g.arc(0, 0, width * 0.5 * 0.8, acc - 0.5 * Math.PI, acc + angle - 0.5 * Math.PI)
g.lineTo(0, 0)
g.stroke() g.stroke()
g.fillStyle = '#333' g.fillStyle = '#333'
g.fillText( g.fillText(
label, label,
Math.cos(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9, Math.cos(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9125,
Math.sin(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9 Math.sin(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9125
) )
acc += angle acc += angle
@ -76,12 +73,5 @@ export const PieChartWidget = ({ title, parts, labels, total }) => {
} }
}, [canvasRef, parts]) }, [canvasRef, parts])
return ( return <canvas ref={canvasRef}></canvas>
<>
<div class="title">{title}</div>
<div class="content">
<canvas ref={canvasRef}></canvas>
</div>
</>
)
} }

@ -0,0 +1,52 @@
import { byteSizeToString, useRemoteState } from '../../util.jsx'
import { PieChart } from '../charts/PieChart.jsx'
import { Icon } from '../Icon.jsx'
import { ToastMessage, useToasts } from '../Toasts.jsx'
export const DiskUsage = ({ disk }) => {
const [showToast] = useToasts()
const [output, err, refreshOutput] = useRemoteState(
`/api/monitor/status?script=${encodeURIComponent(`disk-usage ${disk}`)}`,
'100\n0'
)
const [total, used] = output.split('\n').map(s => parseInt(s))
const title = (
<>
Spazio Utilizzato &bull; <code>{disk}</code>
</>
)
return (
<>
<div class="title">
<div class="row">
<div class="row-group">
<p>{title}</p>
</div>
<div class="row-group">
<button
class="icon"
onClick={() => {
refreshOutput()
showToast(
<ToastMessage icon="info">Aggiornato "{title}"</ToastMessage>
)
}}
>
<Icon name="refresh" />
</button>
</div>
</div>
</div>
<div class="content">
{err ? (
<p>Errore "{err}"</p>
) : (
<PieChart parts={[used]} labels={[byteSizeToString(used)]} total={total} />
)}
</div>
</>
)
}

@ -0,0 +1,14 @@
import { PieChart } from '../charts/PieChart.jsx'
export const PieChartWidget = ({ title, ...chart }) => {
title = title || 'Grafico a torta'
return (
<>
<div class="title">{title}</div>
<div class="content">
<PieChart {...chart} />
</div>
</>
)
}

@ -0,0 +1,43 @@
import { useRemoteState } from '../../util.jsx'
import { Icon } from '../Icon.jsx'
import { ToastMessage, useToasts } from '../Toasts.jsx'
export const ScriptStatus = ({ title, script }) => {
const [showToast] = useToasts()
const [output, err, refreshOutput] = useRemoteState(
`/api/monitor/status?script=${encodeURIComponent(script)}`,
''
)
return (
<>
<div class="title">
<div class="row">
<div class="row-group">{title}</div>
<div class="row-group">
<button
class="icon"
onClick={() => {
refreshOutput()
showToast(
<ToastMessage icon="info">Aggiornato "{title}"</ToastMessage>
)
}}
>
<Icon name="refresh" />
</button>
</div>
</div>
</div>
<div class="content">
{err ? (
<p>Errore "{err}"</p>
) : (
<pre>
<code>{output}</code>
</pre>
)}
</div>
</>
)
}

@ -137,3 +137,24 @@ const toCaseMap = {
export function changeCase(from, to, s) { export function changeCase(from, to, s) {
return toCaseMap[to](fromCaseMap[from](s)) return toCaseMap[to](fromCaseMap[from](s))
} }
//
// Disk space conversions
//
const byteUnits = {
K: 1024,
M: 1024 ** 2,
G: 1024 ** 3,
T: 1024 ** 4,
}
export function byteSizeToString(size) {
let i = 0
while (size >= 450) {
size /= 1024
i++
}
return `${size.toFixed(2)}${Object.keys(byteUnits)[i]}B`
}

@ -528,6 +528,10 @@ p {
} }
} }
pre {
margin: 0;
}
code { code {
font-size: 95%; font-size: 95%;
} }
@ -587,7 +591,8 @@ $heading-scale: 1.33;
min-height: 10rem; min-height: 10rem;
&.pie { &.pie,
&.disk-usage {
grid-row: span 2; grid-row: span 2;
& > .content { & > .content {

@ -1,39 +0,0 @@
package jobs
import (
"os"
"path"
)
type Config struct {
ScriptsDir string `json:"scriptsDir"`
}
type Service struct {
Config Config
scriptPaths []string
}
func New(config Config) *Service {
return &Service{
Config: config,
}
}
func (s *Service) LoadScripts() error {
entries, err := os.ReadDir(s.Config.ScriptsDir)
if err != nil {
return err
}
s.scriptPaths = []string{}
for _, entry := range entries {
s.scriptPaths = append(s.scriptPaths,
path.Join(s.Config.ScriptsDir, entry.Name()),
)
}
return nil
}

@ -8,6 +8,7 @@ import (
"git.phc.dm.unipi.it/phc/storage/config" "git.phc.dm.unipi.it/phc/storage/config"
"git.phc.dm.unipi.it/phc/storage/database" "git.phc.dm.unipi.it/phc/storage/database"
"git.phc.dm.unipi.it/phc/storage/monitor"
"git.phc.dm.unipi.it/phc/storage/routes" "git.phc.dm.unipi.it/phc/storage/routes"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -16,18 +17,46 @@ import (
) )
func main() { func main() {
// Setup services
db := database.NewJSON("database.local.json")
monitorService := monitor.NewService(&monitor.Config{
ScriptsDir: "./scripts",
})
if err := monitorService.LoadScripts(); err != nil {
panic(err)
}
// The router wraps all application dependencies and provides "routing" methods
router := &routes.Router{ router := &routes.Router{
Database: database.NewJSON("database.local.json"), Database: db,
Monitor: monitorService,
} }
// We use go-fiber as the HTTP framework
app := fiber.New() app := fiber.New()
// Main middlewares // Middlewares
app.Use(logger.New()) app.Use(logger.New())
app.Use(recover.New()) app.Use(recover.New())
// Static files // Static files
app.Static("/", "/_frontend/dist") app.Static("/", "./_frontend/dist")
// Setting up dynamic "single page application" routes (all routes these to the same html file)
for _, route := range []string{
"/",
"/api-keys",
"/admin",
"/buckets",
"/buckets/:bucket",
} {
app.Get(route, func(c *fiber.Ctx) error {
return c.SendFile("./_frontend/dist/index.html")
})
}
app.Route("/api", router.Api) app.Route("/api", router.Api)

@ -0,0 +1,58 @@
package monitor
import (
"bytes"
"os"
"os/exec"
"path"
"strings"
)
type Config struct {
ScriptsDir string `json:"scriptsDir"`
}
type Service struct {
Config *Config
scriptPaths map[string]string
}
func NewService(config *Config) *Service {
return &Service{
Config: config,
}
}
func (s *Service) LoadScripts() error {
entries, err := os.ReadDir(s.Config.ScriptsDir)
if err != nil {
return err
}
s.scriptPaths = map[string]string{}
for _, entry := range entries {
newScriptPath := path.Join(s.Config.ScriptsDir, entry.Name())
s.scriptPaths[entry.Name()] = newScriptPath
}
return nil
}
// TODO: Add caching
func (s *Service) GetLastOutput(command string) (string, error) {
args := strings.Fields(command)
scriptPath := s.scriptPaths[args[0]]
cmd := exec.Command(scriptPath, args[1:]...)
var b bytes.Buffer
cmd.Stdout = &b
if err := cmd.Run(); err != nil {
return "", err
}
return b.String(), nil
}

@ -37,6 +37,13 @@ func (r *Router) Api(api fiber.Router) {
return c.Next() return c.Next()
} }
//
// Setup "/api/monitor" routes
//
monitorRoute := api.Group("/monitor")
monitorRoute.Use(isAdminMiddleware)
r.ApiMonitor(monitorRoute)
api.Post("/login", func(c *fiber.Ctx) error { api.Post("/login", func(c *fiber.Ctx) error {
var form struct { var form struct {
Password string `form:"password"` Password string `form:"password"`
@ -67,7 +74,8 @@ func (r *Router) Api(api fiber.Router) {
return c.JSON("ok") return c.JSON("ok")
}) })
api.Get("/current-user", func(c *fiber.Ctx) error { api.Get("/current-user",
func(c *fiber.Ctx) error {
if _, found := adminSessions[c.Cookies("sid")]; !found { if _, found := adminSessions[c.Cookies("sid")]; !found {
return c.JSON("anonymous") return c.JSON("anonymous")
} }
@ -75,7 +83,9 @@ func (r *Router) Api(api fiber.Router) {
return c.JSON("admin") return c.JSON("admin")
}) })
api.Get("/dashboard-state", func(c *fiber.Ctx) error { api.Get("/dashboard-state",
isAdminMiddleware,
func(c *fiber.Ctx) error {
state, err := r.Database.GetDashboardState() state, err := r.Database.GetDashboardState()
if err != nil { if err != nil {
return err return err

@ -0,0 +1,27 @@
package routes
import (
"fmt"
"log"
"github.com/gofiber/fiber/v2"
)
func (r *Router) ApiMonitor(api fiber.Router) {
// Respond to requests like
// - "/api/monitor/status?script=SCRIPT_NAME" where SCRIPT_NAME is the name of a file inside "./scripts"
api.Get("/status", func(c *fiber.Ctx) error {
if qScript := c.Query("script"); qScript != "" {
log.Printf("Script %q", qScript)
output, err := r.Monitor.GetLastOutput(qScript)
if err != nil {
return err
}
return c.JSON(output)
}
return fmt.Errorf("no script, device or entity provided")
})
}

@ -1,7 +1,11 @@
package routes package routes
import "git.phc.dm.unipi.it/phc/storage/database" import (
"git.phc.dm.unipi.it/phc/storage/database"
"git.phc.dm.unipi.it/phc/storage/monitor"
)
type Router struct { type Router struct {
Database database.Database Database database.Database
Monitor *monitor.Service
} }

@ -0,0 +1,5 @@
#!/bin/bash
DEVICE="$1"
df | grep "$DEVICE" | tr -s ' ' | cut -d' ' -f 2-3 | tr ' ' '\n'

@ -0,0 +1,7 @@
#!/bin/bash
echo 'active'
echo 'active'
echo 'active'
echo 'failing'
echo 'missing'
Loading…
Cancel
Save