Repository: orhun/systeroid (language: rust)

Issue:
Verbose logging for TUI
**Is your feature request related to a problem? Please describe.**
Printing to stdout is not possible while running TUI since the output is locked by the terminal backend (`termion`). This causes [verbose logging](https://github.com/orhun/systeroid/blob/17c1ea61f958ad39fcd3d40b312243d6e7a84996/systeroid-core/src/sysctl/controller.rs#L39) to be suppressed.

**Describe the solution you'd like**
Add a new command-line flag named `--verbose` and somehow enable verbose logging while running the TUI.

**Describe alternatives you've considered**
\-

**Additional context**
This will be useful when some of the parameters cannot be retrieved due to insufficient permissions and error logs are printed. Right now there is no way of telling when the parameter list is fetched without errors when the TUI is launched.



Discussion / hints:
Logging to the terminal when a TUI is rendered might cause rendering issues. Have you considered the following alternatives:
- Logging to a log file.
- Logging to the `syslog`.
- Tearing down the TUI right before printing to `stderr`.

Thank you for the ideas @koutheir!

Another alternative would be using [tui-logger](https://github.com/gin66/tui-logger).
Logging inside TUI would make the logs unavailable for further analysis at a later date, which is a very common use case for logs.
What do you suggest here? @koutheir 
I suggest saving the log entries to a log file that can be analyzed after the fact.

Fix patch:
diff --git a/.cargo/audit.toml b/.cargo/audit.toml
new file mode 100644
index 0000000..0e45b63
--- /dev/null
+++ b/.cargo/audit.toml
@@ -0,0 +1,8 @@
+[advisories]
+ignore = [
+  "RUSTSEC-2020-0071", # `chrono` - Potential segfault in the time crate
+]
+
+[output]
+quiet = false
+deny = ["warnings"]
diff --git a/Cargo.lock b/Cargo.lock
index 8cf928c..3109bef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -28,6 +28,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -67,6 +76,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bumpalo"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+
 [[package]]
 name = "byteorder"
 version = "1.4.3"
@@ -79,12 +94,33 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
 
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "chrono"
+version = "0.4.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "time",
+ "wasm-bindgen",
+ "winapi",
+]
+
 [[package]]
 name = "clipboard-win"
 version = "3.1.1"
@@ -95,6 +131,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
 [[package]]
 name = "colored"
 version = "2.0.0"
@@ -138,6 +184,12 @@ dependencies = [
  "x11-clipboard",
 ]
 
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
 [[package]]
 name = "crc32fast"
 version = "1.3.2"
@@ -190,6 +242,50 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "cxx"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
 [[package]]
 name = "dirs-next"
 version = "2.0.0"
@@ -250,6 +346,40 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
 [[package]]
 name = "flate2"
 version = "1.0.25"
@@ -266,6 +396,15 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "gethostname"
 version = "0.2.3"
@@ -293,7 +432,7 @@ checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
 ]
 
 [[package]]
@@ -353,6 +492,42 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
 [[package]]
 name = "ignore"
 version = "0.4.20"
@@ -370,12 +545,44 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
 
+[[package]]
+name = "js-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+dependencies = [
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "lazy-bytes-cast"
 version = "5.0.1"
@@ -404,6 +611,31 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "link-cplusplus"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c"
+
+[[package]]
+name = "lock_api"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
 [[package]]
 name = "log"
 version = "0.4.17"
@@ -492,6 +724,25 @@ dependencies = [
  "minimal-lexical",
 ]
 
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "num_cpus"
 version = "1.15.0"
@@ -553,6 +804,29 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys 0.45.0",
+]
+
 [[package]]
 name = "parseit"
 version = "0.1.2"
