diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8177b548..8a8f1fa6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -59,30 +59,31 @@ jobs: - name: Run cargo audit run: cargo audit - coverage: - runs-on: ubuntu-24.04 - if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} - steps: - - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 - - uses: ./.github/actions/setup-environment + # Disabled temporarily due to failing dependency build + # coverage: + # runs-on: ubuntu-24.04 + # if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} + # steps: + # - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + # - uses: ./.github/actions/setup-environment - - name: Install cargo-llvm-cov from crates.io - uses: baptiste0928/cargo-install@v3 - with: - crate: cargo-llvm-cov - version: '0.6.18' + # - name: Install cargo-llvm-cov from crates.io + # uses: baptiste0928/cargo-install@v3 + # with: + # crate: cargo-llvm-cov + # version: '0.6.18' - - name: Generate coverage report - run: cargo llvm-cov --no-fail-fast --include-ffi --workspace --lcov --output-path lcov.info + # - name: Generate coverage report + # run: cargo llvm-cov --no-fail-fast --include-ffi --workspace --lcov --output-path lcov.info - - name: Upload to Codecov - uses: codecov/codecov-action@v5.5.1 - continue-on-error: true - with: - files: lcov.info - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true + # - name: Upload to Codecov + # uses: codecov/codecov-action@v5.5.1 + # continue-on-error: true + # with: + # files: lcov.info + # fail_ci_if_error: true + # token: ${{ secrets.CODECOV_TOKEN }} + # verbose: true test-miri: runs-on: ubuntu-24.04 diff --git a/Cargo.lock b/Cargo.lock index 4f7318f8..7948982c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,35 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -602,6 +631,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "easy_fuser" version = "0.4.2" @@ -746,6 +784,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fluent" version = "0.17.0" @@ -809,6 +857,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.1" @@ -1261,6 +1318,23 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "i18n-config" version = "0.4.8" @@ -1352,12 +1426,119 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.11.1" @@ -1554,6 +1735,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "ureq", "zbus", ] @@ -1722,6 +1904,18 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "lock_api" version = "0.4.13" @@ -1775,6 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2043,6 +2238,12 @@ dependencies = [ "serde", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2089,6 +2290,15 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2260,6 +2470,20 @@ dependencies = [ "syn", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rle-decode-fast" version = "1.0.3" @@ -2334,6 +2558,50 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2510,6 +2778,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "similar" version = "2.7.0" @@ -2547,6 +2821,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2559,6 +2839,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.106" @@ -2570,6 +2856,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -2958,6 +3255,69 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +dependencies = [ + "base64", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf-8", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3097,6 +3457,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3468,6 +3837,12 @@ dependencies = [ "syn", ] +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "xattr" version = "1.5.1" @@ -3484,6 +3859,30 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zbus" version = "5.11.0" @@ -3564,6 +3963,38 @@ name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" @@ -3571,7 +4002,20 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ + "yoke", "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/README.md b/README.md index e1bf39fa..c163101b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ system. - GPU undervolting (via voltage offset on AMD, [indirectly](https://github.com/ilya-zlobintsev/LACT/wiki/Frequently-asked-questions#how-to-undervolt-nvidia-gpus) on Nvidia) - #### Settings profiles - Automatic profile activation based on running processes or gamemode status +- #### [OpenTelemetry metrics exporter](./docs/EXPORTER.md) GPU configuration is handled by a system service that does not depend on a graphical session (Wayland/X11). @@ -53,6 +54,7 @@ The service can also be used standalone with a config file, for example in headl - [API](./docs/API.md) - [Power profiles daemon note](#power-profiles-daemon-note) - [Recovery from a bad overclock](https://github.com/ilya-zlobintsev/LACT/wiki/Recovering-from-a-bad-overclock) +- [Metrics exporter](./docs/EXPORTER.md) - [Contribute translations](#localization) - [Support the project](#support-the-project) diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 6b8ea5e6..2b1a8030 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -31,6 +31,14 @@ daemon: # By default TCP access is disabled, and only a unix socket is present. # Specifying this option enables the TCP listener. tcp_listen_address: 127.0.0.1:12853 + # Opentelemetry metrics exporter configuration + # Omitted by default, meaning the exporter is disabled. + metrics: + # Opentelemetry collector URL. The following example is for local Prometheus. + collector_address: 'http://localhost:9090/api/v1/otlp/v1/metrics' + # Interval in seconds for how often to collect the metrics. + # Defaults to 30. + interval: 30 # Period in seconds for how long settings should wait to be confirmed. # Most GPU setting change commands require a confirmation command to be used diff --git a/docs/EXPORTER.md b/docs/EXPORTER.md new file mode 100644 index 00000000..e8eb3e90 --- /dev/null +++ b/docs/EXPORTER.md @@ -0,0 +1,36 @@ +# OpenTelemetry exporter + +The LACT daemon supports exporting GPU metrics using the standard [OpenTelemetry metrics API](https://opentelemetry.io/docs/specs/otel/metrics/). +In particular, it uses the [OTLP/HTTP](https://opentelemetry.io/docs/specs/otlp/#otlphttp) protocol with a JSON payload. + +To use this functionality, you need an OpenTelemetry-compatible metrics collector such as Prometheus [with OTLP support enabled](https://prometheus.io/docs/guides/opentelemetry/). + +After this, you need to set the collector URL in `/etc/lact/config.yaml`. For example: +```yaml +daemon: + metrics: + # This example is a local Prometheus URL. Other collectors might simply use `/v1/metrics` as the path. + collector_address: 'http://localhost:9090/api/v1/otlp/v1/metrics' + # Metrics collection interval in seconds. Not necessary to set, defaults to 30 seconds. + interval: 30 +``` + +Restart the daemon after changing the config. + +The following metrics are currently supported : +- lact_gpu_usage +- lact_gpu_power_usage +- lact_gpu_power_usage_cap +- lact_gpu_temperature +- lact_gpu_clockspeed +- lact_gpu_voltage +- lact_gpu_fan_speed +- lact_gpu_fan_speed_max +- lact_gpu_fan_percent +- lact_gpu_vram_used +- lact_gpu_vram_total + + +Note: this is the source metric name, collectors might alter it for queries. For example, Prometheus adds a unit suffix, e.g. `lact_gpu_power_usage` -> `lact_gpu_power_usage_watts` + +![image](../res/screenshots/grafana.png) \ No newline at end of file diff --git a/flatpak/generated-sources.json b/flatpak/generated-sources.json index 4b6da7f7..4de8676a 100644 --- a/flatpak/generated-sources.json +++ b/flatpak/generated-sources.json @@ -608,6 +608,32 @@ "dest": "cargo/vendor/console-0.15.11", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cookie/cookie-0.18.1.crate", + "sha256": "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747", + "dest": "cargo/vendor/cookie-0.18.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747\", \"files\": {}}", + "dest": "cargo/vendor/cookie-0.18.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cookie_store/cookie_store-0.22.0.crate", + "sha256": "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f", + "dest": "cargo/vendor/cookie_store-0.22.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f\", \"files\": {}}", + "dest": "cargo/vendor/cookie_store-0.22.0", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -842,6 +868,19 @@ "dest": "cargo/vendor/dlopen2_derive-0.4.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/document-features/document-features-0.2.11.crate", + "sha256": "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d", + "dest": "cargo/vendor/document-features-0.2.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d\", \"files\": {}}", + "dest": "cargo/vendor/document-features-0.2.11", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -1050,6 +1089,19 @@ "dest": "cargo/vendor/find-msvc-tools-0.1.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/flate2/flate2-1.1.4.crate", + "sha256": "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9", + "dest": "cargo/vendor/flate2-1.1.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9\", \"files\": {}}", + "dest": "cargo/vendor/flate2-1.1.4", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -1128,6 +1180,19 @@ "dest": "cargo/vendor/fnv-1.0.7", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/form_urlencoded/form_urlencoded-1.2.2.crate", + "sha256": "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf", + "dest": "cargo/vendor/form_urlencoded-1.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf\", \"files\": {}}", + "dest": "cargo/vendor/form_urlencoded-1.2.2", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -1635,6 +1700,32 @@ "dest": "cargo/vendor/hex-0.4.3", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/http/http-1.3.1.crate", + "sha256": "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565", + "dest": "cargo/vendor/http-1.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565\", \"files\": {}}", + "dest": "cargo/vendor/http-1.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/httparse/httparse-1.10.1.crate", + "sha256": "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87", + "dest": "cargo/vendor/httparse-1.10.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87\", \"files\": {}}", + "dest": "cargo/vendor/httparse-1.10.1", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -1713,6 +1804,97 @@ "dest": "cargo/vendor/iana-time-zone-haiku-0.1.2", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_collections/icu_collections-2.0.0.crate", + "sha256": "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47", + "dest": "cargo/vendor/icu_collections-2.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47\", \"files\": {}}", + "dest": "cargo/vendor/icu_collections-2.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_locale_core/icu_locale_core-2.0.0.crate", + "sha256": "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a", + "dest": "cargo/vendor/icu_locale_core-2.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a\", \"files\": {}}", + "dest": "cargo/vendor/icu_locale_core-2.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_normalizer/icu_normalizer-2.0.0.crate", + "sha256": "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979", + "dest": "cargo/vendor/icu_normalizer-2.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979\", \"files\": {}}", + "dest": "cargo/vendor/icu_normalizer-2.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_normalizer_data/icu_normalizer_data-2.0.0.crate", + "sha256": "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3", + "dest": "cargo/vendor/icu_normalizer_data-2.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3\", \"files\": {}}", + "dest": "cargo/vendor/icu_normalizer_data-2.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_properties/icu_properties-2.0.1.crate", + "sha256": "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b", + "dest": "cargo/vendor/icu_properties-2.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b\", \"files\": {}}", + "dest": "cargo/vendor/icu_properties-2.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_properties_data/icu_properties_data-2.0.1.crate", + "sha256": "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632", + "dest": "cargo/vendor/icu_properties_data-2.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632\", \"files\": {}}", + "dest": "cargo/vendor/icu_properties_data-2.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/icu_provider/icu_provider-2.0.0.crate", + "sha256": "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af", + "dest": "cargo/vendor/icu_provider-2.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af\", \"files\": {}}", + "dest": "cargo/vendor/icu_provider-2.0.0", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -1726,6 +1908,32 @@ "dest": "cargo/vendor/ident_case-1.0.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/idna/idna-1.1.0.crate", + "sha256": "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de", + "dest": "cargo/vendor/idna-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de\", \"files\": {}}", + "dest": "cargo/vendor/idna-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/idna_adapter/idna_adapter-1.2.1.crate", + "sha256": "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344", + "dest": "cargo/vendor/idna_adapter-1.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344\", \"files\": {}}", + "dest": "cargo/vendor/idna_adapter-1.2.1", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -2038,6 +2246,32 @@ "dest": "cargo/vendor/linux-raw-sys-0.11.0", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/litemap/litemap-0.8.0.crate", + "sha256": "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956", + "dest": "cargo/vendor/litemap-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956\", \"files\": {}}", + "dest": "cargo/vendor/litemap-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/litrs/litrs-0.4.2.crate", + "sha256": "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed", + "dest": "cargo/vendor/litrs-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed\", \"files\": {}}", + "dest": "cargo/vendor/litrs-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -2480,6 +2714,19 @@ "dest": "cargo/vendor/pciid-parser-0.8.0", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/percent-encoding/percent-encoding-2.3.2.crate", + "sha256": "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220", + "dest": "cargo/vendor/percent-encoding-2.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220\", \"files\": {}}", + "dest": "cargo/vendor/percent-encoding-2.3.2", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -2558,6 +2805,19 @@ "dest": "cargo/vendor/plotters-cairo-0.8.0", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/potential_utf/potential_utf-0.1.3.crate", + "sha256": "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a", + "dest": "cargo/vendor/potential_utf-0.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a\", \"files\": {}}", + "dest": "cargo/vendor/potential_utf-0.1.3", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -2792,6 +3052,19 @@ "dest": "cargo/vendor/relm4-macros-0.10.0", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ring/ring-0.17.14.crate", + "sha256": "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7", + "dest": "cargo/vendor/ring-0.17.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7\", \"files\": {}}", + "dest": "cargo/vendor/ring-0.17.14", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -2896,6 +3169,58 @@ "dest": "cargo/vendor/rustix-1.1.2", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls/rustls-0.23.32.crate", + "sha256": "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40", + "dest": "cargo/vendor/rustls-0.23.32" + }, + { + "type": "inline", + "contents": "{\"package\": \"cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40\", \"files\": {}}", + "dest": "cargo/vendor/rustls-0.23.32", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-pemfile/rustls-pemfile-2.2.0.crate", + "sha256": "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50", + "dest": "cargo/vendor/rustls-pemfile-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50\", \"files\": {}}", + "dest": "cargo/vendor/rustls-pemfile-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-pki-types/rustls-pki-types-1.12.0.crate", + "sha256": "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79", + "dest": "cargo/vendor/rustls-pki-types-1.12.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79\", \"files\": {}}", + "dest": "cargo/vendor/rustls-pki-types-1.12.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.103.7.crate", + "sha256": "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf", + "dest": "cargo/vendor/rustls-webpki-0.103.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf\", \"files\": {}}", + "dest": "cargo/vendor/rustls-webpki-0.103.7", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -3143,6 +3468,19 @@ "dest": "cargo/vendor/signal-hook-registry-1.4.6", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/simd-adler32/simd-adler32-0.3.7.crate", + "sha256": "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe", + "dest": "cargo/vendor/simd-adler32-0.3.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe\", \"files\": {}}", + "dest": "cargo/vendor/simd-adler32-0.3.7", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -3208,6 +3546,19 @@ "dest": "cargo/vendor/spin-0.9.8", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/stable_deref_trait/stable_deref_trait-1.2.0.crate", + "sha256": "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3", + "dest": "cargo/vendor/stable_deref_trait-1.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3\", \"files\": {}}", + "dest": "cargo/vendor/stable_deref_trait-1.2.0", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -3234,6 +3585,19 @@ "dest": "cargo/vendor/strsim-0.11.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/subtle/subtle-2.6.1.crate", + "sha256": "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292", + "dest": "cargo/vendor/subtle-2.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292\", \"files\": {}}", + "dest": "cargo/vendor/subtle-2.6.1", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -3247,6 +3611,19 @@ "dest": "cargo/vendor/syn-2.0.106", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/synstructure/synstructure-0.13.2.crate", + "sha256": "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2", + "dest": "cargo/vendor/synstructure-0.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2\", \"files\": {}}", + "dest": "cargo/vendor/synstructure-0.13.2", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -3728,6 +4105,84 @@ "dest": "cargo/vendor/unsafe-libyaml-norway-0.2.15", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/untrusted/untrusted-0.9.0.crate", + "sha256": "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1", + "dest": "cargo/vendor/untrusted-0.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1\", \"files\": {}}", + "dest": "cargo/vendor/untrusted-0.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ureq/ureq-3.1.2.crate", + "sha256": "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537", + "dest": "cargo/vendor/ureq-3.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537\", \"files\": {}}", + "dest": "cargo/vendor/ureq-3.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ureq-proto/ureq-proto-0.5.2.crate", + "sha256": "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2", + "dest": "cargo/vendor/ureq-proto-0.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2\", \"files\": {}}", + "dest": "cargo/vendor/ureq-proto-0.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/url/url-2.5.7.crate", + "sha256": "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b", + "dest": "cargo/vendor/url-2.5.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b\", \"files\": {}}", + "dest": "cargo/vendor/url-2.5.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/utf-8/utf-8-0.7.6.crate", + "sha256": "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9", + "dest": "cargo/vendor/utf-8-0.7.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9\", \"files\": {}}", + "dest": "cargo/vendor/utf-8-0.7.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/utf8_iter/utf8_iter-1.0.4.crate", + "sha256": "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be", + "dest": "cargo/vendor/utf8_iter-1.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be\", \"files\": {}}", + "dest": "cargo/vendor/utf8_iter-1.0.4", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -3923,6 +4378,19 @@ "dest": "cargo/vendor/web-sys-0.3.78", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/webpki-roots/webpki-roots-1.0.3.crate", + "sha256": "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8", + "dest": "cargo/vendor/webpki-roots-1.0.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8\", \"files\": {}}", + "dest": "cargo/vendor/webpki-roots-1.0.3", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -4495,6 +4963,19 @@ "dest": "cargo/vendor/wrapcenum-derive-0.4.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/writeable/writeable-0.6.1.crate", + "sha256": "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb", + "dest": "cargo/vendor/writeable-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb\", \"files\": {}}", + "dest": "cargo/vendor/writeable-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -4521,6 +5002,32 @@ "dest": "cargo/vendor/yansi-1.0.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke/yoke-0.8.0.crate", + "sha256": "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc", + "dest": "cargo/vendor/yoke-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc\", \"files\": {}}", + "dest": "cargo/vendor/yoke-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yoke-derive/yoke-derive-0.8.0.crate", + "sha256": "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6", + "dest": "cargo/vendor/yoke-derive-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6\", \"files\": {}}", + "dest": "cargo/vendor/yoke-derive-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -4599,6 +5106,45 @@ "dest": "cargo/vendor/zerofrom-0.1.6", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerofrom-derive/zerofrom-derive-0.1.6.crate", + "sha256": "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502", + "dest": "cargo/vendor/zerofrom-derive-0.1.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502\", \"files\": {}}", + "dest": "cargo/vendor/zerofrom-derive-0.1.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zeroize/zeroize-1.8.2.crate", + "sha256": "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0", + "dest": "cargo/vendor/zeroize-1.8.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0\", \"files\": {}}", + "dest": "cargo/vendor/zeroize-1.8.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerotrie/zerotrie-0.2.2.crate", + "sha256": "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595", + "dest": "cargo/vendor/zerotrie-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595\", \"files\": {}}", + "dest": "cargo/vendor/zerotrie-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -4612,6 +5158,19 @@ "dest": "cargo/vendor/zerovec-0.11.4", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerovec-derive/zerovec-derive-0.11.1.crate", + "sha256": "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f", + "dest": "cargo/vendor/zerovec-derive-0.11.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f\", \"files\": {}}", + "dest": "cargo/vendor/zerovec-derive-0.11.1", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", diff --git a/lact-daemon/Cargo.toml b/lact-daemon/Cargo.toml index 1b484d5a..aca76723 100644 --- a/lact-daemon/Cargo.toml +++ b/lact-daemon/Cargo.toml @@ -50,6 +50,8 @@ cl3 = { git = "https://github.com/kenba/cl3", branch = "develop", features = [ "static", ] } +ureq = { version = "3.1.2", features = ["json", "rustls"] } + [dev-dependencies] pretty_assertions = { workspace = true } insta = { version = "1.41.1", features = ["json", "yaml"] } diff --git a/lact-daemon/src/config.rs b/lact-daemon/src/config.rs index 047e2f97..484b58a4 100644 --- a/lact-daemon/src/config.rs +++ b/lact-daemon/src/config.rs @@ -68,6 +68,7 @@ pub struct Daemon { pub disable_clocks_cleanup: bool, pub disable_nvapi: Option, pub tcp_listen_address: Option, + pub metrics: Option, } impl Default for Daemon { @@ -88,6 +89,7 @@ impl Default for Daemon { disable_clocks_cleanup: false, tcp_listen_address: None, disable_nvapi: None, + metrics: None, } } } @@ -278,6 +280,17 @@ impl Config { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Metrics { + pub collector_address: String, + #[serde(default = "default_metrics_interval")] + pub interval: u64, +} + +fn default_metrics_interval() -> u64 { + 30 +} + pub fn start_watcher(config_last_saved: Rc>) -> mpsc::UnboundedReceiver { let (config_tx, config_rx) = mpsc::unbounded_channel(); let (event_tx, mut event_rx) = mpsc::channel(64); diff --git a/lact-daemon/src/server.rs b/lact-daemon/src/server.rs index f0e37873..8f2a80df 100644 --- a/lact-daemon/src/server.rs +++ b/lact-daemon/src/server.rs @@ -1,5 +1,6 @@ pub mod gpu_controller; pub mod handler; +mod metrics; mod opencl; mod profiles; mod vulkan; @@ -15,7 +16,7 @@ use tokio::{ io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}, net::{TcpListener, UnixListener}, }; -use tracing::{error, info, instrument, trace}; +use tracing::{debug, error, info, instrument, trace}; pub struct Server { pub handler: Handler, @@ -49,6 +50,12 @@ impl Server { socket::cleanup(); })?; + if let Some(metrics_config) = handler.config.read().await.daemon.metrics.clone() { + metrics::setup(handler.clone(), metrics_config); + } else { + debug!("metrics exporter disabled"); + } + Ok(Self { handler, unix_listener, diff --git a/lact-daemon/src/server/metrics.rs b/lact-daemon/src/server/metrics.rs new file mode 100644 index 00000000..f14f7547 --- /dev/null +++ b/lact-daemon/src/server/metrics.rs @@ -0,0 +1,345 @@ +mod schema; + +use crate::{ + config, + server::{handler::Handler, metrics::schema::NumberValue}, +}; +use chrono::Local; +use indexmap::IndexMap; +use lact_schema::DeviceStats; +use schema::{ + Attribute, Gauge, GaugeDataPoint, Metric, MetricsPayload, Resource, ResourceMetric, Scope, + ScopeMetric, Value, +}; +use std::time::Duration; +use tokio::time::sleep; +use tracing::{debug, error, info}; + +pub fn setup(handler: Handler, config: config::Metrics) { + info!( + "exporting metrics to {} every {} seconds", + config.collector_address, config.interval + ); + + let interval = Duration::from_secs(config.interval); + + tokio::task::spawn_local(async move { + let client = ureq::config::Config::builder() + .http_status_as_error(false) + .build() + .new_agent(); + + loop { + sleep(interval).await; + + match get_stats(&handler).await { + Ok(devices) => { + debug!("collecting metrics for {} devices", devices.len()); + let mut metrics = Vec::with_capacity(10); + let timestamp = Local::now() + .timestamp_nanos_opt() + .expect("Invalid timestamp") + .to_string(); + + for (gpu_id, (gpu_name, stats)) in &devices { + collect_metrics(gpu_id, gpu_name, stats, &mut metrics, ×tamp); + } + + let metric_count = metrics.len(); + + let request = MetricsPayload { + resource_metrics: vec![ResourceMetric { + resource: Resource { + attributes: vec![Attribute { + key: "service.name", + value: Value::String("LACT"), + }], + }, + scope_metrics: vec![ScopeMetric { + scope: Scope { + name: "LACT".to_owned(), + version: env!("CARGO_PKG_VERSION").to_owned(), + attributes: vec![], + }, + metrics, + }], + }], + }; + + let url = config.collector_address.clone(); + let client = client.clone(); + let payload = + serde_json::to_string(&request).expect("Could not serialize request body"); + + let response = tokio::task::spawn_blocking(move || { + client + .post(url) + .content_type("application/json") + .send(payload) + }) + .await + .unwrap(); + + match response { + Ok(response) => { + if response.status().is_success() { + debug!("{metric_count} metrics were uploaded"); + } else { + let status = response.status(); + let body = response + .into_body() + .read_to_string() + .unwrap_or_else(|_| "".to_owned()); + debug!("could not submit metrics, status code {status}: {body}"); + } + } + Err(err) => error!("could not submit metrics: {err}"), + } + } + Err(err) => error!("could not fetch metrics: {err:#}"), + } + } + }); +} + +#[allow( + clippy::cast_possible_wrap, + clippy::cast_precision_loss, + clippy::too_many_lines +)] +fn collect_metrics<'a>( + gpu_id: &'a str, + gpu_name: &'a str, + stats: &'a DeviceStats, + metrics: &mut Vec>, + timestamp: &'a str, +) { + let base_attrs = vec![ + Attribute { + key: "gpu_id", + value: Value::String(gpu_id), + }, + Attribute { + key: "gpu_name", + value: Value::String(gpu_name), + }, + ]; + + if let Some(usage) = stats.busy_percent { + metrics.push(make_metric( + "lact_gpu_usage", + i64::from(usage).into(), + "%", + "Current GPU usage", + timestamp, + base_attrs.clone(), + )); + } + + if let Some(power) = stats.power.current.or(stats.power.average) { + metrics.push(make_metric( + "lact_gpu_power_usage", + power.into(), + "W", + "Current power usage", + timestamp, + base_attrs.clone(), + )); + } + + if let Some(power_cap) = stats.power.cap_current { + metrics.push(make_metric( + "lact_gpu_power_usage_cap", + power_cap.into(), + "W", + "Maximum configured power usage", + timestamp, + base_attrs.clone(), + )); + } + + for (key, temp) in &stats.temps { + let mut temp_attrs = base_attrs.clone(); + temp_attrs.push(Attribute { + key: "sensor", + value: Value::String(key), + }); + + if let Some(current) = temp.current { + metrics.push(make_metric( + "lact_gpu_temperature", + f64::from(current).into(), + "Cel", + "Current temperature", + timestamp, + temp_attrs.clone(), + )); + } + + if let Some(crit) = temp.crit { + metrics.push(make_metric( + "lact_gpu_temperature_crit", + f64::from(crit).into(), + "Cel", + "Critical temperature", + timestamp, + temp_attrs.clone(), + )); + } + } + + if let Some(gpu_clock) = stats.clockspeed.gpu_clockspeed { + let mut attrs = base_attrs.clone(); + attrs.push(Attribute { + key: "clockspeed_type", + value: Value::String("gpu_current"), + }); + + metrics.push(make_metric( + "lact_gpu_clockspeed", + (gpu_clock as i64 * 1000 * 1000).into(), + "Hz", + "Current clockspeed", + timestamp, + attrs, + )); + } + + if let Some(gpu_target_clock) = stats.clockspeed.current_gfxclk { + let mut attrs = base_attrs.clone(); + attrs.push(Attribute { + key: "clockspeed_type", + value: Value::String("gpu_target"), + }); + + metrics.push(make_metric( + "lact_gpu_clockspeed", + (gpu_target_clock as i64 * 1000 * 1000).into(), + "Hz", + "Current clockspeed", + timestamp, + attrs, + )); + } + + if let Some(vram_clock) = stats.clockspeed.vram_clockspeed { + let mut attrs = base_attrs.clone(); + attrs.push(Attribute { + key: "clockspeed_type", + value: Value::String("memory"), + }); + + metrics.push(make_metric( + "lact_gpu_clockspeed", + (vram_clock as i64 * 1000 * 1000).into(), + "Hz", + "Current clockspeed", + timestamp, + attrs, + )); + } + + if let Some(gpu_voltage) = stats.voltage.gpu { + metrics.push(make_metric( + "lact_gpu_voltage", + (gpu_voltage as f64 * 1000.0).into(), + "V", + "Current voltage", + timestamp, + base_attrs.clone(), + )); + } + + if let Some(fan_rpm) = stats.fan.speed_current { + metrics.push(make_metric( + "lact_gpu_fan_speed", + i64::from(fan_rpm).into(), + "RPM", + "Current fan speed (RPM)", + timestamp, + base_attrs.clone(), + )); + } + + if let Some(fan_rpm_max) = stats.fan.speed_max { + metrics.push(make_metric( + "lact_gpu_fan_speed_max", + i64::from(fan_rpm_max).into(), + "RPM", + "Maximum fan speed (RPM)", + timestamp, + base_attrs.clone(), + )); + } + + if let Some(fan_pwm) = stats.fan.pwm_current { + let percent = f64::from(fan_pwm) / f64::from(u8::MAX) * 100.0; + + metrics.push(make_metric( + "lact_gpu_fan_percent", + percent.into(), + "%", + "Current fan speed (%)", + timestamp, + base_attrs.clone(), + )); + } + + if let Some(vram_used) = stats.vram.used { + metrics.push(make_metric( + "lact_gpu_vram_used", + (vram_used as i64).into(), + "By", + "Current VRAM usage", + timestamp, + base_attrs.clone(), + )); + } + + if let Some(vram_total) = stats.vram.total { + metrics.push(make_metric( + "lact_gpu_vram_total", + (vram_total as i64).into(), + "By", + "Total VRAM available", + timestamp, + base_attrs.clone(), + )); + } +} + +fn make_metric<'a>( + name: &'static str, + value: NumberValue, + unit: &'static str, + description: &'static str, + timestamp: &'a str, + attrs: Vec>, +) -> Metric<'a> { + Metric { + name, + unit, + description, + gauge: Gauge { + data_points: vec![GaugeDataPoint { + value, + time_unix_nano: timestamp, + attributes: attrs, + }], + }, + } +} + +async fn get_stats(handler: &Handler) -> anyhow::Result> { + let mut devices = IndexMap::new(); + + let device_list = handler.list_devices().await; + + for device in device_list { + let stats = handler.get_gpu_stats(&device.id).await?; + + devices.insert(device.id, (device.name.unwrap_or_default(), stats)); + } + + Ok(devices) +} diff --git a/lact-daemon/src/server/metrics/schema.rs b/lact-daemon/src/server/metrics/schema.rs new file mode 100644 index 00000000..4dfb7ada --- /dev/null +++ b/lact-daemon/src/server/metrics/schema.rs @@ -0,0 +1,99 @@ +use serde::Serialize; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MetricsPayload<'a> { + pub resource_metrics: Vec>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResourceMetric<'a> { + pub resource: Resource<'a>, + pub scope_metrics: Vec>, +} + +#[derive(Serialize)] +pub struct Resource<'a> { + pub attributes: Vec>, +} + +#[derive(Serialize)] +pub struct ScopeMetric<'a> { + pub scope: Scope<'a>, + pub metrics: Vec>, +} + +#[derive(Serialize)] +pub struct Scope<'a> { + pub name: String, + pub version: String, + pub attributes: Vec>, +} + +#[derive(Serialize, Clone)] +pub struct Attribute<'a> { + pub key: &'static str, + pub value: Value<'a>, +} + +#[derive(Serialize)] +pub struct Metric<'a> { + pub name: &'static str, + pub unit: &'static str, + pub description: &'static str, + pub gauge: Gauge<'a>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Gauge<'a> { + pub data_points: Vec>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GaugeDataPoint<'a> { + #[serde(flatten)] + pub value: NumberValue, + pub time_unix_nano: &'a str, + pub attributes: Vec>, +} + +#[derive(Serialize)] +pub enum NumberValue { + #[serde(rename = "asDouble")] + Float(f64), + #[serde(rename = "asInt")] + Int(i64), +} + +impl From for NumberValue { + fn from(value: f64) -> Self { + Self::Float(value) + } +} + +impl From for NumberValue { + fn from(value: i64) -> Self { + Self::Int(value) + } +} + +#[derive(Serialize, Clone)] +pub enum Value<'a> { + #[serde(rename = "stringValue")] + String(&'a str), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serialize_value() { + let value = Value::String("asd"); + let expected_result = r#"{"stringValue":"asd"}"#; + assert_eq!(expected_result, serde_json::to_string(&value).unwrap()); + } +} diff --git a/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap b/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap index 38721488..452b9bd3 100644 --- a/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap +++ b/lact-daemon/src/snapshots/lact_daemon__config__tests__parse_doc.snap @@ -10,6 +10,9 @@ daemon: disable_clocks_cleanup: false disable_nvapi: false tcp_listen_address: "127.0.0.1:12853" + metrics: + collector_address: "http://localhost:9090/api/v1/otlp/v1/metrics" + interval: 30 apply_settings_timer: 5 gpus: "1002:687F-1043:0555-0000:0b:00.0": diff --git a/res/screenshots/grafana.png b/res/screenshots/grafana.png new file mode 100644 index 00000000..11c8a946 Binary files /dev/null and b/res/screenshots/grafana.png differ