✨ CLI Loading server stats and traces
This commit is contained in:
		
							
								
								
									
										6
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										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> | ||||||
							
								
								
									
										12
									
								
								cli/index.ts
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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 | ||||||
|  |  | ||||||
|  | if (process.env["ENABLE_STARTUP_ASCII_ART"] || ENABLE_STARTUP_ASCII_ART) { | ||||||
|  |   console.log( | ||||||
|     chalk.yellow(figlet.textSync("RoadSign CLI", { horizontalLayout: "default", verticalLayout: "default" })) |     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), | ||||||
|  | 		}, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user