✨ 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> | ||||
| @@ -5,12 +5,18 @@ import chalk from "chalk" | ||||
| import { LoginCommand } from "./src/cmd/login.ts" | ||||
| import { LogoutCommand } from "./src/cmd/logout.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 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" })) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const cli = new Cli({ | ||||
|   binaryLabel: `RoadSign CLI`, | ||||
| @@ -21,4 +27,6 @@ const cli = new Cli({ | ||||
| cli.register(LoginCommand) | ||||
| cli.register(LogoutCommand) | ||||
| cli.register(ListServerCommand) | ||||
| cli.register(StatusCommand) | ||||
| cli.register(InfoCommand) | ||||
| 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 { | ||||
|       const pingRes = await fetch(`${this.host}/cgi/metadata`, { | ||||
|         headers: { | ||||
|           Authorization: createAuthHeader("RoadSign CLI", this.credentials) | ||||
|           Authorization: createAuthHeader(this.credentials) | ||||
|         } | ||||
|       }) | ||||
|       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") | ||||
|   return `Basic ${credentials}` | ||||
| } | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/pelletier/go-toml/v2" | ||||
| ) | ||||
| @@ -19,6 +20,7 @@ func ReadInConfig(root string) error { | ||||
| 			Traffic:      make(map[string]int64), | ||||
| 			TrafficFrom:  make(map[string]int64), | ||||
| 			TotalTraffic: 0, | ||||
| 			StartupAt:    time.Now(), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| package navi | ||||
|  | ||||
| import "github.com/spf13/viper" | ||||
| import ( | ||||
| 	"github.com/spf13/viper" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type RoadMetrics struct { | ||||
| 	Traces []RoadTrace `json:"-"` | ||||
| @@ -8,9 +11,11 @@ type RoadMetrics struct { | ||||
| 	Traffic      map[string]int64 `json:"traffic"` | ||||
| 	TrafficFrom  map[string]int64 `json:"traffic_from"` | ||||
| 	TotalTraffic int64            `json:"total_traffic"` | ||||
| 	StartupAt    time.Time        `json:"startup_at"` | ||||
| } | ||||
|  | ||||
| type RoadTrace struct { | ||||
| 	Timestamp   time.Time      `json:"timestamp"` | ||||
| 	Region      string         `json:"region"` | ||||
| 	Location    string         `json:"location"` | ||||
| 	Destination string         `json:"destination"` | ||||
| @@ -27,6 +32,7 @@ type RoadTraceError struct { | ||||
|  | ||||
| func (v *RoadMetrics) AddTrace(trace RoadTrace) { | ||||
| 	v.TotalTraffic++ | ||||
| 	trace.Timestamp = time.Now() | ||||
| 	if _, ok := v.Traffic[trace.Region]; !ok { | ||||
| 		v.Traffic[trace.Region] = 0 | ||||
| 	} else { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"git.solsynth.dev/goatworks/roadsign/pkg/warden" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/samber/lo" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func getStats(c *fiber.Ctx) error { | ||||
| @@ -23,5 +24,10 @@ func getStats(c *fiber.Ctx) error { | ||||
| 		"locations":    len(locations), | ||||
| 		"destinations": len(destinations), | ||||
| 		"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