@@ -680,6 +954,20 @@ dependencies = [
  "ordered-multimap",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.37.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.13"
@@ -707,6 +995,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
+[[package]]
+name = "scratch"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
+
 [[package]]
 name = "serde"
 version = "1.0.160"
@@ -812,7 +1106,9 @@ dependencies = [
 name = "systeroid"
 version = "0.3.2"
 dependencies = [
+ "env_logger",
  "getopts",
+ "log",
  "parseit",
  "systeroid-core",
 ]
@@ -824,6 +1120,7 @@ dependencies = [
  "colored",
  "dirs-next",
  "lazy_static",
+ "log",
  "parseit",
  "rayon",
  "rust-ini",
@@ -840,13 +1137,24 @@ dependencies = [
  "colorsys",
  "copypasta-ext",
  "getopts",
+ "log",
  "ratatui",
  "systeroid-core",
  "termion",
  "thiserror",
+ "tui-logger",
  "unicode-width",
 ]
 
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "termion"
 version = "2.0.1"
@@ -889,6 +1197,31 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "time"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "tui-logger"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9e708d73f97e548b2d6e75f9931aff915a5b8bbad8fc7c7a63273994af6b5b"
+dependencies = [
+ "chrono",
+ "fxhash",
+ "lazy_static",
+ "log",
+ "parking_lot",
+ "ratatui",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.8"
@@ -923,12 +1256,72 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
 [[package]]
 name = "wayland-client"
 version = "0.29.5"
@@ -1053,6 +1446,147 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
 [[package]]
 name = "x11-clipboard"
 version = "0.7.1"
diff --git a/Cargo.toml b/Cargo.toml
index 56d829c..2654e07 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ members = ["systeroid-core", "systeroid-tui", "systeroid"]
 
 [workspace.dependencies]
 parseit = { version = "0.1.2", features = ["gzip"] }
+log = { version = "0.4.17", features = ["std"] }
 
 [profile.dev]
 opt-level = 0
diff --git a/Dockerfile b/Dockerfile
index fcad436..d53e0da 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM rust:1.66.1-alpine3.17 as builder
+FROM rust:1.69.0-alpine3.17 as builder
 WORKDIR /app
 RUN apk update
 RUN apk add --no-cache musl-dev bash git
diff --git a/README.md b/README.md
index a58613c..0b5e226 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,7 @@ Although **systeroid** does not need the parameter section to be specified expli
     - [Loading values from the system directories](#loading-values-from-the-system-directories)
     - [Searching parameters](#searching-parameters)
     - [Showing information about parameters](#showing-information-about-parameters)
+    - [Verbose logging](#verbose-logging)
 - [TUI](#tui)
   - [Usage](#usage-2)
   - [Key Bindings](#key-bindings)
@@ -93,6 +94,7 @@ Although **systeroid** does not need the parameter section to be specified expli
     - [Changing the colors](#changing-the-colors)
     - [Viewing the parameter documentation](#viewing-the-parameter-documentation)
     - [Setting the refresh rate](#setting-the-refresh-rate)
+    - [Logging](#logging)
 - [Configuration](#configuration)
 - [Resources](#resources)
   - [References](#references)
@@ -398,6 +400,20 @@ It is also possible to retrieve information about multiple parameters:
 systeroid -E --pattern '.*ipv4.*' --no-pager
 ```
 
+#### Verbose logging
+
+`--verbose` flag can be used to enable verbose logging:
+
+```sh
+systeroid --verbose
+```
+
+Also, `RUST_LOG` environment variable can be set accordingly to filter based on different log levels.
+
+```sh
+RUST_LOG=trace systeroid
+```
+
 ## TUI
 
 ### Usage
@@ -440,6 +456,7 @@ systeroid-tui [options]
 | <kbd>enter</kbd>                                           | select / set parameter value |
 | <kbd>s</kbd>                                               | save parameter value         |
 | <kbd>c</kbd>                                               | copy to clipboard            |
+| <kbd>ctrl-l</kbd>, <kbd>f2</kbd>                           | show logs                    |
 | <kbd>r</kbd>, <kbd>f5</kbd>                                | refresh                      |
 | <kbd>esc</kbd>                                             | cancel / exit                |
 | <kbd>q</kbd>, <kbd>ctrl-c/ctrl-d</kbd>                     | exit                         |
@@ -524,15 +541,16 @@ Press <kbd>:</kbd> to open the command prompt for running a command. Available c
 
 | Command                               | Description                                                                                                                   |
 | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
-| `:help`                               | Show help                                                                                                                     |
-| `:search`                             | Enable search                                                                                                                 |
-| `:select`                             | Select the current parameter in the list                                                                                      |
-| `:set <name> <value>`                 | Set parameter value                                                                                                           |
-| `:save <name> <value>`                | Save parameter value to file                                                                                                  |
-| `:scroll [area] [direction] <amount>` | Scroll the list or text<br>- areas: `list`, `docs`, `section`<br>- directions: `up`, `down`, `top`, `bottom`, `right`, `left` |
-| `:copy`                               | Copy to clipboard                                                                                                             |
-| `:refresh`                            | Refresh values                                                                                                                |
-| `:quit`, `:q`                         | Quit                                                                                                                          |
+| `:help`                               | show help                                                                                                                     |
+| `:search`                             | enable search                                                                                                                 |
+| `:select`                             | select the current parameter in the list                                                                                      |
+| `:set <name> <value>`                 | set parameter value                                                                                                           |
+| `:save <name> <value>`                | save parameter value to file                                                                                                  |
+| `:scroll [area] [direction] <amount>` | scroll the list or text<br>- areas: `list`, `docs`, `section`<br>- directions: `up`, `down`, `top`, `bottom`, `right`, `left` |
+| `:copy`                               | copy to clipboard                                                                                                             |
+| `:logs`                               | show logs                                                                                                                     |
+| `:refresh`                            | refresh values                                                                                                                |
+| `:quit`, `:q`                         | quit                                                                                                                          |
 
 #### Copying to clipboard
 
@@ -570,6 +588,55 @@ It is possible to specify a value in milliseconds via `--tick-rate` argument for
 systeroid-tui --tick-rate 500
 ```
 
+#### Logging
+
+To view the log messages, press <kbd>ctrl-l</kbd>. It will bring up a pane in the TUI for analyzing the logs:
+
+![Logs](assets/systeroid-tui-logs.gif)
+
+This pane consists of two parts. Left is the target selector and on the right side the logging messages view scrolling up.
+
+The target selector controls:
+
+- Capturing of log messages by the logger.
+- Selection of levels for display in the logging message view.
+
+The two columns have the following meaning:
+
+- Code `EWIDT`: `E` stands for Error, `W` for Warn, and similarly Info, Debug and Trace.
+  - Inverted characters (EWIDT) are enabled log levels in the view.
+  - Normal characters show enabled capturing of a log level per target.
+  - If any of EWIDT are not shown, then the respective log level is not captured.
+
+This logger pane has the following key bindings and they are only activated while the logs are being shown:
+
+| Key                 | Action                                                         |
+| ------------------- | -------------------------------------------------------------- |
+| <kbd>h</kbd>        | toggles target selector widget hidden/visible                  |
+| <kbd>f</kbd>        | toggle focus on the selected target only                       |
+| <kbd>up</kbd>       | select previous target in target selector widget               |
+| <kbd>down</kbd>     | select next target in target selector widget                   |
+| <kbd>left</kbd>     | reduce SHOWN (!) log messages by one level                     |
+| <kbd>right</kbd>    | increase SHOWN (!) log messages by one level                   |
+| <kbd>-</kbd>        | reduce CAPTURED (!) log messages by one level                  |
+| <kbd>+</kbd>        | increase CAPTURED (!) log messages by one level                |
+| <kbd>pageup</kbd>   | enter Page Mode and scroll approx. half page up in log history |
+| <kbd>pagedown</kbd> | only in page mode: scroll 10 events down in log history        |
+| <kbd>escape</kbd>   | exit page mode and go back to scrolling mode                   |
+| <kbd>space</kbd>    | toggles hiding of targets, which have logfilter set to off     |
+
+For saving the logs to a file, you can use the `--log-file` argument:
+
+```sh
+systeroid-tui --log-file systeroid.log
+```
+
+`RUST_LOG` environment variable can be used to set the log level accordingly.
+
+```sh
+RUST_LOG=debug systeroid-tui
+```
+
 ## Configuration
 
 **systeroid** can be configured with a configuration file that uses the [INI format](https://en.wikipedia.org/wiki/INI_file). It can be specified via `--config` or `SYSTEROID_CONFIG` environment variable. It can also be placed in one of the following global locations:
diff --git a/assets/systeroid-tui-logs.gif b/assets/systeroid-tui-logs.gif
new file mode 100644
index 0000000..53e038b
Binary files /dev/null and b/assets/systeroid-tui-logs.gif differ
diff --git a/config/systeroid.conf b/config/systeroid.conf
index bd37ea5..40f9f57 100644
--- a/config/systeroid.conf
+++ b/config/systeroid.conf
@@ -17,8 +17,6 @@ display_deprecated = false
 kernel_docs = "/usr/share/doc/linux"
 
 [cli]
-; enable verbose logging
-verbose = false
 ; ignore unknown variable errors
 ignore_errors = true
 ; do not print variable after the value is set
@@ -59,6 +57,8 @@ tick_rate = 250
 no_docs = true
 ; path for saving the changed kernel parameters
 save_path = "/etc/sysctl.conf"
+; file to save the logs
+;log_file = "systeroid.log"
 
 [tui.colors]
 ; available colors are defined in https://docs.rs/tui/latest/tui/style/enum.Color.html
diff --git a/man8/systeroid-tui.8 b/man8/systeroid-tui.8
index eb7d241..41e8d86 100644
--- a/man8/systeroid-tui.8
+++ b/man8/systeroid-tui.8
@@ -23,6 +23,9 @@ Use this option to set a custom path for the kernel documentation.
 \fB\-\-save\-path\fR <path>
 Use this option to set the path for saving the changed parameters.
 .TP
+\fB\-l\fR, \fB\-\-log\-file\fR <path>
+Use this option to set the file to save the logs.
+.TP
 \fB\-s\fR, \fB\-\-section\fR <section>
 Use this option to set the section to filter.
 .HP
@@ -61,6 +64,8 @@ systeroid-tui \-D /usr/share/doc/linux
 .br
 systeroid-tui \-\-save-path 99-local.conf
 .br
+systeroid-tui \-\-log-file systeroid.log
+.br
 systeroid-tui -n
 
 .SH KEY BINDINGS
diff --git a/systeroid-core/Cargo.toml b/systeroid-core/Cargo.toml
index b36f342..18bdb7d 100644
--- a/systeroid-core/Cargo.toml
+++ b/systeroid-core/Cargo.toml
@@ -12,6 +12,8 @@ edition = "2021"
 rust-version = "1.64.0"
 
 [dependencies]
+parseit.workspace = true
+log.workspace = true
 sysctl = "0.5.4"
 thiserror = "1.0.40"
 lazy_static = "1.4.0"
@@ -20,5 +22,4 @@ colored = "2.0.0"
 serde = { version = "1.0.160", features = ["derive"] }
 serde_json = "1.0.96"
 dirs-next = "2.0.0"
-parseit.workspace = true
 rust-ini = "0.18.0"
diff --git a/systeroid-core/src/config.rs b/systeroid-core/src/config.rs
index f7c11f9..447f565 100644
--- a/systeroid-core/src/config.rs
+++ b/systeroid-core/src/config.rs
@@ -54,8 +54,6 @@ pub struct Config {
 /// CLI configuration.
 #[derive(Clone, Debug)]
 pub struct CliConfig {
-    /// Whether if the verbose logging is enabled.
-    pub verbose: bool,
     /// Whether if the errors should be ignored.
     pub ignore_errors: bool,
     /// Whether if the quiet mode is enabled.
@@ -88,6 +86,8 @@ pub struct TuiConfig {
     pub no_docs: bool,
     /// Path for saving the changed kernel parameters.
     pub save_path: Option<PathBuf>,
+    /// File to save the logs.
+    pub log_file: Option<String>,
     /// Color configuration.
     pub color: TuiColorConfig,
 }
@@ -116,6 +116,7 @@ impl Config {
             }
         }
         if let Some(path) = config_path {
+            log::trace!(target: "config", "Parsing configuration from {:?}", path);
             let ini = Ini::load_from_file(path)?;
             if let Some(general_section) = ini.section(Some("general")) {
                 if let Some(display_deprecated) = general_section.get("display_deprecated") {
@@ -126,7 +127,6 @@ impl Config {
                 }
             }
             if let Some(section) = ini.section(Some("cli")) {
-                parse_ini_flag!(self, cli, section, verbose);
                 parse_ini_flag!(self, cli, section, ignore_errors);
                 parse_ini_flag!(self, cli, section, quiet);
                 parse_ini_flag!(self, cli, section, no_pager);
@@ -162,6 +162,9 @@ impl Config {
                 if let Some(save_path) = section.get("save_path") {
                     self.tui.save_path = Some(PathBuf::from(save_path));
                 }
+                if let Some(log_file) = section.get("log_file") {
+                    self.tui.log_file = Some(log_file.to_string());
+                }
                 parse_ini_flag!(self, tui, section, no_docs);
             }
             if let Some(section) = ini.section(Some("tui.colors")) {
@@ -183,7 +186,6 @@ impl Default for Config {
             display_deprecated: false,
             kernel_docs: None,
             cli: CliConfig {
-                verbose: false,
                 ignore_errors: false,
                 quiet: false,
                 no_pager: false,
@@ -207,6 +209,7 @@ impl Default for Config {
                 tick_rate: 250,
                 no_docs: false,
                 save_path: None,
+                log_file: None,
                 color: TuiColorConfig {
                     fg_color: String::from("white"),
                     bg_color: String::from("black"),
diff --git a/systeroid-core/src/sysctl/controller.rs b/systeroid-core/src/sysctl/controller.rs
index 2705972..00b02ab 100644
--- a/systeroid-core/src/sysctl/controller.rs
+++ b/systeroid-core/src/sysctl/controller.rs
@@ -51,9 +51,7 @@ impl Sysctl {
                     }
                 }
                 Err(e) => {
-                    if config.cli.verbose {
-                        eprintln!("{} ({})", e, ctl.name()?);
-                    }
+                    log::trace!(target: "sysctl", "{} ({})", e, ctl.name()?);
                 }
             }
         }
@@ -68,6 +66,7 @@ impl Sysctl {
 
     /// Returns the parameters that matches the given query.
     pub fn get_parameters(&self, query: &str) -> Vec<&Parameter> {
+        log::trace!(target: "sysctl", "Querying parameters: {:?}", query);
         let query = query.replace('/', ".");
         let parameters = self
             .parameters
@@ -79,7 +78,8 @@ impl Sysctl {
             })
             .collect::<Vec<&Parameter>>();
         if parameters.is_empty() && !self.config.cli.ignore_errors {
-            eprintln!(
+            log::error!(
+                target: "sysctl",
                 "{}: cannot stat {}{}: No such file or directory",
                 env!("CARGO_PKG_NAME").split('-').collect::<Vec<_>>()[0],
                 PROC_PATH,
@@ -91,6 +91,7 @@ impl Sysctl {
 
     /// Updates the descriptions of the kernel parameters using the given cached data.
     pub fn update_docs_from_cache(&mut self, cache: &Cache) -> Result<()> {
+        log::trace!(target: "cache", "{:?}", cache);
         let mut kernel_docs_path = if let Some(path) = &self.config.kernel_docs {
             vec![path.to_path_buf()]
         } else {
@@ -109,6 +110,7 @@ impl Sysctl {
         }
         if let Some(path) = kernel_docs_path.iter().find(|path| path.exists()) {
             if cache.exists(PARAMETERS_CACHE_LABEL) {
+                log::trace!(target: "cache", "Cache hit for {:?}", path);
                 let cache_data = cache.read(PARAMETERS_CACHE_LABEL)?;
                 if cache_data.timestamp == CacheData::<()>::get_timestamp(path)? {
                     self.update_params(cache_data.data);
@@ -117,13 +119,14 @@ impl Sysctl {
             }
             self.update_docs(path)?;
             if env::var(DISABLE_CACHE_ENV).is_err() {
+                log::trace!(target: "cache", "Writing cache to {:?}", cache);
                 cache.write(
                     CacheData::new(&self.parameters, path)?,
                     PARAMETERS_CACHE_LABEL,
                 )?;
             }
         } else {
-            eprintln!("warning: `Linux kernel documentation cannot be found. Please specify a path via '-D' argument`");
+            log::error!(target: "sysctl", "warning: `Linux kernel documentation cannot be found. Please specify a path via '-D' argument`");
         }
         Ok(())
     }
@@ -146,6 +149,7 @@ impl Sysctl {
 
     /// Updates the descriptions of the kernel parameters.
     fn update_docs(&mut self, kernel_docs: &Path) -> Result<()> {
+        log::trace!(target: "sysctl", "Parsing the kernel documentation from {:?}", kernel_docs);
         let documents = parse_kernel_docs(kernel_docs)?;
         self.parameters
             .par_iter_mut()
@@ -187,6 +191,13 @@ impl Sysctl {
         let save_path = save_path
             .clone()
             .unwrap_or_else(|| PathBuf::from(DEFAULT_PRELOAD));
+        log::trace!(
+            target: "param",
+            "Writing the new value ({:?}) of {:?} to {:?}",
+            new_value,
+            param_name,
+            save_path
+        );
         let data = format!("{param_name} = {new_value}");
         if save_path.exists() {
             let contents = reader::read_to_string(&save_path)?;
diff --git a/systeroid-core/src/sysctl/parameter.rs b/systeroid-core/src/sysctl/parameter.rs
index b8ac014..7e72cf5 100644
--- a/systeroid-core/src/sysctl/parameter.rs
+++ b/systeroid-core/src/sysctl/parameter.rs
@@ -188,6 +188,7 @@ impl Parameter {
         config: &Config,
         output: &mut Output,
     ) -> Result<()> {
+        log::trace!(target: "param", "Setting the value of {:?} to {:?}", self.name, new_value);
         let ctl = Ctl::new(&self.name)?;
         let new_value = ctl.set_value_string(new_value)?;
         self.value = new_value;
diff --git a/systeroid-tui/Cargo.toml b/systeroid-tui/Cargo.toml
index 1c0f2aa..b8d24b7 100644
--- a/systeroid-tui/Cargo.toml
+++ b/systeroid-tui/Cargo.toml
@@ -10,7 +10,7 @@ repository = "https://github.com/orhun/systeroid"
 keywords = ["linux", "kernel", "parameter", "sysctl", "tui"]
 categories = ["command-line-utilities"]
 edition = "2021"
-rust-version = "1.64.0"
+rust-version = "1.68.2"
 
 [features]
 # clipboard support is enabled as default
@@ -24,6 +24,7 @@ thiserror = "1.0.40"
 getopts = "0.2.21"
 copypasta-ext = { version = "0.4.4", optional = true }
 colorsys = "0.6.7"
+log.workspace = true
 
 [dependencies.systeroid-core]
 version = "0.3.2" # managed by release.sh
@@ -35,6 +36,11 @@ version = "0.20.1"
 default-features = false
 features = ["termion"]
 
+[dependencies.tui-logger]
+version = "0.9.0"
+default-features = false
+features = ["ratatui-support"]
+
 # metadata for cargo-binstall to get the right artifacts
 [package.metadata.binstall]
 pkg-url = "{ repo }/releases/download/v{ version }/systeroid-{ version }-{ target }.{ archive-format }"
diff --git a/systeroid-tui/src/app.rs b/systeroid-tui/src/app.rs
index 6c0b8fb..69f83b2 100644
--- a/systeroid-tui/src/app.rs
+++ b/systeroid-tui/src/app.rs
@@ -4,11 +4,13 @@ use crate::options::{CopyOption, Direction, ScrollArea};
 use crate::widgets::SelectableList;
 #[cfg(feature = "clipboard")]
 use copypasta_ext::{display::DisplayServer, ClipboardProviderExt};
+use log::{Level, LevelFilter};
 use std::str::FromStr;
 use std::time::Instant;
 use systeroid_core::sysctl::controller::Sysctl;
 use systeroid_core::sysctl::parameter::Parameter;
 use systeroid_core::sysctl::section::Section;
+use tui_logger::TuiWidgetState;
 use unicode_width::UnicodeWidthStr;
 
 /// Representation of a key binding.
@@ -87,6 +89,11 @@ pub const KEY_BINDINGS: &[&KeyBinding] = &[
         action: "copy to clipboard",
         command: None,
     },
+    &KeyBinding {
+        key: "ctrl-l, f2",
+        action: "show logs",
+        command: Some("logs"),
+    },
     &KeyBinding {
         key: "r, f5",
         action: "refresh",
@@ -113,6 +120,10 @@ pub struct App<'a> {
     pub running: bool,
     /// Whether if the help message is shown.
     pub show_help: bool,
+    /// Whether if the logs are shown.
+    pub show_logs: bool,
+    /// Logger state.
+    pub logger_state: TuiWidgetState,
     /// Input buffer.
     pub input: Option<String>,
     /// Time tracker for measuring the time for clearing the input.
@@ -144,6 +155,8 @@ impl<'a> App<'a> {
         let mut app = Self {
             running: true,
             show_help: false,
+            show_logs: false,
+            logger_state: TuiWidgetState::new().set_default_display_level(LevelFilter::Trace),
             input: None,
             input_time: None,
             input_cursor: 0,
@@ -167,16 +180,16 @@ impl<'a> App<'a> {
         app.parameter_list.items = app.sysctl.parameters.clone();
         #[cfg(feature = "clipboard")]
         {
-            app.clipboard = match DisplayServer::select().try_context() {
-                None => {
-                    app.input = Some(String::from(
+            app.clipboard =
+                match DisplayServer::select().try_context() {
+                    None => {
+                        app.log(Level::Error, String::from(
                         "Failed to initialize clipboard, no suitable clipboard provider found",
                     ));
-                    app.input_time = Some(Instant::now());
-                    None
+                        None
+                    }
+                    clipboard => clipboard,
                 }
-                clipboard => clipboard,
-            }
         }
         app
     }
@@ -186,6 +199,13 @@ impl<'a> App<'a> {
         self.input.is_some() && self.input_time.is_none()
     }
 
+    /// Sets the log message for the application.
+    pub fn log(&mut self, level: Level, message: String) {
+        log::log!(target: "tui", level, "{message}");
+        self.input = Some(message);
+        self.input_time = Some(Instant::now());
+    }
+
     /// Performs a search operation in the kernel parameter list.
     pub fn search(&mut self) {
         let section = self
@@ -228,7 +248,8 @@ impl<'a> App<'a> {
     /// Copies the selected entry to the clipboard.
     #[cfg(feature = "clipboard")]
     fn copy_to_clipboard(&mut self, copy_option: CopyOption) -> Result<()> {
-        self.input = Some(if let Some(clipboard) = self.clipboard.as_mut() {
+        log::debug!(target: "tui", "Copying to clipboard: {:?}", copy_option);
+        if let Some(clipboard) = self.clipboard.as_mut() {
             if let Some(parameter) = self.parameter_list.selected() {
                 match copy_option {
                     CopyOption::Name => clipboard.set_contents(parameter.name.clone()),
@@ -238,22 +259,23 @@ impl<'a> App<'a> {
                     }
                 }
                 .map_err(|e| crate::error::Error::ClipboardError(e.to_string()))?;
-                String::from("Copied to clipboard!")
+                self.log(Level::Info, String::from("Copied to clipboard!"));
             } else {
-                String::from("No parameter is selected")
+                self.log(Level::Warn, String::from("No parameter is selected"));
             }
         } else {
-            String::from("Clipboard is not initialized")
-        });
-        self.input_time = Some(Instant::now());
+            self.log(Level::Error, String::from("Clipboard is not initialized"));
+        }
         Ok(())
     }
 
     /// Shows a message about clipboard being not enabled.
     #[cfg(not(feature = "clipboard"))]
     fn copy_to_clipboard(&mut self, _: CopyOption) -> Result<()> {
-        self.input = Some(String::from("Clipboard support is not enabled"));
-        self.input_time = Some(Instant::now());
+        self.log(
+            Level::Warn,
+            String::from("Clipboard support is not enabled"),
+        );
         Ok(())
     }
 
@@ -268,6 +290,12 @@ impl<'a> App<'a> {
                 self.show_help = true;
                 hide_popup = false;
             }
+            Command::Logs => {
+                self.show_logs = !self.show_logs;
+            }
+            Command::LoggerEvent(event) => {
+                self.logger_state.transition(&event.0);
+            }
             Command::Select => {
                 if let Some(copy_option) = self
                     .options
@@ -312,8 +340,7 @@ impl<'a> App<'a> {
                             self.run_command(Command::Refresh)?;
                         }
                         Err(e) => {
-                            self.input = Some(e.to_string());
-                            self.input_time = Some(Instant::now());
+                            self.log(Level::Error, e.to_string());
                         }
                     }
                     if save_to_file {
@@ -323,17 +350,15 @@ impl<'a> App<'a> {
                             &self.sysctl.config.tui.save_path,
                         ) {
                             Ok(path) => {
-                                self.input = Some(format!("Saved to file: {path:?}"));
+                                self.log(Level::Info, format!("Saved to file: {path:?}"));
                             }
                             Err(e) => {
-                                self.input = Some(format!("Failed to save: {e}"));
+                                self.log(Level::Error, format!("Failed to save: {e}"));
                             }
                         }
-                        self.input_time = Some(Instant::now());
                     }
                 } else {
-                    self.input = Some(String::from("Unknown parameter"));
-                    self.input_time = Some(Instant::now());
+                    self.log(Level::Warn, String::from("Unknown parameter"));
                 }
             }
             Command::Scroll(ScrollArea::List, Direction::Up, amount) => {
@@ -442,8 +467,7 @@ impl<'a> App<'a> {
                         self.run_command(command)?;
                         hide_popup = false;
                     } else {
-                        self.input = Some(String::from("Unknown command"));
-                        self.input_time = Some(Instant::now());
+                        self.log(Level::Warn, String::from("Unknown command"));
                     }
                 }
             }
@@ -516,8 +540,7 @@ impl<'a> App<'a> {
                     hide_popup = false;
                     self.show_help = false;
                 } else {
-                    self.input = Some(String::from("No parameter is selected"));
-                    self.input_time = Some(Instant::now());
+                    self.log(Level::Warn, String::from("No parameter is selected"));
                 }
             }
             Command::Refresh => {
@@ -531,6 +554,7 @@ impl<'a> App<'a> {
                         parameter.value = param.value.to_string();
                     }
                 });
+                self.log(Level::Info, String::from("Refreshed!"));
             }
             Command::Cancel => {
                 if self.input.is_some() {
diff --git a/systeroid-tui/src/args.rs b/systeroid-tui/src/args.rs
index 1ada09c..6cb2412 100644
--- a/systeroid-tui/src/args.rs
+++ b/systeroid-tui/src/args.rs
@@ -26,6 +26,8 @@ pub struct Args {
     pub kernel_docs: Option<PathBuf>,
     /// Path for the changed parameters.
     pub save_path: Option<PathBuf>,
+    /// File to save the logs.
+    pub log_file: Option<String>,
     /// Sysctl section to filter.
     pub section: Option<Section>,
     /// Query to search on startup.
@@ -62,6 +64,7 @@ impl Args {
             "set the path for saving the changed parameters",
             "<path>",
         );
+        opts.optopt("l", "log-file", "set the file to save the logs", "<path>");
         opts.optopt("s", "section", "set the section to filter", "<section>");
         opts.optopt("q", "query", "set the query to search", "<query>");
         opts.optopt(
@@ -123,6 +126,7 @@ impl Args {
                     .or_else(|| env::var(KERNEL_DOCS_ENV).ok())
                     .map(PathBuf::from),
                 save_path: matches.opt_str("save-path").map(PathBuf::from),
+                log_file: matches.opt_str("l"),
                 section: matches.opt_str("s").map(Section::from),
                 search_query: matches.opt_str("q"),
                 fg_color: matches
diff --git a/systeroid-tui/src/command.rs b/systeroid-tui/src/command.rs
index 2518689..f274e2c 100644
--- a/systeroid-tui/src/command.rs
+++ b/systeroid-tui/src/command.rs
@@ -1,12 +1,44 @@
 use crate::options::{Direction, ScrollArea};
 use std::str::FromStr;
 use termion::event::Key;
+use tui_logger::TuiWidgetEvent;
+
+/// Possible logger widget commands.
+#[derive(Debug, PartialEq)]
+pub struct LoggerCommand(pub TuiWidgetEvent);
+
+impl Eq for LoggerCommand {}
+
+impl LoggerCommand {
+    /// Parses a logger command from the given key.
+    pub fn parse(key: Key) -> Option<Self> {
+        match key {
+            Key::Char(' ') => Some(Self(TuiWidgetEvent::SpaceKey)),
+            Key::Esc => Some(Self(TuiWidgetEvent::EscapeKey)),
+            Key::PageUp => Some(Self(TuiWidgetEvent::PrevPageKey)),
+            Key::PageDown => Some(Self(TuiWidgetEvent::NextPageKey)),
+            Key::Up => Some(Self(TuiWidgetEvent::UpKey)),
+            Key::Down => Some(Self(TuiWidgetEvent::DownKey)),
+            Key::Left => Some(Self(TuiWidgetEvent::LeftKey)),
+            Key::Right => Some(Self(TuiWidgetEvent::RightKey)),
+            Key::Char('+') => Some(Self(TuiWidgetEvent::PlusKey)),
+            Key::Char('-') => Some(Self(TuiWidgetEvent::MinusKey)),
+            Key::Char('h') => Some(Self(TuiWidgetEvent::HideKey)),
+            Key::Char('f') => Some(Self(TuiWidgetEvent::FocusKey)),
+            _ => None,
+        }
+    }
+}
 
 /// Possible application commands.
 #[derive(Debug, PartialEq, Eq)]
 pub enum Command {
     /// Show help.
     Help,
+    /// Show logs.
+    Logs,
+    /// Logger event.
+    LoggerEvent(LoggerCommand),
     /// Perform an action based on the selected entry.
     Select,
     /// Save the value of a parameter to a file.
@@ -42,6 +74,7 @@ impl FromStr for Command {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s {
             "help" => Ok(Command::Help),
+            "logs" => Ok(Command::Logs),
             "search" => Ok(Command::Search),
             "select" => Ok(Command::Select),
             "copy" => Ok(Command::Copy),
@@ -91,6 +124,7 @@ impl Command {
         } else {
             match key {
                 Key::Char('?') | Key::F(1) => Command::Help,
+                Key::Ctrl('l') | Key::F(2) => Command::Logs,
                 Key::Up | Key::Char('k') => Command::Scroll(ScrollArea::List, Direction::Up, 1),
                 Key::Down | Key::Char('j') => Command::Scroll(ScrollArea::List, Direction::Down, 1),
                 Key::PageUp => Command::Scroll(ScrollArea::List, Direction::Up, 4),
@@ -135,6 +169,7 @@ mod tests {
     fn test_command() {
         for (command, value) in vec![
             (Command::Help, "help"),
+            (Command::Logs, "logs"),
             (Command::Search, "search"),
             (Command::Select, "select"),
             (Command::Copy, "copy"),
@@ -181,6 +216,7 @@ mod tests {
         assert_command_parser! {
             input_mode: false,
             Key::Char('?') => Command::Help,
+            Key::Ctrl('l') => Command::Logs,
             Key::Up => Command::Scroll(ScrollArea::List, Direction::Up, 1),
             Key::Down => Command::Scroll(ScrollArea::List, Direction::Down, 1),
             Key::PageUp => Command::Scroll(ScrollArea::List, Direction::Up, 4),
diff --git a/systeroid-tui/src/error.rs b/systeroid-tui/src/error.rs
index 9bd4f18..84ad58f 100644
--- a/systeroid-tui/src/error.rs
+++ b/systeroid-tui/src/error.rs
@@ -15,6 +15,12 @@ pub enum Error {
     /// Error that may occur while parsing a color.
     #[error(transparent)]
     ColorParseError(#[from] colorsys::ParseError),
+    /// Error that may occur if the logger is already set.
+    #[error(transparent)]
+    LoggerSetError(#[from] log::SetLoggerError),
+    /// Error that may occur when the string doesn’t match any of the log levels.
+    #[error(transparent)]
+    LoggerParseError(#[from] log::ParseLevelError),
     /// Error that may occur in the core library.
     #[error(transparent)]
     SysctlError(#[from] systeroid_core::error::Error),
diff --git a/systeroid-tui/src/lib.rs b/systeroid-tui/src/lib.rs
index dadad9d..8044c27 100644
--- a/systeroid-tui/src/lib.rs
+++ b/systeroid-tui/src/lib.rs
@@ -27,6 +27,10 @@ use crate::command::Command;
 use crate::error::Result;
 use crate::event::{Event, EventHandler};
 use crate::style::Colors;
+use command::LoggerCommand;
+use log::LevelFilter;
+use std::env;
+use std::str::FromStr;
 use systeroid_core::cache::Cache;
 use systeroid_core::config::Config;
 use systeroid_core::sysctl::controller::Sysctl;
@@ -42,11 +46,22 @@ pub fn run<B: Backend>(args: Args, backend: B) -> Result<()> {
     };
     config.tui.tick_rate = args.tick_rate;
     config.tui.save_path = args.save_path;
+    config.tui.log_file = args.log_file;
     config.tui.no_docs = args.no_docs;
     config.tui.color.fg_color = args.fg_color;
     config.tui.color.bg_color = args.bg_color;
     config.parse(args.config)?;
     let colors = Colors::new(&config.tui.color.bg_color, &config.tui.color.fg_color)?;
+    tui_logger::init_logger(if let Ok(log_level) = env::var("RUST_LOG") {
+        LevelFilter::from_str(&log_level)?
+    } else {
+        LevelFilter::Trace
+    })?;
+    tui_logger::set_default_level(LevelFilter::Trace);
+    if let Some(ref log_file) = config.tui.log_file {
+        tui_logger::set_log_file(log_file)?;
+    }
+    log::trace!(target: "config", "{:?}", config);
     let mut sysctl = Sysctl::init(config)?;
     if !sysctl.config.tui.no_docs {
         sysctl.update_docs_from_cache(&Cache::init()?)?;
@@ -75,7 +90,12 @@ pub fn run<B: Backend>(args: Args, backend: B) -> Result<()> {
         terminal.draw(|frame| ui::render(frame, &mut app, &colors))?;
         match event_handler.next()? {
             Event::KeyPress(key) => {
-                let command = Command::parse(key, app.is_input_mode());
+                let mut command = Command::parse(key, app.is_input_mode());
+                if app.show_logs {
+                    command = LoggerCommand::parse(key)
+                        .map(Command::LoggerEvent)
+                        .unwrap_or(command);
+                }
                 app.run_command(command)?;
             }
             #[cfg(not(test))]
diff --git a/systeroid-tui/src/ui.rs b/systeroid-tui/src/ui.rs
index f2fd0dc..c65f6eb 100644
--- a/systeroid-tui/src/ui.rs
+++ b/systeroid-tui/src/ui.rs
@@ -3,9 +3,11 @@ use crate::style::Colors;
 use crate::widgets::SelectableList;
 use tui::backend::Backend;
 use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
+use tui::style::{Color as TuiColor, Style};
 use tui::text::{Span, Text};
 use tui::widgets::{Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table, Wrap};
 use tui::Frame;
+use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget};
 use unicode_width::UnicodeWidthStr;
 
 /// Renders the user interface.
@@ -16,35 +18,48 @@ pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App, colors: &Colo
         .and_then(|parameter| parameter.get_documentation());
     let rect = frame.size();
     let chunks = Layout::default()
-        .direction(Direction::Horizontal)
-        .constraints(if documentation.is_some() {
-            [Constraint::Percentage(50), Constraint::Percentage(50)]
+        .direction(Direction::Vertical)
+        .constraints(if app.show_logs {
+            [Constraint::Percentage(60), Constraint::Percentage(40)]
         } else {
             [Constraint::Percentage(100), Constraint::Min(0)]
         })
         .split(rect);
     {
         let chunks = Layout::default()
-            .direction(Direction::Vertical)
-            .constraints(if app.input.is_some() {
-                [Constraint::Min(rect.height - 3), Constraint::Min(3)]
+            .direction(Direction::Horizontal)
+            .constraints(if documentation.is_some() {
+                [Constraint::Percentage(50), Constraint::Percentage(50)]
             } else {
                 [Constraint::Percentage(100), Constraint::Min(0)]
             })
             .split(chunks[0]);
-        render_parameter_list(frame, chunks[0], app, colors);
-        if app.input.is_some() {
-            render_input_prompt(frame, chunks[1], rect.height - 2, app, colors);
+        {
+            let chunks = Layout::default()
+                .direction(Direction::Vertical)
+                .constraints(if app.input.is_some() {
+                    [Constraint::Min(chunks[0].height - 3), Constraint::Min(3)]
+                } else {
+                    [Constraint::Percentage(100), Constraint::Min(0)]
+                })
+                .split(chunks[0]);
+            render_parameter_list(frame, chunks[0], app, colors);
+            if app.input.is_some() {
+                render_input_prompt(frame, chunks[1], chunks[0].height + 1, app, colors);
+            }
+        }
+        if let Some(documentation) = documentation {
+            render_parameter_documentation(
+                frame,
+                chunks[1],
+                documentation,
+                &mut app.docs_scroll_amount,
+                colors,
+            );
         }
     }
-    if let Some(documentation) = documentation {
-        render_parameter_documentation(
-            frame,
-            chunks[1],
-            documentation,
-            &mut app.docs_scroll_amount,
-            colors,
-        );
+    if app.show_logs {
+        render_log_view(frame, chunks[1], app, colors);
     }
     if app.show_help {
         render_help_text(frame, rect, &mut app.key_bindings, colors);
@@ -463,3 +478,24 @@ fn render_input_prompt<B: Backend>(
         rect,
     );
 }
+
+/// Renders the log view.
+fn render_log_view<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, app: &App, colors: &Colors) {
+    let logger_widget = TuiLoggerSmartWidget::default()
+        .style_trace(Style::default().fg(TuiColor::DarkGray))
+        .style_debug(Style::default().fg(TuiColor::Blue))
+        .style_warn(Style::default().fg(TuiColor::Yellow))
+        .style_error(Style::default().fg(TuiColor::Red))
+        .style_info(Style::default().fg(TuiColor::Green))
+        .highlight_style(colors.get_fg_style())
+        .border_style(colors.get_fg_style())
+        .style(colors.get_bg_style())
+        .output_separator(':')
+        .output_timestamp(Some("%H:%M:%S".to_string()))
+        .output_level(Some(TuiLoggerLevelOutput::Long))
+        .output_target(true)
+        .output_file(true)
+        .output_line(true)
+        .state(&app.logger_state);
+    frame.render_widget(logger_widget, rect);
+}
diff --git a/systeroid/Cargo.toml b/systeroid/Cargo.toml
index c0ea1b2..50b70ab 100644
--- a/systeroid/Cargo.toml
+++ b/systeroid/Cargo.toml
@@ -10,7 +10,7 @@ repository = "https://github.com/orhun/systeroid"
 keywords = ["linux", "kernel", "parameter", "sysctl"]
 categories = ["command-line-utilities"]
 edition = "2021"
-default-run="systeroid"
+default-run = "systeroid"
 rust-version = "1.64.0"
 
 [features]
@@ -18,8 +18,10 @@ rust-version = "1.64.0"
 live-tests = []
 
 [dependencies]
-getopts = "0.2.21"
 parseit.workspace = true
+log.workspace = true
+env_logger = "0.10.0"
+getopts = "0.2.21"
 
 [dependencies.systeroid-core]
 version = "0.3.2" # managed by release.sh
diff --git a/systeroid/src/app.rs b/systeroid/src/app.rs
index b71c9bc..d30b587 100644
--- a/systeroid/src/app.rs
+++ b/systeroid/src/app.rs
@@ -95,7 +95,7 @@ impl<'a, Output: Write> App<'a, Output> {
                 }
                 Err(e) => {
                     if !pager.is_empty() {
-                        eprintln!("pager error: `{e}`");
+                        log::error!("pager error: `{e}`");
                     }
                     fallback_to_default = true;
                 }
@@ -130,7 +130,7 @@ impl<'a, Output: Write> App<'a, Output> {
             if parameters.len() == 1 {
                 let param = parameters[0];
                 if DEPRECATED_PARAMS.contains(&param.get_absolute_name().unwrap_or_default()) {
-                    eprintln!(
+                    log::error!(
                         "{}: {} is deprecated, value not set",
                         env!("CARGO_PKG_NAME"),
                         parameter
@@ -145,14 +145,14 @@ impl<'a, Output: Write> App<'a, Output> {
                     param.update_value(&new_value, &config, self.output)?;
                 }
             } else {
-                eprintln!(
+                log::error!(
                     "{}: ambiguous parameter name: {}",
                     env!("CARGO_PKG_NAME"),
                     parameter
                 );
             }
         } else if write_mode {
-            eprintln!(
+            log::error!(
                 "{}: {:?} must be in the format: name=value",
                 env!("CARGO_PKG_NAME"),
                 parameter
@@ -171,13 +171,13 @@ impl<'a, Output: Write> App<'a, Output> {
             let lines = stdin.lock().lines();
             for line in lines {
                 if let Err(e) = self.process_parameter(line?, true, false) {
-                    println!("{}: {}", env!("CARGO_PKG_NAME"), e);
+                    log::info!("{}: {}", env!("CARGO_PKG_NAME"), e);
                 }
             }
             return Ok(());
         }
         if !path.exists() {
-            eprintln!(
+            log::error!(
                 "{}: cannot open {:?}: No such file or directory",
                 env!("CARGO_PKG_NAME"),
                 path
@@ -194,7 +194,7 @@ impl<'a, Output: Write> App<'a, Output> {
             if !parameter.starts_with('-') {
                 process_result?;
             } else if let Err(e) = process_result {
-                eprintln!("{}: {}", env!("CARGO_PKG_NAME"), e);
+                log::error!("{}: {}", env!("CARGO_PKG_NAME"), e);
             }
         }
         Ok(())
@@ -208,7 +208,7 @@ impl<'a, Output: Write> App<'a, Output> {
         {
             if let Ok(glob_walker) = globwalk::glob(preload_path.to_string_lossy()) {
                 for file in glob_walker.filter_map(|v| v.ok()) {
-                    println!("* Applying {} ...", file.path().display());
+                    log::info!("* Applying {} ...", file.path().display());
                     self.preload_from_file(file.path().to_path_buf())?;
                 }
             }
diff --git a/systeroid/src/lib.rs b/systeroid/src/lib.rs
index fd6f220..1caca13 100644
--- a/systeroid/src/lib.rs
+++ b/systeroid/src/lib.rs
@@ -23,13 +23,13 @@ pub fn run<Output: Write>(args: Args, output: &mut Output) -> Result<()> {
         kernel_docs: args.kernel_docs,
         ..Default::default()
     };
-    config.cli.verbose = args.verbose;
     config.cli.ignore_errors = args.ignore_errors;
     config.cli.quiet = args.quiet;
     config.cli.no_pager = args.no_pager;
     config.cli.display_type = args.display_type;
     config.cli.output_type = args.output_type;
     config.parse(args.config)?;
+    log::trace!("{:?}", config);
     let mut sysctl = Sysctl::init(config)?;
     if args.explain {
         sysctl.update_docs_from_cache(&Cache::init()?)?;
diff --git a/systeroid/src/main.rs b/systeroid/src/main.rs
index c6a44fc..56487f0 100644
--- a/systeroid/src/main.rs
+++ b/systeroid/src/main.rs
@@ -1,10 +1,19 @@
+use env_logger::Builder as LoggerBuilder;
+use log::LevelFilter;
 use std::env;
-use std::io;
+use std::io::{self, Write};
 use std::process::{self, Command};
 use systeroid::args::Args;
 
 fn main() {
     if let Some(args) = Args::parse(env::args().collect()) {
+        let mut builder = LoggerBuilder::from_default_env();
+        if args.verbose {
+            builder.filter(None, LevelFilter::Trace);
+        }
+        builder
+            .format(|buf, record| writeln!(buf, "{}", record.args()))
+            .init();
         if args.show_tui {
             let bin = format!("{}-tui", env!("CARGO_PKG_NAME"));
             let mut command = Command::new(&bin);
@@ -17,7 +26,7 @@ fn main() {
             match command.spawn().map(|mut child| child.wait()) {
                 Ok(_) => process::exit(0),
                 Err(e) => {
-                    eprintln!("Cannot run `{bin}` ({e})");
+                    log::error!("Cannot run `{bin}` ({e})");
                     process::exit(1)
                 }
             }
@@ -26,7 +35,7 @@ fn main() {
             match systeroid::run(args, &mut stdout) {
                 Ok(_) => process::exit(0),
                 Err(e) => {
-                    eprintln!("{e}");
+                    log::error!("{e}");
                     process::exit(1)
                 }
             }


Test patch:
diff --git a/systeroid-tui/tests/integration_test.rs b/systeroid-tui/tests/integration_test.rs
index 7a5c49a..9a5da5d 100644
--- a/systeroid-tui/tests/integration_test.rs
+++ b/systeroid-tui/tests/integration_test.rs
@@ -307,10 +307,10 @@ fn test_render_tui() -> Result<()> {
             "│vm.stat_interval =││=============     │",
             "│                  ││The time interval │",
             "│                  ││between which vm  │",
-            "│                  ││statistics are    │",
-            "│                  ││updated           │",
-            "│                  ││-                 │",
-            "│              2/2 ││Parameter:        │",
+            "│              2/2 ││statistics are    │",
+            "╰──────────────────╯│updated           │",
+            "╭──────────────────╮│-                 │",
+            "│MSG: Refreshed!   ││Parameter:        │",
             "╰──────────────────╯╰──────────────────╯",
         ]),
         terminal.backend(),