✨ CLI Loading server stats and traces
This commit is contained in:
parent
632d37caf5
commit
f66f144f2e
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
14
cli/index.ts
14
cli/index.ts
@ -5,12 +5,18 @@ import chalk from "chalk"
|
|||||||
import { LoginCommand } from "./src/cmd/login.ts"
|
import { LoginCommand } from "./src/cmd/login.ts"
|
||||||
import { LogoutCommand } from "./src/cmd/logout.ts"
|
import { LogoutCommand } from "./src/cmd/logout.ts"
|
||||||
import { ListServerCommand } from "./src/cmd/list.ts"
|
import { ListServerCommand } from "./src/cmd/list.ts"
|
||||||
|
import { StatusCommand } from "./src/cmd/status.ts"
|
||||||
|
import { InfoCommand } from "./src/cmd/info.ts"
|
||||||
|
|
||||||
const [node, app, ...args] = process.argv
|
const [node, app, ...args] = process.argv
|
||||||
|
|
||||||
console.log(
|
const ENABLE_STARTUP_ASCII_ART = false
|
||||||
chalk.yellow(figlet.textSync("RoadSign CLI", { horizontalLayout: "default", verticalLayout: "default" }))
|
|
||||||
)
|
if (process.env["ENABLE_STARTUP_ASCII_ART"] || ENABLE_STARTUP_ASCII_ART) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(figlet.textSync("RoadSign CLI", { horizontalLayout: "default", verticalLayout: "default" }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const cli = new Cli({
|
const cli = new Cli({
|
||||||
binaryLabel: `RoadSign CLI`,
|
binaryLabel: `RoadSign CLI`,
|
||||||
@ -21,4 +27,6 @@ const cli = new Cli({
|
|||||||
cli.register(LoginCommand)
|
cli.register(LoginCommand)
|
||||||
cli.register(LogoutCommand)
|
cli.register(LogoutCommand)
|
||||||
cli.register(ListServerCommand)
|
cli.register(ListServerCommand)
|
||||||
|
cli.register(StatusCommand)
|
||||||
|
cli.register(InfoCommand)
|
||||||
cli.runExit(args)
|
cli.runExit(args)
|
128
cli/src/cmd/info.ts
Normal file
128
cli/src/cmd/info.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { Command, Option, type Usage } from "clipanion"
|
||||||
|
import { RsConfig } from "../utils/config.ts"
|
||||||
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import ora from "ora"
|
||||||
|
|
||||||
|
export class InfoCommand extends Command {
|
||||||
|
static paths = [[`info`], [`if`]]
|
||||||
|
static usage: Usage = {
|
||||||
|
category: `Networking`,
|
||||||
|
description: `Fetching the stats of RoadSign Server`,
|
||||||
|
details: `Fetching the configured things amount and other things of a connected server`,
|
||||||
|
examples: [["Fetch stats from labeled server", `info <label> [area]`]]
|
||||||
|
}
|
||||||
|
|
||||||
|
label = Option.String({ required: true })
|
||||||
|
area = Option.String({ required: false })
|
||||||
|
loop = Option.Boolean("--loop,--follow,-f", false, { description: "Keep updating the results" })
|
||||||
|
|
||||||
|
private static formatUptime(ms: number): string {
|
||||||
|
let seconds: number = Math.floor(ms / 1000)
|
||||||
|
let minutes: number = Math.floor(seconds / 60)
|
||||||
|
let hours: number = Math.floor(minutes / 60)
|
||||||
|
let days: number = Math.floor(hours / 24)
|
||||||
|
|
||||||
|
seconds = seconds % 60
|
||||||
|
minutes = minutes % 60
|
||||||
|
hours = hours % 24
|
||||||
|
|
||||||
|
const uptimeParts: string[] = []
|
||||||
|
|
||||||
|
if (days > 0) uptimeParts.push(`${days} day${days > 1 ? "s" : ""}`)
|
||||||
|
if (hours > 0) uptimeParts.push(`${hours} hour${hours > 1 ? "s" : ""}`)
|
||||||
|
if (minutes > 0) uptimeParts.push(`${minutes} minute${minutes > 1 ? "s" : ""}`)
|
||||||
|
if (seconds > 0 || uptimeParts.length === 0) uptimeParts.push(`${seconds} second${seconds > 1 ? "s" : ""}`)
|
||||||
|
|
||||||
|
return uptimeParts.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
const config = await RsConfig.getInstance()
|
||||||
|
|
||||||
|
const server = config.config.servers.find(item => item.label === this.label)
|
||||||
|
if (server == null) {
|
||||||
|
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.label)} was not found.\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.area == null) {
|
||||||
|
this.area = "overview"
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora(`Fetching stats from server ${this.label}...`).start()
|
||||||
|
const prefStart = performance.now()
|
||||||
|
|
||||||
|
switch (this.area) {
|
||||||
|
case "overview":
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/stats`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
this.context.stdout.write(`\nServer stats of ${chalk.bold(this.label)}\n`)
|
||||||
|
this.context.stdout.write(`Uptime: ${chalk.bold(InfoCommand.formatUptime(data["uptime"]))}\n`)
|
||||||
|
this.context.stdout.write(`Traffic since last startup: ${chalk.bold(data["traffic"]["total"])}\n`)
|
||||||
|
this.context.stdout.write(`Unique clients since last startup: ${chalk.bold(data["traffic"]["unique_client"])}\n`)
|
||||||
|
this.context.stdout.write(`\nServer info of ${chalk.bold(this.label)}\n`)
|
||||||
|
this.context.stdout.write(`Warden Applications: ${chalk.bold(data["applications"])}\n`)
|
||||||
|
this.context.stdout.write(`Destinations: ${chalk.bold(data["destinations"])}\n`)
|
||||||
|
this.context.stdout.write(`Locations: ${chalk.bold(data["locations"])}\n`)
|
||||||
|
this.context.stdout.write(`Regions: ${chalk.bold(data["regions"])}\n`)
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "trace":
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/traces`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
if (!this.loop) {
|
||||||
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
for (const trace of data) {
|
||||||
|
const ts = new Date(trace["timestamp"]).toLocaleString()
|
||||||
|
const path = [trace["region"], trace["location"], trace["destination"]].join(" ➜ ")
|
||||||
|
const uri = trace["uri"].split("?").length == 1 ? trace["uri"] : trace["uri"].split("?")[0] + ` ${chalk.grey(`w/ query parameters`)}`
|
||||||
|
this.context.stdout.write(`${chalk.bgGrey(`[${ts}]`)} ${chalk.bold(path)} ${chalk.cyan(trace["ip_address"])} ${uri}\n`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.loop) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
spinner.text = 'Updating...'
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
this.context.stdout.write('\x1Bc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
spinner.fail(chalk.red(`Info area was not exists ${chalk.bold(this.area)}...`))
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@ export class LoginCommand extends Command {
|
|||||||
try {
|
try {
|
||||||
const pingRes = await fetch(`${this.host}/cgi/metadata`, {
|
const pingRes = await fetch(`${this.host}/cgi/metadata`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: createAuthHeader("RoadSign CLI", this.credentials)
|
Authorization: createAuthHeader(this.credentials)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (pingRes.status !== 200) {
|
if (pingRes.status !== 200) {
|
||||||
|
46
cli/src/cmd/status.ts
Normal file
46
cli/src/cmd/status.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Command, Option, type Usage } from "clipanion"
|
||||||
|
import { RsConfig } from "../utils/config.ts"
|
||||||
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import ora from "ora"
|
||||||
|
|
||||||
|
export class StatusCommand extends Command {
|
||||||
|
static paths = [[`status`]]
|
||||||
|
static usage: Usage = {
|
||||||
|
category: `Networking`,
|
||||||
|
description: `Check the status of RoadSign Sideload Service`,
|
||||||
|
details: `Check the running status of a connected server`,
|
||||||
|
examples: [["Check the status of labeled server", `status <label>`]]
|
||||||
|
}
|
||||||
|
|
||||||
|
label = Option.String({ required: true })
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
const config = await RsConfig.getInstance()
|
||||||
|
|
||||||
|
const server = config.config.servers.find(item => item.label === this.label)
|
||||||
|
if (server == null) {
|
||||||
|
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.label)} was not found.\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora(`Checking status of ${this.label}...`).start()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/metadata`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
spinner.succeed(`Server with label ${chalk.bold(this.label)} is up and running! 🎉`)
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export function createAuthHeader(username: string, password: string) {
|
export function createAuthHeader(password: string, username: string = "RoadSign CLI") {
|
||||||
const credentials = Buffer.from(`${username}:${password}`).toString("base64")
|
const credentials = Buffer.from(`${username}:${password}`).toString("base64")
|
||||||
return `Basic ${credentials}`
|
return `Basic ${credentials}`
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/pelletier/go-toml/v2"
|
||||||
)
|
)
|
||||||
@ -19,6 +20,7 @@ func ReadInConfig(root string) error {
|
|||||||
Traffic: make(map[string]int64),
|
Traffic: make(map[string]int64),
|
||||||
TrafficFrom: make(map[string]int64),
|
TrafficFrom: make(map[string]int64),
|
||||||
TotalTraffic: 0,
|
TotalTraffic: 0,
|
||||||
|
StartupAt: time.Now(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package navi
|
package navi
|
||||||
|
|
||||||
import "github.com/spf13/viper"
|
import (
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type RoadMetrics struct {
|
type RoadMetrics struct {
|
||||||
Traces []RoadTrace `json:"-"`
|
Traces []RoadTrace `json:"-"`
|
||||||
@ -8,9 +11,11 @@ type RoadMetrics struct {
|
|||||||
Traffic map[string]int64 `json:"traffic"`
|
Traffic map[string]int64 `json:"traffic"`
|
||||||
TrafficFrom map[string]int64 `json:"traffic_from"`
|
TrafficFrom map[string]int64 `json:"traffic_from"`
|
||||||
TotalTraffic int64 `json:"total_traffic"`
|
TotalTraffic int64 `json:"total_traffic"`
|
||||||
|
StartupAt time.Time `json:"startup_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoadTrace struct {
|
type RoadTrace struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
Destination string `json:"destination"`
|
Destination string `json:"destination"`
|
||||||
@ -27,6 +32,7 @@ type RoadTraceError struct {
|
|||||||
|
|
||||||
func (v *RoadMetrics) AddTrace(trace RoadTrace) {
|
func (v *RoadMetrics) AddTrace(trace RoadTrace) {
|
||||||
v.TotalTraffic++
|
v.TotalTraffic++
|
||||||
|
trace.Timestamp = time.Now()
|
||||||
if _, ok := v.Traffic[trace.Region]; !ok {
|
if _, ok := v.Traffic[trace.Region]; !ok {
|
||||||
v.Traffic[trace.Region] = 0
|
v.Traffic[trace.Region] = 0
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
"git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getStats(c *fiber.Ctx) error {
|
func getStats(c *fiber.Ctx) error {
|
||||||
@ -23,5 +24,10 @@ func getStats(c *fiber.Ctx) error {
|
|||||||
"locations": len(locations),
|
"locations": len(locations),
|
||||||
"destinations": len(destinations),
|
"destinations": len(destinations),
|
||||||
"applications": len(applications),
|
"applications": len(applications),
|
||||||
|
"uptime": time.Since(navi.R.Metrics.StartupAt).Milliseconds(),
|
||||||
|
"traffic": fiber.Map{
|
||||||
|
"total": navi.R.Metrics.TotalTraffic,
|
||||||
|
"unique_client": len(navi.R.Metrics.TrafficFrom),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user