├── .cargo └── config.toml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build_release ├── screenshot.png └── src ├── main.rs ├── nginx.rs └── processor.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | linker = "x86_64-linux-musl-gcc" 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: CI 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Install SQLite 11 | run: sudo apt-get update && sudo apt-get install libsqlite3-dev 12 | 13 | - name: Checkout sources 14 | uses: actions/checkout@v4 15 | 16 | - name: Install stable toolchain 17 | uses: dtolnay/rust-toolchain@stable 18 | 19 | - name: Check formatting 20 | run: cargo fmt --all -- --check 21 | 22 | - name: Test 23 | run: cargo test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | nginx.conf 4 | *.log 5 | *.tar.gz 6 | /target 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.0 2 | - Pull Requests 3 | - https://github.com/gsquire/topngx/pull/10 4 | - Update cargo dependencies and the Rust edition. 5 | 6 | ## 0.4.0 7 | - Pull Requests 8 | - https://github.com/gsquire/topngx/pull/9 9 | - Update cargo dependencies. 10 | 11 | ## 0.3.1 12 | - Pull Requests 13 | - https://github.com/gsquire/topngx/pull/8 14 | - Make sure to clean the tail reading thread up. 15 | - Remove the internal buffering count to get a quicker update while tailing. 16 | 17 | ## 0.3.0 18 | - Pull Requests 19 | - https://github.com/gsquire/topngx/pull/7 20 | - Change `--no-follow` to `--follow` allowing users to explicitly opt in for tailing log files. 21 | - Change `-t` to `-i` for the interval argument. 22 | - Bug Fixes 23 | - Only restore the cursor when running in tail mode. 24 | - Return an error if a user tries to tail standard input. 25 | 26 | ## 0.2.0 27 | - Pull Requests 28 | - https://github.com/gsquire/topngx/pull/6 29 | - Implement the first cut of log tailing. 30 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "anstream" 25 | version = "0.6.18" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 28 | dependencies = [ 29 | "anstyle", 30 | "anstyle-parse", 31 | "anstyle-query", 32 | "anstyle-wincon", 33 | "colorchoice", 34 | "is_terminal_polyfill", 35 | "utf8parse", 36 | ] 37 | 38 | [[package]] 39 | name = "anstyle" 40 | version = "1.0.10" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 43 | 44 | [[package]] 45 | name = "anstyle-parse" 46 | version = "0.2.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 49 | dependencies = [ 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-query" 55 | version = "1.1.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 58 | dependencies = [ 59 | "windows-sys 0.59.0", 60 | ] 61 | 62 | [[package]] 63 | name = "anstyle-wincon" 64 | version = "3.0.7" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 67 | dependencies = [ 68 | "anstyle", 69 | "once_cell", 70 | "windows-sys 0.59.0", 71 | ] 72 | 73 | [[package]] 74 | name = "anyhow" 75 | version = "1.0.31" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" 78 | 79 | [[package]] 80 | name = "atty" 81 | version = "0.2.14" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 84 | dependencies = [ 85 | "hermit-abi", 86 | "libc", 87 | "winapi", 88 | ] 89 | 90 | [[package]] 91 | name = "autocfg" 92 | version = "1.1.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 95 | 96 | [[package]] 97 | name = "bitflags" 98 | version = "1.3.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 101 | 102 | [[package]] 103 | name = "bitflags" 104 | version = "2.9.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 107 | 108 | [[package]] 109 | name = "cc" 110 | version = "1.2.18" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" 113 | dependencies = [ 114 | "shlex", 115 | ] 116 | 117 | [[package]] 118 | name = "cfg-if" 119 | version = "1.0.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 122 | 123 | [[package]] 124 | name = "cfg_aliases" 125 | version = "0.2.1" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 128 | 129 | [[package]] 130 | name = "clap" 131 | version = "2.33.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" 134 | dependencies = [ 135 | "ansi_term", 136 | "atty", 137 | "bitflags 1.3.2", 138 | "strsim", 139 | "textwrap", 140 | "unicode-width 0.1.7", 141 | "vec_map", 142 | ] 143 | 144 | [[package]] 145 | name = "colorchoice" 146 | version = "1.0.3" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 149 | 150 | [[package]] 151 | name = "convert_case" 152 | version = "0.7.1" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 155 | dependencies = [ 156 | "unicode-segmentation", 157 | ] 158 | 159 | [[package]] 160 | name = "crossbeam-channel" 161 | version = "0.5.6" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" 164 | dependencies = [ 165 | "cfg-if", 166 | "crossbeam-utils", 167 | ] 168 | 169 | [[package]] 170 | name = "crossbeam-deque" 171 | version = "0.8.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" 174 | dependencies = [ 175 | "cfg-if", 176 | "crossbeam-epoch", 177 | "crossbeam-utils", 178 | ] 179 | 180 | [[package]] 181 | name = "crossbeam-epoch" 182 | version = "0.9.10" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" 185 | dependencies = [ 186 | "autocfg", 187 | "cfg-if", 188 | "crossbeam-utils", 189 | "memoffset", 190 | "once_cell", 191 | "scopeguard", 192 | ] 193 | 194 | [[package]] 195 | name = "crossbeam-utils" 196 | version = "0.8.11" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" 199 | dependencies = [ 200 | "cfg-if", 201 | "once_cell", 202 | ] 203 | 204 | [[package]] 205 | name = "crossterm" 206 | version = "0.29.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 209 | dependencies = [ 210 | "bitflags 2.9.0", 211 | "crossterm_winapi", 212 | "derive_more", 213 | "document-features", 214 | "mio", 215 | "parking_lot", 216 | "rustix", 217 | "signal-hook", 218 | "signal-hook-mio", 219 | "winapi", 220 | ] 221 | 222 | [[package]] 223 | name = "crossterm_winapi" 224 | version = "0.9.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 227 | dependencies = [ 228 | "winapi", 229 | ] 230 | 231 | [[package]] 232 | name = "ctrlc" 233 | version = "3.4.6" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" 236 | dependencies = [ 237 | "nix", 238 | "windows-sys 0.59.0", 239 | ] 240 | 241 | [[package]] 242 | name = "derive_more" 243 | version = "2.0.1" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 246 | dependencies = [ 247 | "derive_more-impl", 248 | ] 249 | 250 | [[package]] 251 | name = "derive_more-impl" 252 | version = "2.0.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 255 | dependencies = [ 256 | "convert_case", 257 | "proc-macro2", 258 | "quote", 259 | "syn 2.0.100", 260 | ] 261 | 262 | [[package]] 263 | name = "document-features" 264 | version = "0.2.11" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 267 | dependencies = [ 268 | "litrs", 269 | ] 270 | 271 | [[package]] 272 | name = "either" 273 | version = "1.5.3" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 276 | 277 | [[package]] 278 | name = "env_filter" 279 | version = "0.1.3" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 282 | dependencies = [ 283 | "log", 284 | "regex", 285 | ] 286 | 287 | [[package]] 288 | name = "env_logger" 289 | version = "0.11.8" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 292 | dependencies = [ 293 | "anstream", 294 | "anstyle", 295 | "env_filter", 296 | "jiff", 297 | "log", 298 | ] 299 | 300 | [[package]] 301 | name = "errno" 302 | version = "0.3.11" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 305 | dependencies = [ 306 | "libc", 307 | "windows-sys 0.59.0", 308 | ] 309 | 310 | [[package]] 311 | name = "fallible-iterator" 312 | version = "0.3.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 315 | 316 | [[package]] 317 | name = "fallible-streaming-iterator" 318 | version = "0.1.9" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 321 | 322 | [[package]] 323 | name = "foldhash" 324 | version = "0.1.5" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 327 | 328 | [[package]] 329 | name = "hashbrown" 330 | version = "0.15.2" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 333 | dependencies = [ 334 | "foldhash", 335 | ] 336 | 337 | [[package]] 338 | name = "hashlink" 339 | version = "0.10.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 342 | dependencies = [ 343 | "hashbrown", 344 | ] 345 | 346 | [[package]] 347 | name = "heck" 348 | version = "0.3.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 351 | dependencies = [ 352 | "unicode-segmentation", 353 | ] 354 | 355 | [[package]] 356 | name = "hermit-abi" 357 | version = "0.1.14" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" 360 | dependencies = [ 361 | "libc", 362 | ] 363 | 364 | [[package]] 365 | name = "is_terminal_polyfill" 366 | version = "1.70.1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 369 | 370 | [[package]] 371 | name = "jiff" 372 | version = "0.2.5" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" 375 | dependencies = [ 376 | "jiff-static", 377 | "log", 378 | "portable-atomic", 379 | "portable-atomic-util", 380 | "serde", 381 | ] 382 | 383 | [[package]] 384 | name = "jiff-static" 385 | version = "0.2.5" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" 388 | dependencies = [ 389 | "proc-macro2", 390 | "quote", 391 | "syn 2.0.100", 392 | ] 393 | 394 | [[package]] 395 | name = "lazy_static" 396 | version = "1.4.0" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 399 | 400 | [[package]] 401 | name = "libc" 402 | version = "0.2.171" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 405 | 406 | [[package]] 407 | name = "libsqlite3-sys" 408 | version = "0.32.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" 411 | dependencies = [ 412 | "cc", 413 | "pkg-config", 414 | "vcpkg", 415 | ] 416 | 417 | [[package]] 418 | name = "linux-raw-sys" 419 | version = "0.9.3" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 422 | 423 | [[package]] 424 | name = "litrs" 425 | version = "0.4.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 428 | 429 | [[package]] 430 | name = "lock_api" 431 | version = "0.4.7" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 434 | dependencies = [ 435 | "autocfg", 436 | "scopeguard", 437 | ] 438 | 439 | [[package]] 440 | name = "log" 441 | version = "0.4.27" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 444 | 445 | [[package]] 446 | name = "memchr" 447 | version = "2.7.4" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 450 | 451 | [[package]] 452 | name = "memoffset" 453 | version = "0.6.5" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 456 | dependencies = [ 457 | "autocfg", 458 | ] 459 | 460 | [[package]] 461 | name = "mio" 462 | version = "1.0.3" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 465 | dependencies = [ 466 | "libc", 467 | "log", 468 | "wasi", 469 | "windows-sys 0.52.0", 470 | ] 471 | 472 | [[package]] 473 | name = "nix" 474 | version = "0.29.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 477 | dependencies = [ 478 | "bitflags 2.9.0", 479 | "cfg-if", 480 | "cfg_aliases", 481 | "libc", 482 | ] 483 | 484 | [[package]] 485 | name = "once_cell" 486 | version = "1.21.3" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 489 | 490 | [[package]] 491 | name = "parking_lot" 492 | version = "0.12.1" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 495 | dependencies = [ 496 | "lock_api", 497 | "parking_lot_core", 498 | ] 499 | 500 | [[package]] 501 | name = "parking_lot_core" 502 | version = "0.9.3" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" 505 | dependencies = [ 506 | "cfg-if", 507 | "libc", 508 | "redox_syscall", 509 | "smallvec", 510 | "windows-sys 0.36.1", 511 | ] 512 | 513 | [[package]] 514 | name = "pkg-config" 515 | version = "0.3.25" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 518 | 519 | [[package]] 520 | name = "portable-atomic" 521 | version = "1.11.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 524 | 525 | [[package]] 526 | name = "portable-atomic-util" 527 | version = "0.2.4" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 530 | dependencies = [ 531 | "portable-atomic", 532 | ] 533 | 534 | [[package]] 535 | name = "proc-macro-error" 536 | version = "1.0.2" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" 539 | dependencies = [ 540 | "proc-macro-error-attr", 541 | "proc-macro2", 542 | "quote", 543 | "syn 1.0.33", 544 | "version_check", 545 | ] 546 | 547 | [[package]] 548 | name = "proc-macro-error-attr" 549 | version = "1.0.2" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" 552 | dependencies = [ 553 | "proc-macro2", 554 | "quote", 555 | "syn 1.0.33", 556 | "syn-mid", 557 | "version_check", 558 | ] 559 | 560 | [[package]] 561 | name = "proc-macro2" 562 | version = "1.0.94" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 565 | dependencies = [ 566 | "unicode-ident", 567 | ] 568 | 569 | [[package]] 570 | name = "quote" 571 | version = "1.0.40" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 574 | dependencies = [ 575 | "proc-macro2", 576 | ] 577 | 578 | [[package]] 579 | name = "rayon" 580 | version = "1.10.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 583 | dependencies = [ 584 | "either", 585 | "rayon-core", 586 | ] 587 | 588 | [[package]] 589 | name = "rayon-core" 590 | version = "1.12.1" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 593 | dependencies = [ 594 | "crossbeam-deque", 595 | "crossbeam-utils", 596 | ] 597 | 598 | [[package]] 599 | name = "redox_syscall" 600 | version = "0.2.16" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 603 | dependencies = [ 604 | "bitflags 1.3.2", 605 | ] 606 | 607 | [[package]] 608 | name = "regex" 609 | version = "1.11.1" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 612 | dependencies = [ 613 | "aho-corasick", 614 | "memchr", 615 | "regex-automata", 616 | "regex-syntax", 617 | ] 618 | 619 | [[package]] 620 | name = "regex-automata" 621 | version = "0.4.9" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 624 | dependencies = [ 625 | "aho-corasick", 626 | "memchr", 627 | "regex-syntax", 628 | ] 629 | 630 | [[package]] 631 | name = "regex-syntax" 632 | version = "0.8.5" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 635 | 636 | [[package]] 637 | name = "rusqlite" 638 | version = "0.34.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" 641 | dependencies = [ 642 | "bitflags 2.9.0", 643 | "fallible-iterator", 644 | "fallible-streaming-iterator", 645 | "hashlink", 646 | "libsqlite3-sys", 647 | "smallvec", 648 | ] 649 | 650 | [[package]] 651 | name = "rustix" 652 | version = "1.0.5" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 655 | dependencies = [ 656 | "bitflags 2.9.0", 657 | "errno", 658 | "libc", 659 | "linux-raw-sys", 660 | "windows-sys 0.59.0", 661 | ] 662 | 663 | [[package]] 664 | name = "scopeguard" 665 | version = "1.1.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 668 | 669 | [[package]] 670 | name = "serde" 671 | version = "1.0.219" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 674 | dependencies = [ 675 | "serde_derive", 676 | ] 677 | 678 | [[package]] 679 | name = "serde_derive" 680 | version = "1.0.219" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 683 | dependencies = [ 684 | "proc-macro2", 685 | "quote", 686 | "syn 2.0.100", 687 | ] 688 | 689 | [[package]] 690 | name = "shlex" 691 | version = "1.3.0" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 694 | 695 | [[package]] 696 | name = "signal-hook" 697 | version = "0.3.17" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 700 | dependencies = [ 701 | "libc", 702 | "signal-hook-registry", 703 | ] 704 | 705 | [[package]] 706 | name = "signal-hook-mio" 707 | version = "0.2.4" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 710 | dependencies = [ 711 | "libc", 712 | "mio", 713 | "signal-hook", 714 | ] 715 | 716 | [[package]] 717 | name = "signal-hook-registry" 718 | version = "1.4.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 721 | dependencies = [ 722 | "libc", 723 | ] 724 | 725 | [[package]] 726 | name = "smallvec" 727 | version = "1.9.0" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 730 | 731 | [[package]] 732 | name = "strsim" 733 | version = "0.8.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 736 | 737 | [[package]] 738 | name = "structopt" 739 | version = "0.3.15" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "de2f5e239ee807089b62adce73e48c625e0ed80df02c7ab3f068f5db5281065c" 742 | dependencies = [ 743 | "clap", 744 | "lazy_static", 745 | "structopt-derive", 746 | ] 747 | 748 | [[package]] 749 | name = "structopt-derive" 750 | version = "0.4.8" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "510413f9de616762a4fbeab62509bf15c729603b72d7cd71280fbca431b1c118" 753 | dependencies = [ 754 | "heck", 755 | "proc-macro-error", 756 | "proc-macro2", 757 | "quote", 758 | "syn 1.0.33", 759 | ] 760 | 761 | [[package]] 762 | name = "syn" 763 | version = "1.0.33" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" 766 | dependencies = [ 767 | "proc-macro2", 768 | "quote", 769 | "unicode-xid", 770 | ] 771 | 772 | [[package]] 773 | name = "syn" 774 | version = "2.0.100" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 777 | dependencies = [ 778 | "proc-macro2", 779 | "quote", 780 | "unicode-ident", 781 | ] 782 | 783 | [[package]] 784 | name = "syn-mid" 785 | version = "0.5.0" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 788 | dependencies = [ 789 | "proc-macro2", 790 | "quote", 791 | "syn 1.0.33", 792 | ] 793 | 794 | [[package]] 795 | name = "tabwriter" 796 | version = "1.4.1" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "fce91f2f0ec87dff7e6bcbbeb267439aa1188703003c6055193c821487400432" 799 | dependencies = [ 800 | "unicode-width 0.2.0", 801 | ] 802 | 803 | [[package]] 804 | name = "textwrap" 805 | version = "0.11.0" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 808 | dependencies = [ 809 | "unicode-width 0.1.7", 810 | ] 811 | 812 | [[package]] 813 | name = "topngx" 814 | version = "0.5.0" 815 | dependencies = [ 816 | "anyhow", 817 | "atty", 818 | "crossbeam-channel", 819 | "crossterm", 820 | "ctrlc", 821 | "env_logger", 822 | "log", 823 | "once_cell", 824 | "rayon", 825 | "regex", 826 | "rusqlite", 827 | "structopt", 828 | "tabwriter", 829 | ] 830 | 831 | [[package]] 832 | name = "unicode-ident" 833 | version = "1.0.18" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 836 | 837 | [[package]] 838 | name = "unicode-segmentation" 839 | version = "1.12.0" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 842 | 843 | [[package]] 844 | name = "unicode-width" 845 | version = "0.1.7" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 848 | 849 | [[package]] 850 | name = "unicode-width" 851 | version = "0.2.0" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 854 | 855 | [[package]] 856 | name = "unicode-xid" 857 | version = "0.2.0" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 860 | 861 | [[package]] 862 | name = "utf8parse" 863 | version = "0.2.2" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 866 | 867 | [[package]] 868 | name = "vcpkg" 869 | version = "0.2.15" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 872 | 873 | [[package]] 874 | name = "vec_map" 875 | version = "0.8.2" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 878 | 879 | [[package]] 880 | name = "version_check" 881 | version = "0.9.2" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 884 | 885 | [[package]] 886 | name = "wasi" 887 | version = "0.11.0+wasi-snapshot-preview1" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 890 | 891 | [[package]] 892 | name = "winapi" 893 | version = "0.3.9" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 896 | dependencies = [ 897 | "winapi-i686-pc-windows-gnu", 898 | "winapi-x86_64-pc-windows-gnu", 899 | ] 900 | 901 | [[package]] 902 | name = "winapi-i686-pc-windows-gnu" 903 | version = "0.4.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 906 | 907 | [[package]] 908 | name = "winapi-x86_64-pc-windows-gnu" 909 | version = "0.4.0" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 912 | 913 | [[package]] 914 | name = "windows-sys" 915 | version = "0.36.1" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 918 | dependencies = [ 919 | "windows_aarch64_msvc 0.36.1", 920 | "windows_i686_gnu 0.36.1", 921 | "windows_i686_msvc 0.36.1", 922 | "windows_x86_64_gnu 0.36.1", 923 | "windows_x86_64_msvc 0.36.1", 924 | ] 925 | 926 | [[package]] 927 | name = "windows-sys" 928 | version = "0.52.0" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 931 | dependencies = [ 932 | "windows-targets", 933 | ] 934 | 935 | [[package]] 936 | name = "windows-sys" 937 | version = "0.59.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 940 | dependencies = [ 941 | "windows-targets", 942 | ] 943 | 944 | [[package]] 945 | name = "windows-targets" 946 | version = "0.52.6" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 949 | dependencies = [ 950 | "windows_aarch64_gnullvm", 951 | "windows_aarch64_msvc 0.52.6", 952 | "windows_i686_gnu 0.52.6", 953 | "windows_i686_gnullvm", 954 | "windows_i686_msvc 0.52.6", 955 | "windows_x86_64_gnu 0.52.6", 956 | "windows_x86_64_gnullvm", 957 | "windows_x86_64_msvc 0.52.6", 958 | ] 959 | 960 | [[package]] 961 | name = "windows_aarch64_gnullvm" 962 | version = "0.52.6" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 965 | 966 | [[package]] 967 | name = "windows_aarch64_msvc" 968 | version = "0.36.1" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 971 | 972 | [[package]] 973 | name = "windows_aarch64_msvc" 974 | version = "0.52.6" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 977 | 978 | [[package]] 979 | name = "windows_i686_gnu" 980 | version = "0.36.1" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 983 | 984 | [[package]] 985 | name = "windows_i686_gnu" 986 | version = "0.52.6" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 989 | 990 | [[package]] 991 | name = "windows_i686_gnullvm" 992 | version = "0.52.6" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 995 | 996 | [[package]] 997 | name = "windows_i686_msvc" 998 | version = "0.36.1" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1001 | 1002 | [[package]] 1003 | name = "windows_i686_msvc" 1004 | version = "0.52.6" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1007 | 1008 | [[package]] 1009 | name = "windows_x86_64_gnu" 1010 | version = "0.36.1" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1013 | 1014 | [[package]] 1015 | name = "windows_x86_64_gnu" 1016 | version = "0.52.6" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1019 | 1020 | [[package]] 1021 | name = "windows_x86_64_gnullvm" 1022 | version = "0.52.6" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1025 | 1026 | [[package]] 1027 | name = "windows_x86_64_msvc" 1028 | version = "0.36.1" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1031 | 1032 | [[package]] 1033 | name = "windows_x86_64_msvc" 1034 | version = "0.52.6" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1037 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "topngx" 3 | version = "0.5.0" 4 | authors = ["Garrett Squire "] 5 | edition = "2024" 6 | description = "Top for NGINX" 7 | repository = "https://github.com/gsquire/topngx" 8 | license = "MIT" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | anyhow = "1.0" 13 | atty = "0.2" 14 | crossbeam-channel = "0.5" 15 | crossterm = "0.29" 16 | ctrlc = "3.4" 17 | env_logger = "0.11" 18 | log = "0.4" 19 | once_cell = "1.21" 20 | rayon = "1.10" 21 | regex = "1.11" 22 | rusqlite = "0.34" 23 | structopt = "0.3" 24 | tabwriter = "1.4" 25 | 26 | [features] 27 | bundled-sqlite = ["rusqlite/bundled"] 28 | 29 | [profile.release] 30 | lto = true 31 | codegen-units = 1 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Garrett Squire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # topngx 2 | [![CI](https://github.com/gsquire/topngx/workflows/CI/badge.svg)](https://github.com/gsquire/topngx/actions) 3 | 4 | This tool is a rewrite of [ngxtop](https://github.com/lebinh/ngxtop) to make it easier to install 5 | and hopefully run faster. For those unfamiliar with the ngxtop, it is a tool that helps you 6 | parse NGINX access logs and print various statistics from them regardless of format. It is 7 | currently not as feature complete as the original version but it should have enough functionality 8 | to be usable. 9 | 10 | ![screenshot](screenshot.png) 11 | 12 | ## Installation 13 | There are a few ways to install it. The easiest way is to grab a release from [here](https://github.com/gsquire/topngx/releases). 14 | Otherwise, you can install it from [crates.io](https://crates.io/crates/topngx) with a working Rust 15 | installation: 16 | 17 | ```sh 18 | cargo install topngx 19 | 20 | # If you do not have SQLite headers installed on your system, you can use the bundled feature. 21 | cargo install topngx --features bundled-sqlite 22 | ``` 23 | 24 | SQLite development headers are easy to get on Mac and Linux: 25 | 26 | ```sh 27 | # On Mac. 28 | brew install sqlite 29 | 30 | # On Debian based Linux. 31 | sudo apt-get update && sudo apt-get install libsqlite3-dev 32 | ``` 33 | 34 | ## CHANGELOG 35 | [See here](CHANGELOG.md) 36 | 37 | ## Usage 38 | ```sh 39 | topngx 0.3.0 40 | Garrett Squire 41 | top for NGINX 42 | 43 | USAGE: 44 | topngx [FLAGS] [OPTIONS] [SUBCOMMAND] 45 | 46 | FLAGS: 47 | -t, --follow Tail the specified log file. You cannot tail standard input 48 | -h, --help Prints help information 49 | -V, --version Prints version information 50 | 51 | OPTIONS: 52 | -a, --access-log The access log to parse 53 | -f, --format The specific log format with which to parse [default: combined] 54 | -g, --group-by Group by this variable [default: request_path] 55 | -w, --having Having clause [default: 1] 56 | -i, --interval Refresh the statistics using this interval which is given in seconds [default: 2] 57 | -l, --limit The number of records to limit for each query [default: 10] 58 | -o, --order-by Order of output for the default queries [default: count] 59 | 60 | SUBCOMMANDS: 61 | avg Print the average of the given fields 62 | help Prints this message or the help of the given subcommand(s) 63 | info List the available fields as well as the access log and format being used 64 | print Print out the supplied fields with the given limit 65 | query Supply a custom query 66 | sum Compute the sum of the given fields 67 | top Find the top values for the given fields 68 | ``` 69 | 70 | Some example queries are: 71 | 72 | ```sh 73 | # Run with the default queries and format (combined). 74 | # Or use the --access-log and --no-follow flags if you do not want to read from standard input. 75 | topngx < /path/to/access.log 76 | 77 | # Output: 78 | count avg_bytes_sent 2XX 3XX 4XX 5XX 79 | 2 346.5 2 0 0 0 80 | request_path count avg_bytes_sent 2XX 3XX 4XX 5XX 81 | GET / HTTP/1.1 1 612 1 0 0 0 82 | GET /some_file1 HTTP/1.1 1 81 1 0 0 0 83 | 84 | # See the fields that you can use for queries. 85 | topngx info < access.log 86 | 87 | # Use a custom log format. 88 | topngx -f '$remote_addr - $remote_user [$time_local] "$request" $status $bytes_sent' info 89 | 90 | # Output: 91 | access log file: STDIN 92 | access log format: $remote_addr - $remote_user [$time_local] "$request" $status $bytes_sent 93 | available variables to query: remote_addr, remote_user, time_local, request_path, status_type, bytes_sent 94 | 95 | # Run a custom query. 96 | # The fields passed in can be viewed via the info sub command. 97 | topngx query -q 'select * from log where bytes_sent > 100' -f request_path bytes_sent < access.log 98 | ``` 99 | 100 | ## Limitations 101 | There is no option to filter the data but this could be added in the future. The original version 102 | allowed for automatic detection of NGINX configuration files, log file paths, and log format styles. 103 | topngx currently has command line options for these and may add this functionality in a later version. 104 | 105 | If you find any other issues or features that may be missing, feel free to open an issue. You can 106 | also utilize logging via the [env_logger](https://github.com/sebasmagri/env_logger/) crate. 107 | 108 | ```sh 109 | # See the env_logger README for the various levels. 110 | RUST_LOG=debug topngx < /path/to/access.log 111 | ``` 112 | 113 | ## License 114 | MIT 115 | 116 | The ngxtop license can be seen [here](https://github.com/lebinh/ngxtop/blob/master/LICENSE.txt). 117 | -------------------------------------------------------------------------------- /build_release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | # The semantic version is the first and only argument. 6 | VERSION="$1" 7 | 8 | cargo build --release 9 | 10 | # Cross compile for Linux via the MUSL toolchain. 11 | TARGET_CC=x86_64-linux-musl-gcc \ 12 | cargo build \ 13 | --release \ 14 | --target x86_64-unknown-linux-musl \ 15 | --features bundled-sqlite 16 | 17 | # Build archives. 18 | shasum -a 256 target/release/topngx > target/release/SHA256 19 | shasum -a 256 target/x86_64-unknown-linux-musl/release/topngx \ 20 | > target/x86_64-unknown-linux-musl/release/SHA256 21 | 22 | tar -czf "topngx-$VERSION-x86_64-apple-darwin.tar.gz" \ 23 | -C target/release \ 24 | SHA256 topngx 25 | 26 | tar -czf "topngx-$VERSION-x86_64-unknown-linux-musl.tar.gz" \ 27 | -C target/x86_64-unknown-linux-musl/release \ 28 | SHA256 topngx 29 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gsquire/topngx/e49e4398c7ee7bd59e13b0b471f00e60d849a961/screenshot.png -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, BufRead, BufReader, Seek, SeekFrom}; 3 | use std::sync::Arc; 4 | use std::sync::atomic::{AtomicBool, Ordering}; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | use anyhow::{Result, anyhow}; 9 | use crossbeam_channel::{bounded, select, tick, unbounded}; 10 | use crossterm::cursor::SavePosition; 11 | use crossterm::execute; 12 | use crossterm::terminal::{Clear, ClearType}; 13 | use log::{debug, info}; 14 | use rayon::prelude::*; 15 | use regex::Regex; 16 | use rusqlite::types::ToSql; 17 | use structopt::StructOpt; 18 | 19 | use nginx::{available_variables, format_to_pattern}; 20 | use processor::{Processor, generate_processor}; 21 | 22 | mod nginx; 23 | mod processor; 24 | 25 | const STDIN: &str = "STDIN"; 26 | 27 | // Common field names. 28 | const STATUS_TYPE: &str = "status_type"; 29 | const BYTES_SENT: &str = "bytes_sent"; 30 | const REQUEST_PATH: &str = "request_path"; 31 | 32 | #[derive(Debug, StructOpt)] 33 | #[structopt( 34 | author, 35 | name = "topngx", 36 | about = "top for NGINX", 37 | rename_all = "kebab-case" 38 | )] 39 | struct Options { 40 | /// The access log to parse. 41 | #[structopt(short, long)] 42 | access_log: Option, 43 | 44 | /// The specific log format with which to parse. 45 | #[structopt(short, long, default_value = "combined")] 46 | format: String, 47 | 48 | /// Group by this variable. 49 | #[structopt(short, long, default_value = "request_path")] 50 | group_by: String, 51 | 52 | /// Having clause. 53 | #[structopt(short = "w", long, default_value = "1")] 54 | having: u64, 55 | 56 | /// Refresh the statistics using this interval which is given in seconds. 57 | #[structopt(short, long, default_value = "2")] 58 | interval: u64, 59 | 60 | /// Tail the specified log file. You cannot tail standard input. 61 | #[structopt(short = "t", long)] 62 | follow: bool, 63 | 64 | /// The number of records to limit for each query. 65 | #[structopt(short, long, default_value = "10")] 66 | limit: u64, 67 | 68 | /// Order of output for the default queries. 69 | #[structopt(short, long, default_value = "count")] 70 | order_by: String, 71 | 72 | #[structopt(subcommand)] 73 | subcommand: Option, 74 | } 75 | 76 | // The list of subcommands available to use. 77 | #[derive(Debug, StructOpt)] 78 | enum SubCommand { 79 | /// Print the average of the given fields. 80 | Avg(Fields), 81 | 82 | /// List the available fields as well as the access log and format being used. 83 | Info, 84 | 85 | /// Print out the supplied fields with the given limit. 86 | Print(Fields), 87 | 88 | /// Supply a custom query. 89 | Query(Query), 90 | 91 | /// Compute the sum of the given fields. 92 | Sum(Fields), 93 | 94 | /// Find the top values for the given fields. 95 | Top(Fields), 96 | } 97 | 98 | #[derive(Debug, StructOpt)] 99 | struct Fields { 100 | /// A space Separated list of field names. 101 | fields: Vec, 102 | } 103 | 104 | #[derive(Debug, StructOpt)] 105 | struct Query { 106 | /// A space separated list of field names. 107 | #[structopt(short, long)] 108 | fields: Vec, 109 | 110 | /// The supplied query. You typically will want to use your shell to quote it. 111 | #[structopt(short, long)] 112 | query: String, 113 | } 114 | 115 | fn tail( 116 | opts: &Options, 117 | access_log: &str, 118 | fields: Option>, 119 | queries: Option>, 120 | ) -> Result<()> { 121 | const SLEEP: u64 = 100; 122 | 123 | // Save our cursor position. 124 | execute!(io::stdout(), SavePosition)?; 125 | 126 | let f = File::open(access_log)?; 127 | let stat = f.metadata()?; 128 | let mut len = stat.len(); 129 | let mut tail_reader = BufReader::new(f); 130 | tail_reader.seek(SeekFrom::Start(len))?; 131 | 132 | let pattern = format_to_pattern(&opts.format)?; 133 | let processor = generate_processor(opts, fields, queries)?; 134 | let (tx, rx) = unbounded(); 135 | let ticker = tick(Duration::from_secs(opts.interval)); 136 | 137 | // The interrupt handling plumbing. 138 | let (stop_tx, stop_rx) = bounded(0); 139 | let running = Arc::new(AtomicBool::new(true)); 140 | let handler_r = Arc::clone(&running); 141 | 142 | ctrlc::set_handler(move || { 143 | handler_r.store(false, Ordering::SeqCst); 144 | })?; 145 | 146 | let reader_handle = thread::spawn(move || -> Result<()> { 147 | loop { 148 | select! { 149 | recv(stop_rx) -> _ => { return Ok(()); } 150 | default => { 151 | let mut line = String::new(); 152 | let n_read = tail_reader.read_line(&mut line)?; 153 | 154 | if n_read > 0 { 155 | len += n_read as u64; 156 | tail_reader.seek(SeekFrom::Start(len))?; 157 | line.pop(); // Remove the newline character. 158 | debug!("tail read: {}", line); 159 | tx.send(line)?; 160 | } else { 161 | debug!("tail sleeping for {} milliseconds", SLEEP); 162 | thread::sleep(Duration::from_millis(SLEEP)); 163 | } 164 | } 165 | } 166 | } 167 | }); 168 | 169 | let mut lines = Vec::new(); 170 | while running.load(Ordering::SeqCst) { 171 | select! { 172 | recv(rx) -> line => { 173 | lines.push(line?); 174 | parse_input(&lines, &pattern, &processor)?; 175 | lines.clear(); 176 | } 177 | recv(ticker) -> _ => { 178 | execute!(io::stdout(), Clear(ClearType::All))?; 179 | processor.report(opts.follow)?; 180 | } 181 | } 182 | } 183 | 184 | // We got an interrupt, so stop the reading thread. 185 | stop_tx.send(())?; 186 | 187 | // The join will panic if the thread panics but otherwise it will propagate the return value up 188 | // to the main thread. 189 | reader_handle 190 | .join() 191 | .expect("the file reading thread should not have panicked") 192 | } 193 | 194 | // Either read from STDIN or the file specified. 195 | fn input_source(access_log: &str) -> Result> { 196 | if access_log == STDIN { 197 | return Ok(Box::new(BufReader::new(io::stdin()))); 198 | } 199 | Ok(Box::new(BufReader::new(File::open(access_log)?))) 200 | } 201 | 202 | fn run(opts: &Options, fields: Option>, queries: Option>) -> Result<()> { 203 | let access_log = match &opts.access_log { 204 | Some(l) => l, 205 | None => { 206 | if atty::isnt(atty::Stream::Stdin) { 207 | STDIN 208 | } else { 209 | return Err(anyhow!("STDIN is a TTY")); 210 | } 211 | } 212 | }; 213 | info!("access log: {}", access_log); 214 | info!("access log format: {}", opts.format); 215 | 216 | // We cannot tail STDIN. 217 | if opts.follow && access_log == STDIN { 218 | return Err(anyhow!("cannot tail STDIN")); 219 | } 220 | 221 | // We need to tail the log file. 222 | if opts.follow { 223 | return tail(opts, access_log, fields, queries); 224 | } 225 | 226 | let input = input_source(access_log)?; 227 | let lines = input 228 | .lines() 229 | .filter_map(|l| l.ok()) 230 | .collect::>(); 231 | let pattern = format_to_pattern(&opts.format)?; 232 | let processor = generate_processor(opts, fields, queries)?; 233 | parse_input(&lines, &pattern, &processor)?; 234 | processor.report(opts.follow) 235 | } 236 | 237 | fn parse_input(lines: &[String], pattern: &Regex, processor: &Processor) -> Result<()> { 238 | let fields = processor.fields.clone(); 239 | let records: Vec<_> = lines 240 | .par_iter() 241 | .filter_map(|line| match pattern.captures(line) { 242 | None => None, 243 | Some(c) => { 244 | let mut record: Vec<(String, Box)> = vec![]; 245 | 246 | for field in &fields { 247 | if field == STATUS_TYPE { 248 | let status = c.name("status").map_or("", |m| m.as_str()); 249 | let status_type = status.parse::().unwrap_or(0) / 100; 250 | record.push((format!(":{}", field), Box::new(status_type))); 251 | } else if field == BYTES_SENT { 252 | let bytes_sent = c.name("body_bytes_sent").map_or("", |m| m.as_str()); 253 | let bytes_sent = bytes_sent.parse::().unwrap_or(0); 254 | record.push((format!(":{}", field), Box::new(bytes_sent))); 255 | } else if field == REQUEST_PATH { 256 | if c.name("request_uri").is_some() { 257 | record.push(( 258 | format!(":{}", field), 259 | Box::new(c.name("request_uri").unwrap().as_str().to_string()), 260 | )); 261 | } else { 262 | let uri = c.name("request").map_or("", |m| m.as_str()); 263 | record.push((format!(":{}", field), Box::new(uri.to_string()))); 264 | } 265 | } else { 266 | let value = c.name(field).map_or("", |m| m.as_str()); 267 | record.push((format!(":{}", field), Box::new(String::from(value)))); 268 | } 269 | } 270 | 271 | Some(record) 272 | } 273 | }) 274 | .collect(); 275 | 276 | processor.process(records) 277 | } 278 | 279 | fn avg_subcommand(opts: &Options, fields: Vec) -> Result<()> { 280 | let avg_fields: Vec = fields.iter().map(|f| format!("AVG({f})", f = f)).collect(); 281 | let selections = avg_fields.join(", "); 282 | let query = format!("SELECT {selections} FROM log", selections = selections); 283 | debug!("average sub command query: {}", query); 284 | run(opts, Some(fields), Some(vec![query])) 285 | } 286 | 287 | fn info_subcommand(opts: &Options) -> Result<()> { 288 | println!( 289 | "access log file: {}", 290 | opts.access_log 291 | .clone() 292 | .unwrap_or_else(|| String::from(STDIN)) 293 | ); 294 | println!("access log format: {}", opts.format); 295 | println!( 296 | "available variables to query: {}", 297 | available_variables(&opts.format)? 298 | ); 299 | 300 | Ok(()) 301 | } 302 | 303 | fn print_subcommand(opts: &Options, fields: Vec) -> Result<()> { 304 | let selections = fields.join(", "); 305 | let query = format!( 306 | "SELECT {selections} FROM log GROUP BY {selections}", 307 | selections = selections 308 | ); 309 | debug!("print sub command query: {}", query); 310 | run(opts, Some(fields), Some(vec![query])) 311 | } 312 | 313 | fn query_subcommand(opts: &Options, fields: Vec, query: String) -> Result<()> { 314 | debug!("custom query: {}", query); 315 | run(opts, Some(fields), Some(vec![query])) 316 | } 317 | 318 | fn sum_subcommand(opts: &Options, fields: Vec) -> Result<()> { 319 | let sum_fields: Vec = fields.iter().map(|f| format!("SUM({f})", f = f)).collect(); 320 | let selections = sum_fields.join(", "); 321 | let query = format!("SELECT {selections} FROM log", selections = selections); 322 | debug!("sum sub command query: {}", query); 323 | run(opts, Some(fields), Some(vec![query])) 324 | } 325 | 326 | fn top_subcommand(opts: &Options, fields: Vec) -> Result<()> { 327 | let mut queries = Vec::with_capacity(fields.len()); 328 | 329 | for f in &fields { 330 | let query = format!( 331 | "SELECT {field}, COUNT(1) AS count FROM log \ 332 | GROUP BY {field} ORDER BY COUNT DESC LIMIT {limit}", 333 | field = f, 334 | limit = opts.limit 335 | ); 336 | debug!("top sub command query: {}", query); 337 | queries.push(query); 338 | } 339 | 340 | run(opts, Some(fields), Some(queries)) 341 | } 342 | 343 | fn main() -> Result<()> { 344 | env_logger::init(); 345 | 346 | let opts = Options::from_args(); 347 | debug!("options: {:?}", opts); 348 | 349 | if let Some(sc) = &opts.subcommand { 350 | match sc { 351 | SubCommand::Avg(f) => avg_subcommand(&opts, f.fields.clone())?, 352 | SubCommand::Info => info_subcommand(&opts)?, 353 | SubCommand::Print(f) => print_subcommand(&opts, f.fields.clone())?, 354 | SubCommand::Query(q) => query_subcommand(&opts, q.fields.clone(), q.query.clone())?, 355 | SubCommand::Sum(f) => sum_subcommand(&opts, f.fields.clone())?, 356 | SubCommand::Top(f) => top_subcommand(&opts, f.fields.clone())?, 357 | } 358 | return Ok(()); 359 | } 360 | 361 | run(&opts, None, None) 362 | } 363 | -------------------------------------------------------------------------------- /src/nginx.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use once_cell::sync::Lazy; 3 | use regex::Regex; 4 | 5 | const COMBINED: &str = "combined"; 6 | const LOG_FORMAT_COMBINED: &str = r#"$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent""#; 7 | 8 | // We know that these patterns will compile. 9 | static NGINX_VARIABLE_REGEX: Lazy = Lazy::new(|| Regex::new(r"\$([a-zA-Z0-9_]+)").unwrap()); 10 | static SPECIAL_CHARS_REGEX: Lazy = 11 | Lazy::new(|| Regex::new(r"([\.\*\+\?\|\(\)\{\}\[\]])").unwrap()); 12 | 13 | pub(crate) fn format_to_pattern(mut format: &str) -> Result { 14 | if format == COMBINED { 15 | format = LOG_FORMAT_COMBINED; 16 | } 17 | 18 | // Escape all of the existing special characters. 19 | let pattern = SPECIAL_CHARS_REGEX.replace_all(format, r"\$1"); 20 | 21 | // Name our capture groups based on their name in the specified log format. 22 | let captures = NGINX_VARIABLE_REGEX.replace_all(&pattern, r"(?P<$1>.*)"); 23 | Ok(Regex::new(&captures)?) 24 | } 25 | 26 | // List the available variables based on the supplied log format. 27 | pub(crate) fn available_variables(format: &str) -> Result { 28 | Ok(format_to_pattern(format)? 29 | .capture_names() 30 | .filter_map(|c| match c { 31 | Some(n) => { 32 | // Make some adjustments based on the schema. 33 | if n == "status" { 34 | return Some(String::from(super::STATUS_TYPE)); 35 | } else if n == "body_bytes_sent" { 36 | return Some(String::from(super::BYTES_SENT)); 37 | } else if n == "request" { 38 | return Some(String::from(super::REQUEST_PATH)); 39 | } 40 | Some(n.to_string()) 41 | } 42 | None => None, 43 | }) 44 | .collect::>() 45 | .join(", ")) 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | 52 | #[test] 53 | fn combined_matches() { 54 | let line = r#"172.17.0.1 - - [06/Jun/2020:23:16:43 +0000] "GET / HTTP/1.1" 403 153 "-" "curl/7.54.0""#; 55 | let pattern = format_to_pattern(LOG_FORMAT_COMBINED).unwrap(); 56 | assert!(pattern.captures(line).is_some()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/processor.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::io::{self, Write}; 3 | 4 | use anyhow::Result; 5 | use crossterm::cursor::RestorePosition; 6 | use crossterm::execute; 7 | use log::debug; 8 | use rusqlite::types::{ToSql, Value}; 9 | use rusqlite::{Connection, params}; 10 | use tabwriter::TabWriter; 11 | 12 | use super::Options; 13 | 14 | /// The main processing engine for all of the statistics. 15 | pub(crate) struct Processor { 16 | columns: String, 17 | conn: Connection, 18 | pub(crate) fields: Vec, 19 | placeholders: String, 20 | queries: Vec, 21 | } 22 | 23 | impl Processor { 24 | /// Given the fields to keep track of and the respective queries, return a new Processor. 25 | fn new(fields: Vec, queries: Vec) -> Result { 26 | Ok(Processor { 27 | columns: fields.join(", "), 28 | conn: Connection::open_in_memory()?, 29 | fields: fields.clone(), 30 | placeholders: fields 31 | .iter() 32 | .map(|f| format!(":{}", f)) 33 | .collect::>() 34 | .join(", "), 35 | queries, 36 | }) 37 | } 38 | 39 | /// After establishing a new connection, create the table and indexes we need. 40 | fn initialize(&self) -> Result<()> { 41 | let create_stmt = format!("CREATE TABLE log ({})", self.columns); 42 | debug!("create table statement: {}", create_stmt); 43 | self.conn.execute(&create_stmt, params![])?; 44 | 45 | for (i, field) in self.fields.iter().enumerate() { 46 | let index_stmt = format!( 47 | "CREATE INDEX log_idx{i} on log ({field})", 48 | i = i, 49 | field = field 50 | ); 51 | debug!("create index statement: {}", index_stmt); 52 | self.conn.execute(&index_stmt, params![])?; 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | /// Insert all of the given records into the database. 59 | pub(crate) fn process( 60 | &self, 61 | records: Vec)>>, 62 | ) -> Result<()> { 63 | let insert_stmt = format!( 64 | "INSERT INTO LOG ({columns}) VALUES ({placeholders})", 65 | columns = self.columns, 66 | placeholders = self.placeholders 67 | ); 68 | debug!("insert records statement: {}", insert_stmt); 69 | 70 | let mut stmt = self.conn.prepare_cached(&insert_stmt)?; 71 | for record in records { 72 | stmt.execute( 73 | &*record 74 | .iter() 75 | .map(|r| (r.0.as_str(), &r.1 as &dyn ToSql)) 76 | .collect::>(), 77 | )?; 78 | } 79 | 80 | Ok(()) 81 | } 82 | 83 | /// Run the queries as specified by the user. 84 | pub(crate) fn report(&self, save_cursor: bool) -> Result<()> { 85 | for query in &self.queries { 86 | debug!("report query: {}", query); 87 | 88 | let mut stmt = self.conn.prepare_cached(query)?; 89 | let columns = stmt 90 | .column_names() 91 | .iter() 92 | .map(|c| c.to_string()) 93 | .collect::>(); 94 | let col_count = stmt.column_count(); 95 | let rows = stmt.query_map(params![], |r| { 96 | let mut row = Vec::with_capacity(col_count); 97 | for i in 0..col_count { 98 | row.push(r.get_ref(i)?.into()); 99 | } 100 | 101 | let columns = columns.clone(); 102 | Ok(QueryResult { columns, row }) 103 | })?; 104 | 105 | let stdout = io::stdout(); 106 | let mut tw = TabWriter::new(stdout.lock()); 107 | let mut wrote_headers = false; 108 | for r in rows { 109 | let r = r?; 110 | 111 | if !wrote_headers { 112 | writeln!(&mut tw, "{}", r.columns.join("\t"))?; 113 | wrote_headers = true; 114 | } 115 | 116 | for val in r.row { 117 | match val { 118 | Value::Null => write!(&mut tw, "null\t")?, 119 | Value::Integer(i) => write!(&mut tw, "{}\t", i)?, 120 | Value::Real(r) => write!(&mut tw, "{:.2}\t", r)?, 121 | Value::Text(t) => write!(&mut tw, "{}\t", t)?, 122 | Value::Blob(b) => write!(&mut tw, "{}\t", String::from_utf8(b)?)?, 123 | } 124 | } 125 | writeln!(&mut tw)?; 126 | } 127 | tw.flush()?; 128 | } 129 | 130 | // Restore our original cursor position only in tail mode. 131 | if save_cursor { 132 | execute!(io::stdout(), RestorePosition)?; 133 | } 134 | 135 | Ok(()) 136 | } 137 | } 138 | 139 | /// This represents a generic query result with column names and a row as a result. 140 | #[derive(Debug)] 141 | pub(crate) struct QueryResult { 142 | columns: Vec, 143 | row: Vec, 144 | } 145 | 146 | pub(crate) fn generate_processor( 147 | opts: &Options, 148 | fields: Option>, 149 | queries: Option>, 150 | ) -> Result { 151 | let mut log_fields; 152 | match fields { 153 | Some(f) => log_fields = f, 154 | None => { 155 | log_fields = vec![ 156 | String::from(super::STATUS_TYPE), 157 | String::from(super::BYTES_SENT), 158 | ]; 159 | if !log_fields.contains(&opts.group_by) { 160 | log_fields.push(opts.group_by.clone()); 161 | } 162 | } 163 | } 164 | 165 | let default_summary_query = format!( 166 | "SELECT count(1) AS count, 167 | AVG(bytes_sent) as avg_bytes_sent, 168 | COUNT(CASE WHEN status_type = 2 THEN 1 END) AS '2XX', 169 | COUNT(CASE WHEN status_type = 3 THEN 1 END) AS '3XX', 170 | COUNT(CASE WHEN status_type = 4 THEN 1 END) AS '4XX', 171 | COUNT(CASE WHEN status_type = 5 THEN 1 END) AS '5XX' 172 | FROM log 173 | ORDER BY {order_by} DESC 174 | LIMIT {limit};", 175 | order_by = opts.order_by, 176 | limit = opts.limit 177 | ); 178 | 179 | let default_detailed_query = format!( 180 | "SELECT {group_by}, 181 | COUNT(1) AS count, 182 | AVG(bytes_sent) AS avg_bytes_sent, 183 | COUNT(CASE WHEN status_type = 2 THEN 1 END) AS '2XX', 184 | COUNT(CASE WHEN status_type = 3 THEN 1 END) AS '3XX', 185 | COUNT(CASE WHEN status_type = 4 THEN 1 END) AS '4XX', 186 | COUNT(CASE WHEN status_type = 5 THEN 1 END) AS '5XX' 187 | FROM log 188 | GROUP BY {group_by} 189 | HAVING {having_opt} 190 | ORDER BY {order_by} DESC 191 | LIMIT {limit};", 192 | group_by = opts.group_by, 193 | having_opt = opts.having, 194 | order_by = opts.order_by, 195 | limit = opts.limit 196 | ); 197 | 198 | let log_queries = match queries { 199 | Some(q) => q, 200 | None => vec![default_summary_query, default_detailed_query], 201 | }; 202 | 203 | let p = Processor::new(log_fields, log_queries)?; 204 | p.initialize()?; 205 | 206 | Ok(p) 207 | } 208 | --------------------------------------------------------------------------------