diff --git a/.cargo/config b/.cargo/config index 16f1e73ac..f2ea191eb 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,5 @@ [build] -rustflags = ["--cfg", "tokio_unstable"] \ No newline at end of file +rustflags = ["--cfg", "tokio_unstable"] + +[alias] +xtask = "run --manifest-path ./xtask/Cargo.toml --" \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bd41a4a4d..7f321f945 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -75,6 +75,11 @@ jobs: override: true - uses: Swatinem/rust-cache@v1 + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Run cargo test (API) uses: actions-rs/cargo@v1 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0d6bbfeac..b8e00e19a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,12 +32,26 @@ jobs: needs: create-release strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-apple-darwin + os: macos-latest + - target: aarch64-pc-windows-msvc + os: windows-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - uses: taiki-e/upload-rust-binary-action@v1 with: bin: tokio-console + target: ${{ matrix.target }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 3a11057ae..6c5770ef4 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,7 +1,10 @@ # Code of Conduct -The Tokio project adheres to the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). This describes the minimum behavior expected from all contributors. +The Tokio project adheres to the +[Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). +This describes the minimum behavior expected from all contributors. ## Enforcement -Instances of violations of the Code of Conduct can be reported by contacting the project team at [moderation@tokio.rs](mailto:moderation@tokio.rs). +Instances of violations of the Code of Conduct can be reported by contacting +the project team at [moderation@tokio.rs](mailto:moderation@tokio.rs). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 674fe4191..8e36c61fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,9 +3,9 @@ :balloon: Thanks for your help improving the project! We are so happy to have you! -There are opportunities to contribute to Tokio at any level. It doesn't matter if -you are just getting started with Rust or are the most weathered expert, we can -use your help. +There are opportunities to contribute to Tokio at any level. It doesn't matter +if you are just getting started with Rust or are the most weathered expert, we +can use your help. **No contribution is too small and all contributions are valued.** @@ -20,8 +20,8 @@ not covered in this guide, please joinus! ## Conduct The Tokio project adheres to the [Rust Code of Conduct][coc]. This describes -the _minimum_ behavior expected from all contributors. Instances of violations of the -Code of Conduct can be reported by contacting the project team at +the _minimum_ behavior expected from all contributors. Instances of violations +of the Code of Conduct can be reported by contacting the project team at [moderation@tokio.rs](mailto:moderation@tokio.rs). [coc]: https://github.com/rust-lang/rust/blob/master/CODE_OF_CONDUCT.md @@ -156,7 +156,7 @@ The provided [example application] can be used for testing UI changes: When opening pull requests that make UI changes, please include one or more screenshots demonstrating your change! For bug fixes, it is often also useful to -include a screenshot showing the console *prior* to the change, in order to +include a screenshot showing the console _prior_ to the change, in order to demonstrate the bug that's being fixed. #### Integration tests @@ -178,7 +178,7 @@ for a reader to understand and actually testing the API. The type level example for `tokio_timer::Timeout` provides a good example of a documentation test: -``` +```rust /// // import the `timeout` function, usually this is done /// // with `use tokio::prelude::*` /// use tokio::prelude::FutureExt; @@ -204,7 +204,7 @@ documentation test: /// # } ``` -Given that this is a *type* level documentation test and the primary way users +Given that this is a _type_ level documentation test and the primary way users of `tokio` will create an instance of `Timeout` is by using `FutureExt::timeout`, this is how the documentation test is structured. @@ -215,7 +215,7 @@ easiest way to execute a future from a test. If this were a documentation test for the `Timeout::new` function, then the example would explicitly use `Timeout::new`. For example: -``` +```rust /// use tokio::timer::Timeout; /// use futures::Future; /// use futures::sync::oneshot; @@ -254,7 +254,7 @@ history**. But also, we use the git commit messages to **generate the change log**. Since commits are merged by [squashing](#commit-squashing), these rules are not -required for individual commits to a development branch. However, they *are* +required for individual commits to a development branch. However, they _are_ required for the final squash commit to the `main` branch. Generally, the PR description and title are used as the commit message for the squash commit. Therefore, please try to follow these rules when writing the description and @@ -266,7 +266,7 @@ Each commit message consists of a **header**, a **body** and a **footer**. The header has a special format that includes a **type**, an (optional) **scope** and a **subject**: -``` +```sh (): @@ -284,6 +284,7 @@ which we use to generate changelogs. [clog]: https://github.com/clog-tool/clog-cli #### Type + Must be one of the following: * **feat**: A new feature @@ -335,7 +336,6 @@ is also the place to reference GitHub issues that this commit The last line of commits introducing breaking changes should be in the form `BREAKING CHANGE: ` - ### Opening the Pull Request Open a new pull request using the GitHub web UI. Please try to follow the @@ -388,7 +388,7 @@ does not land, the submitters should come away from the experience feeling like their effort was not wasted or unappreciated**. Every Pull Request from a new contributor is an opportunity to grow the community. -### Review a bit at a time. +### Review a bit at a time Do not overwhelm new contributors. @@ -407,7 +407,7 @@ Note that only **incremental** improvement is needed to land a PR. This means that the PR does not need to be perfect, only better than the status quo. Follow up PRs may be opened to continue iterating. -When changes are necessary, *request* them, do not *demand* them, and **do not +When changes are necessary, _request_ them, do not _demand_ them, and **do not assume that the submitter already knows how to add a test or run a benchmark**. Specific performance optimization techniques, coding styles and conventions @@ -427,7 +427,7 @@ with the appropriate reason to keep the conversation flow concise and relevant. ### Be aware of the person behind the code -Be aware that *how* you communicate requests and reviews in your feedback can +Be aware that _how_ you communicate requests and reviews in your feedback can have a significant impact on the success of the Pull Request. Yes, we may land a particular change that makes Tokio better, but the individual might just not want to have anything to do with Tokio ever again. The goal is not just having @@ -440,7 +440,7 @@ check with the contributor to see if they intend to continue the work before checking if they would mind if you took it over (especially if it just has nits left). When doing so, it is courteous to give the original contributor credit for the work they started (either by preserving their name and email address in -the commit log, or by using an `Author: ` meta-data tag in the commit. +the commit log, or by using an `Author:` meta-data tag in the commit. _Adapted from the [Node.js contributing guide][node]_. @@ -458,36 +458,35 @@ targeted at maintainers. Most contributors aren't able to set these labels. The area label describes cross-cutting areas of work on the console project. -- **A-instrumentation**: Related to application instrumentation (such as adding +* **A-instrumentation**: Related to application instrumentation (such as adding new instrumentation to an async runtime or other library). -- **A-warnings**: Related to warnings displayed in the console CLI. This +* **A-warnings**: Related to warnings displayed in the console CLI. This includes changes that add new warnings, improve existing warnings, or improvements to the console's warning system as a whole. -- **A-recording**: Related to recording and playing back console data. +* **A-recording**: Related to recording and playing back console data. ### Crate The crate label describes what crates in the repository are involved in an issue or PR. -- **C-api**: Related to the `console-api` crate and/or protobuf definitions. -- **C-console**: Related to the `console` command-line application. -- **C-subscriber**: Related to the `console-subscriber` crate. +* **C-api**: Related to the `console-api` crate and/or protobuf definitions. +* **C-console**: Related to the `console` command-line application. +* **C-subscriber**: Related to the `console-subscriber` crate. ### Effort and calls for participation The effort label represents a _best guess_ for the approximate amount of effort that an issue will likely require. These are not always accurate! :) - -- **E-easy**: This is relatively easy. These issues are often good for newcomers +* **E-easy**: This is relatively easy. These issues are often good for newcomers to the project and/or Rust beginners. -- **E-medium**: Medium effort. This issue is expected to be relatively +* **E-medium**: Medium effort. This issue is expected to be relatively straightforward, but may require a larger amount of work than `E-easy` issues, or require some design work. -- **E-hard** This either involves very tricky code, is something we don't know +* **E-hard** This either involves very tricky code, is something we don't know how to solve, or is difficult for some other reason. -- **E-needs-mvce**: This bug is missing a minimal complete and verifiable +* **E-needs-mvce**: This bug is missing a minimal complete and verifiable example. The "E-" prefix is the same as used in the Rust compiler repository. Some @@ -499,14 +498,14 @@ server if you want to know how difficult an issue likely is. The severity label categorizes what type of issue is described by an issue, or what is implemented by a pull request. -- **S-bug**: This is a bug in the console. If this label is added to an issue, +* **S-bug**: This is a bug in the console. If this label is added to an issue, then that issue describes a bug. If this label is added to a pull request, - then this pull request *fixes* a bug. -- **S-feature**: This is adding a new feature. -- **S-performance**: Related to improving performance, either in the + then this pull request _fixes_ a bug. +* **S-feature**: This is adding a new feature. +* **S-performance**: Related to improving performance, either in the instrumented application or in the `console` CLI. This may be added to performance regressions that don't result in a crash or incorrect data, as well as to pull requests that implement optimizations. -- **S-refactor**: This is a refactor. This label describes proposed or +* **S-refactor**: This is a refactor. This label describes proposed or implemented changes that are related to improve code quality or set up for future changes, but shouldn't effect behavior, fix bugs, or add new APIs. diff --git a/Cargo.lock b/Cargo.lock index cc901b32e..02156e518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,20 +238,11 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "cmake" -version = "0.1.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" -dependencies = [ - "cc", -] - [[package]] name = "color-eyre" -version = "0.5.11" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" dependencies = [ "backtrace", "color-spantrace", @@ -265,9 +256,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.1.6" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" dependencies = [ "once_cell", "owo-colors", @@ -277,9 +268,10 @@ dependencies = [ [[package]] name = "console-api" -version = "0.3.0" +version = "0.4.0" dependencies = [ "prost", + "prost-build", "prost-types", "tonic", "tonic-build", @@ -288,7 +280,7 @@ dependencies = [ [[package]] name = "console-subscriber" -version = "0.1.6" +version = "0.1.8" dependencies = [ "console-api", "crossbeam-channel", @@ -296,7 +288,7 @@ dependencies = [ "futures", "hdrhistogram", "humantime", - "parking_lot 0.11.2", + "parking_lot 0.12.0", "prost-types", "serde", "serde_json", @@ -306,7 +298,7 @@ dependencies = [ "tonic", "tracing", "tracing-core", - "tracing-subscriber 0.3.11", + "tracing-subscriber", ] [[package]] @@ -830,16 +822,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "miow", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", + "windows-sys 0.36.1", ] [[package]] @@ -918,9 +908,9 @@ checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" [[package]] name = "owo-colors" -version = "1.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "parking_lot" @@ -967,7 +957,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.34.0", ] [[package]] @@ -1069,9 +1059,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd5316aa8f5c82add416dfbc25116b84b748a21153f512917e8143640a71bbd" +checksum = "399c3c31cdec40583bb68f0b18403400d01ec4289c383aa047560439952c4dd7" dependencies = [ "bytes", "prost-derive", @@ -1079,13 +1069,11 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328f9f29b82409216decb172d81e936415d21245befa79cd34c3f29d87d1c50b" +checksum = "7f835c582e6bd972ba8347313300219fed5bfa52caf175298d860b61ff6069bb" dependencies = [ "bytes", - "cfg-if", - "cmake", "heck", "itertools", "lazy_static", @@ -1101,9 +1089,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df35198f0777b75e9ff669737c6da5136b59dba33cf5a010a6d1cc4d56defc6f" +checksum = "7345d5f0e08c0536d7ac7229952590239e77abf0a0100a1b1d890add6ea96364" dependencies = [ "anyhow", "itertools", @@ -1114,9 +1102,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "926681c118ae6e512a3ccefd4abbe5521a14f4cc1e207356d4d00c0b7f2006fd" +checksum = "d30bc806a29b347314be074ff0608ef8e547286e8ea68b061a2fe55689edc01f" dependencies = [ "bytes", "prost", @@ -1424,14 +1412,15 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" dependencies = [ + "autocfg", "bytes", "libc", "memchr", - "mio 0.8.2", + "mio 0.8.4", "num_cpus", "once_cell", "parking_lot 0.12.0", @@ -1445,7 +1434,7 @@ dependencies = [ [[package]] name = "tokio-console" -version = "0.1.6" +version = "0.1.7" dependencies = [ "atty", "clap", @@ -1465,9 +1454,10 @@ dependencies = [ "tokio", "toml", "tonic", + "tower", "tracing", "tracing-journald", - "tracing-subscriber 0.3.11", + "tracing-subscriber", "tui", ] @@ -1528,9 +1518,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1a361140b1af3f548e0a5105126b3fc737542f6cd4947b66419c80be07db22" +checksum = "498f271adc46acce75d66f639e4d35b31b2394c295c82496727dafa16d465dd2" dependencies = [ "async-stream", "async-trait", @@ -1560,9 +1550,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d17087af5c80e5d5fc8ba9878e60258065a0a757e35efe7a05b7904bece1943" +checksum = "2fbcd2800e34e743b9ae795867d5f77b535d3a3be69fd731e39145719752df8c" dependencies = [ "prettyplease", "proc-macro2", @@ -1658,12 +1648,12 @@ dependencies = [ [[package]] name = "tracing-error" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" dependencies = [ "tracing", - "tracing-subscriber 0.2.25", + "tracing-subscriber", ] [[package]] @@ -1684,7 +1674,7 @@ checksum = "1ba49f4829f4e95702943ec6b2fad8936b369d20fa27036caf01329fb230e460" dependencies = [ "libc", "tracing-core", - "tracing-subscriber 0.3.11", + "tracing-subscriber", ] [[package]] @@ -1698,17 +1688,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-subscriber" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.11" @@ -1874,11 +1853,24 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", +] + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] [[package]] @@ -1887,26 +1879,65 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + [[package]] name = "windows_i686_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + [[package]] name = "windows_i686_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + [[package]] name = "windows_x86_64_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + [[package]] name = "windows_x86_64_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "clap", + "color-eyre", + "tonic-build", +] diff --git a/Cargo.toml b/Cargo.toml index 0f289d821..153d99d70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "tokio-console", "console-subscriber", - "console-api" + "console-api", + "xtask" ] resolver = "2" diff --git a/README.md b/README.md index 2d2ab4a8d..a8b7b8d39 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,10 @@ toolkit consists of multiple components: [`tracing`]. * tools for **displaying and exploring diagnostic data**, implemented as gRPC - clients using the console wire protocol. the [`tokio-console`] crate implements an - **an interactive command-line tool** that consumes this data, but **other - implementations**, such as graphical or web-based tools, are also possible. + clients using the console wire protocol. the [`tokio-console`] crate + implements an **an interactive command-line tool** that consumes this data, + but **other implementations**, such as graphical or web-based tools, are + also possible. [gRPC]: https://grpc.io/ [protocol buffers]: https://developers.google.com/protocol-buffers @@ -58,7 +59,7 @@ viewing details for a single task: ![task details view](https://user-images.githubusercontent.com/2796466/129774524-288c967b-6066-4f98-973d-099b3e6a2c55.png) -## on the shoulders of giants... +## on the shoulders of giants the console is **part of a much larger effort** to improve debugging tooling for async Rust. **a [2019 Google Summer of Code project][gsoc] by Matthias Prechtl** @@ -86,7 +87,9 @@ others. [Instruments]: https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/MeasuringPerformance.html [**@matprec**]: https://github.com/matprec [**@pnkfelix**]: https://github.com/pnkfelix + ## using it + ### instrumenting your program to **instrument an application using Tokio**, add a dependency on the @@ -101,10 +104,13 @@ notes: * in order to collect task data from Tokio, **the `tokio_unstable` cfg must be enabled**. for example, you could build your project with + ```shell - $ RUSTFLAGS="--cfg tokio_unstable" cargo build + RUSTFLAGS="--cfg tokio_unstable" cargo build ``` + or add the following to your `.cargo/config.toml` file: + ```toml [build] rustflags = ["--cfg", "tokio_unstable"] @@ -116,15 +122,15 @@ notes: * the `tokio` and `runtime` [`tracing` targets] must be enabled at the [`TRACE` level]. - + if you're using the [`console_subscriber::init()`][init] or + * if you're using the [`console_subscriber::init()`][init] or [`console_subscriber::Builder`][builder] APIs, these targets are enabled automatically. - + if you are manually configuring the `tracing` subscriber using the + * if you are manually configuring the `tracing` subscriber using the [`EnvFilter`] or [`Targets`] filters from [`tracing-subscriber`], add `"tokio=trace,runtime=trace"` to your filter configuration. - + also, ensure you have not enabled any of the [compile time filter + * also, ensure you have not enabled any of the [compile time filter features][compile_time_filters] in your `Cargo.toml`. ### running the console @@ -132,13 +138,13 @@ notes: to **run the console command-line tool**, install `tokio-console` from [crates.io](https://crates.io/crates/tokio-console) ```shell -$ cargo install --locked tokio-console +cargo install --locked tokio-console ``` and run locally ```shell -$ tokio-console +tokio-console ``` > **alternative method:** run the tool from a local checkout of this repository @@ -154,14 +160,14 @@ as an argument to the console (either as an `:` or `:`). for example: ```shell -$ cargo run -- http://my.great.console.app.local:5555 +cargo run -- http://my.great.console.app.local:5555 ``` the console command-line tool supports a number of additional flags to configure its behavior. the `-h` or `--help` flag will print a list of supported command-line flags and arguments: -``` +```text USAGE: tokio-console [FLAGS] [OPTIONS] [TARGET_ADDR] @@ -240,7 +246,7 @@ OPTIONS: * `years`, `year`, `y` -- defined as 365.25 days [default: 6s] ``` -## for development: +## for development the `console-subscriber/examples` directory contains **some potentially useful tools**: diff --git a/SECURITY.md b/SECURITY.md index 6bb8f5314..55c3247f8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,3 +1,5 @@ +# Security + ## Report a security issue The Tokio project team welcomes security reports and is committed to providing prompt attention to security issues. Security issues should be reported privately via [security@tokio.rs](mailto:security@tokio.rs). Security issues should not be reported via the public Github Issue tracker. diff --git a/console-api/CHANGELOG.md b/console-api/CHANGELOG.md index aa964ba43..0793b625a 100644 --- a/console-api/CHANGELOG.md +++ b/console-api/CHANGELOG.md @@ -1,3 +1,16 @@ + +## 0.4.0 (2022-08-10) + + +#### Breaking Changes + +* Update `tonic` to `0.8` (#364) ([40e2f6fd](40e2f6fd)) + +#### Features + +* Update `tonic` to `0.8` (#364) ([40e2f6fd](40e2f6fd)) + + ## 0.3.0 (2022-05-23) diff --git a/console-api/Cargo.toml b/console-api/Cargo.toml index 009ed725d..f01d08a08 100644 --- a/console-api/Cargo.toml +++ b/console-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "console-api" -version = "0.3.0" +version = "0.4.0" license = "MIT" edition = "2021" rust-version = "1.58.0" @@ -29,19 +29,21 @@ keywords = [ transport = ["tonic-build/transport", "tonic/transport"] [dependencies] -tonic = { version = "0.7", default-features = false, features = [ +tonic = { version = "0.8", default-features = false, features = [ "prost", "codegen", "transport", ] } -prost = "0.10" -prost-types = "0.10" +prost = "0.11" +prost-types = "0.11" tracing-core = "0.1.17" [dev-dependencies] -tonic-build = { version = "0.7", default-features = false, features = [ +tonic-build = { version = "0.8", default-features = false, features = [ "prost", "transport" ] } +# explicit dep so we can get the version with fixed whitespace. +prost-build = "0.11.1" [package.metadata.docs.rs] all-features = true diff --git a/console-api/src/async_ops.rs b/console-api/src/async_ops.rs index a91cf8d0c..289801e08 100644 --- a/console-api/src/async_ops.rs +++ b/console-api/src/async_ops.rs @@ -1 +1,3 @@ +#![allow(warnings)] + include!("generated/rs.tokio.console.async_ops.rs"); diff --git a/console-api/src/common.rs b/console-api/src/common.rs index 211d70ba4..732479f49 100644 --- a/console-api/src/common.rs +++ b/console-api/src/common.rs @@ -1,7 +1,12 @@ use std::fmt; use std::hash::{Hash, Hasher}; -include!("generated/rs.tokio.console.common.rs"); +pub use generated::*; + +mod generated { + #![allow(warnings)] + include!("generated/rs.tokio.console.common.rs"); +} impl From for metadata::Level { fn from(level: tracing_core::Level) -> Self { @@ -205,7 +210,7 @@ impl From<&dyn std::fmt::Debug> for field::Value { // or vice versa. However, this is unavoidable here, because `prost` generates // a struct with `#[derive(PartialEq)]`, but we cannot add`#[derive(Hash)]` to the // generated code. -#[allow(clippy::derive_hash_xor_eq)] +#[allow(clippy::derived_hash_with_manual_eq)] impl Hash for field::Name { fn hash(&self, state: &mut H) { match self { diff --git a/console-api/src/generated/google.protobuf.rs b/console-api/src/generated/google.protobuf.rs new file mode 100644 index 000000000..e69de29bb diff --git a/console-api/src/generated/rs.tokio.console.common.rs b/console-api/src/generated/rs.tokio.console.common.rs index d9a77c8d3..d651148e3 100644 --- a/console-api/src/generated/rs.tokio.console.common.rs +++ b/console-api/src/generated/rs.tokio.console.common.rs @@ -165,6 +165,18 @@ pub mod metadata { /// Indicates metadata is associated with an event. Event = 1, } + impl Kind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Kind::Span => "SPAN", + Kind::Event => "EVENT", + } + } + } /// Describes the level of verbosity of a span or event. /// /// Corresponds to `Level` in the `tracing` crate. @@ -191,6 +203,21 @@ pub mod metadata { /// Designates very low priority, often extremely verbose, information. Trace = 4, } + impl Level { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Level::Error => "ERROR", + Level::Warn => "WARN", + Level::Info => "INFO", + Level::Debug => "DEBUG", + Level::Trace => "TRACE", + } + } + } } /// Contains stats about objects that can be polled. Currently these can be: /// - tasks that have been spawned diff --git a/console-api/src/generated/rs.tokio.console.instrument.rs b/console-api/src/generated/rs.tokio.console.instrument.rs index bf1f570ab..e39c9331c 100644 --- a/console-api/src/generated/rs.tokio.console.instrument.rs +++ b/console-api/src/generated/rs.tokio.console.instrument.rs @@ -30,7 +30,7 @@ pub struct ResumeRequest { /// - we can use one single timestamp for all the data /// - we can have all the new_metadata in one place /// - things such as async ops and resource ops do not make sense -/// on their own as they have relations to tasks and resources +/// on their own as they have relations to tasks and resources #[derive(Clone, PartialEq, ::prost::Message)] pub struct Update { /// The system time when this update was recorded. @@ -64,6 +64,7 @@ pub struct ResumeResponse { pub mod instrument_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; + use tonic::codegen::http::Uri; /// `InstrumentServer` implements `Instrument` as a service. #[derive(Debug, Clone)] pub struct InstrumentClient { @@ -84,19 +85,24 @@ pub mod instrument_client { where T: tonic::client::GrpcService, T::Error: Into, - T::ResponseBody: Default + Body + Send + 'static, + T::ResponseBody: Body + Send + 'static, ::Error: Into + Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } pub fn with_interceptor( inner: T, interceptor: F, ) -> InstrumentClient> where F: tonic::service::Interceptor, + T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< @@ -109,19 +115,19 @@ pub mod instrument_client { { InstrumentClient::new(InterceptedService::new(inner, interceptor)) } - /// Compress requests with `gzip`. + /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] - pub fn send_gzip(mut self) -> Self { - self.inner = self.inner.send_gzip(); + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); self } - /// Enable decompressing responses with `gzip`. + /// Enable decompressing responses. #[must_use] - pub fn accept_gzip(mut self) -> Self { - self.inner = self.inner.accept_gzip(); + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); self } /// Produces a stream of updates representing the behavior of the instrumented async runtime. @@ -258,8 +264,8 @@ pub mod instrument_server { #[derive(Debug)] pub struct InstrumentServer { inner: _Inner, - accept_compression_encodings: (), - send_compression_encodings: (), + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, } struct _Inner(Arc); impl InstrumentServer { @@ -283,6 +289,18 @@ pub mod instrument_server { { InterceptedService::new(Self::new(inner), interceptor) } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } } impl tonic::codegen::Service> for InstrumentServer where @@ -491,7 +509,7 @@ pub mod instrument_server { write!(f, "{:?}", self.0) } } - impl tonic::transport::NamedService for InstrumentServer { + impl tonic::server::NamedService for InstrumentServer { const NAME: &'static str = "rs.tokio.console.instrument.Instrument"; } } diff --git a/console-api/src/generated/rs.tokio.console.resources.rs b/console-api/src/generated/rs.tokio.console.resources.rs index 10c014758..714391461 100644 --- a/console-api/src/generated/rs.tokio.console.resources.rs +++ b/console-api/src/generated/rs.tokio.console.resources.rs @@ -80,6 +80,17 @@ pub mod resource { /// `TIMER` signals that this is a timer resource, e.g. waiting for a sleep to finish. Timer = 0, } + impl Known { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Known::Timer => "TIMER", + } + } + } /// Every resource is either a known kind or an other (unknown) kind. #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Kind { diff --git a/console-api/src/generated/rs.tokio.console.tasks.rs b/console-api/src/generated/rs.tokio.console.tasks.rs index 10915a6c5..6ff543bc9 100644 --- a/console-api/src/generated/rs.tokio.console.tasks.rs +++ b/console-api/src/generated/rs.tokio.console.tasks.rs @@ -47,7 +47,7 @@ pub struct TaskDetails { /// /// This is either: /// - the raw binary representation of a HdrHistogram.rs `Histogram` - /// serialized to binary in the V2 format (legacy) + /// serialized to binary in the V2 format (legacy) /// - a binary histogram plus details on outliers (current) #[prost(oneof="task_details::PollTimesHistogram", tags="3, 4")] pub poll_times_histogram: ::core::option::Option, @@ -58,7 +58,7 @@ pub mod task_details { /// /// This is either: /// - the raw binary representation of a HdrHistogram.rs `Histogram` - /// serialized to binary in the V2 format (legacy) + /// serialized to binary in the V2 format (legacy) /// - a binary histogram plus details on outliers (current) #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum PollTimesHistogram { @@ -125,6 +125,18 @@ pub mod task { /// (such as `tokio::task::spawn_blocking`). Blocking = 1, } + impl Kind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Kind::Spawn => "SPAWN", + Kind::Blocking => "BLOCKING", + } + } + } } /// Task performance statistics. #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/console-api/src/generated/rs.tokio.console.trace.rs b/console-api/src/generated/rs.tokio.console.trace.rs index ed3e98991..13c2843a5 100644 --- a/console-api/src/generated/rs.tokio.console.trace.rs +++ b/console-api/src/generated/rs.tokio.console.trace.rs @@ -84,6 +84,7 @@ pub mod trace_event { pub mod trace_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] use tonic::codegen::*; + use tonic::codegen::http::Uri; /// Allows observers to stream trace events for a given `WatchRequest` filter. #[derive(Debug, Clone)] pub struct TraceClient { @@ -104,19 +105,24 @@ pub mod trace_client { where T: tonic::client::GrpcService, T::Error: Into, - T::ResponseBody: Default + Body + Send + 'static, + T::ResponseBody: Body + Send + 'static, ::Error: Into + Send, { pub fn new(inner: T) -> Self { let inner = tonic::client::Grpc::new(inner); Self { inner } } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } pub fn with_interceptor( inner: T, interceptor: F, ) -> TraceClient> where F: tonic::service::Interceptor, + T::ResponseBody: Default, T: tonic::codegen::Service< http::Request, Response = http::Response< @@ -129,19 +135,19 @@ pub mod trace_client { { TraceClient::new(InterceptedService::new(inner, interceptor)) } - /// Compress requests with `gzip`. + /// Compress requests with the given encoding. /// /// This requires the server to support it otherwise it might respond with an /// error. #[must_use] - pub fn send_gzip(mut self) -> Self { - self.inner = self.inner.send_gzip(); + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); self } - /// Enable decompressing responses with `gzip`. + /// Enable decompressing responses. #[must_use] - pub fn accept_gzip(mut self) -> Self { - self.inner = self.inner.accept_gzip(); + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); self } /// Produces a stream of trace events for the given filter. @@ -192,8 +198,8 @@ pub mod trace_server { #[derive(Debug)] pub struct TraceServer { inner: _Inner, - accept_compression_encodings: (), - send_compression_encodings: (), + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, } struct _Inner(Arc); impl TraceServer { @@ -217,6 +223,18 @@ pub mod trace_server { { InterceptedService::new(Self::new(inner), interceptor) } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } } impl tonic::codegen::Service> for TraceServer where @@ -310,7 +328,7 @@ pub mod trace_server { write!(f, "{:?}", self.0) } } - impl tonic::transport::NamedService for TraceServer { + impl tonic::server::NamedService for TraceServer { const NAME: &'static str = "rs.tokio.console.trace.Trace"; } } diff --git a/console-api/src/instrument.rs b/console-api/src/instrument.rs index 3a9c6b5d6..b7189bab4 100644 --- a/console-api/src/instrument.rs +++ b/console-api/src/instrument.rs @@ -1 +1,3 @@ +#![allow(warnings)] + include!("generated/rs.tokio.console.instrument.rs"); diff --git a/console-api/src/resources.rs b/console-api/src/resources.rs index 82956509a..78c22b3bf 100644 --- a/console-api/src/resources.rs +++ b/console-api/src/resources.rs @@ -1 +1,3 @@ +#![allow(warnings)] + include!("generated/rs.tokio.console.resources.rs"); diff --git a/console-api/src/tasks.rs b/console-api/src/tasks.rs index 2d4eea2bb..9576d9571 100644 --- a/console-api/src/tasks.rs +++ b/console-api/src/tasks.rs @@ -1 +1,3 @@ +#![allow(warnings)] + include!("generated/rs.tokio.console.tasks.rs"); diff --git a/console-api/src/trace.rs b/console-api/src/trace.rs index 0cdaa8452..4bc972e9c 100644 --- a/console-api/src/trace.rs +++ b/console-api/src/trace.rs @@ -1 +1,3 @@ +#![allow(warnings)] + include!("generated/rs.tokio.console.trace.rs"); diff --git a/console-api/tests/bootstrap.rs b/console-api/tests/bootstrap.rs index ed48c6a13..27c629cf4 100644 --- a/console-api/tests/bootstrap.rs +++ b/console-api/tests/bootstrap.rs @@ -1,38 +1,56 @@ -use std::{path::PathBuf, process::Command}; +use std::{fs, path::PathBuf, process::Command}; #[test] fn bootstrap() { - let iface_files = &[ - "proto/trace.proto", - "proto/common.proto", - "proto/tasks.proto", - "proto/instrument.proto", - "proto/resources.proto", - "proto/async_ops.proto", - ]; - let dirs = &["proto"]; + let root_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")); + let proto_dir = root_dir.join("proto"); + let proto_ext = std::ffi::OsStr::new("proto"); + let proto_files = fs::read_dir(&proto_dir).and_then(|dir| { + dir.filter_map(|entry| { + (|| { + let entry = entry?; + if entry.file_type()?.is_dir() { + return Ok(None); + } - let out_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")) - .join("src") - .join("generated"); + let path = entry.path(); + if path.extension() != Some(proto_ext) { + return Ok(None); + } - tonic_build::configure() + Ok(Some(path)) + })() + .transpose() + }) + .collect::, _>>() + }); + let proto_files = match proto_files { + Ok(files) => files, + Err(error) => panic!("failed to list proto files: {}", error), + }; + + let out_dir = root_dir.join("src").join("generated"); + + if let Err(error) = tonic_build::configure() .build_client(true) .build_server(true) + .emit_rerun_if_changed(false) .protoc_arg("--experimental_allow_proto3_optional") - .out_dir(format!("{}", out_dir.display())) - .compile(iface_files, dirs) - .unwrap(); + .out_dir(&out_dir) + .compile(&proto_files[..], &[proto_dir]) + { + panic!("failed to compile `console-api` protobuf: {}", error); + } let status = Command::new("git") .arg("diff") .arg("--exit-code") .arg("--") - .arg(format!("{}", out_dir.display())) - .status() - .unwrap(); - - if !status.success() { - panic!("You should commit the protobuf files"); + .arg(out_dir) + .status(); + match status { + Ok(status) if !status.success() => panic!("You should commit the protobuf files"), + Err(error) => panic!("failed to run `git diff`: {}", error), + Ok(_) => {} } } diff --git a/console-subscriber/CHANGELOG.md b/console-subscriber/CHANGELOG.md index 977b9fafd..4c5864cb6 100644 --- a/console-subscriber/CHANGELOG.md +++ b/console-subscriber/CHANGELOG.md @@ -1,3 +1,22 @@ + +## 0.1.8 (2022-09-04) + + +#### Bug Fixes + +* fix build on tokio 1.21.0 (#374) ([0106407c](0106407c)) + + + +## 0.1.7 (2022-08-10) + + +#### Features + +* Update `tonic` to `0.8` (#364) ([40e2f6fd](40e2f6fd)) +* Update `console-api` to `0.4` (#364) ([40e2f6fd](40e2f6fd)) + + ## 0.1.6 (2022-05-23) diff --git a/console-subscriber/Cargo.toml b/console-subscriber/Cargo.toml index aec867f43..3c044c8d7 100644 --- a/console-subscriber/Cargo.toml +++ b/console-subscriber/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "console-subscriber" -version = "0.1.6" +version = "0.1.8" license = "MIT" edition = "2021" rust-version = "1.58.0" @@ -32,11 +32,11 @@ env-filter = ["tracing-subscriber/env-filter"] [dependencies] crossbeam-utils = "0.8.7" -tokio = { version = "^1.15", features = ["sync", "time", "macros", "tracing"] } -tokio-stream = "0.1" +tokio = { version = "^1.21", features = ["sync", "time", "macros", "tracing"] } +tokio-stream = { version = "0.1", features = ["net"] } thread_local = "1.1.3" -console-api = { version = "0.3.0", path = "../console-api", features = ["transport"] } -tonic = { version = "0.7", features = ["transport"] } +console-api = { version = "0.4.0", path = "../console-api", features = ["transport"] } +tonic = { version = "0.8", features = ["transport"] } tracing-core = "0.1.24" tracing = "0.1.26" tracing-subscriber = { version = "0.3.11", default-features = false, features = ["fmt", "registry"] } @@ -44,9 +44,9 @@ futures = { version = "0.3", default-features = false } hdrhistogram = { version = "7.3.0", default-features = false, features = ["serialization"] } # The parking_lot dependency is renamed, because we want our `parking_lot` # feature to also enable `tracing-subscriber`'s parking_lot feature flag. -parking_lot_crate = { package = "parking_lot", version = "0.11", optional = true } +parking_lot_crate = { package = "parking_lot", version = "0.12", optional = true } humantime = "2.1.0" -prost-types = "0.10.0" +prost-types = "0.11.0" # Required for recording: serde = { version = "1", features = ["derive"] } @@ -54,7 +54,7 @@ serde_json = "1" crossbeam-channel = "0.5" [dev-dependencies] -tokio = { version = "^1.7", features = ["full", "rt-multi-thread"] } +tokio = { version = "^1.21", features = ["full", "rt-multi-thread"] } futures = "0.3" [package.metadata.docs.rs] diff --git a/console-subscriber/README.md b/console-subscriber/README.md index 9d9e5224c..01c5c05e0 100644 --- a/console-subscriber/README.md +++ b/console-subscriber/README.md @@ -78,7 +78,7 @@ runtime][Tokio] is considered *experimental*. In order to use ``` If you're using a workspace, you should put the `.cargo/config.toml` file in the root of your workspace. Otherwise, put the `.cargo/config.toml` file in the root directory of your crate. - + Putting `.cargo/config.toml` files below the workspace or crate root directory may lead to tools like Rust-Analyzer or VSCode not using your `.cargo/config.toml` since they invoke cargo from the workspace or crate root and cargo only looks for the `.cargo` directory in the current & parent directories. @@ -101,9 +101,9 @@ runtime][Tokio] is considered *experimental*. In order to use [`EnvFilter`] or [`Targets`] filters from [`tracing-subscriber`], add `"tokio=trace,runtime=trace"` to your filter configuration. - + Also, ensure you have not enabled any of the [compile time filter + + Also, ensure you have not enabled any of the [compile time filter features][compile_time_filters] in your `Cargo.toml`. - + #### Required Tokio Versions Because instrumentation for different aspects of the runtime is being added to @@ -111,7 +111,7 @@ Tokio over time, the latest Tokio release is generally *recommended* to access a the console's functionality. However, it should generally be compatible with earlier Tokio versions, although some information may not be available. A minimum version of [Tokio v1.0.0] or later is required to use the console's -task instrumentation. +task instrumentation. Other instrumentation is added in later Tokio releases: @@ -126,8 +126,10 @@ Other instrumentation is added in later Tokio releases: * [Tokio v1.15.0] or later is required to track [`tokio::sync`] resources, such as `Mutex`es, `RwLock`s, `Semaphore`s, `oneshot` channels, `mpsc` channels, et - cetera. - + cetera. + +* [Tokio v1.21.0] or later is required to use newest `task::Builder::spawn*` APIs. + [Tokio v1.0.0]: https://github.com/tokio-rs/tokio/releases/tag/tokio-1.0.0 [Tokio v1.7.0]: https://github.com/tokio-rs/tokio/releases/tag/tokio-1.7.0 [Tokio v1.12.0]:https://github.com/tokio-rs/tokio/releases/tag/tokio-1.12.0 @@ -144,6 +146,7 @@ Other instrumentation is added in later Tokio releases: [builder]: https://docs.rs/console-subscriber/latest/console_subscriber/struct.Builder.html [init]: https://docs.rs/console-subscriber/latest/console_subscriber/fn.init.html [compile_time_filters]: https://docs.rs/tracing/latest/tracing/level_filters/index.html#compile-time-filters +[Tokio v1.21.0]: https://github.com/tokio-rs/tokio/releases/tag/tokio-1.21.0 ### Adding the Console Subscriber diff --git a/console-subscriber/examples/app.rs b/console-subscriber/examples/app.rs index 065778f62..f1247ed62 100644 --- a/console-subscriber/examples/app.rs +++ b/console-subscriber/examples/app.rs @@ -23,15 +23,20 @@ async fn main() -> Result<(), Box> { "blocks" => { tokio::task::Builder::new() .name("blocks") - .spawn(double_sleepy(1, 10)); + .spawn(double_sleepy(1, 10)) + .unwrap(); } "coma" => { tokio::task::Builder::new() .name("coma") - .spawn(std::future::pending::<()>()); + .spawn(std::future::pending::<()>()) + .unwrap(); } "burn" => { - tokio::task::Builder::new().name("burn").spawn(burn(1, 10)); + tokio::task::Builder::new() + .name("burn") + .spawn(burn(1, 10)) + .unwrap(); } "help" | "-h" => { eprintln!("{}", HELP); @@ -47,10 +52,12 @@ async fn main() -> Result<(), Box> { let task1 = tokio::task::Builder::new() .name("task1") - .spawn(spawn_tasks(1, 10)); + .spawn(spawn_tasks(1, 10)) + .unwrap(); let task2 = tokio::task::Builder::new() .name("task2") - .spawn(spawn_tasks(10, 30)); + .spawn(spawn_tasks(10, 30)) + .unwrap(); let result = tokio::try_join! { task1, @@ -66,7 +73,10 @@ async fn spawn_tasks(min: u64, max: u64) { loop { for i in min..max { tracing::trace!(i, "spawning wait task"); - tokio::task::Builder::new().name("wait").spawn(wait(i)); + tokio::task::Builder::new() + .name("wait") + .spawn(wait(i)) + .unwrap(); let sleep = Duration::from_secs(max) - Duration::from_secs(i); tracing::trace!(?sleep, "sleeping..."); diff --git a/console-subscriber/examples/barrier.rs b/console-subscriber/examples/barrier.rs index c9f54c273..f2c62a81f 100644 --- a/console-subscriber/examples/barrier.rs +++ b/console-subscriber/examples/barrier.rs @@ -14,11 +14,15 @@ async fn main() -> Result<(), Box> { for i in 0..30 { let c = barrier.clone(); let task_name = format!("task-{}", i); - handles.push(task::Builder::default().name(&task_name).spawn(async move { - tokio::time::sleep(Duration::from_secs(i)).await; - let wait_result = c.wait().await; - wait_result - })); + handles.push( + task::Builder::default() + .name(&task_name) + .spawn(async move { + tokio::time::sleep(Duration::from_secs(i)).await; + c.wait().await + }) + .unwrap(), + ); } // Will not resolve until all "after wait" messages have been printed @@ -34,6 +38,7 @@ async fn main() -> Result<(), Box> { // Exactly one barrier will resolve as the "leader" assert_eq!(num_leaders, 1); }) + .unwrap() .await?; Ok(()) diff --git a/console-subscriber/examples/mutex.rs b/console-subscriber/examples/mutex.rs index 483ff8ebc..f50c952cc 100644 --- a/console-subscriber/examples/mutex.rs +++ b/console-subscriber/examples/mutex.rs @@ -20,11 +20,13 @@ async fn main() -> Result<(), Box> { *lock += 1; tokio::time::sleep(Duration::from_secs(1)).await; } - }); + }) + .unwrap(); } while *count.lock().await < 50 {} }) + .unwrap() .await?; Ok(()) diff --git a/console-subscriber/examples/rwlock.rs b/console-subscriber/examples/rwlock.rs index 34e06dcc7..437b411b4 100644 --- a/console-subscriber/examples/rwlock.rs +++ b/console-subscriber/examples/rwlock.rs @@ -20,7 +20,8 @@ async fn main() -> Result<(), Box> { *lock += 1; tokio::time::sleep(Duration::from_secs(1)).await; } - }); + }) + .unwrap(); } loop { @@ -31,6 +32,7 @@ async fn main() -> Result<(), Box> { } } }) + .unwrap() .await?; Ok(()) diff --git a/console-subscriber/examples/semaphore.rs b/console-subscriber/examples/semaphore.rs index 223d73400..b9779eb1e 100644 --- a/console-subscriber/examples/semaphore.rs +++ b/console-subscriber/examples/semaphore.rs @@ -21,19 +21,24 @@ async fn main() -> Result<(), Box> { .spawn(async move { let _permit = acquire_sem.acquire_many(i).await.unwrap(); tokio::time::sleep(Duration::from_secs(i as u64 * 2)).await; - }), + }) + .unwrap(), + ); + tasks.push( + tokio::task::Builder::default() + .name(&add_task_name) + .spawn(async move { + tokio::time::sleep(Duration::from_secs(i as u64 * 5)).await; + add_sem.add_permits(i as usize); + }) + .unwrap(), ); - tasks.push(tokio::task::Builder::default().name(&add_task_name).spawn( - async move { - tokio::time::sleep(Duration::from_secs(i as u64 * 5)).await; - add_sem.add_permits(i as usize); - }, - )); } let all_tasks = futures::future::try_join_all(tasks); all_tasks.await.unwrap(); }) + .unwrap() .await?; Ok(()) diff --git a/console-subscriber/examples/uds.rs b/console-subscriber/examples/uds.rs new file mode 100644 index 000000000..03f6f2d4a --- /dev/null +++ b/console-subscriber/examples/uds.rs @@ -0,0 +1,40 @@ +//! Demonstrates serving the console API over a [Unix domain socket] (UDS) +//! connection, rather than over TCP. +//! +//! Note that this example only works on Unix operating systems that +//! support UDS, such as Linux, BSDs, and macOS. +//! +//! [Unix domain socket]: https://en.wikipedia.org/wiki/Unix_domain_socket + +#[cfg(unix)] +use { + std::time::Duration, + tokio::{fs, task, time}, + tracing::info, +}; + +#[cfg(unix)] +#[tokio::main] +async fn main() -> Result<(), Box> { + let cwd = fs::canonicalize(".").await?; + let addr = cwd.join("console-server"); + console_subscriber::ConsoleLayer::builder() + .server_addr(&*addr) + .init(); + info!( + "listening for console connections at file://localhost{}", + addr.display() + ); + task::Builder::default() + .name("sleepy") + .spawn(async move { time::sleep(Duration::from_secs(90)).await }) + .unwrap() + .await?; + + Ok(()) +} + +#[cfg(not(unix))] +fn main() { + panic!("only supported on Unix platforms") +} diff --git a/console-subscriber/src/builder.rs b/console-subscriber/src/builder.rs index 2c9fb0cc6..1e0819dde 100644 --- a/console-subscriber/src/builder.rs +++ b/console-subscriber/src/builder.rs @@ -1,6 +1,8 @@ use super::{ConsoleLayer, Server}; +#[cfg(unix)] +use std::path::Path; use std::{ - net::{SocketAddr, ToSocketAddrs}, + net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}, path::PathBuf, thread, time::Duration, @@ -32,7 +34,7 @@ pub struct Builder { pub(crate) retention: Duration, /// The address on which to serve the RPC server. - pub(super) server_addr: SocketAddr, + pub(super) server_addr: ServerAddr, /// If and where to save a recording of the events. pub(super) recording_path: Option, @@ -58,7 +60,7 @@ impl Default for Builder { publish_interval: ConsoleLayer::DEFAULT_PUBLISH_INTERVAL, retention: ConsoleLayer::DEFAULT_RETENTION, poll_duration_max: ConsoleLayer::DEFAULT_POLL_DURATION_MAX, - server_addr: SocketAddr::new(Server::DEFAULT_IP, Server::DEFAULT_PORT), + server_addr: ServerAddr::Tcp(SocketAddr::new(Server::DEFAULT_IP, Server::DEFAULT_PORT)), recording_path: None, filter_env_var: "RUST_LOG".to_string(), self_trace: false, @@ -137,8 +139,38 @@ impl Builder { /// before falling back on constructing a socket address from those /// defaults. /// + /// The socket address can be either a TCP socket address or a + /// [Unix domain socket] (UDS) address. Unix domain sockets are only + /// supported on Unix-compatible operating systems, such as Linux, BSDs, + /// and macOS. + /// + /// Each call to this method will overwrite the previously set value. + /// + /// # Examples + /// + /// Connect to the TCP address `localhost:1234`: + /// + /// ``` + /// # use console_subscriber::Builder; + /// use std::net::Ipv4Addr; + /// let builder = Builder::default().server_addr((Ipv4Addr::LOCALHOST, 1234)); + /// ``` + /// + /// Connect to the UDS address `/tmp/tokio-console`: + /// + /// ``` + /// # use console_subscriber::Builder; + /// # #[cfg(unix)] + /// use std::path::Path; + /// + /// // Unix domain sockets are only available on Unix-compatible operating systems. + /// #[cfg(unix)] + /// let builder = Builder::default().server_addr(Path::new("/tmp/tokio-console")); + /// ``` + /// /// [environment variable]: `Builder::with_default_env` - pub fn server_addr(self, server_addr: impl Into) -> Self { + /// [Unix domain socket]: https://en.wikipedia.org/wiki/Unix_domain_socket + pub fn server_addr(self, server_addr: impl Into) -> Self { Self { server_addr: server_addr.into(), ..self @@ -231,11 +263,14 @@ impl Builder { } if let Ok(bind) = std::env::var("TOKIO_CONSOLE_BIND") { - self.server_addr = bind - .to_socket_addrs() - .expect("TOKIO_CONSOLE_BIND must be formatted as HOST:PORT, such as localhost:4321") - .next() - .expect("tokio console could not resolve TOKIO_CONSOLE_BIND"); + self.server_addr = ServerAddr::Tcp( + bind.to_socket_addrs() + .expect( + "TOKIO_CONSOLE_BIND must be formatted as HOST:PORT, such as localhost:4321", + ) + .next() + .expect("tokio console could not resolve TOKIO_CONSOLE_BIND"), + ); } if let Some(interval) = duration_from_env("TOKIO_CONSOLE_PUBLISH_INTERVAL") { @@ -456,6 +491,66 @@ impl Builder { } } +/// Specifies the address on which a [`Server`] should listen. +/// +/// This type is passed as an argument to the [`Builder::server_addr`] +/// method, and may be either a TCP socket address, or a [Unix domain socket] +/// (UDS) address. Unix domain sockets are only supported on Unix-compatible +/// operating systems, such as Linux, BSDs, and macOS. +/// +/// [`Server`]: crate::Server +/// [Unix domain socket]: https://en.wikipedia.org/wiki/Unix_domain_socket +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ServerAddr { + /// A TCP address. + Tcp(SocketAddr), + /// A Unix socket address. + #[cfg(unix)] + Unix(PathBuf), +} + +impl From for ServerAddr { + fn from(addr: SocketAddr) -> ServerAddr { + ServerAddr::Tcp(addr) + } +} + +impl From for ServerAddr { + fn from(addr: SocketAddrV4) -> ServerAddr { + ServerAddr::Tcp(addr.into()) + } +} + +impl From for ServerAddr { + fn from(addr: SocketAddrV6) -> ServerAddr { + ServerAddr::Tcp(addr.into()) + } +} + +impl From<(I, u16)> for ServerAddr +where + I: Into, +{ + fn from(pieces: (I, u16)) -> ServerAddr { + ServerAddr::Tcp(pieces.into()) + } +} + +#[cfg(unix)] +impl From for ServerAddr { + fn from(path: PathBuf) -> ServerAddr { + ServerAddr::Unix(path) + } +} + +#[cfg(unix)] +impl<'a> From<&'a Path> for ServerAddr { + fn from(path: &'a Path) -> ServerAddr { + ServerAddr::Unix(path.to_path_buf()) + } +} + /// Initializes the console [tracing `Subscriber`][sub] and starts the console /// subscriber [`Server`] on its own background thread. /// diff --git a/console-subscriber/src/callsites.rs b/console-subscriber/src/callsites.rs index 819f6fc09..feb635638 100644 --- a/console-subscriber/src/callsites.rs +++ b/console-subscriber/src/callsites.rs @@ -23,7 +23,7 @@ impl Callsites { } let idx = self.len.fetch_add(1, Ordering::AcqRel); - if idx <= MAX_CALLSITES { + if idx < MAX_CALLSITES { // If there's still room in the callsites array, stick the address // in there. self.ptrs[idx] diff --git a/console-subscriber/src/lib.rs b/console-subscriber/src/lib.rs index 748b045d3..d91ee392a 100644 --- a/console-subscriber/src/lib.rs +++ b/console-subscriber/src/lib.rs @@ -5,7 +5,7 @@ use serde::Serialize; use std::{ cell::RefCell, fmt, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr}, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -13,7 +13,11 @@ use std::{ time::{Duration, Instant}, }; use thread_local::ThreadLocal; +#[cfg(unix)] +use tokio::net::UnixListener; use tokio::sync::{mpsc, oneshot}; +#[cfg(unix)] +use tokio_stream::wrappers::UnixListenerStream; use tracing_core::{ span::{self, Id}, subscriber::{self, Subscriber}, @@ -36,7 +40,7 @@ pub(crate) mod sync; mod visitors; use aggregator::Aggregator; -pub use builder::Builder; +pub use builder::{Builder, ServerAddr}; use callsites::Callsites; use record::Recorder; use stack::SpanStack; @@ -134,7 +138,7 @@ pub struct ConsoleLayer { /// [cli]: https://crates.io/crates/tokio-console pub struct Server { subscribe: mpsc::Sender, - addr: SocketAddr, + addr: ServerAddr, aggregator: Option, client_buffer: usize, } @@ -945,13 +949,22 @@ impl Server { .take() .expect("cannot start server multiple times"); let aggregate = spawn_named(aggregate.run(), "console::aggregate"); - let addr = self.addr; - let serve = builder - .add_service(proto::instrument::instrument_server::InstrumentServer::new( - self, - )) - .serve(addr); - let res = spawn_named(serve, "console::serve").await; + let addr = self.addr.clone(); + let router = builder.add_service( + proto::instrument::instrument_server::InstrumentServer::new(self), + ); + let res = match addr { + ServerAddr::Tcp(addr) => { + let serve = router.serve(addr); + spawn_named(serve, "console::serve").await + } + #[cfg(unix)] + ServerAddr::Unix(path) => { + let incoming = UnixListener::bind(path)?; + let serve = router.serve_with_incoming(UnixListenerStream::new(incoming)); + spawn_named(serve, "console::serve").await + } + }; aggregate.abort(); res?.map_err(Into::into) } @@ -1063,7 +1076,7 @@ where T: Send + 'static, { #[cfg(tokio_unstable)] - return tokio::task::Builder::new().name(_name).spawn(task); + return tokio::task::Builder::new().name(_name).spawn(task).unwrap(); #[cfg(not(tokio_unstable))] tokio::spawn(task) diff --git a/console-subscriber/src/stats.rs b/console-subscriber/src/stats.rs index 32fb364f1..2bce3c085 100644 --- a/console-subscriber/src/stats.rs +++ b/console-subscriber/src/stats.rs @@ -312,7 +312,7 @@ impl AsyncOpStats { pub(crate) fn task_id(&self) -> Option { let id = self.task_id.load(); if id > 0 { - Some(id as u64) + Some(id) } else { None } @@ -532,7 +532,13 @@ impl ToProto for PollStats { last_poll_ended: timestamps .last_poll_ended .map(|at| base_time.to_timestamp(at)), - busy_time: Some(timestamps.busy_time.into()), + busy_time: Some(timestamps.busy_time.try_into().unwrap_or_else(|error| { + eprintln!( + "failed to convert busy time to protobuf duration: {}", + error + ); + Default::default() + })), } } } diff --git a/tokio-console/CHANGELOG.md b/tokio-console/CHANGELOG.md index f48e07a97..d28c0d5c5 100644 --- a/tokio-console/CHANGELOG.md +++ b/tokio-console/CHANGELOG.md @@ -1,3 +1,16 @@ + +## 0.1.7 (2022-08-10) + + +#### Features + +* include config options in autogenerated issues (#365) ([fcb54dff](fcb54dff)) +* filter out boring frames in backtraces (#365) ([523a44a3](523a44a3)) +* init error handling before subcmds (#365) ([66465689](66465689)) +* only suggest opening issues for panics (#365) ([23cb6bf7](23cb6bf7)) +* update `tonic` to `0.8` (#364) ([40e2f6fd](40e2f6fd)) +* update `console-api` to `0.4` (#364) ([40e2f6fd](40e2f6fd)) + ## 0.1.6 (2022-05-24) diff --git a/tokio-console/Cargo.toml b/tokio-console/Cargo.toml index b5be2873e..a6f0de3f3 100644 --- a/tokio-console/Cargo.toml +++ b/tokio-console/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "tokio-console" -version = "0.1.6" +version = "0.1.7" license = "MIT" repository = "https://github.com/tokio-rs/console" edition = "2021" rust-version = "1.58.0" authors = ["Eliza Weisman ", "Tokio Contributors ",] readme = "README.md" +default-run = "tokio-console" homepage = "https://github.com/tokio-rs/console/tree/main/tokio-console" description = """ The Tokio console: a debugger for async Rust. @@ -27,18 +28,19 @@ keywords = [ [dependencies] atty = "0.2" -console-api = { version = "0.3.0", path = "../console-api", features = ["transport"] } +console-api = { version = "0.4.0", path = "../console-api", features = ["transport"] } clap = { version = "3", features = ["cargo", "derive", "env"] } tokio = { version = "1", features = ["full", "rt-multi-thread"] } -tonic = { version = "0.7", features = ["transport"] } +tonic = { version = "0.8", features = ["transport"] } futures = "0.3" tui = { version = "0.16.0", default-features = false, features = ["crossterm"] } +tower = "0.4.12" tracing = "0.1" tracing-subscriber = { version = "0.3.0", features = ["env-filter"] } tracing-journald = { version = "0.2", optional = true } -prost-types = "0.10" +prost-types = "0.11" crossterm = { version = "0.20", features = ["event-stream"] } -color-eyre = { version = "0.5", features = ["issue-url"] } +color-eyre = { version = "0.6", features = ["issue-url"] } hdrhistogram = { version = "7.3.0", default-features = false, features = ["serialization"] } h2 = "0.3" regex = "1.5" diff --git a/tokio-console/args.example b/tokio-console/args.example index db39c8f85..a6b59aaf8 100644 --- a/tokio-console/args.example +++ b/tokio-console/args.example @@ -7,6 +7,9 @@ ARGS: This may be an IP address and port, or a DNS name. + On Unix platforms, this may also be a URI with the `file` scheme that specifies the path + to a Unix domain socket, as in `file://localhost/path/to/socket`. + [default: http://127.0.0.1:6669] OPTIONS: diff --git a/tokio-console/src/config.rs b/tokio-console/src/config.rs index 9e0153e5d..9513d7609 100644 --- a/tokio-console/src/config.rs +++ b/tokio-console/src/config.rs @@ -26,6 +26,10 @@ pub struct Config { /// /// This may be an IP address and port, or a DNS name. /// + /// On Unix platforms, this may also be a URI with the `file` scheme that + /// specifies the path to a Unix domain socket, as in + /// `file://localhost/path/to/socket`. + /// /// [default: http://127.0.0.1:6669] #[clap(value_hint = ValueHint::Url)] pub(crate) target_addr: Option, @@ -121,6 +125,12 @@ pub enum OptionalCmd { #[derive(Debug, Clone, Copy, Deserialize)] struct RetainFor(Option); +impl Default for RetainFor { + fn default() -> Self { + Self(Some(Duration::from_secs(6))) + } +} + impl fmt::Display for RetainFor { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { @@ -194,7 +204,7 @@ pub struct ColorToggles { color_terminated: Option, } -/// A sturct used to parse the toml config file +/// A struct used to parse the toml config file #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct ConfigFile { @@ -320,7 +330,7 @@ impl Config { } pub(crate) fn retain_for(&self) -> Option { - self.retain_for.as_ref().and_then(|value| value.0) + self.retain_for.unwrap_or_default().0 } pub(crate) fn target_addr(&self) -> Uri { @@ -330,6 +340,42 @@ impl Config { .clone() } + pub(crate) fn add_issue_metadata( + &self, + mut builder: color_eyre::config::HookBuilder, + ) -> color_eyre::config::HookBuilder { + macro_rules! add_issue_metadata { + ($self:ident, $builder:ident => + $( + $($name:ident).+ + ),+ + $(,)? + ) => { + $( + $builder = $builder.add_issue_metadata(concat!("config", $(".", stringify!($name)),+), format!("`{:?}`", $self$(.$name)+)); + )* + } + } + + add_issue_metadata! { + self, builder => + subcmd, + target_addr, + env_filter, + log_directory, + retain_for, + view_options.no_colors, + view_options.lang, + view_options.ascii_only, + view_options.truecolor, + view_options.palette, + view_options.toggles.color_durations, + view_options.toggles.color_terminated, + } + + builder + } + fn from_path(config_path: ConfigPath) -> color_eyre::Result> { ConfigFile::from_path(config_path)? .map(|config| config.try_into()) @@ -354,7 +400,7 @@ impl Default for Config { target_addr: Some(default_target_addr()), env_filter: Some(tracing_subscriber::EnvFilter::new("off")), log_directory: Some(default_log_directory()), - retain_for: Some(RetainFor(Some(Duration::from_secs(6)))), + retain_for: Some(RetainFor::default()), view_options: ViewOptions::default(), subcmd: None, } @@ -375,7 +421,7 @@ fn default_log_directory() -> PathBuf { impl ViewOptions { pub fn is_utf8(&self) -> bool { - if !self.ascii_only.unwrap_or(true) { + if self.ascii_only.unwrap_or(false) { return false; } self.lang.as_deref().unwrap_or_default().ends_with("UTF-8") @@ -456,7 +502,7 @@ impl Default for ViewOptions { fn default() -> Self { Self { no_colors: false, - lang: Some("en_us.UTF8".to_string()), + lang: Some("en_us.UTF-8".to_string()), ascii_only: Some(false), truecolor: Some(true), palette: Some(Palette::All), diff --git a/tokio-console/src/conn.rs b/tokio-console/src/conn.rs index faf42e9e0..6330b329b 100644 --- a/tokio-console/src/conn.rs +++ b/tokio-console/src/conn.rs @@ -5,7 +5,12 @@ use console_api::instrument::{ use console_api::tasks::TaskDetails; use futures::stream::StreamExt; use std::{error::Error, pin::Pin, time::Duration}; -use tonic::{transport::Channel, transport::Uri, Streaming}; +#[cfg(unix)] +use tokio::net::UnixStream; +use tonic::{ + transport::{Channel, Endpoint, Uri}, + Streaming, +}; #[derive(Debug)] pub struct Connection { @@ -78,7 +83,31 @@ impl Connection { tokio::time::sleep(backoff).await; } let try_connect = async { - let mut client = InstrumentClient::connect(self.target.clone()).await?; + let channel = match self.target.scheme_str() { + #[cfg(unix)] + Some("file") => { + // Dummy endpoint is ignored by the connector. + let endpoint = Endpoint::from_static("http://localhost"); + if !matches!(self.target.host(), None | Some("localhost")) { + return Err("cannot connect to non-localhost unix domain socket".into()); + } + let path = self.target.path().to_owned(); + endpoint + .connect_with_connector(tower::service_fn(move |_| { + UnixStream::connect(path.clone()) + })) + .await? + } + #[cfg(not(unix))] + Some("file") => { + return Err("unix domain sockets are not supported on this platform".into()); + } + _ => { + let endpoint = Endpoint::try_from(self.target.clone())?; + endpoint.connect().await? + } + }; + let mut client = InstrumentClient::new(channel); let request = tonic::Request::new(InstrumentRequest {}); let stream = Box::new(client.watch_updates(request).await?.into_inner()); Ok::>(State::Connected { client, stream }) diff --git a/tokio-console/src/main.rs b/tokio-console/src/main.rs index 68bb5f402..c10b062a6 100644 --- a/tokio-console/src/main.rs +++ b/tokio-console/src/main.rs @@ -26,6 +26,13 @@ mod warnings; #[tokio::main] async fn main() -> color_eyre::Result<()> { let mut args = config::Config::parse()?; + // initialize error handling first, in case panics occur while setting up + // other stuff. + let styles = view::Styles::from_config(args.view_options.clone()); + styles.error_init(&args)?; + + args.trace_init()?; + tracing::debug!(?args.target_addr, ?args.view_options); match args.subcmd { Some(config::OptionalCmd::GenConfig) => { @@ -40,16 +47,10 @@ async fn main() -> color_eyre::Result<()> { None => {} } - let retain_for = args.retain_for(); - args.trace_init()?; - tracing::debug!(?args.target_addr, ?args.view_options); - let target = args.target_addr(); tracing::info!(?target, "using target addr"); - let styles = view::Styles::from_config(args.view_options); - styles.error_init()?; - + let retain_for = args.retain_for(); let (mut terminal, _cleanup) = term::init_crossterm()?; terminal.clear()?; let mut conn = conn::Connection::new(target); diff --git a/tokio-console/src/state/mod.rs b/tokio-console/src/state/mod.rs index eb96606af..1458a1065 100644 --- a/tokio-console/src/state/mod.rs +++ b/tokio-console/src/state/mod.rs @@ -271,6 +271,7 @@ impl Metadata { impl Field { const SPAWN_LOCATION: &'static str = "spawn.location"; const NAME: &'static str = "task.name"; + const TASK_ID: &'static str = "task.id"; /// Converts a wire-format `Field` into an internal `Field` representation, /// using the provided `Metadata` for the task span that the field came diff --git a/tokio-console/src/state/tasks.rs b/tokio-console/src/state/tasks.rs index 8be120d8f..ad362f518 100644 --- a/tokio-console/src/state/tasks.rs +++ b/tokio-console/src/state/tasks.rs @@ -5,7 +5,7 @@ use crate::{ histogram::DurationHistogram, pb_duration, store::{self, Id, SpanId, Store}, - Field, Metadata, Visibility, + Field, FieldValue, Metadata, Visibility, }, util::Percentage, view, @@ -58,6 +58,17 @@ pub(crate) enum TaskState { pub(crate) type TaskRef = store::Ref; +/// The Id for a Tokio task. +/// +/// This should be equivalent to [`tokio::task::Id`], which can't be +/// used because it's not possible to construct outside the `tokio` +/// crate. +/// +/// Within the context of `tokio-console`, we don't depend on it +/// being the same as Tokio's own type, as the task id is recorded +/// as a `u64` in tracing and then sent via the wire protocol as such. +pub(crate) type TaskId = u64; + #[derive(Debug)] pub(crate) struct Task { /// The task's pretty (console-generated, sequential) task ID. @@ -65,10 +76,14 @@ pub(crate) struct Task { /// This is NOT the `tracing::span::Id` for the task's tracing span on the /// remote. id: Id, + /// The `tokio::task::Id` in the remote tokio runtime. + task_id: Option, /// The `tracing::span::Id` on the remote process for this task's span. /// /// This is used when requesting a task details stream. span_id: SpanId, + /// A cached string representation of the Id for display purposes. + id_str: String, short_desc: InternedStr, formatted_fields: Vec>>, stats: TaskStats, @@ -147,6 +162,7 @@ impl TasksState { } }; let mut name = None; + let mut task_id = None; let mut fields = task .fields .drain(..) @@ -157,6 +173,13 @@ impl TasksState { name = Some(strings.string(field.value.to_string())); return None; } + if &*field.name == Field::TASK_ID { + task_id = match field.value { + FieldValue::U64(id) => Some(id as TaskId), + _ => None, + }; + return None; + } Some(field) }) .collect::>(); @@ -170,15 +193,19 @@ impl TasksState { // remap the server's ID to a pretty, sequential task ID let id = ids.id_for(span_id); - let short_desc = strings.string(match name.as_ref() { - Some(name) => format!("{} ({})", id, name), - None => format!("{}", id), + let short_desc = strings.string(match (task_id, name.as_ref()) { + (Some(task_id), Some(name)) => format!("{task_id} ({name})"), + (Some(task_id), None) => task_id.to_string(), + (None, Some(name)) => name.as_ref().to_owned(), + (None, None) => "".to_owned(), }); let mut task = Task { name, id, + task_id, span_id, + id_str: task_id.map(|id| id.to_string()).unwrap_or_default(), short_desc, formatted_fields, stats, @@ -245,6 +272,10 @@ impl Task { self.span_id } + pub(crate) fn id_str(&self) -> &str { + &self.id_str + } + pub(crate) fn target(&self) -> &str { &self.target } @@ -290,12 +321,12 @@ impl Task { } pub(crate) fn busy(&self, since: SystemTime) -> Duration { - if let (Some(last_poll_started), None) = - (self.stats.last_poll_started, self.stats.last_poll_ended) - { - // in this case the task is being polled at the moment - let current_time_in_poll = since.duration_since(last_poll_started).unwrap_or_default(); - return self.stats.busy + current_time_in_poll; + if let Some(started) = self.stats.last_poll_started { + if self.stats.last_poll_started > self.stats.last_poll_ended { + // in this case the task is being polled at the moment + let current_time_in_poll = since.duration_since(started).unwrap_or_default(); + return self.stats.busy + current_time_in_poll; + } } self.stats.busy } @@ -426,7 +457,9 @@ impl Default for SortBy { impl SortBy { pub fn sort(&self, now: SystemTime, tasks: &mut [Weak>]) { match self { - Self::Tid => tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().id)), + Self::Tid => { + tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().task_id)) + } Self::Name => { tasks.sort_unstable_by_key(|task| task.upgrade().map(|t| t.borrow().name.clone())) } diff --git a/tokio-console/src/view/async_ops.rs b/tokio-console/src/view/async_ops.rs index 7e804cfeb..0cdf02162 100644 --- a/tokio-console/src/view/async_ops.rs +++ b/tokio-console/src/view/async_ops.rs @@ -7,7 +7,7 @@ use crate::{ view::{ self, bold, table::{self, TableList, TableListState}, - DUR_LEN, DUR_PRECISION, + DUR_LEN, DUR_TABLE_PRECISION, }, }; @@ -106,12 +106,7 @@ impl TableList<9> for AsyncOpsTable { let mut polls_width = view::Width::new(Self::WIDTHS[7] as u16); let dur_cell = |dur: std::time::Duration| -> Cell<'static> { - Cell::from(styles.time_units(format!( - "{:>width$.prec$?}", - dur, - width = DUR_LEN, - prec = DUR_PRECISION, - ))) + Cell::from(styles.time_units(dur, DUR_TABLE_PRECISION, Some(DUR_LEN))) }; let rows = { diff --git a/tokio-console/src/view/durations.rs b/tokio-console/src/view/durations.rs new file mode 100644 index 000000000..29303ed36 --- /dev/null +++ b/tokio-console/src/view/durations.rs @@ -0,0 +1,108 @@ +use std::cmp; + +use tui::{ + layout::{self}, + widgets::Widget, +}; + +use crate::{ + state::histogram::DurationHistogram, + view::{self, mini_histogram::MiniHistogram, percentiles::Percentiles}, +}; + +// This is calculated so that a legend like the below generally fits: +// │0647.17µs 909.31µs │ +// This also gives at characters for the sparkline itself. +const MIN_HISTOGRAM_BLOCK_WIDTH: u16 = 22; + +/// This is a tui-rs widget to visualize durations as a list of percentiles +/// and if possible, a mini-histogram too. +/// +/// This widget wraps the [`Percentiles`] and [`MiniHistogram`] widgets which +/// are displayed side by side. The mini-histogram will only be displayed if +/// a) UTF-8 support is enabled via [`Styles`] +/// b) There is at least a minimum width (22 characters to display the full +/// bottom legend) left after drawing the percentiles +/// +/// This +/// +/// [`Styles`]: crate::view::Styles +pub(crate) struct Durations<'a> { + /// Widget style + styles: &'a view::Styles, + /// The histogram data to render + histogram: Option<&'a DurationHistogram>, + /// Title for percentiles block + percentiles_title: &'a str, + /// Title for histogram sparkline block + histogram_title: &'a str, +} + +impl<'a> Widget for Durations<'a> { + fn render(self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) { + // Only split the durations area in half if we're also drawing a + // sparkline. We require UTF-8 to draw the sparkline and also enough width. + let (percentiles_area, histogram_area) = if self.styles.utf8 { + let percentiles_width = cmp::max(self.percentiles_title.len() as u16, 13_u16) + 2; + + // If there isn't enough width left after drawing the percentiles + // then we won't draw the sparkline at all. + if area.width < percentiles_width + MIN_HISTOGRAM_BLOCK_WIDTH { + (area, None) + } else { + let areas = layout::Layout::default() + .direction(layout::Direction::Horizontal) + .constraints( + [ + layout::Constraint::Length(percentiles_width), + layout::Constraint::Min(MIN_HISTOGRAM_BLOCK_WIDTH), + ] + .as_ref(), + ) + .split(area); + (areas[0], Some(areas[1])) + } + } else { + (area, None) + }; + + let percentiles_widget = Percentiles::new(self.styles) + .title(self.percentiles_title) + .histogram(self.histogram); + percentiles_widget.render(percentiles_area, buf); + + if let Some(histogram_area) = histogram_area { + let histogram_widget = MiniHistogram::default() + .block(self.styles.border_block().title(self.histogram_title)) + .histogram(self.histogram) + .duration_precision(2); + histogram_widget.render(histogram_area, buf); + } + } +} + +impl<'a> Durations<'a> { + pub(crate) fn new(styles: &'a view::Styles) -> Self { + Self { + styles, + histogram: None, + percentiles_title: "Percentiles", + histogram_title: "Histogram", + } + } + + pub(crate) fn histogram(mut self, histogram: Option<&'a DurationHistogram>) -> Self { + self.histogram = histogram; + self + } + + pub(crate) fn percentiles_title(mut self, title: &'a str) -> Self { + self.percentiles_title = title; + self + } + + pub(crate) fn histogram_title(mut self, title: &'a str) -> Self { + self.histogram_title = title; + self + } +} diff --git a/tokio-console/src/view/mini_histogram.rs b/tokio-console/src/view/mini_histogram.rs index 72ff983ef..1bdfcbd9e 100644 --- a/tokio-console/src/view/mini_histogram.rs +++ b/tokio-console/src/view/mini_histogram.rs @@ -7,6 +7,8 @@ use tui::{ widgets::{Block, Widget}, }; +use crate::state::histogram::DurationHistogram; + /// This is a tui-rs widget to visualize a latency histogram in a small area. /// It is based on the [`Sparkline`] widget, so it draws a mini bar chart with /// some labels for clarity. Unlike Sparkline, it does not omit very small @@ -18,10 +20,8 @@ pub(crate) struct MiniHistogram<'a> { block: Option>, /// Widget style style: Style, - /// Values for the buckets of the histogram - data: &'a [u64], - /// Metadata about the histogram - metadata: HistogramMetadata, + /// The histogram data to render + histogram: Option<&'a DurationHistogram>, /// The maximum value to take to compute the maximum bar height (if nothing is specified, the /// widget uses the max of the dataset) max: Option, @@ -51,8 +51,7 @@ impl<'a> Default for MiniHistogram<'a> { MiniHistogram { block: None, style: Default::default(), - data: &[], - metadata: Default::default(), + histogram: None, max: None, bar_set: symbols::bar::NINE_LEVELS, duration_precision: 4, @@ -75,34 +74,43 @@ impl<'a> Widget for MiniHistogram<'a> { return; } - let max_qty_label = self.metadata.max_bucket.to_string(); - let min_qty_label = self.metadata.min_bucket.to_string(); + let (data, metadata) = match self.histogram { + // Bit of a deadlock: We cannot know the highest bucket value without determining the number of buckets, + // and we cannot determine the number of buckets without knowing the width of the chart area which depends on + // the number of digits in the highest bucket value. + // So just assume here the number of digits in the highest bucket value is 3. + // If we overshoot, there will be empty columns/buckets at the right end of the chart. + // If we undershoot, the rightmost 1-2 columns/buckets will be hidden. + // We could get the max bucket value from the previous render though... + Some(h) => chart_data(h, inner_area.width - 3), + None => return, + }; + + let max_qty_label = metadata.max_bucket.to_string(); + let min_qty_label = metadata.min_bucket.to_string(); let max_record_label = format!( "{:.prec$?}", - Duration::from_nanos(self.metadata.max_value), + Duration::from_nanos(metadata.max_value), prec = self.duration_precision, ); let min_record_label = format!( "{:.prec$?}", - Duration::from_nanos(self.metadata.min_value), + Duration::from_nanos(metadata.min_value), prec = self.duration_precision, ); let y_axis_label_width = max_qty_label.len() as u16; - self.render_legend( + render_legend( inner_area, buf, + &metadata, max_record_label, min_record_label, max_qty_label, min_qty_label, ); - let legend_height = if self.metadata.high_outliers > 0 { - 2 - } else { - 1 - }; + let legend_height = if metadata.high_outliers > 0 { 2 } else { 1 }; // Shrink the bars area by 1 row from the bottom // and `y_axis_label_width` columns from the left. @@ -112,74 +120,23 @@ impl<'a> Widget for MiniHistogram<'a> { width: inner_area.width - y_axis_label_width, height: inner_area.height - legend_height, }; - self.render_bars(bars_area, buf); + self.render_bars(bars_area, buf, data); } } impl<'a> MiniHistogram<'a> { - fn render_legend( + fn render_bars( &mut self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer, - max_record_label: String, - min_record_label: String, - max_qty_label: String, - min_qty_label: String, + data: Vec, ) { - // If there are outliers, display a note - let labels_pos = if self.metadata.high_outliers > 0 { - let outliers = format!( - "{} outliers (highest: {:?})", - self.metadata.high_outliers, - self.metadata - .highest_outlier - .expect("if there are outliers, the highest should be set") - ); - buf.set_string( - area.right() - outliers.len() as u16, - area.bottom() - 1, - &outliers, - Style::default(), - ); - 2 - } else { - 1 - }; - - // top left: max quantity - buf.set_string(area.left(), area.top(), &max_qty_label, Style::default()); - // bottom left: 0 aligned to right - let zero_label = format!("{:>width$}", &min_qty_label, width = max_qty_label.len()); - buf.set_string( - area.left(), - area.bottom() - labels_pos, - &zero_label, - Style::default(), - ); - // bottom left below the chart: min time - buf.set_string( - area.left() + max_qty_label.len() as u16, - area.bottom() - labels_pos, - &min_record_label, - Style::default(), - ); - // bottom right: max time - buf.set_string( - area.right() - max_record_label.len() as u16, - area.bottom() - labels_pos, - &max_record_label, - Style::default(), - ); - } - - fn render_bars(&mut self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) { let max = match self.max { Some(v) => v, - None => *self.data.iter().max().unwrap_or(&1u64), + None => *data.iter().max().unwrap_or(&1u64), }; - let max_index = std::cmp::min(area.width as usize, self.data.len()); - let mut data = self - .data + let max_index = std::cmp::min(area.width as usize, data.len()); + let mut data = data .iter() .take(max_index) .map(|e| { @@ -244,15 +201,11 @@ impl<'a> MiniHistogram<'a> { self } - #[allow(dead_code)] - pub fn data(mut self, data: &'a [u64]) -> MiniHistogram<'a> { - self.data = data; - self - } - - #[allow(dead_code)] - pub fn metadata(mut self, metadata: HistogramMetadata) -> MiniHistogram<'a> { - self.metadata = metadata; + pub(crate) fn histogram( + mut self, + histogram: Option<&'a DurationHistogram>, + ) -> MiniHistogram<'a> { + self.histogram = histogram; self } @@ -268,3 +221,106 @@ impl<'a> MiniHistogram<'a> { self } } + +fn render_legend( + area: tui::layout::Rect, + buf: &mut tui::buffer::Buffer, + metadata: &HistogramMetadata, + max_record_label: String, + min_record_label: String, + max_qty_label: String, + min_qty_label: String, +) { + // If there are outliers, display a note + let labels_pos = if metadata.high_outliers > 0 { + let outliers = format!( + "{} outliers (highest: {:?})", + metadata.high_outliers, + metadata + .highest_outlier + .expect("if there are outliers, the highest should be set") + ); + buf.set_string( + area.right() - outliers.len() as u16, + area.bottom() - 1, + &outliers, + Style::default(), + ); + 2 + } else { + 1 + }; + + // top left: max quantity + buf.set_string(area.left(), area.top(), &max_qty_label, Style::default()); + // bottom left: 0 aligned to right + let zero_label = format!("{:>width$}", &min_qty_label, width = max_qty_label.len()); + buf.set_string( + area.left(), + area.bottom() - labels_pos, + &zero_label, + Style::default(), + ); + // bottom left below the chart: min time + buf.set_string( + area.left() + max_qty_label.len() as u16, + area.bottom() - labels_pos, + &min_record_label, + Style::default(), + ); + // bottom right: max time + buf.set_string( + area.right() - max_record_label.len() as u16, + area.bottom() - labels_pos, + &max_record_label, + Style::default(), + ); +} + +/// From the histogram, build a visual representation by trying to make as +/// many buckets as the width of the render area. +fn chart_data(histogram: &DurationHistogram, width: u16) -> (Vec, HistogramMetadata) { + let &DurationHistogram { + ref histogram, + high_outliers, + highest_outlier, + .. + } = histogram; + + let step_size = ((histogram.max() - histogram.min()) as f64 / width as f64).ceil() as u64 + 1; + // `iter_linear` panics if step_size is 0 + let data = if step_size > 0 { + let mut found_first_nonzero = false; + let data: Vec = histogram + .iter_linear(step_size) + .filter_map(|value| { + let count = value.count_since_last_iteration(); + // Remove the 0s from the leading side of the buckets. + // Because HdrHistogram can return empty buckets depending + // on its internal state, as it approximates values. + if count == 0 && !found_first_nonzero { + None + } else { + found_first_nonzero = true; + Some(count) + } + }) + .collect(); + data + } else { + Vec::new() + }; + let max_bucket = data.iter().max().copied().unwrap_or_default(); + let min_bucket = data.iter().min().copied().unwrap_or_default(); + ( + data, + HistogramMetadata { + max_value: histogram.max(), + min_value: histogram.min(), + max_bucket, + min_bucket, + high_outliers, + highest_outlier, + }, + ) +} diff --git a/tokio-console/src/view/mod.rs b/tokio-console/src/view/mod.rs index e22d239af..3d35350b8 100644 --- a/tokio-console/src/view/mod.rs +++ b/tokio-console/src/view/mod.rs @@ -8,7 +8,9 @@ use tui::{ }; mod async_ops; +mod durations; mod mini_histogram; +mod percentiles; mod resource; mod resources; mod styles; @@ -18,11 +20,15 @@ mod tasks; pub(crate) use self::styles::{Palette, Styles}; pub(crate) use self::table::SortBy; -const DUR_LEN: usize = 10; // This data is only updated every second, so it doesn't make a ton of // sense to have a lot of precision in timestamps (and this makes sure // there's room for the unit!) -const DUR_PRECISION: usize = 4; +const DUR_LEN: usize = 6; +// Precision (after decimal point) for durations displayed in a list +// (detail view) +const DUR_LIST_PRECISION: usize = 2; +// Precision (after decimal point) for durations displayed in a table +const DUR_TABLE_PRECISION: usize = 0; const TABLE_HIGHLIGHT_SYMBOL: &str = ">> "; pub struct View { @@ -96,6 +102,17 @@ impl View { pub(crate) fn update_input(&mut self, event: input::Event, state: &State) -> UpdateKind { use ViewState::*; let mut update_kind = UpdateKind::Other; + + if matches!(event, key!(Char('t'))) { + self.state = TasksList; + return update_kind; + } + + if matches!(event, key!(Char('r'))) { + self.state = ResourcesList; + return update_kind; + } + match self.state { TasksList => { // The enter key changes views, so handle here since we can @@ -110,9 +127,6 @@ impl View { )); } } - key!(Char('r')) => { - self.state = ResourcesList; - } _ => { // otherwise pass on to view self.tasks_list.update_input(event); @@ -127,9 +141,6 @@ impl View { self.state = ResourceInstance(self::resource::ResourceView::new(res)); } } - key!(Char('t')) => { - self.state = TasksList; - } _ => { // otherwise pass on to view self.resources_list.update_input(event); diff --git a/tokio-console/src/view/percentiles.rs b/tokio-console/src/view/percentiles.rs new file mode 100644 index 000000000..6f8ba4666 --- /dev/null +++ b/tokio-console/src/view/percentiles.rs @@ -0,0 +1,83 @@ +use std::time::Duration; + +use tui::{ + text::{Spans, Text}, + widgets::{Paragraph, Widget}, +}; + +use crate::{ + state::histogram::DurationHistogram, + view::{self, bold}, +}; + +/// This is a tui-rs widget to display duration percentiles in a list form. +/// It wraps the [`Paragraph`] widget. +pub(crate) struct Percentiles<'a> { + /// Widget style + styles: &'a view::Styles, + /// The histogram data to render + histogram: Option<&'a DurationHistogram>, + /// The title of the paragraph + title: &'a str, +} + +impl<'a> Widget for Percentiles<'a> { + fn render(self, area: tui::layout::Rect, buf: &mut tui::buffer::Buffer) { + let inner = Paragraph::new(self.make_percentiles_inner()) + .block(self.styles.border_block().title(self.title)); + + inner.render(area, buf) + } +} + +impl<'a> Percentiles<'a> { + pub(crate) fn new(styles: &'a view::Styles) -> Self { + Self { + styles, + histogram: None, + title: "Percentiles", + } + } + + pub(crate) fn make_percentiles_inner(&self) -> Text<'static> { + let mut text = Text::default(); + let histogram = match self.histogram { + Some(DurationHistogram { histogram, .. }) => histogram, + _ => return text, + }; + + // Get the important percentile values from the histogram + let pairs = [10f64, 25f64, 50f64, 75f64, 90f64, 95f64, 99f64] + .iter() + .map(move |i| (*i, histogram.value_at_percentile(*i))); + let percentiles = pairs.map(|pair| { + Spans::from(vec![ + bold(format!("p{:>2}: ", pair.0)), + self.styles.time_units( + Duration::from_nanos(pair.1), + view::DUR_LIST_PRECISION, + None, + ), + ]) + }); + + text.extend(percentiles); + text + } + + #[allow(dead_code)] + pub(crate) fn styles(mut self, styles: &'a view::Styles) -> Percentiles<'a> { + self.styles = styles; + self + } + + pub(crate) fn histogram(mut self, histogram: Option<&'a DurationHistogram>) -> Percentiles<'a> { + self.histogram = histogram; + self + } + + pub(crate) fn title(mut self, title: &'a str) -> Percentiles<'a> { + self.title = title; + self + } +} diff --git a/tokio-console/src/view/resources.rs b/tokio-console/src/view/resources.rs index 4b65b00e8..afe9fc1f6 100644 --- a/tokio-console/src/view/resources.rs +++ b/tokio-console/src/view/resources.rs @@ -6,7 +6,7 @@ use crate::{ view::{ self, bold, table::{self, TableList, TableListState}, - DUR_LEN, DUR_PRECISION, + DUR_LEN, DUR_TABLE_PRECISION, }, }; @@ -104,12 +104,11 @@ impl TableList<9> for ResourcesTable { ))), Cell::from(parent_width.update_str(resource.parent_id()).to_owned()), Cell::from(kind_width.update_str(resource.kind()).to_owned()), - Cell::from(styles.time_units(format!( - "{:>width$.prec$?}", + Cell::from(styles.time_units( resource.total(now), - width = DUR_LEN, - prec = DUR_PRECISION, - ))), + DUR_TABLE_PRECISION, + Some(DUR_LEN), + )), Cell::from(target_width.update_str(resource.target()).to_owned()), Cell::from(type_width.update_str(resource.concrete_type()).to_owned()), Cell::from(resource.type_visibility().render(styles)), diff --git a/tokio-console/src/view/styles.rs b/tokio-console/src/view/styles.rs index 515308441..7834cca2b 100644 --- a/tokio-console/src/view/styles.rs +++ b/tokio-console/src/view/styles.rs @@ -1,6 +1,6 @@ use crate::config; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, str::FromStr}; +use std::{str::FromStr, time::Duration}; use tui::{ style::{Color, Modifier, Style}, text::Span, @@ -32,11 +32,41 @@ pub enum Palette { All, } +/// Represents formatted time spans. +/// +/// Distinguishing between different units allows appropriate colouring. +enum FormattedDuration { + /// Days (and no minor unit), e.g. `102d` + Days(String), + /// Days with hours, e.g. `12d03h` + DaysHours(String), + /// Hours with minutes, e.g. `14h32m` + HoursMinutes(String), + /// Minutes with seconds, e.g. `43m02s` + MinutesSeconds(String), + /// The `time::Duration` debug string which uses units ranging from + /// picoseconds (`ps`) to seconds (`s`). May contain decimal digits + /// (e.g. `628.76ms`) or not (e.g. `32ns`) + Debug(String), +} + +impl FormattedDuration { + fn into_inner(self) -> String { + match self { + Self::Days(inner) => inner, + Self::DaysHours(inner) => inner, + Self::HoursMinutes(inner) => inner, + Self::MinutesSeconds(inner) => inner, + Self::Debug(inner) => inner, + } + } +} + fn fg_style(color: Color) -> Style { Style::default().fg(color) } -// === impl Config === +// === impl Styles === impl Styles { pub fn from_config(config: config::ViewOptions) -> Self { @@ -47,12 +77,32 @@ impl Styles { } } - pub fn error_init(&self) -> color_eyre::Result<()> { - use color_eyre::config::{HookBuilder, Theme}; + pub fn error_init(&self, cfg: &crate::config::Config) -> color_eyre::Result<()> { + use color_eyre::{ + config::{HookBuilder, Theme}, + ErrorKind, + }; let mut builder = HookBuilder::new() .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) + .issue_filter(|kind| match kind { + // Only suggest reporting GitHub issues for panics, not for + // errors, so people don't open GitHub issues for stuff like not + // being able to find a config file or connections being + // terminated by remote hosts. + ErrorKind::NonRecoverable(_) => true, + ErrorKind::Recoverable(_) => false, + }) + // filter out `color-eyre`'s default set of frames to skip from + // backtraces. + // + // this includes `std::rt`, `color_eyre`'s own frames, and + // `tokio::runtime` & friends. + .add_default_filters() .add_issue_metadata("version", env!("CARGO_PKG_VERSION")); + // Add all the config values to the GitHub issue metadata + builder = cfg.add_issue_metadata(builder); + if self.palette == Palette::NoColors { // disable colors in error reports builder = builder.theme(Theme::new()); @@ -106,39 +156,100 @@ impl Styles { } } - pub fn time_units<'a>(&self, text: impl Into>) -> Span<'a> { - let mut text = text.into(); - if !self.toggles.color_durations() { - return Span::raw(text); - } + /// Creates a span with a formatted duration inside. + /// + /// The formatted duration will be colored depending on the palette + /// defined for this `Styles` object. + /// + /// If the `width` parameter is `None` then no padding will be + /// added. Otherwise the text in the span will be left-padded to + /// the specified width (right aligned). Passing `Some(0)` is + /// equivalent to `None`. + pub fn time_units<'a>(&self, dur: Duration, prec: usize, width: Option) -> Span<'a> { + let formatted = self.duration_text(dur, width.unwrap_or(0), prec); - if !self.utf8 { - if let Some(mu_offset) = text.find("µs") { - text.to_mut().replace_range(mu_offset.., "us"); - } + if !self.toggles.color_durations() { + return Span::raw(formatted.into_inner()); } let style = match self.palette { - Palette::NoColors => return Span::raw(text), - Palette::Ansi8 | Palette::Ansi16 => match text.as_ref() { - s if s.ends_with("ps") => fg_style(Color::Blue), - s if s.ends_with("ns") => fg_style(Color::Green), - s if s.ends_with("µs") || s.ends_with("us") => fg_style(Color::Yellow), - s if s.ends_with("ms") => fg_style(Color::Red), - s if s.ends_with('s') => fg_style(Color::Magenta), + Palette::NoColors => return Span::raw(formatted.into_inner()), + Palette::Ansi8 | Palette::Ansi16 => match &formatted { + FormattedDuration::Days(_) => fg_style(Color::Blue), + FormattedDuration::DaysHours(_) => fg_style(Color::Blue), + FormattedDuration::HoursMinutes(_) => fg_style(Color::Cyan), + FormattedDuration::MinutesSeconds(_) => fg_style(Color::Green), + FormattedDuration::Debug(s) if s.ends_with("ps") => fg_style(Color::Gray), + FormattedDuration::Debug(s) if s.ends_with("ns") => fg_style(Color::Gray), + FormattedDuration::Debug(s) if s.ends_with("µs") || s.ends_with("us") => { + fg_style(Color::Magenta) + } + FormattedDuration::Debug(s) if s.ends_with("ms") => fg_style(Color::Red), + FormattedDuration::Debug(s) if s.ends_with('s') => fg_style(Color::Yellow), _ => Style::default(), }, - Palette::Ansi256 | Palette::All => match text.as_ref() { - s if s.ends_with("ps") => fg_style(Color::Indexed(40)), // green 3 - s if s.ends_with("ns") => fg_style(Color::Indexed(41)), // spring green 3 - s if s.ends_with("µs") || s.ends_with("us") => fg_style(Color::Indexed(42)), // spring green 2 - s if s.ends_with("ms") => fg_style(Color::Indexed(43)), // cyan 3 - s if s.ends_with('s') => fg_style(Color::Indexed(44)), // dark turquoise, + Palette::Ansi256 | Palette::All => match &formatted { + FormattedDuration::Days(_) => fg_style(Color::Indexed(33)), // dodger blue 1 + FormattedDuration::DaysHours(_) => fg_style(Color::Indexed(33)), // dodger blue 1 + FormattedDuration::HoursMinutes(_) => fg_style(Color::Indexed(39)), // deep sky blue 1 + FormattedDuration::MinutesSeconds(_) => fg_style(Color::Indexed(45)), // turquoise 2 + FormattedDuration::Debug(s) if s.ends_with("ps") => fg_style(Color::Indexed(40)), // green 3 + FormattedDuration::Debug(s) if s.ends_with("ns") => fg_style(Color::Indexed(41)), // spring green 3 + FormattedDuration::Debug(s) if s.ends_with("µs") || s.ends_with("us") => { + fg_style(Color::Indexed(42)) + } // spring green 2 + FormattedDuration::Debug(s) if s.ends_with("ms") => fg_style(Color::Indexed(43)), // cyan 3 + FormattedDuration::Debug(s) if s.ends_with('s') => fg_style(Color::Indexed(44)), // dark turquoise, _ => Style::default(), }, }; - Span::styled(text, style) + Span::styled(formatted.into_inner(), style) + } + + fn duration_text(&self, dur: Duration, width: usize, prec: usize) -> FormattedDuration { + let secs = dur.as_secs(); + + if secs >= 60 * 60 * 24 * 100 { + let days = secs / (60 * 60 * 24); + FormattedDuration::Days(format!("{days:>width$}d", days = days, width = width)) + } else if secs >= 60 * 60 * 24 { + let hours = secs / (60 * 60); + FormattedDuration::DaysHours(format!( + "{days:>leading_width$}d{hours:02.0}h", + days = hours / 24, + hours = hours % 24, + // Subtract the known 4 characters that trail the days value. + leading_width = width.saturating_sub(4), + )) + } else if secs >= 60 * 60 { + let mins = secs / 60; + FormattedDuration::HoursMinutes(format!( + "{hours:>leading_width$}h{minutes:02.0}m", + hours = mins / 60, + minutes = mins % 60, + // Subtract the known 4 characters that trail the hours value. + leading_width = width.saturating_sub(4), + )) + } else if secs >= 60 { + FormattedDuration::MinutesSeconds(format!( + "{minutes:>leading_width$}m{seconds:02.0}s", + minutes = secs / 60, + seconds = secs % 60, + // Subtract the known 4 characters that trail the minutes value. + leading_width = width.saturating_sub(4), + )) + } else { + let mut text = format!("{:>width$.prec$?}", dur, width = width, prec = prec); + + if !self.utf8 { + if let Some(mu_offset) = text.find("µs") { + text.replace_range(mu_offset.., "us"); + } + } + + FormattedDuration::Debug(text) + } } pub fn terminated(&self) -> Style { diff --git a/tokio-console/src/view/task.rs b/tokio-console/src/view/task.rs index 2708bfb92..fea953ab7 100644 --- a/tokio-console/src/view/task.rs +++ b/tokio-console/src/view/task.rs @@ -1,15 +1,8 @@ use crate::{ input, - state::{ - histogram::DurationHistogram, - tasks::{Details, Task}, - DetailsRef, - }, + state::{tasks::Task, DetailsRef}, util::Percentage, - view::{ - self, bold, - mini_histogram::{HistogramMetadata, MiniHistogram}, - }, + view::{self, bold, durations::Durations}, }; use std::{ cell::RefCell, @@ -67,6 +60,7 @@ impl TaskView { }) .collect(); + let fields_len = (task.formatted_fields().len() + 2) as u16; let (controls_area, stats_area, poll_dur_area, fields_area, warnings_area) = if warnings.is_empty() { let chunks = Layout::default() @@ -78,9 +72,9 @@ impl TaskView { // task stats layout::Constraint::Length(8), // poll duration - layout::Constraint::Length(9), + layout::Constraint::Min(9), // fields - layout::Constraint::Percentage(60), + layout::Constraint::Min(fields_len), ] .as_ref(), ) @@ -98,9 +92,9 @@ impl TaskView { // task stats layout::Constraint::Length(8), // poll duration - layout::Constraint::Length(9), + layout::Constraint::Min(9), // fields - layout::Constraint::Percentage(60), + layout::Constraint::Min(fields_len), ] .as_ref(), ) @@ -120,26 +114,6 @@ impl TaskView { ) .split(stats_area); - // Only split the histogram area in half if we're also drawing a - // sparkline (which requires UTF-8 characters). - let poll_dur_area = if styles.utf8 { - Layout::default() - .direction(layout::Direction::Horizontal) - .constraints( - [ - // 24 chars is long enough for the title "Poll Times Percentiles" - layout::Constraint::Length(24), - layout::Constraint::Min(50), - ] - .as_ref(), - ) - .split(poll_dur_area) - } else { - vec![poll_dur_area] - }; - - let percentiles_area = poll_dur_area[0]; - let controls = Spans::from(vec![ Span::raw("controls: "), bold(styles.if_utf8("\u{238B} esc", "esc")), @@ -176,12 +150,15 @@ impl TaskView { let percent = amt.as_secs_f64().percent_of(total.as_secs_f64()); Spans::from(vec![ bold(name), - dur(styles, amt), + styles.time_units(amt, view::DUR_LIST_PRECISION, None), Span::from(format!(" ({:.2}%)", percent)), ]) }; - overview.push(Spans::from(vec![bold("Total Time: "), dur(styles, total)])); + overview.push(Spans::from(vec![ + bold("Total Time: "), + styles.time_units(total, view::DUR_LIST_PRECISION, None), + ])); overview.push(dur_percent("Busy: ", task.busy(now))); overview.push(dur_percent("Idle: ", task.idle(now))); @@ -223,30 +200,6 @@ impl TaskView { let mut fields = Text::default(); fields.extend(task.formatted_fields().iter().cloned().map(Spans::from)); - // If UTF-8 is disabled we can't draw the histogram sparklne. - if styles.utf8 { - let sparkline_area = poll_dur_area[1]; - - // Bit of a deadlock: We cannot know the highest bucket value without determining the number of buckets, - // and we cannot determine the number of buckets without knowing the width of the chart area which depends on - // the number of digits in the highest bucket value. - // So just assume here the number of digits in the highest bucket value is 3. - // If we overshoot, there will be empty columns/buckets at the right end of the chart. - // If we undershoot, the rightmost 1-2 columns/buckets will be hidden. - // We could get the max bucket value from the previous render though... - let (chart_data, metadata) = details - .map(|d| d.make_chart_data(sparkline_area.width - 3)) - .unwrap_or_default(); - - let histogram_sparkline = MiniHistogram::default() - .block(styles.border_block().title("Poll Times Histogram")) - .data(&chart_data) - .metadata(metadata) - .duration_precision(2); - - frame.render_widget(histogram_sparkline, sparkline_area); - } - if let Some(warnings_area) = warnings_area { let warnings = List::new(warnings).block(styles.border_block().title("Warnings")); frame.render_widget(warnings, warnings_area); @@ -254,104 +207,16 @@ impl TaskView { let task_widget = Paragraph::new(overview).block(styles.border_block().title("Task")); let wakers_widget = Paragraph::new(waker_stats).block(styles.border_block().title("Waker")); + let poll_durations_widget = Durations::new(styles) + .histogram(details.and_then(|d| d.poll_times_histogram())) + .percentiles_title("Poll Times Percentiles") + .histogram_title("Poll Times Histogram"); let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Fields")); - let percentiles_widget = Paragraph::new( - details - .map(|details| details.make_percentiles_widget(styles)) - .unwrap_or_default(), - ) - .block(styles.border_block().title("Poll Times Percentiles")); frame.render_widget(Block::default().title(controls), controls_area); frame.render_widget(task_widget, stats_area[0]); frame.render_widget(wakers_widget, stats_area[1]); + frame.render_widget(poll_durations_widget, poll_dur_area); frame.render_widget(fields_widget, fields_area); - frame.render_widget(percentiles_widget, percentiles_area); } } - -impl Details { - /// From the histogram, build a visual representation by trying to make as - // many buckets as the width of the render area. - fn make_chart_data(&self, width: u16) -> (Vec, HistogramMetadata) { - self.poll_times_histogram() - .map( - |&DurationHistogram { - ref histogram, - high_outliers, - highest_outlier, - .. - }| { - let step_size = ((histogram.max() - histogram.min()) as f64 / width as f64) - .ceil() as u64 - + 1; - // `iter_linear` panics if step_size is 0 - let data = if step_size > 0 { - let mut found_first_nonzero = false; - let data: Vec = histogram - .iter_linear(step_size) - .filter_map(|value| { - let count = value.count_since_last_iteration(); - // Remove the 0s from the leading side of the buckets. - // Because HdrHistogram can return empty buckets depending - // on its internal state, as it approximates values. - if count == 0 && !found_first_nonzero { - None - } else { - found_first_nonzero = true; - Some(count) - } - }) - .collect(); - data - } else { - Vec::new() - }; - let max_bucket = data.iter().max().copied().unwrap_or_default(); - let min_bucket = data.iter().min().copied().unwrap_or_default(); - ( - data, - HistogramMetadata { - max_value: histogram.max(), - min_value: histogram.min(), - max_bucket, - min_bucket, - high_outliers, - highest_outlier, - }, - ) - }, - ) - .unwrap_or_default() - } - - /// Get the important percentile values from the histogram - fn make_percentiles_widget(&self, styles: &view::Styles) -> Text<'static> { - let mut text = Text::default(); - let histogram = self.poll_times_histogram(); - let percentiles = - histogram - .iter() - .flat_map(|&DurationHistogram { ref histogram, .. }| { - let pairs = [10f64, 25f64, 50f64, 75f64, 90f64, 95f64, 99f64] - .iter() - .map(move |i| (*i, histogram.value_at_percentile(*i))); - pairs.map(|pair| { - Spans::from(vec![ - bold(format!("p{:>2}: ", pair.0)), - dur(styles, Duration::from_nanos(pair.1)), - ]) - }) - }); - text.extend(percentiles); - text - } -} - -fn dur(styles: &view::Styles, dur: std::time::Duration) -> Span<'static> { - const DUR_PRECISION: usize = 4; - // TODO(eliza): can we not have to use `format!` to make a string here? is - // there a way to just give TUI a `fmt::Debug` implementation, or does it - // have to be given a string in order to do layout stuff? - styles.time_units(format!("{:.prec$?}", dur, prec = DUR_PRECISION)) -} diff --git a/tokio-console/src/view/tasks.rs b/tokio-console/src/view/tasks.rs index eb6b2365a..d103779ab 100644 --- a/tokio-console/src/view/tasks.rs +++ b/tokio-console/src/view/tasks.rs @@ -6,7 +6,7 @@ use crate::{ view::{ self, bold, table::{self, TableList, TableListState}, - DUR_LEN, DUR_PRECISION, + DUR_LEN, DUR_TABLE_PRECISION, }, }; use tui::{ @@ -68,12 +68,7 @@ impl TableList<11> for TasksTable { .sort(now, &mut table_list_state.sorted_items); let dur_cell = |dur: std::time::Duration| -> Cell<'static> { - Cell::from(styles.time_units(format!( - "{:>width$.prec$?}", - dur, - width = DUR_LEN, - prec = DUR_PRECISION, - ))) + Cell::from(styles.time_units(dur, DUR_TABLE_PRECISION, Some(DUR_LEN))) }; // Start out wide enough to display the column headers... @@ -127,7 +122,7 @@ impl TableList<11> for TasksTable { warnings, Cell::from(id_width.update_str(format!( "{:>width$}", - task.id(), + task.id_str(), width = id_width.chars() as usize ))), Cell::from(task.state().render(styles)), diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 000000000..f113b8128 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "xtask" +version = "0.1.0" +license = "MIT" +edition = "2021" +rust-version = "1.56.0" +publish = false + +[dependencies] +tonic-build = { version = "0.8", default-features = false, features = [ + "prost", "transport" +] } +clap = { version = "3", features = ["derive"] } +color-eyre = "0.6" \ No newline at end of file diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 000000000..4e07a0509 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,80 @@ +use clap::Parser; +use color_eyre::{ + eyre::{ensure, WrapErr}, + Result, +}; +use std::{fs, path::PathBuf}; + +/// tokio-console dev tasks +#[derive(Debug, clap::Parser)] +struct Args { + #[clap(subcommand)] + cmd: Command, +} + +#[derive(Debug, clap::Subcommand)] +enum Command { + /// Generate `console-api` protobuf bindings. + GenProto, +} + +fn main() -> Result<()> { + color_eyre::install()?; + Args::parse().cmd.run() +} + +impl Command { + fn run(&self) -> Result<()> { + match self { + Self::GenProto => gen_proto(), + } + } +} + +fn gen_proto() -> Result<()> { + eprintln!("generating `console-api` protos..."); + + let api_dir = { + let mut mydir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")); + ensure!(mydir.pop(), "manifest path should not be relative!"); + mydir.join("console-api") + }; + + let proto_dir = api_dir.join("proto"); + let proto_ext = std::ffi::OsStr::new("proto"); + let proto_files = fs::read_dir(&proto_dir) + .with_context(|| { + format!( + "failed to read protobuf directory `{}`", + proto_dir.display() + ) + })? + .filter_map(|entry| { + (|| { + let entry = entry?; + if entry.file_type()?.is_dir() { + return Ok(None); + } + + let path = entry.path(); + if path.extension() != Some(proto_ext) { + return Ok(None); + } + + Ok(Some(path)) + })() + .transpose() + }) + .collect::>>()?; + + let out_dir = api_dir.join("src").join("generated"); + + tonic_build::configure() + .build_client(true) + .build_server(true) + .emit_rerun_if_changed(false) + .protoc_arg("--experimental_allow_proto3_optional") + .out_dir(out_dir) + .compile(&proto_files[..], &[proto_dir]) + .context("failed to compile protobuf files") +}