chrony-network-stats.sh 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. #!/bin/bash
  2. set -e
  3. #### Configuration ####
  4. INTERFACE="eth0"
  5. PAGE_TITLE="Network Traffic and Chrony Statistics for ${INTERFACE}"
  6. OUTPUT_DIR="/var/www/chrony-network-stats"
  7. HTML_FILENAME="index.html"
  8. ENABLE_LOGGING="yes"
  9. LOG_FILE="/var/log/chrony-network-stats.log"
  10. RRD_DIR="/var/lib/chrony-rrd"
  11. RRD_FILE="$RRD_DIR/chrony.rrd"
  12. WIDTH=800
  13. HEIGHT=300
  14. TIMEOUT_SECONDS=5
  15. #######################
  16. log_message() {
  17. local level="$1"
  18. local message="$2"
  19. if [[ "$ENABLE_LOGGING" == "yes" ]]; then
  20. echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" >> "$LOG_FILE"
  21. fi
  22. echo "[$level] $message"
  23. }
  24. validate_numeric() {
  25. local value="$1"
  26. local name="$2"
  27. if ! [[ "$value" =~ ^[0-9]+$ ]]; then
  28. log_message "ERROR" "Invalid $name: $value. Must be numeric."
  29. exit 1
  30. fi
  31. }
  32. check_commands() {
  33. local commands=("vnstati" "rrdtool" "chronyc" "sudo" "timeout")
  34. for cmd in "${commands[@]}"; do
  35. if ! command -v "$cmd" &>/dev/null; then
  36. log_message "ERROR" "Command '$cmd' not found in PATH."
  37. exit 1
  38. fi
  39. done
  40. }
  41. setup_directories() {
  42. log_message "INFO" "Checking and preparing directories..."
  43. for dir in "$OUTPUT_DIR" "$RRD_DIR"; do
  44. mkdir -p "$dir" || {
  45. log_message "ERROR" "Failed to create directory: $dir"
  46. exit 1
  47. }
  48. if [ ! -w "$dir" ]; then
  49. log_message "ERROR" "Directory '$dir' is not writable."
  50. exit 1
  51. fi
  52. done
  53. }
  54. generate_vnstat_images() {
  55. log_message "INFO" "Generating vnStat images for interface '$INTERFACE'..."
  56. local modes=("s" "d" "t" "h" "m" "y")
  57. for mode in "${modes[@]}"; do
  58. vnstati -"$mode" -i "$INTERFACE" -o "$OUTPUT_DIR/vnstat_${mode}.png" || {
  59. log_message "ERROR" "Failed to generate vnstat image for mode $mode"
  60. exit 1
  61. }
  62. done
  63. }
  64. collect_chrony_data() {
  65. log_message "INFO" "Collecting Chrony data..."
  66. get_html() {
  67. timeout "$TIMEOUT_SECONDS"s sudo chronyc "$1" -v 2>&1 | sed 's/&/\&/g;s/</\</g;s/>/\>/g;s/$/<br>/' || {
  68. log_message "ERROR" "Failed to collect chronyc $1 data"
  69. return 1
  70. }
  71. }
  72. RAW_TRACKING=$(timeout "$TIMEOUT_SECONDS"s sudo chronyc tracking) || {
  73. log_message "ERROR" "Failed to collect chronyc tracking data"
  74. exit 1
  75. }
  76. CHRONYC_TRACKING_HTML=$(echo "$RAW_TRACKING" | sed 's/&/\&/g;s/</\</g;s/>/\>/g;s/$/<br>/')
  77. CHRONYC_SOURCES=$(get_html sources) || exit 1
  78. CHRONYC_SOURCESTATS=$(get_html sourcestats) || exit 1
  79. CHRONYC_SELECTDATA=$(get_html selectdata) || exit 1
  80. }
  81. extract_chronyc_values() {
  82. extract_val() {
  83. echo "$RAW_TRACKING" | awk "/$1/ {print \$($2)}" | grep -E '^[-+]?[0-9.]+$' || echo "U"
  84. }
  85. OFFSET=$(extract_val "Last offset" "NF-1")
  86. FREQ=$(extract_val "Frequency" "NF-2")
  87. RESID_FREQ=$(extract_val "Residual freq" "NF-1")
  88. SKEW=$(extract_val "Skew" "NF-1")
  89. DELAY=$(extract_val "Root delay" "NF-1")
  90. DISPERSION=$(extract_val "Root dispersion" "NF-1")
  91. STRATUM=$(extract_val "Stratum" "3")
  92. RAW_STATS=$(LC_ALL=C sudo chronyc serverstats) || {
  93. log_message "ERROR" "Failed to collect chronyc serverstats"
  94. exit 1
  95. }
  96. get_stat() {
  97. echo "$RAW_STATS" | awk -F'[[:space:]]*:[[:space:]]*' "/$1/ {print \$2}" | grep -E '^[0-9]+$' || echo "U"
  98. }
  99. PKTS_RECV=$(get_stat "NTP packets received")
  100. PKTS_DROP=$(get_stat "NTP packets dropped")
  101. CMD_RECV=$(get_stat "Command packets received")
  102. CMD_DROP=$(get_stat "Command packets dropped")
  103. LOG_DROP=$(get_stat "Client log records dropped")
  104. NTS_KE_ACC=$(get_stat "NTS-KE connections accepted")
  105. NTS_KE_DROP=$(get_stat "NTS-KE connections dropped")
  106. AUTH_PKTS=$(get_stat "Authenticated NTP packets")
  107. INTERLEAVED=$(get_stat "Interleaved NTP packets")
  108. TS_HELD=$(get_stat "NTP timestamps held")
  109. }
  110. create_rrd_database() {
  111. if [ ! -f "$RRD_FILE" ]; then
  112. log_message "INFO" "Creating new RRD file: $RRD_FILE"
  113. LC_ALL=C rrdtool create "$RRD_FILE" --step 300 \
  114. DS:offset:GAUGE:600:U:U DS:frequency:GAUGE:600:U:U DS:resid_freq:GAUGE:600:U:U DS:skew:GAUGE:600:U:U \
  115. DS:delay:GAUGE:600:U:U DS:dispersion:GAUGE:600:U:U DS:stratum:GAUGE:600:0:16 \
  116. DS:pkts_recv:COUNTER:600:0:U DS:pkts_drop:COUNTER:600:0:U DS:cmd_recv:COUNTER:600:0:U \
  117. DS:cmd_drop:COUNTER:600:0:U DS:log_drop:COUNTER:600:0:U DS:nts_ke_acc:COUNTER:600:0:U \
  118. DS:nts_ke_drop:COUNTER:600:0:U DS:auth_pkts:COUNTER:600:0:U DS:interleaved:COUNTER:600:0:U \
  119. DS:ts_held:GAUGE:600:0:U \
  120. RRA:AVERAGE:0.5:1:576 RRA:AVERAGE:0.5:6:672 RRA:AVERAGE:0.5:24:732 RRA:AVERAGE:0.5:288:730 \
  121. RRA:MAX:0.5:1:576 RRA:MAX:0.5:6:672 RRA:MAX:0.5:24:732 RRA:MAX:0.5:288:730 \
  122. RRA:MIN:0.5:1:576 RRA:MIN:0.5:6:672 RRA:MIN:0.5:24:732 RRA:MIN:0.5:288:730 || {
  123. log_message "ERROR" "Failed to create RRD database"
  124. exit 1
  125. }
  126. fi
  127. }
  128. update_rrd_database() {
  129. log_message "INFO" "Updating RRD database..."
  130. UPDATE_STRING="N:$OFFSET:$FREQ:$RESID_FREQ:$SKEW:$DELAY:$DISPERSION:$STRATUM:$PKTS_RECV:$PKTS_DROP:$CMD_RECV:$CMD_DROP:$LOG_DROP:$NTS_KE_ACC:$NTS_KE_DROP:$AUTH_PKTS:$INTERLEAVED:$TS_HELD"
  131. LC_ALL=C rrdtool update "$RRD_FILE" "$UPDATE_STRING" || {
  132. log_message "ERROR" "Failed to update RRD database"
  133. exit 1
  134. }
  135. }
  136. generate_graphs() {
  137. log_message "INFO" "Generating graphs..."
  138. local END_TIME=$(date +%s)
  139. local START_TIME=$((END_TIME - 86400))
  140. declare -A graphs=(
  141. ["chrony_serverstats"]="--title 'Chrony Server Statistics - by day' --vertical-label 'Packets/second' \
  142. --lower-limit 0 --units-exponent 0 \
  143. DEF:pkts_recv='$RRD_FILE':pkts_recv:AVERAGE \
  144. DEF:pkts_drop='$RRD_FILE':pkts_drop:AVERAGE \
  145. DEF:cmd_recv='$RRD_FILE':cmd_recv:AVERAGE \
  146. DEF:cmd_drop='$RRD_FILE':cmd_drop:AVERAGE \
  147. DEF:log_drop='$RRD_FILE':log_drop:AVERAGE \
  148. DEF:nts_ke_acc='$RRD_FILE':nts_ke_acc:AVERAGE \
  149. DEF:nts_ke_drop='$RRD_FILE':nts_ke_drop:AVERAGE \
  150. DEF:auth_pkts='$RRD_FILE':auth_pkts:AVERAGE \
  151. 'COMMENT: \l' \
  152. 'AREA:pkts_recv#C4FFC4:Packets received ' \
  153. 'LINE1:pkts_recv#00E000:' \
  154. 'GPRINT:pkts_recv:LAST:Cur\: %5.2lf%s' \
  155. 'GPRINT:pkts_recv:MIN:Min\: %5.2lf%s' \
  156. 'GPRINT:pkts_recv:AVERAGE:Avg\: %5.2lf%s' \
  157. 'GPRINT:pkts_recv:MAX:Max\: %5.2lf%s\l' \
  158. 'LINE1:pkts_drop#FF8C00:Packets dropped ' \
  159. 'GPRINT:pkts_drop:LAST:Cur\: %5.2lf%s' \
  160. 'GPRINT:pkts_drop:MIN:Min\: %5.2lf%s' \
  161. 'GPRINT:pkts_drop:AVERAGE:Avg\: %5.2lf%s' \
  162. 'GPRINT:pkts_drop:MAX:Max\: %5.2lf%s\l' \
  163. 'LINE1:cmd_recv#4169E1:Command packets received ' \
  164. 'GPRINT:cmd_recv:LAST:Cur\: %5.2lf%s' \
  165. 'GPRINT:cmd_recv:MIN:Min\: %5.2lf%s' \
  166. 'GPRINT:cmd_recv:AVERAGE:Avg\: %5.2lf%s' \
  167. 'GPRINT:cmd_recv:MAX:Max\: %5.2lf%s\l' \
  168. 'LINE1:cmd_drop#FFD700:Command packets dropped ' \
  169. 'GPRINT:cmd_drop:LAST:Cur\: %5.2lf%s' \
  170. 'GPRINT:cmd_drop:MIN:Min\: %5.2lf%s' \
  171. 'GPRINT:cmd_drop:AVERAGE:Avg\: %5.2lf%s' \
  172. 'GPRINT:cmd_drop:MAX:Max\: %5.2lf%s\l' \
  173. 'LINE1:log_drop#9400D3:Client log records dropped' \
  174. 'GPRINT:log_drop:LAST:Cur\: %5.2lf%s' \
  175. 'GPRINT:log_drop:MIN:Min\: %5.2lf%s' \
  176. 'GPRINT:log_drop:AVERAGE:Avg\: %5.2lf%s' \
  177. 'GPRINT:log_drop:MAX:Max\: %5.2lf%s\l' \
  178. 'LINE1:nts_ke_acc#8A2BE2:NTS-KE connections accepted' \
  179. 'GPRINT:nts_ke_acc:LAST:Cur\: %5.2lf%s' \
  180. 'GPRINT:nts_ke_acc:MIN:Min\: %5.2lf%s' \
  181. 'GPRINT:nts_ke_acc:AVERAGE:Avg\: %5.2lf%s' \
  182. 'GPRINT:nts_ke_acc:MAX:Max\: %5.2lf%s\l' \
  183. 'LINE1:nts_ke_drop#9370DB:NTS-KE connections dropped' \
  184. 'GPRINT:nts_ke_drop:LAST:Cur\: %5.2lf%s' \
  185. 'GPRINT:nts_ke_drop:MIN:Min\: %5.2lf%s' \
  186. 'GPRINT:nts_ke_drop:AVERAGE:Avg\: %5.2lf%s' \
  187. 'GPRINT:nts_ke_drop:MAX:Max\: %5.2lf%s\l' \
  188. 'LINE1:auth_pkts#FF0000:Authenticated NTP packets' \
  189. 'GPRINT:auth_pkts:LAST:Cur\: %5.2lf%s' \
  190. 'GPRINT:auth_pkts:MIN:Min\: %5.2lf%s' \
  191. 'GPRINT:auth_pkts:AVERAGE:Avg\: %5.2lf%s' \
  192. 'GPRINT:auth_pkts:MAX:Max\: %5.2lf%s\l'"
  193. ["chrony_tracking"]="--title 'Chrony Tracking Stats - by day' --vertical-label '(seconds,ppm)' \
  194. --units-exponent 0 \
  195. DEF:stratum='$RRD_FILE':stratum:AVERAGE \
  196. DEF:offset='$RRD_FILE':offset:AVERAGE \
  197. DEF:freq='$RRD_FILE':frequency:AVERAGE \
  198. DEF:resid_freq='$RRD_FILE':resid_freq:AVERAGE \
  199. DEF:skew='$RRD_FILE':skew:AVERAGE \
  200. DEF:delay='$RRD_FILE':delay:AVERAGE \
  201. DEF:dispersion='$RRD_FILE':dispersion:AVERAGE \
  202. CDEF:offset_scaled=offset,1000,* \
  203. CDEF:resfreq_scaled=resid_freq,100,* \
  204. CDEF:skew_scaled=skew,100,* \
  205. CDEF:delay_scaled=delay,1000,* \
  206. CDEF:disp_scaled=dispersion,1000,* \
  207. 'COMMENT: \l' \
  208. 'LINE1:stratum#00E000:Stratum ' \
  209. 'GPRINT:stratum:LAST: Cur\: %5.2lf%s' \
  210. 'GPRINT:stratum:MIN:Min\: %5.2lf%s' \
  211. 'GPRINT:stratum:AVERAGE:Avg\: %5.2lf%s' \
  212. 'GPRINT:stratum:MAX:Max\: %5.2lf%s\l' \
  213. 'LINE1:offset_scaled#0000FF:System Time (x1000) ' \
  214. 'GPRINT:offset_scaled:LAST: Cur\: %5.2lf%s' \
  215. 'GPRINT:offset_scaled:MIN:Min\: %5.2lf%s' \
  216. 'GPRINT:offset_scaled:AVERAGE:Avg\: %5.2lf%s' \
  217. 'GPRINT:offset_scaled:MAX:Max\: %5.2lf%s\l' \
  218. 'LINE1:freq#FFC300:Frequency (ppm) ' \
  219. 'GPRINT:freq:LAST: Cur\: %5.2lf%s' \
  220. 'GPRINT:freq:MIN:Min\: %5.2lf%s' \
  221. 'GPRINT:freq:AVERAGE:Avg\: %5.2lf%s' \
  222. 'GPRINT:freq:MAX:Max\: %5.2lf%s\l' \
  223. 'LINE1:resfreq_scaled#FF69B4:Residual Freq (ppm, x100) ' \
  224. 'GPRINT:resfreq_scaled:LAST: Cur\: %5.2lf%s' \
  225. 'GPRINT:resfreq_scaled:MIN:Min\: %5.2lf%s' \
  226. 'GPRINT:resfreq_scaled:AVERAGE:Avg\: %5.2lf%s' \
  227. 'GPRINT:resfreq_scaled:MAX:Max\: %5.2lf%s\l' \
  228. 'LINE1:skew_scaled#9400D3:Skew (ppm, x100) ' \
  229. 'GPRINT:skew_scaled:LAST: Cur\: %5.2lf%s' \
  230. 'GPRINT:skew_scaled:MIN:Min\: %5.2lf%s' \
  231. 'GPRINT:skew_scaled:AVERAGE:Avg\: %5.2lf%s' \
  232. 'GPRINT:skew_scaled:MAX:Max\: %5.2lf%s\l' \
  233. 'LINE1:delay_scaled#00BFFF:Root delay (seconds, x1000) ' \
  234. 'GPRINT:delay_scaled:LAST: Cur\: %5.2lf%s' \
  235. 'GPRINT:delay_scaled:MIN:Min\: %5.2lf%s' \
  236. 'GPRINT:delay_scaled:AVERAGE:Avg\: %5.2lf%s' \
  237. 'GPRINT:delay_scaled:MAX:Max\: %5.2lf%s\l' \
  238. 'LINE1:disp_scaled#FFFF00:Root dispersion (sec, x1000)' \
  239. 'GPRINT:disp_scaled:LAST: Cur\: %5.2lf%s' \
  240. 'GPRINT:disp_scaled:MIN:Min\: %5.2lf%s' \
  241. 'GPRINT:disp_scaled:AVERAGE:Avg\: %5.2lf%s' \
  242. 'GPRINT:disp_scaled:MAX:Max\: %5.2lf%s\l'"
  243. ["chrony_offset"]="--title 'Chrony System Time Offset - by day' --vertical-label 'seconds' \
  244. DEF:offset='$RRD_FILE':offset:AVERAGE \
  245. CDEF:offset_ms=offset,1000,* \
  246. LINE2:offset_ms#00ff00:'System time offset to NTP time' \
  247. GPRINT:offset_ms:LAST:'Cur\: %5.2lf%sms\n'"
  248. ["chrony_delay"]="--title 'Chrony Network Delay - by day' --vertical-label 'seconds' \
  249. DEF:delay='$RRD_FILE':delay:AVERAGE \
  250. CDEF:delay_ms=delay,1000,* \
  251. LINE2:delay_ms#00ff00:'Network path delay' \
  252. GPRINT:delay_ms:LAST:'Cur\: %5.2lf%sms\n'"
  253. ["chrony_frequency"]="--title 'Chrony Clock Frequency Error - by day' --vertical-label 'ppm' \
  254. DEF:freq='$RRD_FILE':frequency:AVERAGE \
  255. LINE2:freq#00ff00:'Local clock frequency error' \
  256. GPRINT:freq:LAST:'Cur\: %5.2lf%sppm\n'"
  257. ["chrony_drift"]="--title 'Chrony Drift - by day' --vertical-label 'Parts Per Million' \
  258. --units-exponent 0 \
  259. DEF:freq='$RRD_FILE':frequency:AVERAGE \
  260. DEF:skew='$RRD_FILE':skew:AVERAGE \
  261. 'COMMENT: \l' \
  262. 'LINE1:freq#32CD32:System Clock Gain/Loss Rate' \
  263. 'GPRINT:freq:LAST:Cur\: %5.2lf%s' \
  264. 'GPRINT:freq:MIN:Min\: %5.2lf%s' \
  265. 'GPRINT:freq:AVERAGE:Avg\: %5.2lf%s' \
  266. 'GPRINT:freq:MAX:Max\: %5.2lf%s\l' \
  267. 'LINE1:skew#4169E1:Estimate of Error Bound ' \
  268. 'GPRINT:skew:LAST:Cur\: %5.2lf%s' \
  269. 'GPRINT:skew:MIN:Min\: %5.2lf%s' \
  270. 'GPRINT:skew:AVERAGE:Avg\: %5.2lf%s' \
  271. 'GPRINT:skew:MAX:Max\: %5.2lf%s\l'"
  272. )
  273. for graph in "${!graphs[@]}"; do
  274. local cmd="LC_ALL=C rrdtool graph '$OUTPUT_DIR/$graph.png' --width '$WIDTH' --height '$HEIGHT' --start end-1d --end now-180s ${graphs[$graph]}"
  275. eval "$cmd" || {
  276. log_message "ERROR" "Failed to generate graph: $graph"
  277. exit 1
  278. }
  279. done
  280. }
  281. generate_html() {
  282. log_message "INFO" "Generating HTML report..."
  283. local GENERATED_TIMESTAMP=$(date)
  284. cat >"$OUTPUT_DIR/$HTML_FILENAME" <<EOF
  285. <!DOCTYPE html>
  286. <html lang="en">
  287. <head>
  288. <meta charset="utf-8">
  289. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  290. <title>${PAGE_TITLE} - Server Status</title>
  291. <style>
  292. :root {
  293. --primary-text: #212529;
  294. --secondary-text: #6c757d;
  295. --background-color: #f8f9fa;
  296. --content-background: #ffffff;
  297. --border-color: #787879;
  298. --code-background: #e1e1e1;
  299. --code-text: #000000;
  300. }
  301. body {
  302. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  303. margin: 0;
  304. padding: 20px;
  305. background-color: var(--background-color);
  306. color: var(--primary-text);
  307. line-height: 1.6;
  308. }
  309. .container {
  310. max-width: 1200px;
  311. margin: 0 auto;
  312. background-color: var(--content-background);
  313. padding: 20px 40px;
  314. border-radius: 8px;
  315. box-shadow: 0 4px 8px rgba(0,0,0,0.05);
  316. }
  317. header {
  318. text-align: center;
  319. border-bottom: 1px solid var(--border-color);
  320. padding-bottom: 20px;
  321. margin-bottom: 30px;
  322. }
  323. header h1 {
  324. margin: 0;
  325. font-size: 2.5em;
  326. color: var(--primary-text);
  327. }
  328. section {
  329. margin-bottom: 40px;
  330. }
  331. h2 {
  332. font-size: 1.8em;
  333. color: var(--primary-text);
  334. border-bottom: 1px solid var(--border-color);
  335. padding-bottom: 10px;
  336. margin-top: 0;
  337. margin-bottom: 20px;
  338. }
  339. h3 {
  340. font-size: 1.3em;
  341. color: var(--primary-text);
  342. margin-top: 25px;
  343. }
  344. .graph-grid {
  345. display: grid;
  346. grid-template-columns: 1fr;
  347. gap: 10px;
  348. text-align: center;
  349. }
  350. @media (min-width: 768px) {
  351. .graph-grid {
  352. grid-template-columns: repeat(2, 1fr);
  353. }
  354. }
  355. figure {
  356. margin: 0;
  357. padding: 0;
  358. }
  359. img {
  360. max-width: 100%;
  361. height: auto;
  362. border: 1px solid var(--border-color);
  363. border-radius: 4px;
  364. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  365. }
  366. pre {
  367. background-color: var(--code-background);
  368. color: var(--code-text);
  369. padding: 10px;
  370. border: 1px solid #c3bebe;
  371. border-radius: 4px;
  372. overflow-x: auto;
  373. white-space: pre-wrap;
  374. word-wrap: break-word;
  375. font-size: 0.8em;
  376. }
  377. footer {
  378. text-align: center;
  379. margin-top: 40px;
  380. padding-top: 20px;
  381. border-top: 1px solid var(--border-color);
  382. font-size: 0.9em;
  383. color: var(--secondary-text);
  384. }
  385. </style>
  386. </head>
  387. <body>
  388. <div class="container">
  389. <main>
  390. <section id="chrony-graphs">
  391. <h2>Chrony Graphs</h2>
  392. <div class="graph-grid">
  393. <figure>
  394. <img src="chrony_serverstats.png" alt="Chrony server statistics graph">
  395. </figure>
  396. <figure>
  397. <img src="chrony_offset.png" alt="Chrony system clock offset graph">
  398. </figure>
  399. <figure>
  400. <img src="chrony_tracking.png" alt="Chrony system clock tracking graph">
  401. </figure>
  402. <figure>
  403. <img src="chrony_delay.png" alt="Chrony sync delay graph">
  404. </figure>
  405. <figure>
  406. <img src="chrony_frequency.png" alt="Chrony clock frequency graph">
  407. </figure>
  408. <figure>
  409. <img src="chrony_drift.png" alt="Chrony clock frequency drift graph">
  410. </figure>
  411. </div>
  412. </section>
  413. <section id="vnstat-graphs">
  414. <h2>vnStati Graphs</h2>
  415. <div class="graph-grid">
  416. <figure>
  417. <img src="vnstat_s.png" alt="vnStat live traffic graph">
  418. </figure>
  419. <figure>
  420. <img src="vnstat_h.png" alt="vnStat hourly traffic graph">
  421. </figure>
  422. <figure>
  423. <img src="vnstat_d.png" alt="vnStat daily traffic graph">
  424. </figure>
  425. <figure>
  426. <img src="vnstat_m.png" alt="vnStat monthly traffic graph">
  427. </figure>
  428. <figure>
  429. <img src="vnstat_y.png" alt="vnStat yearly traffic graph">
  430. </figure>
  431. <figure>
  432. <img src="vnstat_t.png" alt="vnStat top 10 days traffic graph">
  433. </figure>
  434. </div>
  435. </section>
  436. <section id="chrony-stats">
  437. <h2>Chrony - NTP Statistics</h2>
  438. <h3>Command: <code>chronyc sources -v</code></h3>
  439. <pre><code>${CHRONYC_SOURCES}</code></pre>
  440. <h3>Command: <code>chronyc selectdata -v</code></h3>
  441. <pre><code>${CHRONYC_SELECTDATA}</code></pre>
  442. <h3>Command: <code>chronyc sourcestats -v</code></h3>
  443. <pre><code>${CHRONYC_SOURCESTATS}</code></pre>
  444. <h3>Command: <code>chronyc tracking</code></h3>
  445. <pre><code>${CHRONYC_TRACKING_HTML}</code></pre>
  446. </section>
  447. </main>
  448. <footer>
  449. <p>Page generated on: ${GENERATED_TIMESTAMP}</p>
  450. </footer>
  451. </div>
  452. </body>
  453. </html>
  454. EOF
  455. }
  456. main() {
  457. log_message "INFO" "Starting vnstati script..."
  458. validate_numeric "$WIDTH" "WIDTH"
  459. validate_numeric "$HEIGHT" "HEIGHT"
  460. validate_numeric "$TIMEOUT_SECONDS" "TIMEOUT_SECONDS"
  461. check_commands
  462. setup_directories
  463. generate_vnstat_images
  464. collect_chrony_data
  465. extract_chronyc_values
  466. create_rrd_database
  467. update_rrd_database
  468. generate_graphs
  469. generate_html
  470. log_message "INFO" "HTML page and graphs generated in: $OUTPUT_DIR/$HTML_FILENAME"
  471. echo "✅ Successfully generated report"
  472. }
  473. main