chrony-network-stats.sh 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  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/html/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. ## When chrony restarts, it can generate abnormally high statistical values (e.g., 12M packets)
  16. ## This parameter filters out values above the threshold,
  17. ## creating gaps in the graph instead of displaying misleading spikes.
  18. SERVER_STATS_UPPER_LIMIT=100000
  19. ## You can display the link to the repo 'chrony-stats' in the HTML footer
  20. ## Not required | Default: no
  21. GITHUB_REPO_LINK_SHOW="no"
  22. ##############################################################
  23. log_message() {
  24. local level="$1"
  25. local message="$2"
  26. if [[ "$ENABLE_LOGGING" == "yes" ]]; then
  27. echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" >> "$LOG_FILE"
  28. fi
  29. echo "[$level] $message"
  30. }
  31. validate_numeric() {
  32. local value="$1"
  33. local name="$2"
  34. if ! [[ "$value" =~ ^[0-9]+$ ]]; then
  35. log_message "ERROR" "Invalid $name: $value. Must be numeric."
  36. exit 1
  37. fi
  38. }
  39. check_commands() {
  40. local commands=("vnstati" "rrdtool" "chronyc" "sudo" "timeout")
  41. for cmd in "${commands[@]}"; do
  42. if ! command -v "$cmd" &>/dev/null; then
  43. log_message "ERROR" "Command '$cmd' not found in PATH."
  44. exit 1
  45. fi
  46. done
  47. }
  48. setup_directories() {
  49. log_message "INFO" "Checking and preparing directories..."
  50. for dir in "$OUTPUT_DIR" "$RRD_DIR" "$OUTPUT_DIR/img"; do
  51. mkdir -p "$dir" || {
  52. log_message "ERROR" "Failed to create directory: $dir"
  53. exit 1
  54. }
  55. if [ ! -w "$dir" ]; then
  56. log_message "ERROR" "Directory '$dir' is not writable."
  57. exit 1
  58. fi
  59. done
  60. }
  61. generate_vnstat_images() {
  62. log_message "INFO" "Generating vnStat images for interface '$INTERFACE'..."
  63. local modes=("s" "d" "t" "h" "m" "y")
  64. for mode in "${modes[@]}"; do
  65. vnstati -"$mode" -i "$INTERFACE" -o "$OUTPUT_DIR/img/vnstat_${mode}.png" || {
  66. log_message "ERROR" "Failed to generate vnstat image for mode $mode Check configuaration section : INTERFACE=\"here\""
  67. exit 1
  68. }
  69. done
  70. }
  71. collect_chrony_data() {
  72. log_message "INFO" "Collecting Chrony data..."
  73. get_html() {
  74. timeout "$TIMEOUT_SECONDS"s sudo chronyc "$1" -v 2>&1 | sed 's/&/\&/g;s/</\</g;s/>/\>/g;s/$/<br>/' || {
  75. log_message "ERROR" "Failed to collect chronyc $1 data"
  76. return 1
  77. }
  78. }
  79. RAW_TRACKING=$(timeout "$TIMEOUT_SECONDS"s sudo chronyc tracking) || {
  80. log_message "ERROR" "Failed to collect chronyc tracking data"
  81. exit 1
  82. }
  83. CHRONYC_TRACKING_HTML=$(echo "$RAW_TRACKING" | sed 's/&/\&/g;s/</\</g;s/>/\>/g;s/$/<br>/')
  84. CHRONYC_SOURCES=$(get_html sources) || exit 1
  85. CHRONYC_SOURCESTATS=$(get_html sourcestats) || exit 1
  86. CHRONYC_SELECTDATA=$(get_html selectdata) || exit 1
  87. }
  88. extract_chronyc_values() {
  89. extract_val() {
  90. echo "$RAW_TRACKING" | awk "/$1/ {print \$($2)}" | grep -E '^[-+]?[0-9.]+$' || echo "U"
  91. }
  92. OFFSET=$(extract_val "Last offset" "NF-1")
  93. local systime_line
  94. systime_line=$(echo "$RAW_TRACKING" | grep "System time")
  95. if [[ -n "$systime_line" ]]; then
  96. local value
  97. value=$(echo "$systime_line" | awk '{print $4}')
  98. if [[ "$systime_line" == *"slow"* ]]; then
  99. SYSTIME="-$value"
  100. else
  101. SYSTIME="$value"
  102. fi
  103. else
  104. SYSTIME="U"
  105. fi
  106. FREQ=$(extract_val "Frequency" "NF-2")
  107. RESID_FREQ=$(extract_val "Residual freq" "NF-1")
  108. SKEW=$(extract_val "Skew" "NF-1")
  109. DELAY=$(extract_val "Root delay" "NF-1")
  110. DISPERSION=$(extract_val "Root dispersion" "NF-1")
  111. STRATUM=$(extract_val "Stratum" "3")
  112. RAW_STATS=$(LC_ALL=C sudo chronyc serverstats) || {
  113. log_message "ERROR" "Failed to collect chronyc serverstats"
  114. exit 1
  115. }
  116. get_stat() {
  117. echo "$RAW_STATS" | awk -F'[[:space:]]*:[[:space:]]*' "/$1/ {print \$2}" | grep -E '^[0-9]+$' || echo "U"
  118. }
  119. PKTS_RECV=$(get_stat "NTP packets received")
  120. PKTS_DROP=$(get_stat "NTP packets dropped")
  121. CMD_RECV=$(get_stat "Command packets received")
  122. CMD_DROP=$(get_stat "Command packets dropped")
  123. LOG_DROP=$(get_stat "Client log records dropped")
  124. NTS_KE_ACC=$(get_stat "NTS-KE connections accepted")
  125. NTS_KE_DROP=$(get_stat "NTS-KE connections dropped")
  126. AUTH_PKTS=$(get_stat "Authenticated NTP packets")
  127. INTERLEAVED=$(get_stat "Interleaved NTP packets")
  128. TS_HELD=$(get_stat "NTP timestamps held")
  129. }
  130. create_rrd_database() {
  131. if [ ! -f "$RRD_FILE" ]; then
  132. log_message "INFO" "Creating new RRD file: $RRD_FILE"
  133. LC_ALL=C rrdtool create "$RRD_FILE" --step 300 \
  134. 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 \
  135. DS:delay:GAUGE:600:U:U DS:dispersion:GAUGE:600:U:U DS:stratum:GAUGE:600:0:16 \
  136. DS:systime:GAUGE:600:U:U \
  137. DS:pkts_recv:COUNTER:600:0:U DS:pkts_drop:COUNTER:600:0:U DS:cmd_recv:COUNTER:600:0:U \
  138. DS:cmd_drop:COUNTER:600:0:U DS:log_drop:COUNTER:600:0:U DS:nts_ke_acc:COUNTER:600:0:U \
  139. DS:nts_ke_drop:COUNTER:600:0:U DS:auth_pkts:COUNTER:600:0:U DS:interleaved:COUNTER:600:0:U \
  140. DS:ts_held:GAUGE:600:0:U \
  141. RRA:AVERAGE:0.5:1:576 RRA:AVERAGE:0.5:6:672 RRA:AVERAGE:0.5:24:732 RRA:AVERAGE:0.5:288:730 \
  142. RRA:MAX:0.5:1:576 RRA:MAX:0.5:6:672 RRA:MAX:0.5:24:732 RRA:MAX:0.5:288:730 \
  143. RRA:MIN:0.5:1:576 RRA:MIN:0.5:6:672 RRA:MIN:0.5:24:732 RRA:MIN:0.5:288:730 || {
  144. log_message "ERROR" "Failed to create RRD database"
  145. exit 1
  146. }
  147. fi
  148. }
  149. update_rrd_database() {
  150. log_message "INFO" "Updating RRD database..."
  151. UPDATE_STRING="N:$OFFSET:$FREQ:$RESID_FREQ:$SKEW:$DELAY:$DISPERSION:$STRATUM:$SYSTIME:$PKTS_RECV:$PKTS_DROP:$CMD_RECV:$CMD_DROP:$LOG_DROP:$NTS_KE_ACC:$NTS_KE_DROP:$AUTH_PKTS:$INTERLEAVED:$TS_HELD"
  152. LC_ALL=C rrdtool update "$RRD_FILE" "$UPDATE_STRING" || {
  153. log_message "ERROR" "Failed to update RRD database"
  154. exit 1
  155. }
  156. }
  157. generate_graphs() {
  158. log_message "INFO" "Generating graphs..."
  159. local END_TIME=$(date +%s)
  160. declare -A time_periods=(
  161. ["day"]="end-1d"
  162. ["week"]="end-1w"
  163. ["month"]="end-1m"
  164. )
  165. declare -A period_titles=(
  166. ["day"]="by day"
  167. ["week"]="by week"
  168. ["month"]="by month"
  169. )
  170. declare -A graphs=(
  171. ["chrony_serverstats"]="--title 'Chrony Server Statistics - PERIOD_TITLE' --vertical-label 'Packets/second' \
  172. --lower-limit 0 --rigid --units-exponent 0 \
  173. DEF:pkts_recv_raw='$RRD_FILE':pkts_recv:AVERAGE \
  174. DEF:pkts_drop_raw='$RRD_FILE':pkts_drop:AVERAGE \
  175. DEF:cmd_recv_raw='$RRD_FILE':cmd_recv:AVERAGE \
  176. DEF:cmd_drop_raw='$RRD_FILE':cmd_drop:AVERAGE \
  177. DEF:log_drop_raw='$RRD_FILE':log_drop:AVERAGE \
  178. DEF:nts_ke_acc_raw='$RRD_FILE':nts_ke_acc:AVERAGE \
  179. DEF:nts_ke_drop_raw='$RRD_FILE':nts_ke_drop:AVERAGE \
  180. DEF:auth_pkts_raw='$RRD_FILE':auth_pkts:AVERAGE \
  181. CDEF:pkts_recv=pkts_recv_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,pkts_recv_raw,IF \
  182. CDEF:pkts_drop=pkts_drop_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,pkts_drop_raw,IF \
  183. CDEF:cmd_recv=cmd_recv_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,cmd_recv_raw,IF \
  184. CDEF:cmd_drop=cmd_drop_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,cmd_drop_raw,IF \
  185. CDEF:log_drop=log_drop_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,log_drop_raw,IF \
  186. CDEF:nts_ke_acc=nts_ke_acc_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,nts_ke_acc_raw,IF \
  187. CDEF:nts_ke_drop=nts_ke_drop_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,nts_ke_drop_raw,IF \
  188. CDEF:auth_pkts=auth_pkts_raw,$SERVER_STATS_UPPER_LIMIT,GT,UNKN,auth_pkts_raw,IF \
  189. 'COMMENT: \l' \
  190. 'AREA:pkts_recv#C4FFC4:Packets received ' \
  191. 'LINE1:pkts_recv#00E000:' \
  192. 'GPRINT:pkts_recv:LAST:Cur\: %5.2lf%s' \
  193. 'GPRINT:pkts_recv:MIN:Min\: %5.2lf%s' \
  194. 'GPRINT:pkts_recv:AVERAGE:Avg\: %5.2lf%s' \
  195. 'GPRINT:pkts_recv:MAX:Max\: %5.2lf%s\l' \
  196. 'LINE1:pkts_drop#FF8C00:Packets dropped ' \
  197. 'GPRINT:pkts_drop:LAST:Cur\: %5.2lf%s' \
  198. 'GPRINT:pkts_drop:MIN:Min\: %5.2lf%s' \
  199. 'GPRINT:pkts_drop:AVERAGE:Avg\: %5.2lf%s' \
  200. 'GPRINT:pkts_drop:MAX:Max\: %5.2lf%s\l' \
  201. 'LINE1:cmd_recv#4169E1:Command packets received ' \
  202. 'GPRINT:cmd_recv:LAST:Cur\: %5.2lf%s' \
  203. 'GPRINT:cmd_recv:MIN:Min\: %5.2lf%s' \
  204. 'GPRINT:cmd_recv:AVERAGE:Avg\: %5.2lf%s' \
  205. 'GPRINT:cmd_recv:MAX:Max\: %5.2lf%s\l' \
  206. 'LINE1:cmd_drop#FFD700:Command packets dropped ' \
  207. 'GPRINT:cmd_drop:LAST:Cur\: %5.2lf%s' \
  208. 'GPRINT:cmd_drop:MIN:Min\: %5.2lf%s' \
  209. 'GPRINT:cmd_drop:AVERAGE:Avg\: %5.2lf%s' \
  210. 'GPRINT:cmd_drop:MAX:Max\: %5.2lf%s\l' \
  211. 'LINE1:log_drop#9400D3:Client log records dropped ' \
  212. 'GPRINT:log_drop:LAST:Cur\: %5.2lf%s' \
  213. 'GPRINT:log_drop:MIN:Min\: %5.2lf%s' \
  214. 'GPRINT:log_drop:AVERAGE:Avg\: %5.2lf%s' \
  215. 'GPRINT:log_drop:MAX:Max\: %5.2lf%s\l' \
  216. 'LINE1:nts_ke_acc#8A2BE2:NTS-KE connections accepted ' \
  217. 'GPRINT:nts_ke_acc:LAST:Cur\: %5.2lf%s' \
  218. 'GPRINT:nts_ke_acc:MIN:Min\: %5.2lf%s' \
  219. 'GPRINT:nts_ke_acc:AVERAGE:Avg\: %5.2lf%s' \
  220. 'GPRINT:nts_ke_acc:MAX:Max\: %5.2lf%s\l' \
  221. 'LINE1:nts_ke_drop#9370DB:NTS-KE connections dropped ' \
  222. 'GPRINT:nts_ke_drop:LAST:Cur\: %5.2lf%s' \
  223. 'GPRINT:nts_ke_drop:MIN:Min\: %5.2lf%s' \
  224. 'GPRINT:nts_ke_drop:AVERAGE:Avg\: %5.2lf%s' \
  225. 'GPRINT:nts_ke_drop:MAX:Max\: %5.2lf%s\l' \
  226. 'LINE1:auth_pkts#FF0000:Authenticated NTP packets ' \
  227. 'GPRINT:auth_pkts:LAST:Cur\: %5.2lf%s' \
  228. 'GPRINT:auth_pkts:MIN:Min\: %5.2lf%s' \
  229. 'GPRINT:auth_pkts:AVERAGE:Avg\: %5.2lf%s' \
  230. 'GPRINT:auth_pkts:MAX:Max\: %5.2lf%s\l'"
  231. ["chrony_tracking"]="--title 'Chrony Dispersion + Stratum - PERIOD_TITLE' --vertical-label 'milliseconds' --alt-autoscale \
  232. --units-exponent 0 \
  233. DEF:stratum='$RRD_FILE':stratum:AVERAGE \
  234. DEF:freq='$RRD_FILE':frequency:AVERAGE \
  235. DEF:skew='$RRD_FILE':skew:AVERAGE \
  236. DEF:delay='$RRD_FILE':delay:AVERAGE \
  237. DEF:dispersion='$RRD_FILE':dispersion:AVERAGE \
  238. CDEF:skew_scaled=skew,100,* \
  239. CDEF:delay_scaled=delay,1000,* \
  240. CDEF:disp_scaled=dispersion,1000,* \
  241. 'COMMENT: \l' \
  242. 'LINE1:stratum#00ff00:Stratum ' \
  243. 'GPRINT:stratum:LAST: Cur\: %5.2lf%s' \
  244. 'GPRINT:stratum:MIN:Min\: %5.2lf%s' \
  245. 'GPRINT:stratum:AVERAGE:Avg\: %5.2lf%s' \
  246. 'GPRINT:stratum:MAX:Max\: %5.2lf%s\l' \
  247. 'LINE1:disp_scaled#9400D3:Root dispersion [Root dispersion] ' \
  248. 'GPRINT:disp_scaled:LAST: Cur\: %5.2lf%s' \
  249. 'GPRINT:disp_scaled:MIN:Min\: %5.2lf%s' \
  250. 'GPRINT:disp_scaled:AVERAGE:Avg\: %5.2lf%s' \
  251. 'GPRINT:disp_scaled:MAX:Max\: %5.2lf%s\l'"
  252. ["chrony_offset"]="--title 'Chrony System Time Offset - PERIOD_TITLE' --vertical-label 'milliseconds' \
  253. DEF:offset='$RRD_FILE':offset:AVERAGE \
  254. DEF:systime='$RRD_FILE':systime:AVERAGE \
  255. CDEF:systime_scaled=systime,1000,* \
  256. CDEF:offset_ms=offset,1000,* \
  257. 'LINE2:offset_ms#00ff00:Actual Offset from NTP Source [Last Offset] ' \
  258. 'GPRINT:offset_ms:LAST: Cur\: %5.2lf%s' \
  259. 'GPRINT:offset_ms:MIN:Min\: %5.2lf%s' \
  260. 'GPRINT:offset_ms:AVERAGE:Avg\: %5.2lf%s' \
  261. 'GPRINT:offset_ms:MAX:Max\: %5.2lf%s\l' \
  262. 'LINE1:systime_scaled#4169E1:System Clock Adjustment [System Time] ' \
  263. 'GPRINT:systime_scaled:LAST: Cur\: %5.2lf%s' \
  264. 'GPRINT:systime_scaled:MIN:Min\: %5.2lf%s' \
  265. 'GPRINT:systime_scaled:AVERAGE:Avg\: %5.2lf%s' \
  266. 'GPRINT:systime_scaled:MAX:Max\: %5.2lf%s\l'"
  267. ["chrony_delay"]="--title 'Chrony Root Delay - PERIOD_TITLE' --vertical-label 'milliseconds' --units-exponent 0 \
  268. DEF:delay='$RRD_FILE':delay:AVERAGE \
  269. CDEF:delay_ms=delay,1000,* \
  270. LINE2:delay_ms#00ff00:'Network Delay to Root Source [Root Delay] ' \
  271. 'GPRINT:delay_ms:LAST:Cur\: %5.2lf%s' \
  272. 'GPRINT:delay_ms:MIN:Min\: %5.2lf%s' \
  273. 'GPRINT:delay_ms:AVERAGE:Avg\: %5.2lf%s' \
  274. 'GPRINT:delay_ms:MAX:Max\: %5.2lf%s\l'"
  275. ["chrony_frequency"]="--title 'Chrony Clock Frequency Error - PERIOD_TITLE' --vertical-label 'ppm'\
  276. DEF:freq='$RRD_FILE':frequency:AVERAGE \
  277. DEF:resid_freq='$RRD_FILE':resid_freq:AVERAGE \
  278. CDEF:resfreq_scaled=resid_freq,100,* \
  279. CDEF:freq_scaled=freq,1,* \
  280. 'LINE2:freq_scaled#00ff00:Natural Clock Drift [Frequency] ' \
  281. 'GPRINT:freq_scaled:LAST:Cur\: %5.2lf%s' \
  282. 'GPRINT:freq_scaled:MIN:Min\: %5.2lf%s' \
  283. 'GPRINT:freq_scaled:AVERAGE:Avg\: %5.2lf%s' \
  284. 'GPRINT:freq_scaled:MAX:Max\: %5.2lf%s\n' \
  285. 'LINE1:resfreq_scaled#4169E1:Residual Drift (x100) [Residual freq] ' \
  286. 'GPRINT:resfreq_scaled:LAST:Cur\: %5.2lf%s' \
  287. 'GPRINT:resfreq_scaled:MIN:Min\: %5.2lf%s' \
  288. 'GPRINT:resfreq_scaled:AVERAGE:Avg\: %5.2lf%s' \
  289. 'GPRINT:resfreq_scaled:MAX:Max\: %5.2lf%s\l'"
  290. ["chrony_drift"]="--title 'Chrony Drift Margin Error - PERIOD_TITLE' --vertical-label 'ppm' \
  291. --units-exponent 0 \
  292. DEF:resid_freq='$RRD_FILE':resid_freq:AVERAGE \
  293. DEF:skew_raw='$RRD_FILE':skew:AVERAGE \
  294. CDEF:resfreq_scaled=resid_freq,100,* \
  295. CDEF:skew_scaled=skew_raw,100,* \
  296. 'COMMENT: \l' \
  297. 'LINE1:skew_scaled#00ff00:Estimate Drift Error Margin (x100) [Skew] ' \
  298. 'GPRINT:skew_scaled:LAST:Cur\: %5.2lf' \
  299. 'GPRINT:skew_scaled:MIN:Min\: %5.2lf' \
  300. 'GPRINT:skew_scaled:AVERAGE:Avg\: %5.2lf' \
  301. 'GPRINT:skew_scaled:MAX:Max\: %5.2lf\l'"
  302. )
  303. for period in "${!time_periods[@]}"; do
  304. for graph in "${!graphs[@]}"; do
  305. local graph_title="${graphs[$graph]//PERIOD_TITLE/${period_titles[$period]}}"
  306. local output_file="$OUTPUT_DIR/img/${graph}_${period}.png"
  307. local time_range="${time_periods[$period]}"
  308. local cmd="LC_ALL=C rrdtool graph '$output_file' --width '$WIDTH' --height '$HEIGHT' --start $time_range --end now-180s $graph_title"
  309. eval "$cmd" || {
  310. log_message "ERROR" "Failed to generate graph: ${graph}_${period}"
  311. exit 1
  312. }
  313. done
  314. done
  315. }
  316. generate_html() {
  317. log_message "INFO" "Generating HTML report..."
  318. local GENERATED_TIMESTAMP=$(date)
  319. cat >"$OUTPUT_DIR/$HTML_FILENAME" <<EOF
  320. <!DOCTYPE html>
  321. <html lang="en">
  322. <head>
  323. <meta charset="utf-8">
  324. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  325. <title>${PAGE_TITLE} - Server Status</title>
  326. <style>
  327. :root {
  328. --primary-text: #212529;
  329. --secondary-text: #6c757d;
  330. --background-color: #f8f9fa;
  331. --content-background: #ffffff;
  332. --border-color: #787879;
  333. --code-background: #e1e1e1;
  334. --code-text: #000000;
  335. }
  336. body {
  337. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  338. margin: 0;
  339. padding: 20px;
  340. background-color: var(--background-color);
  341. color: var(--primary-text);
  342. line-height: 1.6;
  343. }
  344. .container {
  345. max-width: 1400px;
  346. margin: 0 auto;
  347. background-color: var(--content-background);
  348. padding: 20px 20px;
  349. border-radius: 8px;
  350. box-shadow: 0 4px 8px rgba(0,0,0,0.05);
  351. }
  352. header {
  353. text-align: center;
  354. border-bottom: 1px solid var(--border-color);
  355. padding-bottom: 20px;
  356. margin-bottom: 30px;
  357. }
  358. header h1 {
  359. margin: 0;
  360. font-size: 2.5em;
  361. color: var(--primary-text);
  362. }
  363. section {
  364. margin-bottom: 40px;
  365. }
  366. h2 {
  367. font-size: 1.8em;
  368. color: var(--primary-text);
  369. border-bottom: 1px solid var(--border-color);
  370. padding-bottom: 10px;
  371. margin-top: 0;
  372. margin-bottom: 20px;
  373. }
  374. h2 a {
  375. font-size: 0.8em;
  376. font-weight: normal;
  377. vertical-align: middle;
  378. margin-left: 10px;
  379. }
  380. h3 {
  381. font-size: 1.3em;
  382. color: var(--primary-text);
  383. margin-top: 25px;
  384. }
  385. @media (max-width: 767px) {
  386. #vnstat-graphs table,
  387. #vnstat-graphs tbody,
  388. #vnstat-graphs tr,
  389. #vnstat-graphs td {
  390. display: block;
  391. width: 100%;
  392. }
  393. #vnstat-graphs td {
  394. padding-left: 0;
  395. padding-right: 0;
  396. text-align: center;
  397. }
  398. }
  399. .graph-grid {
  400. display: grid;
  401. grid-template-columns: 1fr;
  402. gap: 10px;
  403. text-align: center;
  404. }
  405. @media (min-width: 768px) {
  406. .graph-grid {
  407. grid-template-columns: repeat(2, 1fr);
  408. }
  409. }
  410. figure {
  411. margin: 0;
  412. padding: 0;
  413. }
  414. img {
  415. max-width: 100%;
  416. height: auto;
  417. border: 1px solid var(--border-color);
  418. border-radius: 4px;
  419. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  420. }
  421. pre {
  422. background-color: var(--code-background);
  423. color: var(--code-text);
  424. padding: 10px;
  425. border: 1px solid #c3bebe;
  426. border-radius: 4px;
  427. overflow-x: auto;
  428. white-space: pre-wrap;
  429. word-wrap: break-word;
  430. font-size: 0.8em;
  431. }
  432. footer {
  433. text-align: center;
  434. margin-top: 40px;
  435. padding-top: 20px;
  436. border-top: 1px solid var(--border-color);
  437. font-size: 0.9em;
  438. color: var(--secondary-text);
  439. }
  440. .tabs {
  441. display: flex;
  442. border-bottom: 1px solid var(--border-color);
  443. margin-bottom: 20px;
  444. }
  445. .tab {
  446. padding: 10px 20px;
  447. cursor: pointer;
  448. background-color: var(--background-color);
  449. border: 1px solid var(--border-color);
  450. border-bottom: none;
  451. margin-right: 2px;
  452. transition: background-color 0.3s;
  453. }
  454. .tab:hover {
  455. background-color: #e9ecef;
  456. }
  457. .tab.active {
  458. background-color: var(--content-background);
  459. border-bottom: 1px solid var(--content-background);
  460. margin-bottom: -1px;
  461. }
  462. .tab-content {
  463. display: none;
  464. }
  465. .tab-content.active {
  466. display: block;
  467. }
  468. </style>
  469. </head>
  470. <body>
  471. <div class="container">
  472. <main>
  473. <section id="chrony-graphs">
  474. <h2>Chrony Graphs <a target="_blank" href="https://chrony-project.org/doc/4.3/chronyc.html#:~:text=System%20clock-,tracking,-The%20tracking%20command">[Data Legend]</a></h2>
  475. <div class="tabs">
  476. <div class="tab active" onclick="showTab('day')">Day</div>
  477. <div class="tab" onclick="showTab('week')">Week</div>
  478. <div class="tab" onclick="showTab('month')">Month</div>
  479. </div>
  480. <div id="day-content" class="tab-content active">
  481. <div class="graph-grid">
  482. <figure>
  483. <img src="img/chrony_serverstats_day.png" alt="Chrony server statistics graph - day">
  484. </figure>
  485. <figure>
  486. <img src="img/chrony_offset_day.png" alt="Chrony system clock offset graph - day">
  487. </figure>
  488. <figure>
  489. <img src="img/chrony_tracking_day.png" alt="Chrony system clock tracking graph - day">
  490. </figure>
  491. <figure>
  492. <img src="img/chrony_delay_day.png" alt="Chrony sync delay graph - day">
  493. </figure>
  494. <figure>
  495. <img src="img/chrony_frequency_day.png" alt="Chrony clock frequency graph - day">
  496. </figure>
  497. <figure>
  498. <img src="img/chrony_drift_day.png" alt="Chrony clock frequency drift graph - day">
  499. </figure>
  500. </div>
  501. </div>
  502. <div id="week-content" class="tab-content">
  503. <div class="graph-grid">
  504. <figure>
  505. <img src="img/chrony_serverstats_week.png" alt="Chrony server statistics graph - week">
  506. </figure>
  507. <figure>
  508. <img src="img/chrony_offset_week.png" alt="Chrony system clock offset graph - week">
  509. </figure>
  510. <figure>
  511. <img src="img/chrony_tracking_week.png" alt="Chrony system clock tracking graph - week">
  512. </figure>
  513. <figure>
  514. <img src="img/chrony_delay_week.png" alt="Chrony sync delay graph - week">
  515. </figure>
  516. <figure>
  517. <img src="img/chrony_frequency_week.png" alt="Chrony clock frequency graph - week">
  518. </figure>
  519. <figure>
  520. <img src="img/chrony_drift_week.png" alt="Chrony clock frequency drift graph - week">
  521. </figure>
  522. </div>
  523. </div>
  524. <div id="month-content" class="tab-content">
  525. <div class="graph-grid">
  526. <figure>
  527. <img src="img/chrony_serverstats_month.png" alt="Chrony server statistics graph - month">
  528. </figure>
  529. <figure>
  530. <img src="img/chrony_offset_month.png" alt="Chrony system clock offset graph - month">
  531. </figure>
  532. <figure>
  533. <img src="img/chrony_tracking_month.png" alt="Chrony system clock tracking graph - month">
  534. </figure>
  535. <figure>
  536. <img src="img/chrony_delay_month.png" alt="Chrony sync delay graph - month">
  537. </figure>
  538. <figure>
  539. <img src="img/chrony_frequency_month.png" alt="Chrony clock frequency graph - month">
  540. </figure>
  541. <figure>
  542. <img src="img/chrony_drift_month.png" alt="Chrony clock frequency drift graph - month">
  543. </figure>
  544. </div>
  545. </div>
  546. </section>
  547. <section id="vnstat-graphs">
  548. <h2>vnStati Graphs</h2>
  549. <table border="0" style="margin-left: auto; margin-right: auto;">
  550. <tbody>
  551. <tr>
  552. <td valign="top" style="padding: 0 10px;">
  553. <img src="img/vnstat_s.png" alt="vnStat summary"><br>
  554. <img src="img/vnstat_d.png" alt="vnStat daily" style="margin-top: 4px;"><br>
  555. <img src="img/vnstat_t.png" alt="vnStat top 10" style="margin-top: 4px;"><br>
  556. </td>
  557. <td valign="top" style="padding: 0 10px;">
  558. <img src="img/vnstat_h.png" alt="vnStat hourly"><br>
  559. <img src="img/vnstat_m.png" alt="vnStat monthly" style="margin-top: 4px;"><br>
  560. <img src="img/vnstat_y.png" alt="vnStat yearly" style="margin-top: 4px;"><br>
  561. </td>
  562. </tr>
  563. </tbody>
  564. </table>
  565. </section>
  566. <section id="chrony-stats">
  567. <h2>Chrony - NTP Statistics</h2>
  568. <h3>Command: <code>chronyc sources -v</code></h3>
  569. <pre><code>${CHRONYC_SOURCES}</code></pre>
  570. <h3>Command: <code>chronyc selectdata -v</code></h3>
  571. <pre><code>${CHRONYC_SELECTDATA}</code></pre>
  572. <h3>Command: <code>chronyc sourcestats -v</code></h3>
  573. <pre><code>${CHRONYC_SOURCESTATS}</code></pre>
  574. <h3>Command: <code>chronyc tracking</code></h3>
  575. <pre><code>${CHRONYC_TRACKING_HTML}</code></pre>
  576. </section>
  577. </main>
  578. <footer>
  579. <p>Page generated on: ${GENERATED_TIMESTAMP}</p>
  580. EOF
  581. if [[ "$GITHUB_REPO_LINK_SHOW" == "yes" ]]; then
  582. cat >>"$OUTPUT_DIR/$HTML_FILENAME" <<EOF
  583. <p>Made with ❤️ by TheHuman00 | <a href="https://github.com/TheHuman00/chrony-stats" target="_blank">View on GitHub</a></p>
  584. EOF
  585. fi
  586. cat >>"$OUTPUT_DIR/$HTML_FILENAME" <<EOF
  587. </footer>
  588. </div>
  589. <script>
  590. function showTab(period) {
  591. const contents = document.querySelectorAll('.tab-content');
  592. contents.forEach(content => content.classList.remove('active'));
  593. const tabs = document.querySelectorAll('.tab');
  594. tabs.forEach(tab => tab.classList.remove('active'));
  595. document.getElementById(period + '-content').classList.add('active');
  596. event.target.classList.add('active');
  597. }
  598. </script>
  599. </body>
  600. </html>
  601. EOF
  602. }
  603. main() {
  604. log_message "INFO" "Starting vnstati script..."
  605. validate_numeric "$WIDTH" "WIDTH"
  606. validate_numeric "$HEIGHT" "HEIGHT"
  607. validate_numeric "$TIMEOUT_SECONDS" "TIMEOUT_SECONDS"
  608. validate_numeric "$SERVER_STATS_UPPER_LIMIT" "SERVER_STATS_UPPER_LIMIT"
  609. check_commands
  610. setup_directories
  611. generate_vnstat_images
  612. collect_chrony_data
  613. extract_chronyc_values
  614. create_rrd_database
  615. update_rrd_database
  616. generate_graphs
  617. generate_html
  618. log_message "INFO" "HTML page and graphs generated in: $OUTPUT_DIR/$HTML_FILENAME"
  619. echo "✅ Successfully generated report"
  620. }
  621. main