diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..280b830 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,71 @@ +name: Linux + +on: + push: + pull_request: + branches: [ "latest" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + default_features: + name: Default Features + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init + - name: Update Rust + run: | + rustup update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup component add rustfmt + - name: Build + run: cargo build --verbose + - name: Run Tests + run: cargo test --verbose + - name: Test Formatting + run: cargo fmt --all -- --check + - name: Build Docs + run: cargo doc + - name: Build Package + run: cargo package --verbose + linux_features: + name: All Linux Features + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - name: Install Prereqs + run: | + sudo apt-get update + sudo apt-get install libsystemd-dev + - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init + - name: Update Rust + run: | + rustup update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup component add rustfmt + - name: Build + run: cargo build --verbose --features journald,network,socket + - name: Run Tests + run: cargo test --verbose --features journald,network,socket + - name: Test Formatting + run: cargo fmt --all -- --check + - name: Build Docs + run: cargo doc --features journald,network,socket + - name: Build Package + run: cargo package --verbose --features journald,network,socket diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml new file mode 100644 index 0000000..83b3be9 --- /dev/null +++ b/.github/workflows/mac.yml @@ -0,0 +1,67 @@ +name: Mac + +on: + push: + pull_request: + branches: [ "latest" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + default_features: + name: Default Features + runs-on: macos-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init + - name: Update Rust + run: | + rustup update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup component add rustfmt + - name: Build + run: cargo build --verbose + - name: Run Tests + run: cargo test --verbose + - name: Test Formatting + run: cargo fmt --all -- --check + - name: Build Docs + run: cargo doc + - name: Build Package + run: cargo package --verbose + mac_features: + name: All Mac Features + runs-on: macos-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init + - name: Update Rust + run: | + rustup update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup component add rustfmt + - name: Build + run: cargo build --verbose --features network,socket + - name: Run Tests + run: cargo test --verbose --features network,socket + - name: Test Formatting + run: cargo fmt --all -- --check + - name: Build Docs + run: cargo doc --features network,socket + - name: Build Package + run: cargo package --verbose --features network,socket diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..92b01f7 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,67 @@ +name: Windows + +on: + push: + pull_request: + branches: [ "latest" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + default_features: + name: Default Features + runs-on: windows-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init + - name: Update Rust + run: | + rustup update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup component add rustfmt + - name: Build + run: cargo build --verbose + - name: Run Tests + run: cargo test --verbose + - name: Test Formatting + run: cargo fmt --all -- --check + - name: Build Docs + run: cargo doc + - name: Build Package + run: cargo package --verbose + windows_features: + name: All Windows Features + runs-on: windows-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init + - name: Update Rust + run: | + rustup update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} + rustup component add rustfmt + - name: Build + run: cargo build --verbose --features network,wel + - name: Run Tests + run: cargo test --verbose --features network,wel + - name: Test Formatting + run: cargo fmt --all -- --check + - name: Build Docs + run: cargo doc --features network,wel + - name: Build Package + run: cargo package --verbose --features network,wel diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..714c8c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/src/stumpless_bindings.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1d79963 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog +All notable changes to the stumpless logger will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [0.1.0] - 2023-01-28 +### Added + - Logging to stdout, stderr, files, journald, network, socket, and Windows + Event Log endpoints. + - `journald`, `network`, `socket`, and `wel` features based on the target types + within stumpless-sys. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..53befa2 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,24 @@ +cff-version: 1.2.0 +title: Stumpless CLI and Rust Library +type: software +license: Apache-2.0 +repository-code: "https://github.com/goatshriek/stumpless-logger" +version: 0.1.0 +date-released: 2023-01-28 +keywords: + - cli + - journald + - logging + - library + - rust + - "structured logging" + - syslog + - systemd + - "windows event log" +authors: + - given-names: Joel + family-names: Anderson + email: joelanderson333@gmail.com +message: >- + If you use this software, please cite it using the + metadata from this file. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0791c9c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,546 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" +dependencies = [ + "bitflags", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_lex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cmake" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +dependencies = [ + "cc", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "embed-resource" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" +dependencies = [ + "cc", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[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 = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "stumpless" +version = "0.1.0" +dependencies = [ + "clap", + "embed-resource", + "itertools", + "regex", + "stumpless-sys", +] + +[[package]] +name = "stumpless-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea23c397037476cebeabb4d54903a1087ced0a91b7304ae1f9ae190e3b57576c" +dependencies = [ + "bindgen", + "cmake", + "libc", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9da8b36 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "stumpless" +description = "Sends log information to a variety of destinations, local and remote." +version = "0.1.0" +authors = ["Joel Anderson "] +edition = "2021" +repository = "https://github.com/goasthriek/stumpless-logger/" +license = "Apache-2.0" +keywords = ["cli", "library", "log", "logging", "utility"] +categories = ["command-line-utilities"] + +[dependencies] +clap = { version = "4.0.32", features = ["cargo"] } +itertools = "0.10.5" +regex = "1.7.0" +stumpless-sys = "0.2.0" + +[build-dependencies] +embed-resource = "1.8.0" +stumpless-sys = "0.2.0" + +[features] +journald = ["stumpless-sys/journald"] +network = ["stumpless-sys/network"] +socket = ["stumpless-sys/socket"] +wel = ["stumpless-sys/wel"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..24c4a83 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +[![Linux Builds](https://github.com/goatshriek/stumpless-logger/actions/workflows/linux.yml/badge.svg)](https://github.com/goatshriek/stumpless-logger/actions/workflows/linux.yml) +[![Windows Builds](https://github.com/goatshriek/stumpless-logger/actions/workflows/windows.yml/badge.svg)](https://github.com/goatshriek/stumpless-logger/actions/workflows/windows.yml) +[![Mac Builds](https://github.com/goatshriek/stumpless-logger/actions/workflows/mac.yml/badge.svg)](https://github.com/goatshriek/stumpless-logger/actions/workflows/mac.yml) +[![Apache 2.0 License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.1-ff69b4.svg)](https://github.com/goatshriek/stumpless-logger/blob/latest/docs/CODE_OF_CONDUCT.md) + + +**An enhanced command line logging utility.** + + +## Key Features +The stumpless logger aims to be a replacement for and improvement over the +traditional `logger` utility. It is written with +[Rust](https://www.rust-lang.org/) and +[Stumpless](https://github.com/goatshriek/stumpless), and offers a number of +improvements over legacy tools, including: + + * more logging target options (files, Windows Event Log) + * log to multiple destinations with a single invocation + * portable and tested on Windows and Linux + * separate thread for each log target + + +## Send Your Logs Anywhere +The stumpless logger supports all of the target types that Stumpless provides, +which include everything `logger` has and then some. + + +### The Default Target +Stumpless has the concept of a default target, which attempts to abstract away +the most logical place to send logs on your system. For Windows this is the +Windows Event Log in a log named Stumpless, for Linux and Mac systems this is +/var/run/syslog or /dev/log, and if all else fails this is a file named +`stumpless-default.log`. If you don't provide an explicit target to stumpless, +this is what it will send logs to. + +```sh +stumpless Send it! +# where this goes depends on your system! +``` + +You can explicitly send logs to the default target if you want to, for example +if you need to send to other locations as well as this one, using the +`--default` option like this: + +```sh +stumpless --default Send it! +# same as before! +``` + + +#### `stdout` and `stderr` +If you just want to print logs, then use the stdout or stderr. + +```sh +stumpless --stdout Hello from Stumpless! +# <13>1 2023-01-01T19:32:19.802953Z dante stumpless-cli - - - Hello from Stumpless! + +stumpless --stderr Stumpless says something went wrong... +# <13>1 2023-01-01T19:33:08.957079Z dante stumpless-cli - - - Stumpless says something went wrong... +``` + + +#### Files +Stumpless provides an easy way to write logs to files without going through a +syslog daemon or abusing stream redirection. The `--log-file` flag lets you +specify a file to send logs to, with the predictable choice of `-l` as the short +option. + +```sh +stumpless --log-file round.log Everything is a file these days + +cat round.log +# <13>1 2023-01-19T02:20:22.984425Z dante stumpless-cli - - - Everything is a file these days +``` + +If you want to write log entries into more than one file, you can just specify +this flag multiple times. This is true for most log targets in stumpless, and +means that it's simple to send messages to a variety of diverse locations +straight from a shell. Each different log target is handled in its own +thread to prevent them from blocking one another. + +```sh +stumpless --log-file square.log --log-file triangle.log You get a message, and you get a message! + +# note that the timestamp is slightly different in these two messages +cat square.log +# <13>1 2023-01-22T01:35:07.112856Z dante stumpless-cli - - - You get a message, and you get a message! + +cat triangle.log +# <13>1 2023-01-22T01:35:07.112963Z dante stumpless-cli - - - You get a message, and you get a message! +``` + + +#### Network +Sending logs to network servers is a common task. Stumpless supports this with +the `network` feature, which is enabled by default. This supports both IPv4 and +IPv6, for both TCP and UDP. You can specify these with the `--tcp4`, `--udp4`, +`--tcp6`, and `--udp6` options. + +```sh +stumpless --tcp4 one-log-server.example Send this message over TCP on IPv4. + +stumpless --udp6 two-log-server.example Send this message over UDP on IPv6. + +# of course, you can send multiple messages at once, as with other target types: +stumpless --tcp6 red-log-server.example \ + --udp4 blue-log-server.example \ + Send this message to two servers at once! +``` + +By default, these targets use port 514. If you want to use a different port, +then use the `--port` option (or `-P` short option) to customize this. + +```sh +stumpless --tcp4 special-snowflake-1.example --port 7777 + --udp6 special-snowflake-2.exampel --port 8888 + This message goes to two servers on different ports! +``` + + +#### Sockets +If you want to send messages to Unix sockets (such as the traditional +`/dev/log`), then you can use `--socket`, or `-u` for short (think +'u' for Unix). You'll note that this is the same option as `logger` uses. + +```sh +stumpless --socket /dev/log Say hello to the daemon for me +``` + +Socket logging is only available in builds where the `socket` feature has been +enabled. + + +#### Journald +Of course, stumpless can log to systemd's journaling service if desired. This +uses the same `--journald` option that `logger` users may already be familiar +with. + +```sh +stumpless --journald Send this message to the local journald service. +``` + +Journald logging is only available in builds where the `journald` feature has +been enabled. + + +#### Windows Event Logs +On machines where a Windows Event Log is present, you can send messages to it as +well. By default this will go to an application log named "Stumpless", but you +change this if you want. + +```sh +stumpless --windows-event-log This will go into the Stumpless Application log. + +# if you have your own special log, you can send to that instead +stumpless --windows-event-log=MySpecialLog This is a message for my own special log. +``` + +Note that logs don't show up for applications that aren't configured, including +the default Stumpless log. If you just want to install the default and use it +this way, you can run stumpless with the `--install-wel-default-source` option +to do this. You can run this by itself if you want to install. Note that this +requires enough privileges to make registry changes. It will also point registry +entries it creates at the stumpless executable for resources it needs, so don't +do this until you have stumpless in a place where you plan to leave it. + +```sh +# makes registry entries to install the Stumpless application log +# these will point at stumpless.exe, so make sure it's where you want it! +# and that it is in a place that Event Viewer has permissions, if you intend to +# browse the logs through that application +stumpless --install-wel-default-source +``` + +Windows Event Logging is only available in builds where the `wel` feature has +been enabled. + + +#### Structured Data +Log entries can often be made easier to parse by using structured data fields. +You can add these with the same options as `logger` uses: `--sd-id` adds an +element, and any `--sd-param` after this adds more detailed fields to the +element. This is easier to understand with some examples: + +```sh +# Note that, as with logger, the quotes are required, and may need to be escaped +# for your shell. For bash or cmd.exe this might instead be color=\"red\", for +#PowerShell color='\"red\"', and so on. +stumpless --stdout --sd-id ball --sd-param color="red" --sd-param size="medium" Caught a ball! +# <13>1 2023-01-28T02:34:50.127Z Angus stumpless-cli - - [ball color="red" size="medium"] Caught a ball! + +stumpless --stdout \ + --sd-id breadsticks \ + --sd-id mainCourse --sd-param meat="beef" --sd-param side="potatoes" \ + --sd-id dessert --sd-param type="cake" \ + Ate a feast! +# <13>1 2023-01-28T18:53:14.721851Z dante stumpless-cli - - [breadsticks][mainCourse meat="beef" side="potatoes"][dessert type="cake"] Ate a feast! +``` + + +## Differences Between `stumpless` and `logger` +This tool is _not_ written as a drop-in replacement for other `logger` +implementations. This is not to say that it is completely different: most of the +options are the same, and the general modes of use are the same. But there are +differences that arise from decisions made for simplicity, performance, or +necessity. Here are the deviations that are relevant to you if you're already +familiar with or using other loggers. + + * The default output with no arguments is determined by the + [default target](https://goatshriek.github.io/stumpless/docs/c/latest/target_8h.html#a137ec6ade02951be14bff3572725f076) + of the underlying stumpless build instead of `/dev/log`. + * The time quality structured data element is not included (pending Stumpless + implementation of + [this feature](https://github.com/goatshriek/stumpless/issues/223)). + * Network servers IP version and protocol are specified together such as + `--tcp4` rather than separately via `-T` or `-d` flags independent of the + `-n` flag. This is to support the specification of multiple targets using + different combinations in a single invocation. + * The following flags/modes of operation are not supported: + * `--rfc3164` for the RFC 3164 BSD syslog format of messages diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..e5ef70d --- /dev/null +++ b/build.rs @@ -0,0 +1,31 @@ +#[cfg(feature = "wel")] +use std::path::PathBuf; + +#[cfg(feature = "wel")] +use std::env; + +#[cfg(feature = "wel")] +use embed_resource; + +fn main() { + #[cfg(feature = "wel")] + if cfg!(feature = "wel") { + let out_dir = env::var_os("OUT_DIR").unwrap(); + + let mut bin_file = PathBuf::new(); + bin_file.push(&out_dir); + bin_file.push("default_events_MSG00409.bin"); + stumpless_sys::write_default_events_bin_file(&bin_file).expect("couldn't write bin file"); + + let mut resource_file = PathBuf::new(); + resource_file.push(&out_dir); + resource_file.push("default_events.rc"); + stumpless_sys::write_default_events_resource_file(&resource_file) + .expect("couldn't write resource file"); + + let mut compile_file = PathBuf::new(); + compile_file.push(&out_dir); + compile_file.push("default_events.rc"); + embed_resource::compile(&compile_file); + } +} diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..cbe16dc --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ +# Stumpless Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +joelanderson333@gmail.com. Email sent to this address is read only by Joel +Anderson. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + diff --git a/src/entry.rs b/src/entry.rs new file mode 100644 index 0000000..92c3409 --- /dev/null +++ b/src/entry.rs @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::{ + stumpless_add_new_element, stumpless_add_new_param_to_entry, + stumpless_destroy_entry_and_contents, stumpless_entry, stumpless_new_entry_str, + stumpless_set_entry_prival, +}; + +use crate::error::last_error; +use crate::facility::Facility; +use crate::severity::Severity; +use std::error::Error; +use std::ffi::CString; + +pub struct Entry { + pub entry: *mut stumpless_entry, +} + +impl Entry { + pub fn new( + facility: Facility, + severity: Severity, + app_name: &str, + msgid: &str, + message: &str, + ) -> Result> { + let c_app_name = CString::new(app_name)?; + let c_msgid = CString::new(msgid)?; + let c_message = CString::new(message)?; + let new_entry = unsafe { + stumpless_new_entry_str( + (facility as u32).try_into().unwrap(), + (severity as u32).try_into().unwrap(), + c_app_name.as_ptr(), + c_msgid.as_ptr(), + c_message.as_ptr(), + ) + }; + + if new_entry.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(Entry { entry: new_entry }) + } + } + + pub fn add_new_element(&self, element: &str) -> Result<&Self, Box> { + let c_element_name = CString::new(element)?; + let add_result = unsafe { stumpless_add_new_element(self.entry, c_element_name.as_ptr()) }; + + if !add_result.is_null() { + Ok(self) + } else { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } + } + + pub fn add_new_param( + &self, + element: &str, + param_name: &str, + param_value: &str, + ) -> Result<&Self, Box> { + let c_element_name = CString::new(element)?; + let c_param_name = CString::new(param_name)?; + let c_param_value = CString::new(param_value)?; + let add_result = unsafe { + stumpless_add_new_param_to_entry( + self.entry, + c_element_name.as_ptr(), + c_param_name.as_ptr(), + c_param_value.as_ptr(), + ) + }; + + if !add_result.is_null() { + Ok(self) + } else { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } + } + + pub fn set_prival(&self, prival: i32) -> Result<&Entry, Box> { + let set_result = unsafe { stumpless_set_entry_prival(self.entry, prival) }; + + if set_result.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(self) + } + } +} + +impl Drop for Entry { + fn drop(&mut self) { + unsafe { + stumpless_destroy_entry_and_contents(self.entry); + } + } +} + +unsafe impl Send for Entry {} +unsafe impl Sync for Entry {} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..450d38e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::{stumpless_get_error, stumpless_perror}; + +use std::error::Error; +use std::ffi::{CStr, CString}; +use std::fmt; + +#[derive(Debug, Clone)] +pub struct StumplessError { + //id: i32, + message: &'static str, + //code: i32, + //code_type: &'static str, +} + +impl Error for StumplessError {} + +impl fmt::Display for StumplessError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.message) + } +} + +pub fn invalid_facility_error() -> StumplessError { + StumplessError { + //id: stumpless_error_id_STUMPLESS_INVALID_FACILITY, + message: "invalid facility name", + //code: 0, + //code_type: "unused", + } +} + +pub fn invalid_prival_error() -> StumplessError { + StumplessError { + //id: 1, + message: "invalid prival format", + //code: 0, + //code_type: "unused", + } +} + +pub fn invalid_severity_error() -> StumplessError { + StumplessError { + //id: stumpless_error_id_STUMPLESS_INVALID_SEVERITY, + message: "invalid severity name", + //code: 0, + //code_type: "unused", + } +} + +pub fn last_error() -> Result<(), StumplessError> { + let err = unsafe { stumpless_get_error() }; + + if err.is_null() { + Ok(()) + } else { + Err(StumplessError { + //id: unsafe { (*err).id }, + message: unsafe { CStr::from_ptr((*err).message).to_str().unwrap() }, + //code: unsafe { (*err).code }, + //code_type: unsafe { CStr::from_ptr((*err).code_type).to_str().unwrap() }, + }) + } +} + +pub fn perror(prefix: &str) { + let c_prefix = CString::new(prefix).expect("couldn't make a C string"); + + unsafe { stumpless_perror(c_prefix.as_ptr()) } +} diff --git a/src/facility.rs b/src/facility.rs new file mode 100644 index 0000000..b2c3300 --- /dev/null +++ b/src/facility.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::*; + +pub enum Facility { + Kernel = stumpless_facility_STUMPLESS_FACILITY_KERN as isize, + User = stumpless_facility_STUMPLESS_FACILITY_USER as isize, + Mail = stumpless_facility_STUMPLESS_FACILITY_MAIL as isize, + Daemon = stumpless_facility_STUMPLESS_FACILITY_DAEMON as isize, + Auth = stumpless_facility_STUMPLESS_FACILITY_AUTH as isize, + Syslog = stumpless_facility_STUMPLESS_FACILITY_SYSLOG as isize, + Lpr = stumpless_facility_STUMPLESS_FACILITY_LPR as isize, + News = stumpless_facility_STUMPLESS_FACILITY_NEWS as isize, + Uucp = stumpless_facility_STUMPLESS_FACILITY_UUCP as isize, + Cron = stumpless_facility_STUMPLESS_FACILITY_CRON as isize, + Auth2 = stumpless_facility_STUMPLESS_FACILITY_AUTH2 as isize, + FTP = stumpless_facility_STUMPLESS_FACILITY_FTP as isize, + NTP = stumpless_facility_STUMPLESS_FACILITY_NTP as isize, + Audit = stumpless_facility_STUMPLESS_FACILITY_AUDIT as isize, + Alert = stumpless_facility_STUMPLESS_FACILITY_ALERT as isize, + Cron2 = stumpless_facility_STUMPLESS_FACILITY_CRON2 as isize, + Local0 = stumpless_facility_STUMPLESS_FACILITY_LOCAL0 as isize, + Local1 = stumpless_facility_STUMPLESS_FACILITY_LOCAL1 as isize, + Local2 = stumpless_facility_STUMPLESS_FACILITY_LOCAL2 as isize, + Local3 = stumpless_facility_STUMPLESS_FACILITY_LOCAL3 as isize, + Local4 = stumpless_facility_STUMPLESS_FACILITY_LOCAL4 as isize, + Local5 = stumpless_facility_STUMPLESS_FACILITY_LOCAL5 as isize, + Local6 = stumpless_facility_STUMPLESS_FACILITY_LOCAL6 as isize, + Local7 = stumpless_facility_STUMPLESS_FACILITY_LOCAL7 as isize, +} diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..7edfd4f --- /dev/null +++ b/src/file.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::*; + +use std::error::Error; +use std::ffi::CString; + +use crate::error::last_error; +use crate::Target; + +pub struct FileTarget { + target: *mut stumpless_target, +} + +impl FileTarget { + pub fn new(filename: &str) -> Result> { + let c_filename = CString::new(filename)?; + let file_target = unsafe { stumpless_open_file_target(c_filename.as_ptr()) }; + + if file_target.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(FileTarget { + target: file_target, + }) + } + } +} + +unsafe impl Sync for FileTarget {} + +impl Target for FileTarget { + fn get_pointer(&self) -> *mut stumpless_target { + self.target + } +} + +impl Drop for FileTarget { + fn drop(&mut self) { + unsafe { + stumpless_close_file_target(self.target); + } + } +} diff --git a/src/journald.rs b/src/journald.rs new file mode 100644 index 0000000..abd2b06 --- /dev/null +++ b/src/journald.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::*; + +use std::error::Error; +use std::ffi::CString; + +use crate::error::last_error; +use crate::Target; + +pub struct JournaldTarget { + target: *mut stumpless_target, +} + +impl JournaldTarget { + pub fn new() -> Result> { + let target_name = CString::new("stumpless-cli")?; + let journald_target = unsafe { stumpless_open_journald_target(target_name.as_ptr()) }; + + if journald_target.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(JournaldTarget { + target: journald_target, + }) + } + } +} + +unsafe impl Sync for JournaldTarget {} + +impl Target for JournaldTarget { + fn get_pointer(&self) -> *mut stumpless_target { + self.target + } +} + +impl Drop for JournaldTarget { + fn drop(&mut self) { + unsafe { + stumpless_close_journald_target(self.target); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6fe0f74 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Rust bindings for the Stumpless logging library. +//! +//! These wrappings are intended to be a natural-feeling Rust SDK with the same +//! functionality as the underlying Stumpless library. This is different than +//! the stumpless-sys crate, which are raw FFI bindings with no Rust-specific +//! functionality. +//! +//! +//! # Create Features +//! Stumpless provides a number of build configuration options that can be used +//! to enable or disable different functionality. This crate exposes these +//! options as the following features. +//! +//! +//! ### Target Features +//! +//! * **journald** - +//! Enables targets that can send logs to a systemd journald daemon. +//! * **network** - +//! Enables targets that can send logs to a server over a network connection. +//! * **socket** - +//! Enables targets that can send logs to Unix sockets. +//! * **wel** - +//! Enables targets that can send logs to the Windows Event Log. + +use regex::Regex; + +mod entry; +pub use crate::entry::Entry; + +mod error; +pub use crate::error::{ + invalid_facility_error, invalid_prival_error, invalid_severity_error, perror, StumplessError, +}; + +mod facility; +pub use crate::facility::Facility; + +mod file; +pub use crate::file::FileTarget; + +mod severity; +pub use crate::severity::Severity; + +mod stream; +pub use crate::stream::StreamTarget; + +mod target; +pub use crate::target::{DefaultTarget, Target}; + +#[cfg(feature = "journald")] +mod journald; +#[cfg(feature = "journald")] +pub use crate::journald::JournaldTarget; + +#[cfg(feature = "network")] +mod network; +#[cfg(feature = "network")] +pub use crate::network::NetworkTarget; + +#[cfg(feature = "socket")] +mod socket; +#[cfg(feature = "socket")] +pub use crate::socket::SocketTarget; + +#[cfg(feature = "wel")] +mod wel; +#[cfg(feature = "wel")] +pub use crate::wel::{add_default_wel_event_source, WelTarget}; + +// ideally this will become a wrapper for a stumpless native function +// this will give more flexibility to allowed values, and make the logic of the +// cli application simpler +pub fn prival_from_string(priority: &str) -> Result { + if let Ok(prival) = priority.parse::() { + if (0..=191).contains(&prival) { + return Ok(prival); + } + } + + let priority_re = Regex::new(r"^(\w+).(\w+)$").unwrap(); + match priority_re.captures(priority) { + Some(caps) => { + let facility = match caps.get(1).unwrap().as_str() { + "kern" => 0, + "user" => 1, + "mail" => 2, + "daemon" => 3, + "auth" | "security" => 4, + "syslog" => 5, + "lpr" => 6, + "news" => 7, + "uucp" => 8, + "cron" => 9, + "authpriv" => 10, + "ftp" => 11, + "ntp" => 12, + "local0" => 16, + "local1" => 17, + "local2" => 18, + "local3" => 19, + "local4" => 20, + "local5" => 21, + "local6" => 22, + "local7" => 23, + _ => { + return Err(invalid_facility_error()); + } + }; + + let severity = match caps.get(2).unwrap().as_str() { + "emerg" | "panic" => 0, + "alert" => 1, + "crit" => 2, + "err" | "error" => 3, + "warning" | "warn" => 4, + "notice" => 5, + "info" => 6, + "debug" => 7, + _ => { + return Err(invalid_severity_error()); + } + }; + + Ok((facility * 8) + severity) + } + None => Err(invalid_prival_error()), + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7165b7f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::{command, crate_version, parser::ValueSource, value_parser, Arg, ArgAction}; +use itertools::Itertools; +use regex::Regex; +use std::{ + sync::Arc, + thread::{spawn, JoinHandle}, +}; +use stumpless::{ + perror, prival_from_string, DefaultTarget, Entry, Facility, FileTarget, Severity, StreamTarget, + Target, +}; + +#[cfg(feature = "journald")] +use stumpless::JournaldTarget; + +#[cfg(feature = "network")] +use stumpless::NetworkTarget; + +#[cfg(feature = "socket")] +use stumpless::SocketTarget; + +#[cfg(feature = "wel")] +use stumpless::{add_default_wel_event_source, WelTarget}; + +fn main() { + let default_long_help = "\ + If no other targets are specified, then the default target will be \ + used. You can explicitly ask for logs to be sent to the default target \ + as well others by specifying this option.\ + \n\nThe default target depends on the build of stumpless used. \ + Generally, if Windows Event Log targets are supported, then the \ + default target will be an event log named Stumpless. If Windows Event \ + Log targets are not supported and socket targets are, then the default \ + target will be the socket named either /var/run/syslog or /dev/log. If \ + neither of these target types are supported then logs are written to a \ + file target is opened to log to a file named stumpless-default.log.\ + \n\nConsult the stumpless documentation for \ + stumpless_get_default_target for the nuances of the default target."; + let default_arg = Arg::new("default") + .long("default") + .help("Log to the default log target.") + .long_help(default_long_help) + .required(false); + + let file_arg = Arg::new("file") + .short('f') + .long("file") + .help("Log the contents of the file instead of reading from stdin or message arg.") + .required(false); + + let id_long_help = "\ + When the optional argument id is specified, then it is used instead of \ + the executable's PID. It's recommended to set this to a single value \ + in scripts that send multiple messages, for example the script's own \ + process id.\ + \n\n\ + Note that some logging infrastructure (for example systemd when \ + listening on /dev/log) may overwrite this value, for example with the \ + one derived from the connecting socket."; + let id_arg = Arg::new("id") + .short('i') + .long("id") + .value_name("id") + .num_args(0..=1) + .require_equals(true) + .default_missing_value("todo") + .help("Log a PID in each entry. Defaults to the PID of the CLI process.") + .long_help(id_long_help); + + let journald_arg = Arg::new("journald") + .short('j') + .long("journald") + .help("Log the entry to the journald system.") + .required(false) + .num_args(0..=1) + .require_equals(true); + + let log_file_long_help = "\ + This option can be provided as many times as needed with different files + to log to multiple files with one invocation."; + let log_file_arg = Arg::new("log-file") + .short('l') + .long("log-file") + .value_name("file") + .help("Log the entry to the given file.") + .long_help(log_file_long_help) + .required(false) + .action(ArgAction::Append); + + let message_arg = Arg::new("message") + .help("The message to send in the log entry.") + .num_args(1..) + .required_unless_present("install-wel-default-source"); + + let msgid_arg = Arg::new("msgid") + .short('m') + .long("msgid") + .help("The msgid to use in the message.") + .default_value("-") + .required(false); + + let priority_long_help = "\ + The priority may be specified as an integer, in which case it must be \ + defined as what is specified in RFC 5424 as the prival.\ + \n\n\ + This may also be provided in a human readable format of \ + .. Capitalization is ignored.\ + \n\nSeverity levels:\n\ + emerg or panic\n\ + alert\n\ + crit\n\ + err or error\n\ + warning or warn\n\ + notice\n\ + info\n\ + debug\n\ + \n\nFacility levels:\n\ + kern kernel messages\n\ + user user-level messages\n\ + mail mail system facility code value\n\ + daemon system daemons\n\ + auth or security security/authorization messages\n\ + syslog message generated by the logging daemon\n\ + lpr line printer subsystem\n\ + news network news\n\ + uucp uucp subsystem\n\ + cron clock daemon\n\ + auth2 security/authorization messages\n\ + ftp ftp daemon\n\ + ntp ntp subsystem\n\ + audit log audit\n\ + alert log alert\n\ + cron2 clock daemon\n\ + local0 local use 0\n\ + local1 local use 1\n\ + local2 local use 2\n\ + local3 local use 3\n\ + local4 local use 4\n\ + local5 local use 5\n\ + local6 local use 6\n\ + local7 local use 7"; + let priority_arg = Arg::new("priority") + .short('p') + .long("priority") + .value_name("priority") + .help("The priority of the message to be sent.") + .long_help(priority_long_help) + .required(false); + + let sd_id_arg = Arg::new("sd-id") + .long("sd-id") + .value_name("name") + .value_parser(value_parser!(String)) + .help("Include the structured data element id.") + .required(false) + .action(ArgAction::Append); + + let sd_param_arg = Arg::new("sd-param") + .long("sd-param") + .value_name("name=\"value\"") + .value_parser(value_parser!(String)) + .help("Add a parameter name and value to the previous element id.") + .required(false) + .action(ArgAction::Append); + + let socket_arg = Arg::new("socket") + .short('u') + .long("socket") + .value_name("socket") + .num_args(0..=1) + .require_equals(true) + .default_missing_value("/dev/log") + .help("Log to the provided socket, defaulting to /dev/log.") + .required(false) + .action(ArgAction::Append); + + let stderr_arg = Arg::new("stderr") + .short('s') + .long("stderr") + .action(ArgAction::SetTrue) + .help("Log to stderr.") + .required(false); + + let stdout_arg = Arg::new("stdout") + .long("stdout") + .action(ArgAction::SetTrue) + .help("Log to stdout.") + .required(false); + + let tcp4_arg = Arg::new("tcp4") + .short('T') + .long("tcp4") + .value_name("server") + .help("Send the entry to the given server using TCP over IPv4.") + .required(false) + .action(ArgAction::Append); + + let tcp6_arg = Arg::new("tcp6") + .long("tcp6") + .value_name("server") + .help("Send the entry to the given server using TCP over IPv6.") + .required(false) + .action(ArgAction::Append); + + let udp4_arg = Arg::new("udp4") + .short('d') + .long("udp4") + .value_name("server") + .help("Send the entry to the given server using UDP over IPv4.") + .required(false) + .action(ArgAction::Append); + + let udp6_arg = Arg::new("udp6") + .long("udp6") + .value_name("server") + .help("Send the entry to the given server using UDP over IPv6.") + .required(false) + .action(ArgAction::Append); + + let wel_arg = Arg::new("windows-event-log") + .short('w') + .long("windows-event-log") + .value_name("log") + .help("Log to the Windows Event Log provided, defaulting to Stumpless.") + .num_args(0..=1) + .default_missing_value("Stumpless") + .require_equals(true) + .required(false) + .action(ArgAction::Append); + + let wel_install_long_help = "\ + Having the event source information installed is required for the \ + Event Viewer to properly display events logged to it. This only needs \ + to happen once, and can be done after the events themselves are logged \ + with no loss of information. This option requires privileges to access \ + and modify the Windows Registry to function properly. The created \ + registry entries will point at the stumpless executable, so having it \ + in a location with restricted privileges or moving it after running \ + this may break some log visibility."; + let wel_install_arg = Arg::new("install-wel-default-source") + .long("install-wel-default-source") + .help("Installs the stumpless default Windows Event Log source.") + .long_help(wel_install_long_help) + .num_args(0) + .required(false); + + let cli_matches = command!() + .version(crate_version!()) + .arg(default_arg) + .arg(file_arg) + .arg(id_arg) + .arg(journald_arg) + .arg(log_file_arg) + .arg(message_arg) + .arg(msgid_arg) + .arg(priority_arg) + .arg(sd_id_arg) + .arg(sd_param_arg) + .arg(socket_arg) + .arg(stderr_arg) + .arg(stdout_arg) + .arg(tcp4_arg) + .arg(tcp6_arg) + .arg(udp4_arg) + .arg(udp6_arg) + .arg(wel_arg) + .arg(wel_install_arg) + .get_matches(); + + #[cfg(feature = "wel")] + if cli_matches.value_source("install-wel-default-source") == Some(ValueSource::CommandLine) { + add_default_wel_event_source() + .expect("adding the default Windows Event Log source failed!"); + } + + #[cfg(not(feature = "wel"))] + if cli_matches.value_source("install-wel-default-source") == Some(ValueSource::CommandLine) { + eprintln!("Windows Event Log functionality is not enabled, ignoring --install-wel-default-source option") + } + + if !cli_matches.contains_id("message") { + // we are all done if there is no message to log + println!("exiting with no message"); + return; + } + + let message_iterator = cli_matches + .get_many::("message") + .unwrap() + .map(|s| s.as_str()); + let message = Itertools::intersperse(message_iterator, " ").collect::(); + + let entry = Entry::new( + Facility::User, + Severity::Notice, + "stumpless-cli", + cli_matches.get_one::("msgid").unwrap(), + &message, + ) + .expect("entry creation failed!"); + + // build the elements and param structured data entries + let element_indices: Vec = match cli_matches.indices_of("sd-id") { + Some(index_iterator) => index_iterator.collect(), + None => Vec::new(), + }; + let elements: Vec<&str> = match cli_matches.get_many::("sd-id") { + Some(element_iterator) => element_iterator.map(|s| s.as_str()).collect(), + None => Vec::new(), + }; + assert_eq!(element_indices.len(), elements.len()); + + let param_indices: Vec = match cli_matches.indices_of("sd-param") { + Some(index_iterator) => index_iterator.collect(), + None => Vec::new(), + }; + let params: Vec<&str> = match cli_matches.get_many::("sd-param") { + Some(param_iterator) => param_iterator.map(|s| s.as_str()).collect(), + None => Vec::new(), + }; + assert_eq!(param_indices.len(), params.len()); + + let mut param_i: usize = 0; + let param_regex = Regex::new(r#"(.*)="(.*)""#).expect("the param regex failed to compile"); + for element_i in 0..elements.len() { + entry + .add_new_element(elements[element_i]) + .expect("couldn't add an element to the entry"); + + while param_i < params.len() + && param_indices[param_i] > element_indices[element_i] + && (elements.len() == element_i + 1 + || param_indices[param_i] < element_indices[element_i + 1]) + { + let param_captures = param_regex + .captures(params[param_i]) + .expect("provided param value was not formatted correctly"); + let param_name = param_captures.get(1).map_or("", |m| m.as_str()); + let param_value = param_captures.get(2).map_or("", |m| m.as_str()); + entry + .add_new_param(elements[element_i], param_name, param_value) + .expect("couldn't add a new param to the entry"); + param_i += 1; + } + } + + if cli_matches.contains_id("priority") { + let priority = cli_matches.get_one::("priority").unwrap(); + let prival = prival_from_string(priority).expect("could not parse priority"); + entry.set_prival(prival).expect("priority invalid"); + } + + let mut log_threads: Vec> = Vec::with_capacity(64); // arbitrary size + let mut default_needed = true; + let entry_arc = Arc::new(entry); + + if let Some(true) = cli_matches.get_one::("stderr") { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + log_threads.push(spawn(move || { + let stderr_target = StreamTarget::stderr("stderr").unwrap(); + stderr_target + .add_entry(&entry_clone) + .expect("logging to stderr failed!"); + })); + } + + if let Some(true) = cli_matches.get_one::("stdout") { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + log_threads.push(spawn(move || { + let stdout_target = StreamTarget::stdout("stdout").unwrap(); + stdout_target + .add_entry(&entry_clone) + .expect("logging to stdout failed!"); + })); + } + + if let Some(log_files) = cli_matches.get_many::("log-file") { + for log_file in log_files { + default_needed = false; + let log_filename = log_file.clone(); + let entry_clone = Arc::clone(&entry_arc); + log_threads.push(spawn(move || { + match FileTarget::new(&log_filename) { + Err(_error) => perror("opening the file target failed"), + Ok(target) => { + if let Err(_error) = target.add_entry(&entry_clone) { + perror("logging to the file target failed"); + } + } + }; + })); + } + } + + #[cfg(feature = "journald")] + if cli_matches.contains_id("journald") { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + log_threads.push(spawn(move || { + let journald_target = JournaldTarget::new().unwrap(); + journald_target + .add_entry(&entry_clone) + .expect("logging to journald failed!"); + })); + } + + #[cfg(not(feature = "journald"))] + if cli_matches.contains_id("journald") { + eprintln!("journald logging not enabled, ignoring --journald option"); + } + + #[cfg(feature = "socket")] + if let Some(sockets) = cli_matches.get_many::("socket") { + for socket in sockets { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + let socket_name = socket.clone(); + log_threads.push(spawn(move || { + let socket_target = SocketTarget::new(&socket_name).unwrap(); + socket_target + .add_entry(&entry_clone) + .expect("logging to socket failed!"); + })); + } + } + + #[cfg(not(feature = "socket"))] + if cli_matches.contains_id("socket") { + eprintln!("socket logging not enabled, ignoring --socket option"); + } + + #[cfg(feature = "network")] + if let Some(servers) = cli_matches.get_many::("tcp4") { + for server in servers { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + let server_name = server.clone(); + log_threads.push(spawn(move || { + let tcp4_target = NetworkTarget::tcp4(&server_name, "514").unwrap(); + tcp4_target + .add_entry(&entry_clone) + .expect("logging to tcp4 failed"); + })); + } + } + + #[cfg(not(feature = "network"))] + if cli_matches.contains_id("tcp4") { + eprintln!("network logging not enabled, ignoring --tcp4 option"); + } + + #[cfg(feature = "network")] + if let Some(servers) = cli_matches.get_many::("tcp6") { + for server in servers { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + let server_name = server.clone(); + log_threads.push(spawn(move || { + let tcp6_target = NetworkTarget::tcp6(&server_name, "514").unwrap(); + tcp6_target + .add_entry(&entry_clone) + .expect("logging to tcp6 failed"); + })); + } + } + + #[cfg(not(feature = "network"))] + if cli_matches.contains_id("tcp6") { + eprintln!("network logging not enabled, ignoring --tcp6 option"); + } + + #[cfg(feature = "network")] + if let Some(servers) = cli_matches.get_many::("udp4") { + for server in servers { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + let server_name = server.clone(); + log_threads.push(spawn(move || { + let udp4_target = NetworkTarget::udp4(&server_name, "514").unwrap(); + udp4_target + .add_entry(&entry_clone) + .expect("logging to udp4 failed"); + })); + } + } + + #[cfg(not(feature = "network"))] + if cli_matches.contains_id("udp4") { + eprintln!("network logging not enabled, ignoring --udp4 option"); + } + + #[cfg(feature = "network")] + if let Some(servers) = cli_matches.get_many::("udp6") { + for server in servers { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + let server_name = server.clone(); + log_threads.push(spawn(move || { + let udp6_target = NetworkTarget::tcp6(&server_name, "514").unwrap(); + udp6_target + .add_entry(&entry_clone) + .expect("logging to udp6 failed"); + })); + } + } + + #[cfg(not(feature = "network"))] + if cli_matches.contains_id("udp6") { + eprintln!("network logging not enabled, ignoring --udp6 option"); + } + + #[cfg(feature = "wel")] + if cli_matches.value_source("windows-event-log") == Some(ValueSource::CommandLine) { + if let Some(wel_logs) = cli_matches.get_many::("windows-event-log") { + for wel_log in wel_logs { + default_needed = false; + let entry_clone = Arc::clone(&entry_arc); + let wel_log_name = wel_log.clone(); + log_threads.push(spawn(move || { + let wel_target = WelTarget::new(&wel_log_name).unwrap(); + wel_target + .add_entry(&entry_clone) + .expect("logging to the Windows Event Log failed!"); + })); + } + } + } + + #[cfg(not(feature = "wel"))] + if cli_matches.value_source("windows-event-log") == Some(ValueSource::CommandLine) { + eprintln!("Windows Event Log logging is not enabled, ignoring --windows-event-log option"); + } + + if cli_matches.contains_id("default") || default_needed { + let entry_clone = Arc::clone(&entry_arc); + log_threads.push(spawn(move || { + let default_target = DefaultTarget::get_default_target().unwrap(); + default_target + .add_entry(&entry_clone) + .expect("logging to the default target failed!"); + })); + } + + for handle in log_threads { + handle + .join() + .expect("Couldn't join one of the logging threads!"); + } +} diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..15943a8 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::*; + +use std::error::Error; +use std::ffi::CString; + +use crate::error::last_error; +use crate::Target; + +pub struct NetworkTarget { + target: *mut stumpless_target, +} + +impl NetworkTarget { + pub fn tcp4(server: &str, port: &str) -> Result> { + let server_name = CString::new(server)?; + let network_target = unsafe { stumpless_new_tcp4_target(server_name.as_ptr()) }; + + if network_target.is_null() { + return match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + }; + } + + let tcp_target = NetworkTarget { + target: network_target, + }; + + tcp_target.set_transport_port(port)?; + tcp_target.open()?; + Ok(tcp_target) + } + + pub fn tcp6(server: &str, port: &str) -> Result> { + let server_name = CString::new(server)?; + let network_target = unsafe { stumpless_new_tcp6_target(server_name.as_ptr()) }; + + if network_target.is_null() { + return match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + }; + } + + let tcp_target = NetworkTarget { + target: network_target, + }; + + tcp_target.set_transport_port(port)?; + tcp_target.open()?; + Ok(tcp_target) + } + + pub fn udp4(server: &str, port: &str) -> Result> { + let server_name = CString::new(server)?; + let network_target = unsafe { stumpless_new_udp4_target(server_name.as_ptr()) }; + + if network_target.is_null() { + return match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + }; + } + + let udp_target = NetworkTarget { + target: network_target, + }; + + udp_target.set_transport_port(port)?; + udp_target.open()?; + Ok(udp_target) + } + + pub fn udp6(server: &str, port: &str) -> Result> { + let server_name = CString::new(server)?; + let network_target = unsafe { stumpless_new_udp6_target(server_name.as_ptr()) }; + + if network_target.is_null() { + return match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + }; + } + + let udp_target = NetworkTarget { + target: network_target, + }; + + udp_target.set_transport_port(port)?; + udp_target.open()?; + Ok(udp_target) + } + + fn set_transport_port(&self, port: &str) -> Result<(), Box> { + let port_name = CString::new(port)?; + let port_result = unsafe { stumpless_set_transport_port(self.target, port_name.as_ptr()) }; + if port_result.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(()) + } + } +} + +unsafe impl Sync for NetworkTarget {} + +impl Target for NetworkTarget { + fn get_pointer(&self) -> *mut stumpless_target { + self.target + } +} + +impl Drop for NetworkTarget { + fn drop(&mut self) { + unsafe { + stumpless_close_network_target(self.target); + } + } +} diff --git a/src/severity.rs b/src/severity.rs new file mode 100644 index 0000000..f1d69cb --- /dev/null +++ b/src/severity.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::*; + +pub enum Severity { + Emergency = stumpless_severity_STUMPLESS_SEVERITY_EMERG as isize, + Alert = stumpless_severity_STUMPLESS_SEVERITY_ALERT as isize, + Critical = stumpless_severity_STUMPLESS_SEVERITY_CRIT as isize, + Error = stumpless_severity_STUMPLESS_SEVERITY_ERR as isize, + Warning = stumpless_severity_STUMPLESS_SEVERITY_WARNING as isize, + Notice = stumpless_severity_STUMPLESS_SEVERITY_NOTICE as isize, + Info = stumpless_severity_STUMPLESS_SEVERITY_INFO as isize, + Debug = stumpless_severity_STUMPLESS_SEVERITY_DEBUG as isize, +} diff --git a/src/socket.rs b/src/socket.rs new file mode 100644 index 0000000..bccfddd --- /dev/null +++ b/src/socket.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::*; + +use std::error::Error; +use std::ffi::CString; + +use crate::error::last_error; +use crate::Target; + +pub struct SocketTarget { + target: *mut stumpless_target, +} + +impl SocketTarget { + pub fn new(socket_name: &str) -> Result> { + let c_socket_name = CString::new(socket_name)?; + let socket_target = + unsafe { stumpless_open_socket_target(c_socket_name.as_ptr(), std::ptr::null()) }; + + if socket_target.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(SocketTarget { + target: socket_target, + }) + } + } +} + +unsafe impl Sync for SocketTarget {} + +impl Target for SocketTarget { + fn get_pointer(&self) -> *mut stumpless_target { + self.target + } +} + +impl Drop for SocketTarget { + fn drop(&mut self) { + unsafe { + stumpless_close_socket_target(self.target); + } + } +} diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..8bb3a7a --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::*; + +use std::error::Error; +use std::ffi::CString; + +use crate::error::last_error; +use crate::Target; + +pub struct StreamTarget { + target: *mut stumpless_target, +} + +impl StreamTarget { + pub fn stderr(filename: &str) -> Result> { + let c_filename = CString::new(filename)?; + let stream_target = unsafe { stumpless_open_stderr_target(c_filename.as_ptr()) }; + + if stream_target.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(StreamTarget { + target: stream_target, + }) + } + } + + pub fn stdout(filename: &str) -> Result> { + let c_filename = CString::new(filename)?; + let stream_target = unsafe { stumpless_open_stdout_target(c_filename.as_ptr()) }; + + if stream_target.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(StreamTarget { + target: stream_target, + }) + } + } +} + +unsafe impl Sync for StreamTarget {} + +impl Target for StreamTarget { + fn get_pointer(&self) -> *mut stumpless_target { + self.target + } +} + +impl Drop for StreamTarget { + fn drop(&mut self) { + unsafe { + stumpless_close_stream_target(self.target); + } + } +} diff --git a/src/target.rs b/src/target.rs new file mode 100644 index 0000000..9114127 --- /dev/null +++ b/src/target.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::entry::Entry; +use crate::error::{last_error, StumplessError}; +use std::error::Error; +use std::ffi::CString; +use stumpless_sys::{ + stumpless_add_entry, stumpless_add_message_str, stumpless_get_default_target, + stumpless_open_target, stumpless_target, +}; + +pub trait Target: Sync { + fn get_pointer(&self) -> *mut stumpless_target; + + fn add_entry(&self, entry: &Entry) -> Result { + let add_result = unsafe { stumpless_add_entry(self.get_pointer(), entry.entry) }; + + if add_result >= 0 { + Ok(add_result.try_into().unwrap()) + } else { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(err), + } + } + } + + fn add_message(&self, message: &str) -> Result> { + let c_message = CString::new(message)?; + + let add_result = + unsafe { stumpless_add_message_str(self.get_pointer(), c_message.as_ptr()) }; + + if add_result >= 0 { + Ok(add_result.try_into().unwrap()) + } else { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } + } + + fn open(&self) -> Result<(), StumplessError> { + let open_result = unsafe { stumpless_open_target(self.get_pointer()) }; + if open_result.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(err), + } + } else { + Ok(()) + } + } +} + +pub struct DefaultTarget { + target: *mut stumpless_target, +} + +impl DefaultTarget { + pub fn get_default_target() -> Result> { + let default_target = unsafe { stumpless_get_default_target() }; + + if default_target.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(DefaultTarget { + target: default_target, + }) + } + } +} + +unsafe impl Sync for DefaultTarget {} + +impl Target for DefaultTarget { + fn get_pointer(&self) -> *mut stumpless_target { + self.target + } +} diff --git a/src/wel.rs b/src/wel.rs new file mode 100644 index 0000000..834c919 --- /dev/null +++ b/src/wel.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2023 Joel E. Anderson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use stumpless_sys::{ + stumpless_add_default_wel_event_source, stumpless_close_wel_target, + stumpless_open_local_wel_target, stumpless_target, +}; + +use std::error::Error; +use std::ffi::CString; + +use crate::error::last_error; +use crate::Target; + +pub struct WelTarget { + target: *mut stumpless_target, +} + +impl WelTarget { + pub fn new(log_name: &str) -> Result> { + let c_log_name = CString::new(log_name)?; + let wel_target = unsafe { stumpless_open_local_wel_target(c_log_name.as_ptr()) }; + + if wel_target.is_null() { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } else { + Ok(WelTarget { target: wel_target }) + } + } +} + +unsafe impl Sync for WelTarget {} + +impl Target for WelTarget { + fn get_pointer(&self) -> *mut stumpless_target { + self.target + } +} + +impl Drop for WelTarget { + fn drop(&mut self) { + unsafe { + stumpless_close_wel_target(self.target); + } + } +} + +pub fn add_default_wel_event_source() -> Result> { + let add_result = unsafe { stumpless_add_default_wel_event_source() }; + + if add_result == 0 { + Ok(add_result.try_into().unwrap()) + } else { + match last_error() { + Ok(_success) => panic!("inconsistent stumpless error state"), + Err(err) => Err(Box::new(err)), + } + } +}