├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── ci ├── cargo-check.yml ├── cargo-clippy.yml ├── cargo-cross.yml ├── github-deploy-doc.yml ├── github-release.yml ├── install-cross-rust.yml ├── install-rust.yml ├── rustfmt.yml └── test.yml ├── console ├── Cargo.toml ├── build.rs └── src │ ├── connection.rs │ ├── filter │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ ├── storage │ ├── messages.rs │ ├── mod.rs │ └── store.rs │ └── ui │ ├── app.rs │ ├── command.rs │ ├── events.rs │ ├── mod.rs │ └── query.rs ├── example ├── Cargo.toml └── src │ └── main.rs ├── gsoc.md ├── proto └── tracing.proto └── subscriber ├── Cargo.toml ├── build.rs └── src ├── lib.rs ├── messages.rs ├── server.rs └── subscriber.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | #Added by cargo 14 | # 15 | #already existing elements are commented out 16 | 17 | /target 18 | #**/*.rs.bk -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "console", 5 | "example", 6 | "subscriber", 7 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tokio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # console 2 | 3 | Tokio provides an instrumentation API using `tracing` as well as a number of instrumentation points built into Tokio itself and the Tokio ecosystem. The goal of the project is to implement a library for aggregation, metrics of said instrumentation points and a console-based UI that connects to the process, allowing users to quickly visualize, browse and debug the data. 4 | 5 | Because processes can encode structured and typed business logic with instrumentation points based on `tracing`, a domain-specific debugger built upon those can provide powerful, ad hoc tooling, e.g. filtering events by connection id, execution context etcetera. As instrumentation points of underlying libraries are collected as well, it is easy to observe their behaviour and interaction. This is an eminent advantage over traditional debuggers, where the user instead observes the implementation. 6 | 7 | ## GSoC 8 | This project has been part of Google summer of code. For more information, see [gsoc.md](https://github.com/tokio-rs/console/blob/master/gsoc.md). -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: ['*'] 4 | tags: 5 | include: ['*'] 6 | 7 | jobs: 8 | # Check formatting 9 | - template: ci/rustfmt.yml 10 | parameters: 11 | name: rustfmt 12 | displayName: Check formatting 13 | 14 | # Cargo check 15 | - template: ci/cargo-check.yml 16 | parameters: 17 | name: cargo_check 18 | displayName: Cargo check 19 | 20 | # This represents the minimum Rust version supported. 21 | # Tests are not run as tests may require newer versions of rust. 22 | - template: ci/cargo-check.yml 23 | parameters: 24 | name: minrust 25 | rust_version: 1.31.0 # The 2018 edition 26 | displayName: Check rust min ver 27 | 28 | ################ 29 | # Test stage # 30 | ############### 31 | 32 | # Test stable 33 | - template: ci/test.yml 34 | parameters: 35 | dependsOn: 36 | - cargo_check 37 | name: cargo_test_stable 38 | displayName: Cargo test 39 | cross: true # Test on Windows and macOS 40 | 41 | # Test nightly 42 | - template: ci/test.yml 43 | parameters: 44 | name: cargo_test_nightly 45 | displayName: Cargo test 46 | rust_version: nightly 47 | -------------------------------------------------------------------------------- /ci/cargo-check.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | rust_version: stable 3 | 4 | jobs: 5 | - job: ${{ parameters.name }} 6 | displayName: ${{ parameters.displayName }} 7 | pool: 8 | vmImage: ubuntu-16.04 9 | steps: 10 | - template: install-rust.yml 11 | 12 | - script: cargo check 13 | displayName: Check features 14 | 15 | - ${{ if parameters.benches }}: 16 | - script: cargo check --benches --all 17 | displayName: Check benchmarks 18 | -------------------------------------------------------------------------------- /ci/cargo-clippy.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: ${{ parameters.name }} 3 | displayName: ${{ parameters.displayName }} 4 | pool: 5 | vmImage: ubuntu-16.04 6 | steps: 7 | - template: install-rust.yml 8 | parameters: 9 | rust_version: ${{ parameters.rust }} 10 | components: 11 | - clippy 12 | 13 | - script: cargo clippy --all 14 | displayName: Run clippy 15 | -------------------------------------------------------------------------------- /ci/cargo-cross.yml: -------------------------------------------------------------------------------- 1 | # SUPPORTED TARGETS https://github.com/rust-embedded/cross#supported-targets 2 | 3 | parameters: 4 | rust_version: stable 5 | check_target: [] 6 | test_targets: [] 7 | 8 | jobs: 9 | - job: ${{ parameters.name }} 10 | displayName: ${{parameters.displayName}} 11 | pool: 12 | vmImage: 'ubuntu-16.04' 13 | steps: 14 | - template: install-cross-rust.yml 15 | 16 | # Checking all targets 17 | - ${{ each check_target in parameters.check_targets }}: 18 | - script: cargo clean 19 | - script: cross check --target ${{ check_target }} 20 | 21 | # Testing targets 22 | - ${{ each test_target in parameters.test_targets }}: 23 | - script: cross test --target ${{ test_target }} 24 | - script: cargo clean 25 | 26 | -------------------------------------------------------------------------------- /ci/github-deploy-doc.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | dependsOn: [] 3 | rust_version: stable 4 | displayName: 'Deploy master API doc to Github' 5 | branch: master 6 | 7 | jobs: 8 | - job: documentation 9 | displayName: ${{ parameters.displayName }} 10 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/${{ parameters.branch }}')) 11 | pool: 12 | vmImage: 'Ubuntu 16.04' 13 | dependsOn: 14 | - ${{ parameters.dependsOn }} 15 | steps: 16 | - template: install-rust.yml 17 | parameters: 18 | rust_version: ${{ parameters.rust_version}} 19 | - script: | 20 | cargo doc --all --no-deps 21 | cp -R target/doc '$(Build.BinariesDirectory)' 22 | displayName: 'Generate Documentation' 23 | - script: | 24 | set -e 25 | 26 | git --version 27 | ls -la 28 | git init 29 | git config user.name 'Deployment Bot (from Azure Pipelines)' 30 | git config user.email '${{ parameters.github.email }}' 31 | git config --global credential.helper 'store --file ~/.my-credentials' 32 | printf "protocol=https\nhost=github.com\nusername=$USER\npassword=%s\n\n" "$GITHUB_TOKEN" | git credential-store --file ~/.my-credentials store 33 | git remote add origin ${{ parameters.github.repo }} 34 | git checkout -b gh-pages 35 | git add . 36 | git commit -m 'Deploy API documentation' 37 | git push -f origin gh-pages 38 | env: 39 | GITHUB_TOKEN: $(DocPublishToken) 40 | USER: ${{ parameters.github.user }} 41 | workingDirectory: '$(Build.BinariesDirectory)' 42 | displayName: 'Deploy Documentation' 43 | -------------------------------------------------------------------------------- /ci/github-release.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | rust_version: stable 3 | github: 4 | isPreRelease: false 5 | repositoryName: '$(Build.Repository.Name)' 6 | dependsOn: [] 7 | displayName: "Release to github" 8 | tarCompression: 'none' 9 | archiveType: 'zip' 10 | archiveName: '$(Build.Repository.Name)' 11 | 12 | jobs: 13 | - job: ${{ parameters.name }} 14 | condition: ${{ parameters.condition }} 15 | displayName: ${{ parameters.displayName }} 16 | dependsOn: ${{ parameters.dependsOn }} 17 | pool: 18 | vmImage: ubuntu-16.04 19 | steps: 20 | - template: install-cross-rust.yml 21 | 22 | - bash: | 23 | MY_TAG="$(Build.SourceBranch)" 24 | MY_TAG=${MY_TAG#refs/tags/} 25 | echo $MY_TAG 26 | echo "##vso[task.setvariable variable=build.my_tag]$MY_TAG" 27 | DATE="$(date +%Y-%m-%d)" 28 | echo "##vso[task.setvariable variable=build.date]$DATE" 29 | displayName: "Create date and tag variables" 30 | 31 | - ${{ each build_target in parameters.targets }}: 32 | - ${{ if not(or(eq(build_target, 'x86_64-apple-darwin'), eq(build_target, 'x86_64-pc-windows-msvc'))) }}: 33 | - script: | 34 | echo Start building ${{ build_target }} 35 | cross build --target ${{ build_target }} --release 36 | ls -l 37 | ls -l target/${{ build_target }}/release/* 38 | displayName: Relase build for target ${{ build_target }} 39 | - task: CopyFiles@2 40 | displayName: Copy files for target ${{ build_target }} 41 | inputs: 42 | sourceFolder: '$(Build.SourcesDirectory)/target/${{ build_target }}/release' 43 | contents: ${{ parameters.contents }} 44 | targetFolder: '$(Build.BinariesDirectory)/${{ build_target }}' 45 | - task: ArchiveFiles@2 46 | displayName: Gather assets 47 | inputs: 48 | rootFolderOrFile: '$(Build.BinariesDirectory)/${{ build_target }}' 49 | archiveType: ${{ parameters.archiveType }} 50 | tarCompression: ${{ parameters.tarCompression }} 51 | archiveFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.archiveName }}-$(build.my_tag)-${{ build_target }}.zip' 52 | 53 | - task: GitHubRelease@0 54 | displayName: Create release 55 | inputs: 56 | gitHubConnection: ${{ parameters.github.gitHubConnection }} 57 | tagSource: manual 58 | title: '$(build.my_tag) - $(build.date)' 59 | tag: '$(build.my_tag)' 60 | assetUploadMode: replace 61 | action: edit 62 | assets: '$(Build.ArtifactStagingDirectory)/${{ parameters.archiveName }}*' 63 | repositoryName: ${{ parameters.github.repositoryName }} 64 | isPreRelease: ${{ parameters.github.isPreRelease }} 65 | 66 | - ${{ each build_target in parameters.targets }}: 67 | - ${{ if eq(build_target, 'x86_64-apple-darwin') }}: 68 | - job: ${{ parameters.name }}_macOS 69 | condition: ${{ parameters.condition }} 70 | displayName: ${{ parameters.displayName }} (macOS) 71 | dependsOn: ${{ parameters.dependsOn }} 72 | pool: 73 | vmImage: macOS-10.13 74 | steps: 75 | - template: install-rust.yml 76 | 77 | - bash: | 78 | MY_TAG="$(Build.SourceBranch)" 79 | MY_TAG=${MY_TAG#refs/tags/} 80 | echo $MY_TAG 81 | echo "##vso[task.setvariable variable=build.my_tag]$MY_TAG" 82 | DATE="$(date +%Y-%m-%d)" 83 | echo "##vso[task.setvariable variable=build.date]$DATE" 84 | displayName: "Create date and tag variables" 85 | 86 | - script: | 87 | echo Start building ${{ build_target }} 88 | cargo build --release 89 | ls -l 90 | ls -l target/release/* 91 | displayName: Relase build for target ${{ build_target }} 92 | - task: CopyFiles@2 93 | displayName: Copy files for target ${{ build_target }} 94 | inputs: 95 | sourceFolder: '$(Build.SourcesDirectory)/target/release' 96 | contents: ${{ parameters.contents }} 97 | targetFolder: '$(Build.BinariesDirectory)/${{ build_target }}' 98 | - task: ArchiveFiles@2 99 | displayName: Gather assets 100 | inputs: 101 | rootFolderOrFile: '$(Build.BinariesDirectory)/${{ build_target }}' 102 | archiveType: ${{ parameters.archiveType }} 103 | tarCompression: ${{ parameters.tarCompression }} 104 | archiveFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.archiveName }}-$(build.my_tag)-${{ build_target }}.zip' 105 | 106 | - task: GitHubRelease@0 107 | displayName: Create release 108 | inputs: 109 | gitHubConnection: ${{ parameters.github.gitHubConnection }} 110 | tagSource: manual 111 | title: '$(build.my_tag) - $(build.date)' 112 | tag: '$(build.my_tag)' 113 | assetUploadMode: replace 114 | action: edit 115 | assets: '$(Build.ArtifactStagingDirectory)/${{ parameters.archiveName }}*' 116 | repositoryName: ${{ parameters.github.repositoryName }} 117 | isPreRelease: ${{ parameters.github.isPreRelease }} 118 | 119 | - ${{ if eq(build_target, 'x86_64-pc-windows-msvc') }}: 120 | - job: ${{ parameters.name }}_msvc 121 | condition: ${{ parameters.condition }} 122 | displayName: ${{ parameters.displayName }} (Windows) 123 | dependsOn: ${{ parameters.dependsOn }} 124 | pool: 125 | vmImage: vs2017-win2016 126 | steps: 127 | - template: install-rust.yml 128 | 129 | - bash: | 130 | MY_TAG="$(Build.SourceBranch)" 131 | MY_TAG=${MY_TAG#refs/tags/} 132 | echo $MY_TAG 133 | echo "##vso[task.setvariable variable=build.my_tag]$MY_TAG" 134 | DATE="$(date +%Y-%m-%d)" 135 | echo "##vso[task.setvariable variable=build.date]$DATE" 136 | displayName: "Create date and tag variables" 137 | 138 | - script: | 139 | echo Start building ${{ build_target }} 140 | cargo build --release 141 | ls -l 142 | ls -l target/release/* 143 | displayName: Relase build for target ${{ build_target }} 144 | - task: CopyFiles@2 145 | displayName: Copy files for target ${{ build_target }} 146 | inputs: 147 | sourceFolder: '$(Build.SourcesDirectory)/target/release' 148 | contents: ${{ parameters.contents }} 149 | targetFolder: '$(Build.BinariesDirectory)/${{ build_target }}' 150 | - task: ArchiveFiles@2 151 | displayName: Gather assets 152 | inputs: 153 | rootFolderOrFile: '$(Build.BinariesDirectory)/${{ build_target }}' 154 | archiveType: ${{ parameters.archiveType }} 155 | tarCompression: ${{ parameters.tarCompression }} 156 | archiveFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.archiveName }}-$(build.my_tag)-${{ build_target }}.zip' 157 | 158 | - task: GitHubRelease@0 159 | displayName: Create release 160 | inputs: 161 | gitHubConnection: ${{ parameters.github.gitHubConnection }} 162 | tagSource: manual 163 | title: '$(build.my_tag) - $(build.date)' 164 | tag: '$(build.my_tag)' 165 | assetUploadMode: replace 166 | action: edit 167 | assets: '$(Build.ArtifactStagingDirectory)/${{ parameters.archiveName }}*' 168 | repositoryName: ${{ parameters.github.repositoryName }} 169 | isPreRelease: ${{ parameters.github.isPreRelease }} 170 | -------------------------------------------------------------------------------- /ci/install-cross-rust.yml: -------------------------------------------------------------------------------- 1 | # defaults for any parameters that aren't specified 2 | parameters: 3 | rust_version: stable 4 | 5 | steps: 6 | # Linux and macOS. 7 | - script: | 8 | set -e 9 | curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN 10 | echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin" 11 | env: 12 | RUSTUP_TOOLCHAIN: ${{parameters.rust_version}} 13 | displayName: "Install rust (*nix)" 14 | condition: not(eq(variables['Agent.OS'], 'Windows_NT')) 15 | 16 | # Windows. 17 | - script: | 18 | curl -sSf -o rustup-init.exe https://win.rustup.rs 19 | rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% 20 | set PATH=%PATH%;%USERPROFILE%\.cargo\bin 21 | echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin" 22 | env: 23 | RUSTUP_TOOLCHAIN: ${{parameters.rust_version}} 24 | displayName: "Install rust (windows)" 25 | condition: eq(variables['Agent.OS'], 'Windows_NT') 26 | 27 | # Install additional components: 28 | - ${{ each component in parameters.components }}: 29 | - script: rustup component add ${{ component }} 30 | 31 | # TEMPORATY FIX UNTIL https://github.com/rust-embedded/cross/pull/169 merged. 32 | - script: | 33 | git config --global user.email "not_necessery@dont.needed" 34 | git config --global user.name "I am the merger one" 35 | git clone https://github.com/rust-embedded/cross 36 | cd cross 37 | git remote add pitkley https://github.com/pitkley/cross 38 | git fetch pitkley 39 | git checkout 718a19c 40 | git merge -m "No pseudo tty" pitkley/docker-no-pseudo-tty 41 | cargo install --force --path . 42 | displayName: Instaling cross supprot 43 | 44 | 45 | # All platforms. 46 | - script: | 47 | rustup -V 48 | rustup component list --installed 49 | rustc -Vv 50 | cargo -V 51 | displayName: Query rust and cargo versions 52 | -------------------------------------------------------------------------------- /ci/install-rust.yml: -------------------------------------------------------------------------------- 1 | # defaults for any parameters that aren't specified 2 | parameters: 3 | rust_version: stable 4 | 5 | steps: 6 | # Linux and macOS. 7 | - script: | 8 | set -e 9 | curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN 10 | echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin" 11 | env: 12 | RUSTUP_TOOLCHAIN: ${{parameters.rust_version}} 13 | displayName: "Install rust (*nix)" 14 | condition: not(eq(variables['Agent.OS'], 'Windows_NT')) 15 | 16 | # Windows. 17 | - script: | 18 | curl -sSf -o rustup-init.exe https://win.rustup.rs 19 | rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% 20 | set PATH=%PATH%;%USERPROFILE%\.cargo\bin 21 | echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin" 22 | env: 23 | RUSTUP_TOOLCHAIN: ${{parameters.rust_version}} 24 | displayName: "Install rust (windows)" 25 | condition: eq(variables['Agent.OS'], 'Windows_NT') 26 | 27 | # Install additional components: 28 | - ${{ each component in parameters.components }}: 29 | - script: rustup component add ${{ component }} 30 | 31 | # All platforms. 32 | - script: | 33 | rustup -V 34 | rustup component list --installed 35 | rustc -Vv 36 | cargo -V 37 | displayName: Query rust and cargo versions 38 | -------------------------------------------------------------------------------- /ci/rustfmt.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | # Check formatting 3 | - job: ${{ parameters.name }} 4 | displayName: Check rustfmt 5 | pool: 6 | vmImage: ubuntu-16.04 7 | steps: 8 | - template: install-rust.yml 9 | parameters: 10 | rust_version: stable 11 | - script: | 12 | rustup component add rustfmt 13 | displayName: Install rustfmt 14 | - script: | 15 | cargo fmt --all -- --check 16 | displayName: Check formatting 17 | -------------------------------------------------------------------------------- /ci/test.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | rust_version: stable 3 | 4 | jobs: 5 | - job: ${{ parameters.name }} 6 | displayName: ${{ parameters.displayName }} ${{parameters.rust_version}} 7 | strategy: 8 | matrix: 9 | Linux: 10 | vmImage: ubuntu-16.04 11 | 12 | ${{ if parameters.cross }}: 13 | MacOS: 14 | vmImage: macOS-10.13 15 | Windows: 16 | vmImage: vs2017-win2016 17 | pool: 18 | vmImage: $(vmImage) 19 | 20 | steps: 21 | - template: install-rust.yml 22 | 23 | - script: | 24 | cargo test 25 | env: 26 | LOOM_MAX_DURATION: 10 27 | CI: 'True' 28 | displayName: cargo test 29 | -------------------------------------------------------------------------------- /console/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "console" 3 | version = "0.1.0" 4 | authors = ["Matthias Prechtl "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bytes = "0.4" 11 | crossbeam = "0.7.1" 12 | crossterm = "^0.9" 13 | failure = "0.1.5" 14 | futures = "0.1" 15 | http = "0.1" 16 | tokio = "0.1" 17 | hyper = "0.12" 18 | prost = "0.5.0" 19 | tower-request-modifier = "0.1.0" 20 | tower-hyper = "0.1" 21 | tower-grpc = { features = ["tower-hyper"], version = "0.1.0" } 22 | tower-service = "0.2" 23 | tower-util = "0.1" 24 | regex = "1.2.0" 25 | indexmap = "1.0.2" 26 | 27 | [dependencies.tui] 28 | version = "0.6" 29 | features = ["crossterm"] 30 | default-features = false 31 | 32 | [build-dependencies] 33 | tower-grpc-build = { version = "0.1.0", features = ["tower-hyper"] } 34 | -------------------------------------------------------------------------------- /console/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tower_grpc_build::Config::new() 3 | .enable_client(true) 4 | .enable_server(false) 5 | .build(&["../proto/tracing.proto"], &["../proto/"]) 6 | .unwrap_or_else(|e| panic!("protobuf compilation failed: {}", e)); 7 | println!("cargo:rerun-if-changed=../proto/tracing.proto"); 8 | } 9 | -------------------------------------------------------------------------------- /console/src/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::*; 2 | 3 | use futures::{Future, Stream}; 4 | 5 | use hyper::client::connect::{Destination, HttpConnector}; 6 | 7 | use tower_grpc::Request; 8 | use tower_hyper::{client, util}; 9 | use tower_util::MakeService; 10 | 11 | /// Connects to the remote endpoint 12 | /// Internally locks and updates the `Store` 13 | /// 14 | /// Blocks until the connection is reset by the endpoint 15 | pub fn listen(store: StoreHandle, addr: &str) { 16 | let uri: http::Uri = addr.parse().unwrap(); 17 | 18 | let dst = Destination::try_from_uri(uri.clone()).unwrap(); 19 | let connector = util::Connector::new(HttpConnector::new(4)); 20 | let settings = client::Builder::new().http2_only(true).clone(); 21 | let mut make_client = client::Connect::with_builder(connector, settings); 22 | 23 | let fetch_events = make_client 24 | .make_service(dst) 25 | .map_err(|e| panic!("connect error: {:?}", e)) 26 | .and_then(move |conn| { 27 | use messages::client::ConsoleForwarder; 28 | 29 | let conn = tower_request_modifier::Builder::new() 30 | .set_origin(uri) 31 | .build(conn) 32 | .unwrap(); 33 | 34 | // Wait until the client is ready... 35 | ConsoleForwarder::new(conn).ready() 36 | }) 37 | .and_then(|mut client| client.listen(Request::new(ListenRequest {}))) 38 | .and_then(move |stream_response| { 39 | stream_response.into_inner().for_each(move |response| { 40 | store.handle(response.variant.expect("No variant on response")); 41 | Ok(()) 42 | }) 43 | }) 44 | .map_err(|_| { 45 | // TODO: Errors like connection reset are ignored for now 46 | }); 47 | 48 | tokio::run(fetch_events); 49 | } 50 | -------------------------------------------------------------------------------- /console/src/filter/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::EventEntry; 2 | 3 | use std::fmt::{Display, Formatter, Result}; 4 | 5 | use indexmap::IndexMap; 6 | 7 | use regex::Regex; 8 | 9 | #[derive(Clone, Debug, Default)] 10 | pub(crate) struct Filter { 11 | pub(crate) name: String, 12 | pub(crate) modifier: IndexMap, 13 | } 14 | 15 | impl Filter { 16 | pub(crate) fn insert_modifier(&mut self, modifier: Modifier) { 17 | self.modifier.insert( 18 | modifier 19 | .field_name() 20 | .expect("BUG: No field name found!") 21 | .to_string(), 22 | modifier, 23 | ); 24 | } 25 | 26 | pub(crate) fn filter(&self, entry: &EventEntry) -> bool { 27 | self.modifier 28 | .values() 29 | .all(|m| m.filter(entry).unwrap_or(false)) 30 | } 31 | } 32 | 33 | #[derive(Clone, Debug, PartialEq)] 34 | pub(crate) enum Modifier { 35 | // Field methods 36 | FieldContains { name: String, value: String }, 37 | FieldEquals { name: String, value: String }, 38 | // TODO: Move regex instance into enum 39 | FieldMatches { name: String, regex: String }, 40 | FieldStartsWith { name: String, value: String }, 41 | } 42 | 43 | impl Display for Modifier { 44 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 45 | match self { 46 | Modifier::FieldContains { name, value } => { 47 | write!(f, "event.field.{} contains \"{}\"", name, value) 48 | } 49 | Modifier::FieldEquals { name, value } => { 50 | write!(f, "event.field.{} == \"{}\"", name, value) 51 | } 52 | Modifier::FieldMatches { name, regex } => { 53 | write!(f, "event.field.{} matches \"{}\"", name, regex) 54 | } 55 | Modifier::FieldStartsWith { name, value } => { 56 | write!(f, "event.field.{} starts_with \"{}\"", name, value) 57 | } 58 | } 59 | } 60 | } 61 | 62 | impl Modifier { 63 | fn field_name(&self) -> Option<&str> { 64 | match self { 65 | Modifier::FieldContains { name, .. } => Some(&name), 66 | Modifier::FieldEquals { name, .. } => Some(&name), 67 | Modifier::FieldMatches { name, .. } => Some(&name), 68 | Modifier::FieldStartsWith { name, .. } => Some(&name), 69 | } 70 | } 71 | 72 | fn filter(&self, entry: &EventEntry) -> Option { 73 | match self { 74 | Modifier::FieldStartsWith { name, value } => entry 75 | .event 76 | .any_by_name(name) 77 | .map(|string| string.starts_with(value)), 78 | Modifier::FieldEquals { name, value } => { 79 | entry.event.any_by_name(name).map(|string| &string == value) 80 | } 81 | Modifier::FieldContains { name, value } => entry 82 | .event 83 | .any_by_name(name) 84 | .map(|string| string.contains(value)), 85 | Modifier::FieldMatches { name, regex } => entry 86 | .event 87 | .any_by_name(name) 88 | .and_then(|string| Regex::new(regex).ok().map(|re| re.is_match(&string))), 89 | } 90 | } 91 | 92 | pub(crate) fn equals(name: String, value: String) -> Modifier { 93 | Modifier::FieldEquals { name, value } 94 | } 95 | 96 | pub(crate) fn contains(name: String, value: String) -> Modifier { 97 | Modifier::FieldContains { name, value } 98 | } 99 | 100 | pub(crate) fn starts_with(name: String, value: String) -> Modifier { 101 | Modifier::FieldStartsWith { name, value } 102 | } 103 | 104 | pub(crate) fn matches(name: String, regex: String) -> Modifier { 105 | Modifier::FieldMatches { name, regex } 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | 113 | use crate::storage::*; 114 | 115 | fn event_entry() -> EventEntry { 116 | let mut event = Event::default(); 117 | event.values.push(Value { 118 | field: Some(Field { 119 | name: "foo".to_string(), 120 | }), 121 | value: Some(value::Value::Str("barbazboz".to_string())), 122 | }); 123 | EventEntry { span: None, event } 124 | } 125 | 126 | #[test] 127 | fn modifier_equals() { 128 | let entry = event_entry(); 129 | 130 | let doesnt_exist = Modifier::equals("blah".to_string(), "example".to_string()); 131 | assert_eq!(doesnt_exist.filter(&entry), None); 132 | 133 | let not_equal = Modifier::equals("foo".to_string(), "example".to_string()); 134 | assert_eq!(not_equal.filter(&entry), Some(false)); 135 | 136 | let equals = Modifier::equals("foo".to_string(), "barbazboz".to_string()); 137 | assert_eq!(equals.filter(&entry), Some(true)); 138 | } 139 | 140 | #[test] 141 | fn modifier_contains() { 142 | let entry = event_entry(); 143 | 144 | let not_contained = Modifier::contains("foo".to_string(), "example".to_string()); 145 | assert_eq!(not_contained.filter(&entry), Some(false)); 146 | 147 | let contained = Modifier::contains("foo".to_string(), "baz".to_string()); 148 | assert_eq!(contained.filter(&entry), Some(true)); 149 | } 150 | 151 | #[test] 152 | fn modifier_regex() { 153 | let entry = event_entry(); 154 | 155 | let no_match = Modifier::matches("foo".to_string(), "example".to_string()); 156 | assert_eq!(no_match.filter(&entry), Some(false)); 157 | 158 | let matches = Modifier::matches("foo".to_string(), "b[aeiou]z".to_string()); 159 | assert_eq!(matches.filter(&entry), Some(true)); 160 | } 161 | 162 | #[test] 163 | fn modifier_starts_with() { 164 | let entry = event_entry(); 165 | 166 | let no_match = Modifier::starts_with("foo".to_string(), "example".to_string()); 167 | assert_eq!(no_match.filter(&entry), Some(false)); 168 | 169 | let matches = Modifier::starts_with("foo".to_string(), "bar".to_string()); 170 | assert_eq!(matches.filter(&entry), Some(true)); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /console/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | pub mod filter; 3 | pub mod storage; 4 | pub mod ui; 5 | -------------------------------------------------------------------------------- /console/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | 3 | use console::storage::*; 4 | use console::ui; 5 | 6 | fn main() -> Result<(), failure::Error> { 7 | // Share store between the gRPC client and the app 8 | let grpc_handle = StoreHandle::default(); 9 | let app_handle = grpc_handle.clone(); 10 | 11 | // Fetch events, spans, etc. 12 | thread::spawn(|| console::connection::listen(grpc_handle, "http://[::1]:50051")); 13 | 14 | let mut app = ui::App::new(app_handle)?; 15 | app.run()?; 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /console/src/storage/messages.rs: -------------------------------------------------------------------------------- 1 | //! Types generated by gRPC "/proto/tracing.proto" 2 | include!(concat!(env!("OUT_DIR"), "/tracing.rs")); 3 | 4 | impl Event { 5 | pub fn value_by_name(&self, name: &str) -> Option<&value::Value> { 6 | for value in &self.values { 7 | if &value.field.as_ref()?.name == name { 8 | return value.value.as_ref(); 9 | } 10 | } 11 | None 12 | } 13 | 14 | pub fn str_by_name(&self, name: &str) -> Option<&str> { 15 | match self.value_by_name(name)? { 16 | value::Value::Str(string) => Some(string), 17 | _ => None, 18 | } 19 | } 20 | pub fn debug_by_name(&self, name: &str) -> Option<&DebugRecord> { 21 | match self.value_by_name(name)? { 22 | value::Value::Debug(debug) => Some(debug), 23 | _ => None, 24 | } 25 | } 26 | pub fn signed_by_name(&self, name: &str) -> Option { 27 | match self.value_by_name(name)? { 28 | value::Value::Signed(signed) => Some(*signed), 29 | _ => None, 30 | } 31 | } 32 | pub fn unsigned_by_name(&self, name: &str) -> Option { 33 | match self.value_by_name(name)? { 34 | value::Value::Unsigned(unsigned) => Some(*unsigned), 35 | _ => None, 36 | } 37 | } 38 | pub fn bool_by_name(&self, name: &str) -> Option { 39 | match self.value_by_name(name)? { 40 | value::Value::Boolean(boolean) => Some(*boolean), 41 | _ => None, 42 | } 43 | } 44 | pub fn any_by_name(&self, name: &str) -> Option { 45 | Some(match self.value_by_name(name)? { 46 | value::Value::Str(string) => string.clone(), 47 | value::Value::Signed(i) => format!("{}", i), 48 | value::Value::Unsigned(u) => format!("{}", u), 49 | value::Value::Debug(d) => d.debug.clone(), 50 | value::Value::Boolean(b) => format!("{}", b), 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /console/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod messages; 2 | mod store; 3 | 4 | pub use messages::*; 5 | pub use store::*; 6 | -------------------------------------------------------------------------------- /console/src/storage/store.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | use crate::storage::messages::listen_response::Variant; 5 | use crate::storage::messages::*; 6 | 7 | /// # IDs 8 | /// The subscriber obviously want to reuse span ids, to preserve memory 9 | /// The console however, must preserve history. 10 | /// As a result, we internally assign and map our own ids. 11 | /// When a span id is reused in the subscriber, a new span message is send. 12 | /// This replaces the entry in the `id_map` and a new internal id is assigned. 13 | /// 14 | /// The console itself won't reuse ids. 15 | /// In the future, old/unused span information will be flushed to disk. 16 | /// Currently, the console doesn't do such kind of memory optimization. 17 | #[derive(Debug, Default)] 18 | pub struct Store { 19 | events: Vec, 20 | spans: Vec, 21 | 22 | updated: bool, 23 | id_counter: usize, 24 | id_map: HashMap, 25 | } 26 | 27 | impl Store { 28 | pub fn new() -> Store { 29 | Store::default() 30 | } 31 | 32 | pub fn updated(&self) -> bool { 33 | self.updated 34 | } 35 | 36 | pub fn clear(&mut self) { 37 | self.updated = false; 38 | } 39 | 40 | pub fn events(&self) -> &[EventEntry] { 41 | &self.events 42 | } 43 | 44 | pub fn spans(&self) -> &[Span] { 45 | &self.spans 46 | } 47 | } 48 | 49 | /// See `Store` documentation 50 | #[derive(Debug, Clone, Copy, PartialEq)] 51 | pub struct InternalId(usize); 52 | 53 | #[derive(Debug)] 54 | pub struct Span { 55 | id: InternalId, 56 | span: NewSpan, 57 | 58 | records: Vec, 59 | follows: Vec, 60 | } 61 | 62 | #[derive(Clone, Debug, PartialEq)] 63 | pub struct EventEntry { 64 | pub span: Option, 65 | pub event: Event, 66 | } 67 | 68 | impl EventEntry { 69 | pub fn level(&self) -> Option { 70 | Level::from_i32(self.event.attributes.as_ref()?.metadata.as_ref()?.level) 71 | } 72 | } 73 | 74 | /// Convenience Wrapper around `Arc>` 75 | #[derive(Clone, Default)] 76 | pub struct StoreHandle(pub Arc>); 77 | 78 | impl StoreHandle { 79 | pub fn new() -> StoreHandle { 80 | StoreHandle::default() 81 | } 82 | 83 | /// Locks and updates the underlying `Store` 84 | pub fn handle(&self, variant: Variant) { 85 | let mut store = self.0.lock().unwrap(); 86 | match variant { 87 | Variant::NewSpan(span) => store.new_span(span), 88 | Variant::Record(record) => store.record(record), 89 | Variant::Follows(follows) => store.record_follows_from(follows), 90 | Variant::Event(event) => store.event(event), 91 | } 92 | } 93 | } 94 | 95 | impl Store { 96 | fn new_span(&mut self, span: NewSpan) { 97 | // Update id mapping for span, see `Store` documentation 98 | self.id_map.insert( 99 | span.span 100 | .as_ref() 101 | .expect("BUG: No id assined to NewSpan") 102 | .id, 103 | InternalId(self.id_counter), 104 | ); 105 | 106 | self.spans.push(Span { 107 | id: InternalId(self.id_counter), 108 | span, 109 | records: vec![], 110 | follows: vec![], 111 | }); 112 | self.id_counter += 1; 113 | } 114 | 115 | fn record_follows_from(&mut self, follows: RecordFollowsFrom) { 116 | let span = self.id_map[&follows 117 | .span 118 | .as_ref() 119 | .expect("BUG: No id set on follows.span") 120 | .id]; 121 | self.spans[span.0] 122 | .follows 123 | .push(follows.follows.expect("BUG: No id set on follows.follows")); 124 | } 125 | 126 | fn record(&mut self, record: Record) { 127 | self.updated = true; 128 | let span = self.id_map[&record 129 | .span 130 | .as_ref() 131 | .expect("BUG: No id set on record.span") 132 | .id]; 133 | self.spans[span.0].records.push(record); 134 | } 135 | 136 | fn event(&mut self, event: Event) { 137 | self.updated = true; 138 | self.events.push(EventEntry { 139 | span: event.span.as_ref().map(|span| self.id_map[&span.id]), 140 | event, 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /console/src/ui/app.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::StoreHandle; 2 | 3 | use tui::backend::CrosstermBackend; 4 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 5 | use tui::widgets::{Paragraph, Text, Widget}; 6 | use tui::Frame; 7 | use tui::Terminal; 8 | 9 | use crossterm::{InputEvent, KeyEvent, MouseEvent, RawScreen}; 10 | 11 | use crate::filter::*; 12 | use crate::ui::Command; 13 | use crate::ui::{Action, EventList, Hitbox, Input, QueryView}; 14 | 15 | use std::cell::Cell; 16 | use std::sync::mpsc; 17 | use std::thread; 18 | use std::time::Duration; 19 | 20 | #[derive(Debug, PartialEq)] 21 | pub(crate) enum Focus { 22 | Events, 23 | Query, 24 | } 25 | 26 | enum Event { 27 | Input(InputEvent), 28 | Update, 29 | } 30 | 31 | fn setup_input_handling() -> mpsc::Receiver { 32 | // Setup input handling 33 | let (tx, rx) = mpsc::channel(); 34 | { 35 | let tx = tx.clone(); 36 | thread::spawn(move || { 37 | let input = crossterm::input(); 38 | let _ = input.enable_mouse_mode(); 39 | loop { 40 | let reader = input.read_sync(); 41 | for event in reader { 42 | let close = InputEvent::Keyboard(KeyEvent::Esc) == event; 43 | if let Err(_) = tx.send(Event::Input(event)) { 44 | return; 45 | } 46 | if close { 47 | return; 48 | } 49 | } 50 | } 51 | }); 52 | } 53 | 54 | // Setup 250ms tick rate 55 | { 56 | let tx = tx.clone(); 57 | thread::spawn(move || { 58 | let tx = tx.clone(); 59 | loop { 60 | tx.send(Event::Update).unwrap(); 61 | thread::sleep(Duration::from_millis(250)); 62 | } 63 | }); 64 | } 65 | rx 66 | } 67 | 68 | pub struct App { 69 | store: StoreHandle, 70 | focus: Focus, 71 | 72 | event_list: EventList, 73 | query_view: QueryView, 74 | 75 | filter: Filter, 76 | filter_updated: bool, 77 | 78 | rect: Cell>, 79 | rx: mpsc::Receiver, 80 | } 81 | 82 | impl App { 83 | pub fn new(store: StoreHandle) -> Result { 84 | Ok(App { 85 | store, 86 | focus: Focus::Query, 87 | 88 | event_list: EventList::new(), 89 | query_view: QueryView::new(), 90 | 91 | filter: Filter::default(), 92 | filter_updated: false, 93 | 94 | rect: Cell::new(None), 95 | rx: setup_input_handling(), 96 | }) 97 | } 98 | 99 | pub fn run(&mut self) -> Result<(), failure::Error> { 100 | let backend = CrosstermBackend::new(); 101 | RawScreen::into_raw_mode()?.disable_drop(); 102 | let mut terminal = Terminal::new(backend)?; 103 | terminal.hide_cursor()?; 104 | terminal.clear()?; 105 | let mut set_cursor = None; 106 | loop { 107 | let draw = match self.rx.recv()? { 108 | Event::Input(event) => { 109 | if let Some(redraw) = self.input(event) { 110 | redraw 111 | } else { 112 | break; 113 | } 114 | } 115 | Event::Update => self.update(), 116 | }; 117 | if draw { 118 | terminal.draw(|mut f| { 119 | self.render_to(&mut f); 120 | })?; 121 | if let Some((x, y)) = set_cursor { 122 | let _ = terminal.set_cursor(x, y); 123 | let _ = terminal.show_cursor(); 124 | } else { 125 | let _ = terminal.hide_cursor(); 126 | } 127 | } 128 | let cursor = self.show_cursor(); 129 | if cursor != set_cursor { 130 | if let Some((x, y)) = cursor { 131 | let _ = terminal.set_cursor(x, y); 132 | let _ = terminal.show_cursor(); 133 | set_cursor = cursor; 134 | } else { 135 | let _ = terminal.hide_cursor(); 136 | } 137 | } 138 | } 139 | terminal.clear()?; 140 | Ok(()) 141 | } 142 | 143 | pub fn update(&mut self) -> bool { 144 | let store = self.store.0.lock().unwrap(); 145 | if store.updated() || self.filter_updated { 146 | let event_list = self.event_list.update(&store, &self.filter); 147 | self.filter_updated = false; 148 | let query_view = self.query_view.update(self.filter.clone()); 149 | 150 | let rerender = event_list || query_view; 151 | rerender 152 | } else { 153 | false 154 | } 155 | } 156 | 157 | fn show_cursor(&self) -> Option<(u16, u16)> { 158 | match self.focus { 159 | Focus::Events => self.event_list.show_cursor(), 160 | Focus::Query => self.query_view.show_cursor(), 161 | } 162 | } 163 | 164 | fn on_up(&mut self) -> bool { 165 | match self.focus { 166 | Focus::Events => self.event_list.on_up(), 167 | Focus::Query => self.query_view.on_up(), 168 | } 169 | } 170 | 171 | fn on_down(&mut self) -> bool { 172 | match self.focus { 173 | Focus::Events => self.event_list.on_down(), 174 | Focus::Query => self.query_view.on_down(), 175 | } 176 | } 177 | fn on_char(&mut self, c: char) -> Action { 178 | match self.focus { 179 | Focus::Events => self.event_list.on_char(c), 180 | Focus::Query => self.query_view.on_char(c), 181 | } 182 | } 183 | fn on_backspace(&mut self) -> bool { 184 | match self.focus { 185 | Focus::Events => self.event_list.on_backspace(), 186 | Focus::Query => self.query_view.on_backspace(), 187 | } 188 | } 189 | 190 | fn focus_event(&mut self) -> bool { 191 | let rerender = self.focus != Focus::Events; 192 | self.focus = Focus::Events; 193 | self.query_view.set_focused(false); 194 | self.event_list.set_focused(true); 195 | rerender 196 | } 197 | 198 | fn focus_query(&mut self) -> bool { 199 | let rerender = self.focus != Focus::Query; 200 | self.focus = Focus::Query; 201 | self.query_view.set_focused(true); 202 | self.event_list.set_focused(false); 203 | rerender 204 | } 205 | 206 | fn on_left(&mut self) -> bool { 207 | self.focus_query() 208 | } 209 | 210 | fn on_right(&mut self) -> bool { 211 | self.focus_event() 212 | } 213 | 214 | /// Returns if the scene has to be redrawn 215 | pub fn input(&mut self, event: InputEvent) -> Option { 216 | let redraw = match event { 217 | InputEvent::Keyboard(key) => match key { 218 | KeyEvent::Esc => return None, 219 | KeyEvent::Char(c) => { 220 | let action = self.on_char(c); 221 | let redraw = action.redraw(); 222 | match action { 223 | Action::Command(Command::Event(modifier)) => { 224 | self.filter.insert_modifier(modifier); 225 | self.filter_updated = true; 226 | } 227 | _ => {} 228 | } 229 | redraw 230 | } 231 | KeyEvent::Backspace => self.on_backspace(), 232 | KeyEvent::Up => self.on_up(), 233 | KeyEvent::Down => self.on_down(), 234 | KeyEvent::Left => self.on_left(), 235 | KeyEvent::Right => self.on_right(), 236 | _ => false, 237 | }, 238 | InputEvent::Mouse(event) => match event { 239 | MouseEvent::Release(x, y) => { 240 | let (query_rect, event_rect) = self.rect.get().unwrap_or_default(); 241 | if query_rect.hit(x, y) { 242 | let rerender = self.focus_query(); 243 | self.query_view.on_click(x, y); 244 | rerender 245 | } else if event_rect.hit(x, y) { 246 | let rerender = self.focus_event(); 247 | self.event_list.on_click(x, y); 248 | rerender 249 | } else { 250 | false 251 | } 252 | } 253 | _ => false, 254 | }, 255 | _ => false, 256 | }; 257 | Some(redraw) 258 | } 259 | 260 | pub fn render_to(&mut self, f: &mut Frame) { 261 | let mut rect = f.size(); 262 | let mut legend_rect = rect; 263 | // Reserve space for legend 264 | rect.height -= 1; 265 | legend_rect.y += legend_rect.height - 1; 266 | legend_rect.height = 1; 267 | 268 | let chunks = Layout::default() 269 | .constraints([Constraint::Length(50), Constraint::Min(10)].as_ref()) 270 | .direction(Direction::Horizontal) 271 | .split(rect); 272 | 273 | self.query_view.render_to(f, chunks[0]); 274 | self.event_list.render_to(f, chunks[1]); 275 | Paragraph::new([Text::raw(" q: close, ← → ↑ ↓ click: navigate")].iter()) 276 | .render(f, legend_rect); 277 | Paragraph::new([Text::raw("prerelease version ")].iter()) 278 | .alignment(Alignment::Right) 279 | .render(f, legend_rect); 280 | self.rect.set(Some((chunks[0], chunks[1]))); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /console/src/ui/command.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::*; 2 | use std::str::FromStr; 3 | 4 | #[derive(Debug, PartialEq)] 5 | pub(crate) enum Command { 6 | Event(Modifier), 7 | } 8 | 9 | impl FromStr for Command { 10 | type Err = (); 11 | fn from_str(s: &str) -> Result { 12 | Command::from_str(s).ok_or(()) 13 | } 14 | } 15 | 16 | impl Command { 17 | fn from_str(string: &str) -> Option { 18 | let command_end = string.find(char::is_whitespace)?; 19 | let (command_str, remaining) = string.split_at(command_end); 20 | match command_str { 21 | _ if command_str.starts_with("event.") => Command::parse_event(command_str, remaining), 22 | _ => None, 23 | } 24 | } 25 | 26 | fn parse_event(command: &str, remaining: &str) -> Option { 27 | let mut segments = command.split('.'); 28 | if !(segments.next() == Some("event") && segments.next() == Some("field")) { 29 | return None; 30 | } 31 | let fieldname = segments.next()?; 32 | Some(Command::Event(Command::parse_operator( 33 | fieldname, remaining, 34 | )?)) 35 | } 36 | 37 | fn parse_operator(fieldname: &str, mut remaining: &str) -> Option { 38 | let fieldname = fieldname.to_string(); 39 | // remaining: ' == "example"' 40 | remaining = remaining.trim(); 41 | // remaining: '== "example"' 42 | let operator_end = remaining.find(char::is_whitespace)?; 43 | let (operator, mut remaining) = remaining.split_at(operator_end); 44 | // remaining: ' "example"' 45 | remaining = remaining.trim(); 46 | // remaining = '"example"' 47 | let value = Command::parse_string(remaining)?; 48 | let modifier_ty = match operator { 49 | "==" => Modifier::equals, 50 | "matches" => Modifier::matches, 51 | "contains" => Modifier::contains, 52 | "starts_with" => Modifier::starts_with, 53 | _ => None?, 54 | }; 55 | Some(modifier_ty(fieldname, value)) 56 | } 57 | 58 | fn parse_string(string: &str) -> Option { 59 | let mut chars = string.chars(); 60 | if string.len() < 2 || chars.next() != Some('"') || chars.last() != Some('"') { 61 | None? 62 | } 63 | let inner = &string[1..string.len() - 1]; 64 | Some(inner.to_string()) 65 | } 66 | } 67 | #[cfg(test)] 68 | mod tests { 69 | use super::*; 70 | 71 | #[test] 72 | fn parse_single_string() { 73 | assert_eq!( 74 | Command::parse_string(r#""example""#), 75 | Some("example".to_string()) 76 | ); 77 | } 78 | 79 | #[test] 80 | fn parse_multi_string() { 81 | assert_eq!( 82 | Command::parse_string(r#""foo bar baz""#), 83 | Some("foo bar baz".to_string()) 84 | ); 85 | } 86 | 87 | #[test] 88 | fn parse_command() { 89 | assert_eq!( 90 | r#"event.field.message == "example""#.parse(), 91 | Ok(Command::Event(Modifier::equals( 92 | "message".to_string(), 93 | "example".to_string() 94 | ))) 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /console/src/ui/events.rs: -------------------------------------------------------------------------------- 1 | use crate::storage::*; 2 | 3 | use crate::filter::*; 4 | use crate::ui::{Hitbox, Input}; 5 | 6 | use tui::backend::CrosstermBackend; 7 | use tui::layout::Rect; 8 | use tui::style::{Color, Modifier, Style}; 9 | use tui::widgets::{Block, Borders, Paragraph, Text, Widget}; 10 | use tui::Frame; 11 | 12 | use std::cell::Cell; 13 | use std::fmt::Write; 14 | 15 | pub struct EventList { 16 | /// Cached rows, gets populated by `EventList::update` 17 | logs: Vec, 18 | /// Index into logs vec, indicates which row the user selected 19 | selection: usize, 20 | /// How far the frame is offset by scrolling 21 | offset: usize, 22 | 23 | focused: bool, 24 | rect: Cell>, 25 | } 26 | 27 | impl EventList { 28 | pub(crate) fn new() -> EventList { 29 | EventList { 30 | focused: false, 31 | logs: Vec::new(), 32 | 33 | selection: 0, 34 | offset: 0, 35 | rect: Cell::new(None), 36 | } 37 | } 38 | 39 | pub(crate) fn update(&mut self, store: &Store, filter: &Filter) -> bool { 40 | let logs = store 41 | .events() 42 | .iter() 43 | .filter(|entry| filter.filter(entry)) 44 | .cloned() 45 | .collect(); 46 | let rerender = self.logs != logs; 47 | self.logs = logs; 48 | rerender 49 | } 50 | 51 | /// Adjusts the window if necessary, to make sure the selection is in frame 52 | /// 53 | /// In case we don't adjust ourself, `SelectableList` will do it on its own 54 | /// with unexpected ux effects, like the whole selection moves even though 55 | /// the selection has "space" to move without adjusting 56 | fn adjust_window_to_selection(&mut self) -> bool { 57 | // Calc the largest index that will be still in frame 58 | let rowcount = self.rect.get().unwrap_or_default().height as usize - 2; 59 | let upper_limit = self.offset + rowcount; 60 | 61 | if self.selection < self.offset { 62 | // The text cursor wants to move out on the upper side 63 | // Set the 64 | self.offset = self.selection; 65 | true 66 | } else if self.selection + 1 > upper_limit { 67 | // + 1: Upper_limit is a length, offset the index 68 | self.offset += (self.selection + 1) - upper_limit; 69 | true 70 | } else { 71 | false 72 | } 73 | } 74 | 75 | pub(crate) fn on_up(&mut self) -> bool { 76 | let new_offset = self.selection.saturating_sub(1); 77 | self.select(new_offset) 78 | } 79 | 80 | pub(crate) fn on_down(&mut self) -> bool { 81 | let new_offset = self.selection.saturating_add(1); 82 | self.select(new_offset) 83 | } 84 | 85 | fn select(&mut self, mut new_offset: usize) -> bool { 86 | if self.logs.len() < new_offset { 87 | new_offset = self.logs.len() - 1; 88 | } 89 | 90 | let rerender = new_offset != self.selection; 91 | self.selection = new_offset; 92 | 93 | // If the frame or the index changed, rerender for correct frame / highlighting 94 | // Adjust has side effects, it needs to be called first 95 | self.adjust_window_to_selection() || rerender 96 | } 97 | 98 | fn style_event(&self, i: usize, entry: &EventEntry) -> Vec> { 99 | let level = match entry.level() { 100 | None => Text::styled(" NONE ", Style::default().fg(Color::White)), 101 | Some(Level::Info) => Text::styled(" INFO ", Style::default().fg(Color::White)), 102 | Some(Level::Debug) => Text::styled("DEBUG ", Style::default().fg(Color::LightCyan)), 103 | Some(Level::Error) => Text::styled("ERROR ", Style::default().fg(Color::Red)), 104 | Some(Level::Trace) => Text::styled("TRACE ", Style::default().fg(Color::Green)), 105 | Some(Level::Warn) => Text::styled(" WARN ", Style::default().fg(Color::Yellow)), 106 | }; 107 | let mut text = String::new(); 108 | let mut first = true; 109 | for value in &entry.event.values { 110 | if first { 111 | first = false; 112 | } else { 113 | text.push_str(", "); 114 | } 115 | if let Some(field) = &value.field { 116 | write!(text, r#"{}(""#, field.name).unwrap(); 117 | match &value.value { 118 | Some(value::Value::Signed(i)) => write!(text, "{}", i).unwrap(), 119 | Some(value::Value::Unsigned(u)) => write!(text, "{}", u).unwrap(), 120 | Some(value::Value::Boolean(b)) => write!(text, "{}", b).unwrap(), 121 | Some(value::Value::Str(s)) => write!(text, "{}", s).unwrap(), 122 | Some(value::Value::Debug(d)) => write!(text, "{}", d.debug).unwrap(), 123 | None => {} 124 | } 125 | text.push_str(r#"")"#); 126 | } 127 | } 128 | text.push('\n'); 129 | if i == self.selection - self.offset { 130 | vec![ 131 | level, 132 | Text::styled(text, Style::default().modifier(Modifier::BOLD)), 133 | ] 134 | } else { 135 | vec![level, Text::raw(text)] 136 | } 137 | } 138 | 139 | pub(crate) fn render_to(&self, f: &mut Frame, r: Rect) { 140 | self.rect.set(Some(r)); 141 | // - 2: Upper and lower border of window 142 | let rowcount = r.height as usize - 2; 143 | 144 | let (border_color, title_color) = self.border_color(); 145 | let block_title = format!( 146 | "Events {}-{}/{}", 147 | 1 + self.offset, 148 | self.offset + std::cmp::min(rowcount, self.logs.len()), 149 | self.logs.len(), 150 | ); 151 | Paragraph::new( 152 | self.logs 153 | .iter() 154 | .skip(self.offset) 155 | .take(rowcount) 156 | .enumerate() 157 | .map(|(i, e)| self.style_event(i, e)) 158 | .flatten() 159 | .collect::>>() 160 | .iter(), 161 | ) 162 | .block( 163 | Block::default() 164 | .borders(Borders::ALL) 165 | .border_style(Style::default().fg(border_color)) 166 | .title(&block_title) 167 | .title_style(Style::default().fg(title_color)), 168 | ) 169 | .render(f, r); 170 | } 171 | } 172 | 173 | impl Input for EventList { 174 | fn set_focused(&mut self, focused: bool) { 175 | self.focused = focused; 176 | } 177 | fn focused(&self) -> bool { 178 | self.focused 179 | } 180 | 181 | fn on_click(&mut self, x: u16, y: u16) -> bool { 182 | let rect = self.rect.get().unwrap_or_default().inner(1); 183 | if !rect.hit(x, y) { 184 | return false; 185 | } 186 | 187 | self.select((y - rect.y - 1) as usize) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /console/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod app; 2 | pub(crate) mod command; 3 | pub(crate) mod events; 4 | pub(crate) mod query; 5 | 6 | pub use self::app::*; 7 | pub(crate) use self::command::*; 8 | pub(crate) use self::events::*; 9 | pub(crate) use self::query::*; 10 | 11 | use tui::layout::Rect; 12 | use tui::style::Color; 13 | 14 | pub(crate) enum Action { 15 | Command(Command), 16 | Redraw, 17 | Nothing, 18 | } 19 | 20 | impl Action { 21 | fn redraw(&self) -> bool { 22 | match self { 23 | Action::Nothing => false, 24 | _ => true, 25 | } 26 | } 27 | } 28 | 29 | pub(crate) trait Input { 30 | fn set_focused(&mut self, focused: bool); 31 | fn focused(&self) -> bool; 32 | 33 | fn show_cursor(&self) -> Option<(u16, u16)> { 34 | None 35 | } 36 | 37 | fn on_click(&mut self, _x: u16, _y: u16) -> bool { 38 | false 39 | } 40 | fn on_char(&mut self, _c: char) -> Action { 41 | Action::Nothing 42 | } 43 | fn on_backspace(&mut self) -> bool { 44 | false 45 | } 46 | 47 | fn border_color(&self) -> (Color, Color) { 48 | if self.focused() { 49 | (Color::Rgb(50, 205, 50), Color::Rgb(0, 255, 0)) 50 | } else { 51 | (Color::Reset, Color::Reset) 52 | } 53 | } 54 | } 55 | 56 | pub(crate) trait Hitbox { 57 | fn hit(&self, x: u16, y: u16) -> bool; 58 | } 59 | 60 | impl Hitbox for Rect { 61 | fn hit(&self, x: u16, y: u16) -> bool { 62 | (self.x..(self.x + self.width)).contains(&x) 63 | && (self.y..(self.y + self.height)).contains(&y) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /console/src/ui/query.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::*; 2 | use crate::ui::{Action, Input}; 3 | 4 | use tui::backend::CrosstermBackend; 5 | use tui::layout::{Constraint, Direction, Layout, Rect}; 6 | use tui::style::{Color, Style}; 7 | use tui::widgets::{Block, Borders, Paragraph, Text, Widget}; 8 | use tui::Frame; 9 | 10 | use std::borrow::Cow; 11 | use std::cell::Cell; 12 | 13 | pub struct QueryView { 14 | buffer: String, 15 | history: Vec, 16 | /// 0: User didn't request a previous value 17 | /// 1..: Users wants the `self.history.len() - self.history_index` value 18 | /// Since most recent values are pushed last, this will retrieve the last value 19 | history_index: usize, 20 | 21 | filter: Option, 22 | 23 | focused: bool, 24 | rect: Cell>, 25 | } 26 | 27 | impl QueryView { 28 | pub(crate) fn new() -> QueryView { 29 | QueryView { 30 | buffer: String::new(), 31 | history: Vec::new(), 32 | history_index: 0, 33 | 34 | filter: None, 35 | 36 | focused: true, 37 | rect: Cell::default(), 38 | } 39 | } 40 | 41 | pub(crate) fn update(&mut self, filter: Filter) -> bool { 42 | self.filter = Some(filter); 43 | false 44 | } 45 | 46 | pub(crate) fn on_up(&mut self) -> bool { 47 | if self.history.len() == 0 { 48 | // We don't have a history, ignore 49 | return false; 50 | } 51 | let old = self.history_index; 52 | self.history_index += 1; 53 | if self.history_index > self.history.len() { 54 | // Cap history_index 55 | // Remember, history_index is 1 based and points to the back of the vec 56 | self.history_index = self.history.len(); 57 | } 58 | // Retrieve from history 59 | self.buffer.clear(); 60 | let history_value = &self.history[self.history.len() - self.history_index]; 61 | self.buffer.clone_from(history_value); 62 | 63 | let rerender = old != self.history_index; 64 | rerender 65 | } 66 | 67 | pub(crate) fn on_down(&mut self) -> bool { 68 | let old = self.history_index; 69 | self.history_index = self.history_index.saturating_sub(1); 70 | let rerender = old != self.history_index; 71 | if rerender { 72 | // Index changed, buffer will needs to be cleared anyways 73 | self.buffer.clear(); 74 | // Do we need to retrieve from history or does clearing suffice 75 | if self.history_index != 0 { 76 | // Retrieve from history 77 | let history_value = &self.history[self.history.len() - self.history_index]; 78 | self.buffer.clone_from(history_value); 79 | } 80 | } 81 | rerender 82 | } 83 | 84 | pub(crate) fn render_to(&self, f: &mut Frame, r: Rect) { 85 | let (border_color, title_color) = self.border_color(); 86 | const HELP: [Text<'static>; 7] = [ 87 | Text::Raw(Cow::Borrowed("Commands\n")), 88 | Text::Raw(Cow::Borrowed("> event.field. \n")), 89 | Text::Raw(Cow::Borrowed("Operators\n")), 90 | Text::Raw(Cow::Borrowed("- == \"\"\n")), 91 | Text::Raw(Cow::Borrowed("- contains \"\"\n")), 92 | Text::Raw(Cow::Borrowed("- startsWith \"\"\n")), 93 | Text::Raw(Cow::Borrowed("- matches \"\"\n")), 94 | ]; 95 | let chunks = Layout::default() 96 | .constraints( 97 | [ 98 | Constraint::Length(3), 99 | Constraint::Min(1), 100 | Constraint::Length(HELP.len() as u16 + 2), 101 | ] 102 | .as_ref(), 103 | ) 104 | .direction(Direction::Vertical) 105 | .split(r); 106 | let text = [ 107 | Text::raw("> "), 108 | Text::styled(&self.buffer, Style::default().fg(Color::White)), 109 | ]; 110 | Paragraph::new(text.into_iter()) 111 | .block( 112 | Block::default() 113 | .title("Query") 114 | .borders(Borders::ALL & !Borders::BOTTOM) 115 | .border_style(Style::default().fg(border_color)) 116 | .title_style(Style::default().fg(title_color)), 117 | ) 118 | .render(f, chunks[0]); 119 | self.rect.set(Some(chunks[0])); 120 | 121 | let items: Vec> = if let Some(filter) = self.filter.as_ref() { 122 | filter 123 | .modifier 124 | .values() 125 | .map(|m| Text::raw(format!("{}\n", m))) 126 | .collect::>>() 127 | } else { 128 | vec![] 129 | }; 130 | Paragraph::new(items.iter()) 131 | .block( 132 | Block::default() 133 | .title("Current Filter") 134 | .borders(Borders::ALL & !Borders::BOTTOM) 135 | .border_style(Style::default().fg(border_color)) 136 | .title_style(Style::default().fg(title_color)), 137 | ) 138 | .render(f, chunks[1]); 139 | 140 | Paragraph::new(HELP.iter()) 141 | .block( 142 | Block::default() 143 | .title("Help") 144 | .borders(Borders::ALL) 145 | .border_style(Style::default().fg(border_color)) 146 | .title_style(Style::default().fg(title_color)), 147 | ) 148 | .render(f, chunks[2]); 149 | } 150 | 151 | fn handle_command(&mut self) -> Action { 152 | let action = if let Ok(command) = self.buffer.parse() { 153 | Action::Command(command) 154 | } else { 155 | // Just issue redraw for cleared buffer 156 | Action::Redraw 157 | }; 158 | self.history.push(self.buffer.clone()); 159 | self.buffer.clear(); 160 | action 161 | } 162 | 163 | fn last_char_is_whitespace(&self) -> bool { 164 | self.buffer 165 | .chars() 166 | .last() 167 | .map(char::is_whitespace) 168 | .unwrap_or(true) 169 | } 170 | } 171 | 172 | impl Input for QueryView { 173 | fn set_focused(&mut self, focused: bool) { 174 | self.focused = focused; 175 | } 176 | fn focused(&self) -> bool { 177 | self.focused 178 | } 179 | fn show_cursor(&self) -> Option<(u16, u16)> { 180 | if self.focused() { 181 | if let Some(rect) = self.rect.get() { 182 | let rect = rect.inner(1); 183 | let x = rect.x 184 | + 2 // "> ".len() 185 | + self.buffer.len() as u16; 186 | return Some((x, rect.y)); 187 | } 188 | } 189 | None 190 | } 191 | fn on_char(&mut self, c: char) -> Action { 192 | // We don't wrap the text 193 | // Excess text can still be typed, just requires precision by the user 194 | // TODO: Decide to implement moving cursor or adjust width of frame 195 | match c { 196 | '\n' => self.handle_command(), 197 | _ if self.last_char_is_whitespace() && c.is_whitespace() => { 198 | // We are at the start of a command 199 | // Or the last char is already a whitespace 200 | // Don't insert redudant whitespace 201 | Action::Nothing 202 | } 203 | _ => { 204 | self.buffer.push(c); 205 | Action::Redraw 206 | } 207 | } 208 | } 209 | fn on_backspace(&mut self) -> bool { 210 | let rerender = self.buffer.len() != 0; 211 | let new_len = self.buffer.len().wrapping_sub(1); 212 | self.buffer.truncate(new_len); 213 | rerender 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | authors = ["Matthias Prechtl "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tracing = "0.1" 11 | tracing-test = { git = "https://github.com/MSleepyPanda/tokio-trace-nursery", branch="beacon" } 12 | 13 | [dependencies.console-subscriber] 14 | path = "../subscriber" 15 | -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | use console_subscriber::*; 2 | 3 | use std::thread; 4 | use std::time::Duration; 5 | 6 | fn main() { 7 | let handle = BackgroundThreadHandle::new(); 8 | let subscriber = handle.new_subscriber(); 9 | 10 | thread::Builder::new() 11 | .name("Server".to_string()) 12 | .spawn(|| { 13 | tracing::subscriber::with_default(subscriber, || { 14 | thread::sleep(Duration::from_millis(1000)); 15 | let kind = tracing_test::ApplicationKind::YakShave; 16 | loop { 17 | thread::sleep(Duration::from_millis(2000)); 18 | println!("Emitting"); 19 | kind.emit(); 20 | } 21 | }); 22 | }) 23 | .expect("Couldn't start background thread"); 24 | handle.run_background("[::1]:50051").join().unwrap(); 25 | } 26 | -------------------------------------------------------------------------------- /gsoc.md: -------------------------------------------------------------------------------- 1 | # Google Summer of Code 2 | This project has been part of [Google Summer of Code](https://summerofcode.withgoogle.com/). 3 | 4 | It has been proposed under the following description, which gives a good high level overview of what we want to achieve: 5 | 6 | > Tokio provides an instrumentation API using `tracing` as well as a number of instrumentation points built into Tokio itself and the Tokio ecosystem. The goal of the project is to implement a library for aggregation, metrics of said instrumentation points and a console-based UI that connects to the process, allowing users to quickly visualize, browse and debug the data. 7 | > 8 | > Because processes can encode structured and typed business logic with instrumentation points based on `tracing`, a domain-specific debugger built upon those can provide powerful, ad hoc tooling, e.g. filtering events by connection id, execution context etcetera. As instrumentation points of underlying libraries are collected as well, it is easy to observe their behaviour and interaction. This is an eminent advantage over traditional debuggers, where the user instead observes the implementation. 9 | 10 | ## Summary 11 | 12 | Initial research started in the application issue [#1](https://github.com/tokio-rs/gsoc/issues/1). A dedicated [repository](https://github.com/tokio-rs/console/) was set up and code submitted via [pull requests](https://github.com/tokio-rs/console/pulls?q=is%3Apr). 13 | 14 | All in all, we now support*: 15 | - UI Navigation: Arrow keys + Mouse** 16 | - A small, text based query DSL 17 | - `group_by` operator with access to event fields and their spans 18 | - filtering operators for event fields: 19 | - Equality `==` 20 | - `contains ""` 21 | - `starts_with ""` 22 | - `matches ""` 23 | - Subscriber implementations for remote access 24 | - Transport layer: gRPC / protobuf definition 25 | - Threaded implementation, when no tokio runtime is available 26 | - Tokio/Task based implementation, no additional threads 27 | - Filter de-/serialization with `load ` and `save ` 28 | 29 | In summary, over the course of the three months, we've implemented a prototype that showcases the power of structured instrumentation points provided by [tracing](https://github.com/tokio-rs/tracing/). The initial set of features is pretty solid and imo (@msleepypanda) we achieved what we set out to do! 30 | 31 | Some things did fall short though. We're still experimenting on ways to effectively present the data. The following work items will be revisited once we reach a more stable state: 32 | - Verification on production applications 33 | - Debugging/Application guides for users 34 | 35 | \*: Some PRs are pending review atm 36 |
37 | **: Depends on the settings of your terminal application 38 | 39 | ## More than a prototype 40 | 41 | Working on something that tickles you curiosity and ambitions, seeing something with potential slowly but steadily reaching a usable state _really_ drives creativity. Unsursprisingly, the list of features / directions in which we want to drive `console` doesn't fall short (no specific order): 42 | 43 | - More/Mightier operators 44 | - Filter nesting, e.g. `show "filter_a" within "filter_b"` 45 | - Time travel debugging 46 | - Revise query workflow 47 | - Recording / Plaback capabilities 48 | - Overlaying traces from multiple (distributed?) applications 49 | - Custom filters and widgets for libraries 50 | - Generate diagrams, timelines from traces 51 | - Web frontend 52 | - docs: Debugging/Application guides 53 | - Transition to async/await 54 | 55 | I hope to maintain/develop the console project beyond GSoC and i'm very much excited for the future! 56 | 57 | ## Get involved 58 | 59 | If you're interested, we'd like to hear from you! Get involved in the issue tracker or ping us on [gitter](https://gitter.im/tokio-rs/tracing) (@hawkw, @msleepypanda). 60 | 61 | ## Thank you 62 | 63 | I'm very thankful for the opportunity working together with tokio and @hawkw, sponsered by Google. The project and thorough review process has certainly improved my style and organizational abilities. If you're eligible for GSoC, i can highly recommend applying, especially at the tokio project. I sincerely hope that they'll have the opportunity to mentor students next year. -------------------------------------------------------------------------------- /proto/tracing.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tracing; 4 | 5 | service ConsoleForwarder { 6 | rpc Listen(ListenRequest) returns (stream ListenResponse) {} 7 | } 8 | 9 | message ListenRequest {} 10 | 11 | message ListenResponse { 12 | oneof variant { 13 | NewSpan newSpan = 1; 14 | Record record = 2; 15 | RecordFollowsFrom follows = 3; 16 | Event event = 4; 17 | } 18 | } 19 | 20 | /* 21 | * Subscriber events 22 | * 23 | * Enter/Exit/Clone/Drop Span are only tracked within the subscriber. 24 | * The console has no interest in these events, so they are not transmitted. 25 | */ 26 | 27 | message NewSpan { 28 | Attributes attributes = 1; 29 | SpanId span = 2; 30 | Timestamp timestamp = 3; 31 | repeated Value values = 4; 32 | } 33 | 34 | message Record { 35 | SpanId span = 1; 36 | repeated Value values = 2; 37 | ThreadId thread = 3; 38 | Timestamp timestamp = 4; 39 | } 40 | 41 | message RecordFollowsFrom { 42 | SpanId span = 1; 43 | SpanId follows = 2; 44 | } 45 | 46 | message Event { 47 | SpanId span = 1; 48 | repeated Value values = 2; 49 | repeated Field fields = 3; 50 | Attributes attributes = 4; 51 | ThreadId thread = 5; 52 | Timestamp timestamp = 6; 53 | } 54 | 55 | // Wrapper types 56 | 57 | message LineNum { uint32 num = 1; } 58 | 59 | message SpanId { uint64 id = 1; } 60 | 61 | message ThreadId { uint64 id = 1; } 62 | 63 | message Timestamp { int64 nano = 1; } 64 | 65 | message DebugRecord { 66 | string debug = 1; 67 | string pretty = 2; 68 | } 69 | 70 | // `tracing` data types 71 | 72 | message Field { string name = 1; } 73 | 74 | enum Level { 75 | ERROR = 0; 76 | WARN = 1; 77 | INFO = 2; 78 | DEBUG = 3; 79 | TRACE = 5; 80 | } 81 | 82 | message Metadata { 83 | repeated Field fieldset = 1; 84 | Level level = 2; 85 | string name = 3; 86 | string target = 4; 87 | string module_path = 5; 88 | string file = 6; 89 | LineNum line = 7; 90 | bool is_event = 8; 91 | bool is_span = 9; 92 | } 93 | 94 | message Value { 95 | Field field = 1; 96 | oneof value { 97 | int64 signed = 2; 98 | uint64 unsigned = 3; 99 | bool boolean = 4; 100 | string str = 5; 101 | DebugRecord debug = 6; 102 | } 103 | } 104 | 105 | message Attributes { 106 | Metadata metadata = 1; 107 | bool is_root = 2; 108 | bool is_contextual = 3; 109 | SpanId parent = 4; 110 | } -------------------------------------------------------------------------------- /subscriber/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "console-subscriber" 3 | version = "0.1.0" 4 | authors = ["Matthias Prechtl "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bytes = "0.4" 11 | chrono = "0.4.7" 12 | crossbeam = "0.7.1" 13 | futures = "0.1" 14 | http = "0.1" 15 | hyper = "0.12" 16 | tokio = "0.1" 17 | tower-hyper = "0.1" 18 | tower-grpc = { features = ["tower-hyper"], version = "0.1.0" } 19 | tower-service = "0.2" 20 | tower-util = "0.1" 21 | tracing-core = "0.1" 22 | prost = "0.5.0" 23 | 24 | [build-dependencies] 25 | tower-grpc-build = { version = "0.1.0", features = ["tower-hyper"] } -------------------------------------------------------------------------------- /subscriber/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tower_grpc_build::Config::new() 3 | .enable_client(false) 4 | .enable_server(true) 5 | .build(&["../proto/tracing.proto"], &["../proto/"]) 6 | .unwrap_or_else(|e| panic!("protobuf compilation failed: {}", e)); 7 | println!("cargo:rerun-if-changed=../proto/tracing.proto"); 8 | } 9 | -------------------------------------------------------------------------------- /subscriber/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A remote endpoint for `tracing-console` 2 | //! 3 | //! The subscriber currently spawns two threads two manage the endpoint. 4 | //! 5 | //! When an application interacts with `tracing`, under the hood, 6 | //! the current subscriber is called. 7 | //! This happens, for example, when a span is created (`span!(...)`) 8 | //! or an event is issued (`warn!("Cookies are empty")`). 9 | //! 10 | //! Those calls get translated to a message and sent to an aggregator thread. 11 | //! This aggregator thread then passes those messages to a network thread, 12 | //! which communicates with the client/console. 13 | //! 14 | //! # Network 15 | //! The following information will not be send to the console, but tracked locally: 16 | //! - `span.enter()/exit()`, tracked via Thread-Local-Storage. 17 | //! - `span.clone()/` and dropping, currently involves a mutex access 18 | //! 19 | //! # Thread overview: 20 | //! 21 | //! ```schematic,ignore 22 | //! ┌──────────────────┐ span!(...) ┌───────────────────┐ 23 | //! │Application Thread│----------->│ │ 24 | //! └──────────────────┘ │ │ 25 | //! ┌──────────────────┐ warn!(...) │ │ ┌──────────────────┐ 26 | //! │Application Thread│----------->│ Aggregator Thread │----->│ Network Thread │ 27 | //! └──────────────────┘ │ │ └──────────────────┘ 28 | //! ┌──────────────────┐ debug!(..) │ │ 29 | //! │Application Thread│----------->│ │ 30 | //! └──────────────────┘ └───────────────────┘ 31 | //! ``` 32 | //! 33 | //! # Usage 34 | //! 35 | //! ```rust,ignore 36 | //! # fn main() { 37 | //! use console_subscriber::BackgroundThreadHandle; 38 | //! use std::thread; 39 | //! 40 | //! let handle = BackgroundThreadHandle::new(); 41 | //! let subscriber = handle.new_subscriber(); 42 | //! std::thread::spawn(|| { 43 | //! tracing::subscriber::with_default(subscriber, || { 44 | //! use tracing::{event, Level}; 45 | //! 46 | //! event!(Level::INFO, "something has happened!"); 47 | //! }); 48 | //! }); 49 | //! 50 | //! handle.run_background("[::1]:50051").join().unwrap(); 51 | //! # } 52 | //! ``` 53 | 54 | // Borrowed from `tracing` 55 | 56 | #[macro_use] 57 | macro_rules! try_lock { 58 | ($lock:expr) => { 59 | try_lock!($lock, else return) 60 | }; 61 | ($lock:expr, else $els:expr) => { 62 | match $lock { 63 | Ok(l) => l, 64 | Err(_) if std::thread::panicking() => $els, 65 | Err(_) => panic!("lock poisoned"), 66 | } 67 | }; 68 | } 69 | 70 | mod messages; 71 | mod server; 72 | mod subscriber; 73 | 74 | use tracing_core::span; 75 | 76 | use std::collections::HashMap; 77 | use std::num::NonZeroU64; 78 | use std::sync::atomic::AtomicUsize; 79 | 80 | pub use server::*; 81 | 82 | #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] 83 | pub struct ThreadId(pub usize); 84 | 85 | impl From for messages::ThreadId { 86 | fn from(id: ThreadId) -> Self { 87 | messages::ThreadId { id: id.0 as u64 } 88 | } 89 | } 90 | 91 | #[derive(Debug)] 92 | pub struct Span { 93 | refcount: AtomicUsize, 94 | follows: Vec, 95 | } 96 | 97 | #[derive(Debug)] 98 | pub struct SpanId(NonZeroU64); 99 | 100 | impl SpanId { 101 | fn new(id: u64) -> SpanId { 102 | SpanId(NonZeroU64::new(id).expect("IDs must be nonzero")) 103 | } 104 | 105 | fn as_index(&self) -> usize { 106 | (self.0.get() - 1) as usize 107 | } 108 | 109 | fn as_span(&self) -> span::Id { 110 | span::Id::from_u64(self.0.get()) 111 | } 112 | 113 | fn as_message(&self) -> messages::SpanId { 114 | messages::SpanId { id: self.0.get() } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /subscriber/src/messages.rs: -------------------------------------------------------------------------------- 1 | use tracing_core::field::Visit; 2 | use tracing_core::span; 3 | 4 | use std::fmt::Debug; 5 | 6 | include!(concat!(env!("OUT_DIR"), "/tracing.rs")); 7 | 8 | #[derive(Default)] 9 | pub struct Recorder(pub Vec); 10 | 11 | impl Visit for Recorder { 12 | fn record_debug(&mut self, field: &tracing_core::Field, value: &dyn Debug) { 13 | self.0.push(Value { 14 | field: Some(Field { 15 | name: field.name().to_string(), 16 | }), 17 | value: Some(value::Value::Debug(DebugRecord { 18 | debug: format!("{:?}", value), 19 | pretty: format!("{:#?}", value), 20 | })), 21 | }) 22 | } 23 | 24 | fn record_i64(&mut self, field: &tracing_core::Field, value: i64) { 25 | self.0.push(Value { 26 | field: Some(Field { 27 | name: field.name().to_string(), 28 | }), 29 | value: Some(value::Value::Signed(value)), 30 | }) 31 | } 32 | fn record_u64(&mut self, field: &tracing_core::Field, value: u64) { 33 | self.0.push(Value { 34 | field: Some(Field { 35 | name: field.name().to_string(), 36 | }), 37 | value: Some(value::Value::Unsigned(value)), 38 | }) 39 | } 40 | fn record_bool(&mut self, field: &tracing_core::Field, value: bool) { 41 | self.0.push(Value { 42 | field: Some(Field { 43 | name: field.name().to_string(), 44 | }), 45 | value: Some(value::Value::Boolean(value)), 46 | }) 47 | } 48 | fn record_str(&mut self, field: &tracing_core::Field, value: &str) { 49 | self.0.push(Value { 50 | field: Some(Field { 51 | name: field.name().to_string(), 52 | }), 53 | value: Some(value::Value::Str(value.to_string())), 54 | }) 55 | } 56 | } 57 | 58 | impl From<&span::Id> for SpanId { 59 | fn from(id: &span::Id) -> Self { 60 | SpanId { id: id.into_u64() } 61 | } 62 | } 63 | 64 | impl From<&'static tracing_core::Metadata<'static>> for Metadata { 65 | fn from(meta: &tracing_core::Metadata) -> Self { 66 | let fieldset = meta 67 | .fields() 68 | .iter() 69 | .map(|field| Field { 70 | name: field.name().to_string(), 71 | }) 72 | .collect(); 73 | 74 | let level = match *meta.level() { 75 | tracing_core::Level::DEBUG => Level::Debug, 76 | tracing_core::Level::ERROR => Level::Error, 77 | tracing_core::Level::INFO => Level::Info, 78 | tracing_core::Level::TRACE => Level::Trace, 79 | tracing_core::Level::WARN => Level::Warn, 80 | } 81 | .into(); 82 | 83 | Metadata { 84 | fieldset, 85 | level, 86 | name: meta.name().to_string(), 87 | target: meta.name().to_string(), 88 | module_path: meta.name().to_string(), 89 | file: meta.name().to_string(), 90 | line: meta.line().map(|num| LineNum { num }), 91 | is_event: meta.is_event(), 92 | is_span: meta.is_span(), 93 | } 94 | } 95 | } 96 | 97 | impl<'a> From<&'a span::Attributes<'a>> for Attributes { 98 | fn from(attr: &span::Attributes) -> Self { 99 | Attributes { 100 | metadata: Some(attr.metadata().into()), 101 | is_root: attr.is_root(), 102 | is_contextual: attr.is_contextual(), 103 | parent: attr.parent().map(|id| SpanId { id: id.into_u64() }), 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /subscriber/src/server.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::Ordering, Arc, RwLock}; 2 | use std::thread; 3 | 4 | use crossbeam::channel::{unbounded, Receiver, Sender}; 5 | 6 | use crate::messages::listen_response::Variant; 7 | use crate::subscriber::*; 8 | use crate::*; 9 | 10 | use futures::sink::{Sink, Wait}; 11 | use futures::sync::mpsc; 12 | use futures::Future; 13 | use futures::Stream; 14 | 15 | use tower_hyper::server::{Http, Server}; 16 | 17 | use tower_grpc::codegen::server::grpc::{Request, Response}; 18 | 19 | use tokio::net::TcpListener; 20 | 21 | #[derive(Default)] 22 | pub(crate) struct Registry { 23 | pub spans: Vec, 24 | pub reusable: Vec, 25 | 26 | pub thread_names: HashMap, 27 | } 28 | 29 | impl Registry { 30 | pub(crate) fn new_id(&mut self) -> SpanId { 31 | self.reusable 32 | .pop() 33 | .map(|id| { 34 | self.spans[id.as_index()] 35 | .refcount 36 | .fetch_add(1, Ordering::SeqCst); 37 | id 38 | }) 39 | .unwrap_or_else(|| { 40 | let id = SpanId::new(self.spans.len() as u64 + 1); 41 | self.spans.push(Span { 42 | refcount: AtomicUsize::new(1), 43 | follows: vec![], 44 | }); 45 | id 46 | }) 47 | } 48 | } 49 | 50 | #[derive(Clone)] 51 | /// A factory for ConsoleForwarder 52 | pub struct BackgroundThreadHandle { 53 | sender: Sender, 54 | tx_sender: Sender>>, 55 | registry: Arc>, 56 | } 57 | 58 | impl BackgroundThreadHandle { 59 | pub fn new() -> BackgroundThreadHandle { 60 | let (tx, rx): (Sender, Receiver) = unbounded(); 61 | let (txtx, rxrx) = unbounded(); 62 | thread::spawn(move || { 63 | let mut senders: Vec>> = Vec::new(); 64 | while let Ok(message) = rx.recv() { 65 | while let Ok(tx) = rxrx.try_recv() { 66 | // TODO: Track and rebroadcast newspan information for live spans 67 | senders.push(tx); 68 | } 69 | let mut closed = vec![]; 70 | for (i, sender) in senders.iter_mut().enumerate() { 71 | let response = messages::ListenResponse { 72 | variant: Some(message.clone()), 73 | }; 74 | if sender.send(response).is_err() { 75 | // Connection reset, mark for removal 76 | closed.push(i); 77 | } 78 | } 79 | // Traverse in reverse order, to keep index valid during removal 80 | for &i in closed.iter().rev() { 81 | let _ = senders.remove(i); 82 | } 83 | } 84 | }); 85 | BackgroundThreadHandle { 86 | sender: tx, 87 | tx_sender: txtx, 88 | registry: Arc::default(), 89 | } 90 | } 91 | 92 | pub fn into_server(self, addr: &str) -> impl Future { 93 | let service = messages::server::ConsoleForwarderServer::new(self); 94 | let mut server = Server::new(service); 95 | let http = Http::new().http2_only(true).clone(); 96 | 97 | let bind = TcpListener::bind(&addr.parse().expect("Invalid address")).expect("bind"); 98 | 99 | bind.incoming() 100 | .for_each(move |sock| { 101 | if let Err(e) = sock.set_nodelay(true) { 102 | return Err(e); 103 | } 104 | 105 | let serve = server.serve_with(sock, http.clone()); 106 | tokio::spawn(serve.map_err(|_| { 107 | // Ignore connection reset 108 | })); 109 | 110 | Ok(()) 111 | }) 112 | .map_err(|e| eprintln!("accept error: {}", e)) 113 | } 114 | 115 | pub fn run_background(self, addr: &'static str) -> thread::JoinHandle<()> { 116 | thread::spawn(move || tokio::run(self.into_server(addr))) 117 | } 118 | 119 | pub fn new_subscriber(&self) -> ConsoleForwarder { 120 | ConsoleForwarder { 121 | tx: self.sender.clone(), 122 | registry: self.registry.clone(), 123 | } 124 | } 125 | } 126 | 127 | impl messages::server::ConsoleForwarder for BackgroundThreadHandle { 128 | type ListenStream = 129 | Box + Send>; 130 | type ListenFuture = 131 | futures::future::FutureResult, tower_grpc::Status>; 132 | 133 | fn listen(&mut self, _request: Request) -> Self::ListenFuture { 134 | let (tx, rx) = mpsc::channel(8); 135 | self.tx_sender 136 | .send(tx.wait()) 137 | .expect("BUG: No aggregation thread available"); 138 | let rx = rx.map_err(|_| unimplemented!("")); 139 | futures::future::ok(Response::new(Box::new(rx))) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /subscriber/src/subscriber.rs: -------------------------------------------------------------------------------- 1 | use tracing_core::span; 2 | use tracing_core::Event; 3 | use tracing_core::Subscriber; 4 | use tracing_core::{Interest, Metadata}; 5 | 6 | use crossbeam::channel::Sender; 7 | 8 | use std::cell::{Cell, RefCell}; 9 | use std::sync::atomic::{AtomicUsize, Ordering}; 10 | use std::sync::{Arc, Once, RwLock}; 11 | use std::thread; 12 | 13 | use chrono::prelude::*; 14 | 15 | use crate::messages::listen_response::Variant; 16 | use crate::messages::Recorder; 17 | use crate::*; 18 | 19 | static THREAD_COUNTER: AtomicUsize = AtomicUsize::new(1); 20 | 21 | thread_local! { 22 | static THREAD_ID_INIT: Once = Once::new(); 23 | static THREAD_ID: Cell = Cell::new(0); 24 | 25 | static STACK: RefCell> = RefCell::new(Vec::new()); 26 | } 27 | 28 | fn get_thread_id(console: &ConsoleForwarder) -> ThreadId { 29 | THREAD_ID_INIT.with(|init_guard| { 30 | init_guard.call_once(|| { 31 | THREAD_ID.with(|id| { 32 | let new_id = THREAD_COUNTER.fetch_add(1, Ordering::SeqCst); 33 | if let Some(name) = thread::current().name() { 34 | console.register_thread_name(ThreadId(new_id), name.to_string()); 35 | } 36 | id.set(new_id); 37 | }) 38 | }); 39 | THREAD_ID.with(|id| ThreadId(id.get())) 40 | }) 41 | } 42 | 43 | pub struct ConsoleForwarder { 44 | pub(crate) tx: Sender, 45 | pub(crate) registry: Arc>, 46 | } 47 | 48 | impl ConsoleForwarder { 49 | fn register_thread_name(&self, id: ThreadId, name: String) { 50 | self.registry.write().unwrap().thread_names.insert(id, name); 51 | } 52 | } 53 | 54 | impl Subscriber for ConsoleForwarder { 55 | fn enabled(&self, _metadata: &Metadata) -> bool { 56 | true 57 | } 58 | fn new_span(&self, span: &span::Attributes) -> span::Id { 59 | let id = self.registry.write().unwrap().new_id(); 60 | let mut rec = Recorder::default(); 61 | span.record(&mut rec); 62 | self.tx 63 | .send(Variant::NewSpan(messages::NewSpan { 64 | attributes: Some(span.into()), 65 | span: Some(id.as_message()), 66 | timestamp: Some(messages::Timestamp { 67 | nano: Utc::now().timestamp_nanos(), 68 | }), 69 | values: rec.0, 70 | })) 71 | .expect("BUG: No Backgroundthread"); 72 | 73 | id.as_span() 74 | } 75 | fn record(&self, span: &span::Id, values: &span::Record) { 76 | let mut recorder = messages::Recorder::default(); 77 | values.record(&mut recorder); 78 | self.tx 79 | .send(Variant::Record(messages::Record { 80 | span: Some(span.into()), 81 | values: recorder.0, 82 | thread: Some(get_thread_id(self).into()), 83 | timestamp: Some(messages::Timestamp { 84 | nano: Utc::now().timestamp_nanos(), 85 | }), 86 | })) 87 | .expect("BUG: No Backgroundthread"); 88 | } 89 | fn record_follows_from(&self, span: &span::Id, follows: &span::Id) { 90 | self.tx 91 | .send(Variant::Follows(messages::RecordFollowsFrom { 92 | span: Some(span.into()), 93 | follows: Some(follows.into()), 94 | })) 95 | .expect("BUG: No Backgroundthread"); 96 | } 97 | fn event(&self, event: &Event) { 98 | let mut recorder = messages::Recorder::default(); 99 | event.record(&mut recorder); 100 | let fields = event 101 | .fields() 102 | .map(|field| messages::Field { 103 | name: field.name().to_string(), 104 | }) 105 | .collect(); 106 | let attributes = messages::Attributes { 107 | is_contextual: event.is_contextual(), 108 | is_root: event.is_root(), 109 | metadata: Some(event.metadata().into()), 110 | parent: event.parent().map(|p| p.into()), 111 | }; 112 | self.tx 113 | .send(Variant::Event(messages::Event { 114 | span: STACK.with(|stack| stack.borrow().last().map(SpanId::as_message)), 115 | values: recorder.0, 116 | thread: Some(get_thread_id(self).into()), 117 | attributes: Some(attributes), 118 | fields, 119 | timestamp: Some(messages::Timestamp { 120 | nano: Utc::now().timestamp_nanos(), 121 | }), 122 | })) 123 | .expect("BUG: No Backgroundthread"); 124 | } 125 | fn enter(&self, span: &span::Id) { 126 | STACK.with(|stack| stack.borrow_mut().push(SpanId::new(span.into_u64()))) 127 | } 128 | fn exit(&self, _: &span::Id) { 129 | STACK.with(|stack| stack.borrow_mut().pop()); 130 | } 131 | fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest { 132 | match self.enabled(metadata) { 133 | true => Interest::always(), 134 | false => Interest::never(), 135 | } 136 | } 137 | fn clone_span(&self, id: &span::Id) -> span::Id { 138 | let index = SpanId::new(id.into_u64()).as_index(); 139 | self.registry.read().unwrap().spans[index] 140 | .refcount 141 | .fetch_add(1, Ordering::SeqCst); 142 | id.clone() 143 | } 144 | fn drop_span(&self, ref id: span::Id) { 145 | let index = SpanId::new(id.into_u64()).as_index(); 146 | let old_count = try_lock!(self.registry.read()).spans[index] 147 | .refcount 148 | .fetch_sub(1, Ordering::SeqCst); 149 | if old_count == 1 { 150 | let mut registry = try_lock!(self.registry.write()); 151 | registry.spans[index].follows.clear(); 152 | 153 | registry.reusable.push(SpanId::new(id.into_u64())); 154 | } 155 | } 156 | } 157 | --------------------------------------------------------------------------------