tracing-tunnel-0.1.0/.cargo_vcs_info.json0000644000000001440000000000100137630ustar { "git": { "sha1": "c52ec241ecdca498f6f355d3f39fb1aab7296a6e" }, "path_in_vcs": "tunnel" }tracing-tunnel-0.1.0/CHANGELOG.md000064400000000000000000000004000072674642500144070ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] ## 0.1.0 - 2022-12-09 The initial release of `tracing-tunnel`. tracing-tunnel-0.1.0/Cargo.toml0000644000000036700000000000100117700ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.60" name = "tracing-tunnel" version = "0.1.0" authors = ["Alex Ostrovski "] description = "Tunnelling tracing information across API boundary" readme = "README.md" keywords = [ "tracing", "logging", "wasm", ] categories = [ "development-tools::debugging", "development-tools::ffi", "wasm", ] license = "MIT OR Apache-2.0" repository = "https://github.com/slowli/tracing-toolbox" resolver = "1" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [[test]] name = "integration" path = "tests/integration/main.rs" required-features = [ "sender", "receiver", ] [dependencies.once_cell] version = "1.16.0" optional = true [dependencies.serde] version = "1" features = [ "alloc", "derive", ] default-features = false [dependencies.tracing-core] version = "0.1.30" default-features = false [dev-dependencies.assert_matches] version = "1.5.0" [dev-dependencies.doc-comment] version = "0.3.3" [dev-dependencies.insta] version = "1.22.0" features = ["yaml"] [dev-dependencies.serde_json] version = "1" [dev-dependencies.tracing] version = "0.1.37" [dev-dependencies.tracing-subscriber] version = "0.3.16" features = [ "registry", "fmt", ] [dev-dependencies.version-sync] version = "0.9.4" [features] default = ["std"] receiver = [ "std", "once_cell", ] sender = [] std = ["tracing-core/std"] [badges.maintenance] status = "experimental" tracing-tunnel-0.1.0/Cargo.toml.orig000064400000000000000000000031040072674642500154710ustar 00000000000000[package] name = "tracing-tunnel" version = "0.1.0" edition = "2021" rust-version = "1.60" authors = ["Alex Ostrovski "] license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["tracing", "logging", "wasm"] categories = ["development-tools::debugging", "development-tools::ffi", "wasm"] description = "Tunnelling tracing information across API boundary" repository = "https://github.com/slowli/tracing-toolbox" [package.metadata.docs.rs] all-features = true # Set `docsrs` to enable unstable `doc(cfg(...))` attributes. rustdoc-args = ["--cfg", "docsrs"] [badges] maintenance = { status = "experimental" } [dependencies] # Public dependencies (present in the public API of the crate). serde = { version = "1", default-features = false, features = ["alloc", "derive"] } tracing-core = { version = "0.1.30", default-features = false } # Private dependencies. once_cell = { version = "1.16.0", optional = true } [dev-dependencies] assert_matches = "1.5.0" doc-comment = "0.3.3" insta = { version = "1.22.0", features = ["yaml"] } serde_json = "1" tracing = "0.1.37" tracing-subscriber = { version = "0.3.16", features = ["registry", "fmt"] } version-sync = "0.9.4" [features] default = ["std"] # Enables std-related functionality. Note that this is required on the `receiver` # end of the tunnel. std = ["tracing-core/std"] # Enables `TracingEventSender`. sender = [] # Enables `TracingEventReceiver` and closely related types. receiver = ["std", "once_cell"] [[test]] name = "integration" path = "tests/integration/main.rs" required-features = ["sender", "receiver"] tracing-tunnel-0.1.0/LICENSE-APACHE000064400000000000000000000251360072674642500145370ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.tracing-tunnel-0.1.0/LICENSE-MIT000064400000000000000000000020650072674642500142430ustar 00000000000000Copyright 2022-current Developers of tracing-toolbox Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tracing-tunnel-0.1.0/README.md000064400000000000000000000103370072674642500140670ustar 00000000000000# Tunnelling Tracing Information Across API Boundary [![Build Status](https://github.com/slowli/tracing-toolbox/workflows/CI/badge.svg?branch=main)](https://github.com/slowli/tracing-toolbox/actions) [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/tracing-toolbox#license) ![rust 1.60+ required](https://img.shields.io/badge/rust-1.60+-blue.svg?label=Required%20Rust) ![no_std tested](https://img.shields.io/badge/no__std-tested-green.svg) **Documentation:** [![crate docs (main)](https://img.shields.io/badge/main-yellow.svg?label=docs)](https://slowli.github.io/tracing-toolbox/tracing_tunnel/) This crate provides [tracing] infrastructure helpers allowing to transfer tracing events across an API boundary: - `TracingEventSender` is a tracing [`Subscriber`] that converts tracing events into (de)serializable presentation that can be sent elsewhere using a customizable hook. - `TracingEventReceiver` consumes events produced by a `TracingEventSender` and relays them to the tracing infrastructure. It is assumed that the source of events may outlive both the lifetime of a particular `TracingEventReceiver` instance, and the lifetime of the program encapsulating the receiver. To deal with this, the receiver provides the means to persist / restore its state. This solves the problem of having *dynamic* call sites for tracing spans / events, i.e., ones not known during compilation. This may occur if call sites are defined in dynamically loaded modules the execution of which is embedded into the program, such as WASM modules. See the crate docs for the details about the crate design and potential use cases. ## Usage Add this to your `Crate.toml`: ```toml [dependencies] tracing-tunnel = "0.1.0" ``` Note that the both pieces of functionality described above are gated behind opt-in features; consult the crate docs for details. ### Sending tracing events ```rust use std::sync::mpsc; use tracing_tunnel::{TracingEvent, TracingEventSender, TracingEventReceiver}; // Let's collect tracing events using an MPSC channel. let (events_sx, events_rx) = mpsc::sync_channel(10); let subscriber = TracingEventSender::new(move |event| { events_sx.send(event).ok(); }); tracing::subscriber::with_default(subscriber, || { tracing::info_span!("test", num = 42_i64).in_scope(|| { tracing::warn!("I feel disturbance in the Force..."); }); }); let events: Vec = events_rx.iter().collect(); println!("{events:?}"); // Do something with events... ``` ### Receiving tracing events ```rust use std::sync::mpsc; use tracing_tunnel::{ LocalSpans, PersistedMetadata, PersistedSpans, TracingEvent, TracingEventReceiver, }; tracing_subscriber::fmt().pretty().init(); fn replay_events(events: &[TracingEvent]) { let mut spans = PersistedSpans::default(); let mut local_spans = LocalSpans::default(); let mut receiver = TracingEventReceiver::default(); for event in events { if let Err(err) = receiver.try_receive(event.clone()) { tracing::warn!(%err, "received invalid tracing event"); } } // Persist the resulting receiver state. There are two pieces // of the state: metadata and alive spans. The spans are further split // into the persisted and local parts. let metadata = receiver.persist_metadata(); let (spans, local_spans) = receiver.persist(); // Store `metadata` and `spans`, e.g., in a DB, and `local_spans` // in a local data struct such as `HashMap` keyed by the executable ID. } ``` ## License Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in `tracing-toolbox` by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. [`tardigrade`]: https://github.com/slowli/tardigrade [tracing]: https://docs.rs/tracing/0.1/tracing [`Subscriber`]: https://docs.rs/tracing-core/0.1/tracing_core/trait.Subscriber.html [The Tardigrade runtime]: https://github.com/slowli/tardigrade tracing-tunnel-0.1.0/src/lib.rs000064400000000000000000000165630072674642500145220ustar 00000000000000//! Tunnelling tracing information across an API boundary. //! //! This crate provides [tracing] infrastructure helpers allowing to transfer tracing events //! across an API boundary: //! //! - [`TracingEventSender`] is a tracing [`Subscriber`] that converts tracing events //! into (de)serializable presentation that can be sent elsewhere using a customizable hook. //! - [`TracingEventReceiver`] consumes events produced by a `TracingEventSender` and relays them //! to the tracing infrastructure. It is assumed that the source of events may outlive //! both the lifetime of a particular `TracingEventReceiver` instance, and the lifetime //! of the program encapsulating the receiver. To deal with this, the receiver provides //! the means to persist / restore its state. //! //! # When is this needed? //! //! This crate solves the problem of having *dynamic* call sites for tracing //! spans / events, i.e., ones not known during compilation. This may occur if call sites //! are defined in dynamically loaded modules, the execution of which is embedded into the program, //! e.g., WASM modules. //! //! It *could* be feasible to treat such a module as a separate program and //! collect / analyze its traces in conjunction with host traces using distributed tracing software //! (e.g., [OpenTelemetry] / [Jaeger]). However, this would significantly bloat the API surface //! of the module, bloat its dependency tree, and would arguably break encapsulation. //! //! The approach proposed in this crate keeps the module API as simple as possible: essentially, //! a single function to smuggle [`TracingEvent`]s through the client–host boundary. //! The client side (i.e., the [`TracingEventSender`]) is almost stateless; //! it just streams tracing events to the host, which can have tracing logic as complex as required. //! //! Another problem that this crate solves is having module executions that can outlive //! the host program. For example, WASM module instances can be fully persisted and resumed later, //! potentially after the host is restarted. To solve this, [`TracingEventReceiver`] allows //! persisting call site data and alive spans, and resuming from the previously saved state //! (notifying the tracing infra about call sites / spans if necessary). //! //! ## Use case: workflow automation //! //! Both components are used by the [Tardigrade][`tardigrade`] workflows, in case of which //! the API boundary is the WASM client–host boundary. //! //! - The [`tardigrade`] client library uses [`TracingEventSender`] to send tracing events //! from a workflow (i.e., a WASM module instance) to the host using a WASM import function. //! - [The Tardigrade runtime] uses [`TracingEventReceiver`] to pass traces from the workflow //! to the host tracing infrastructure. //! //! [tracing]: https://docs.rs/tracing/0.1/tracing //! [`Subscriber`]: tracing_core::Subscriber //! [OpenTelemetry]: https://opentelemetry.io/ //! [Jaeger]: https://www.jaegertracing.io/ //! [`tardigrade`]: https://github.com/slowli/tardigrade //! [The Tardigrade runtime]: https://github.com/slowli/tardigrade //! //! # Crate features //! //! Each of the two major features outlined above is gated by the corresponding opt-in feature, //! [`sender`](#sender) and [`receiver`](#receiver). //! Without these features enabled, the crate only provides data types to capture tracing data. //! //! ## `std` //! //! *(On by default)* //! //! Enables support of types from `std`, such as the `Error` trait. Propagates to [`tracing-core`], //! enabling `Error` support there. //! //! Even if this feature is off, the crate requires the global allocator (i.e., the `alloc` crate) //! and `u32` atomics. //! //! ## `sender` //! //! *(Off by default)* //! //! Provides [`TracingEventSender`]. //! //! ## `receiver` //! //! *(Off by default; requires `std`)* //! //! Provides [`TracingEventReceiver`] and related types. //! //! [`tracing-core`]: https://docs.rs/tracing-core/0.1/tracing_core //! //! # Examples //! //! ## Sending events with `TracingEventSender` //! //! ``` //! # use assert_matches::assert_matches; //! # use std::sync::mpsc; //! use tracing_tunnel::{TracingEvent, TracingEventSender, TracingEventReceiver}; //! //! // Let's collect tracing events using an MPSC channel. //! let (events_sx, events_rx) = mpsc::sync_channel(10); //! let subscriber = TracingEventSender::new(move |event| { //! events_sx.send(event).ok(); //! }); //! //! tracing::subscriber::with_default(subscriber, || { //! tracing::info_span!("test", num = 42_i64).in_scope(|| { //! tracing::warn!("I feel disturbance in the Force..."); //! }); //! }); //! //! let events: Vec<_> = events_rx.iter().collect(); //! assert!(!events.is_empty()); //! // There should be one "new span". //! let span_count = events //! .iter() //! .filter(|event| matches!(event, TracingEvent::NewSpan { .. })) //! .count(); //! assert_eq!(span_count, 1); //! ``` //! //! ## Receiving events from `TracingEventReceiver` //! //! ``` //! # use tracing_tunnel::{ //! # LocalSpans, PersistedMetadata, PersistedSpans, TracingEvent, TracingEventReceiver //! # }; //! tracing_subscriber::fmt().pretty().init(); //! //! let events: Vec = // ... //! # vec![]; //! //! let mut spans = PersistedSpans::default(); //! let mut local_spans = LocalSpans::default(); //! // Replay `events` using the default subscriber. //! let mut receiver = TracingEventReceiver::default(); //! for event in events { //! if let Err(err) = receiver.try_receive(event) { //! tracing::warn!(%err, "received invalid tracing event"); //! } //! } //! // Persist the resulting receiver state. There are two pieces //! // of the state: metadata and alive spans. //! let metadata = receiver.persist_metadata(); //! let (spans, local_spans) = receiver.persist(); //! // `metadata` can be shared among multiple executions of the same executable //! // (e.g., a WASM module). //! // `spans` and `local_spans` are specific to the execution; `spans` should //! // be persisted, while `local_spans` should be stored in RAM. //! ``` #![cfg_attr(not(feature = "std"), no_std)] // Documentation settings. #![cfg_attr(docsrs, feature(doc_cfg))] #![doc(html_root_url = "https://docs.rs/tracing-tunnel/0.1.0")] // Linter settings. #![warn(missing_debug_implementations, missing_docs, bare_trait_objects)] #![warn(clippy::all, clippy::pedantic)] #![allow(clippy::must_use_candidate, clippy::module_name_repetitions)] #[cfg(feature = "receiver")] #[cfg_attr(docsrs, doc(cfg(feature = "receiver")))] mod receiver; #[cfg(feature = "sender")] #[cfg_attr(docsrs, doc(cfg(feature = "sender")))] mod sender; mod types; mod value; mod values; // Polyfill for `alloc` types. mod alloc { #[cfg(not(feature = "std"))] extern crate alloc; #[cfg(feature = "std")] use std as alloc; pub use alloc::{ borrow::{Cow, ToOwned}, format, string::String, vec::{self, Vec}, }; } #[cfg(feature = "receiver")] pub use crate::receiver::{ LocalSpans, PersistedMetadata, PersistedSpans, ReceiveError, TracingEventReceiver, }; #[cfg(feature = "sender")] pub use crate::sender::TracingEventSender; #[cfg(feature = "std")] pub use crate::value::TracedError; pub use crate::{ types::{CallSiteData, CallSiteKind, MetadataId, RawSpanId, TracingEvent, TracingLevel}, value::{DebugObject, FromTracedValue, TracedValue}, values::{TracedValues, TracedValuesIter}, }; #[cfg(doctest)] doc_comment::doctest!("../README.md"); tracing-tunnel-0.1.0/src/receiver/arena.rs000064400000000000000000000142450072674642500166410ustar 00000000000000//! Simple string arena. use once_cell::sync::{Lazy, OnceCell}; use tracing_core::{field::FieldSet, Callsite, Interest, Kind, Level, Metadata}; use std::{ borrow::Cow, collections::{hash_map::DefaultHasher, HashMap, HashSet}, hash::{Hash, Hasher}, ops, sync::RwLock, }; use crate::types::{CallSiteData, CallSiteKind, TracingLevel}; // An emulation of a hash map with keys equivalent to `CallSiteData` (obviously, // we don't want to store `CallSiteData` explicitly because of its size). type MetadataMap = HashMap>>; impl From for Level { fn from(level: TracingLevel) -> Self { match level { TracingLevel::Error => Self::ERROR, TracingLevel::Warn => Self::WARN, TracingLevel::Info => Self::INFO, TracingLevel::Debug => Self::DEBUG, TracingLevel::Trace => Self::TRACE, } } } impl From for Kind { fn from(kind: CallSiteKind) -> Self { match kind { CallSiteKind::Span => Self::SPAN, CallSiteKind::Event => Self::EVENT, } } } #[derive(Debug, Default)] struct DynamicCallSite { metadata: OnceCell<&'static Metadata<'static>>, } impl Callsite for DynamicCallSite { fn set_interest(&self, _interest: Interest) { // Does nothing } fn metadata(&self) -> &Metadata<'_> { self.metadata .get() .copied() .expect("metadata not initialized") } } #[derive(Debug, Default)] pub(crate) struct Arena { strings: RwLock>, metadata: RwLock, } impl Arena { fn leak(s: Cow<'static, str>) -> &'static str { match s { Cow::Borrowed(s) => s, Cow::Owned(string) => Box::leak(string.into_boxed_str()), } } fn new_call_site() -> &'static DynamicCallSite { let call_site = Box::new(DynamicCallSite::default()); Box::leak(call_site) } fn lock_strings(&self) -> impl ops::Deref> + '_ { self.strings.read().unwrap() } fn lock_strings_mut(&self) -> impl ops::DerefMut> + '_ { self.strings.write().unwrap() } fn alloc_string(&self, s: Cow<'static, str>) -> &'static str { if let Some(existing) = self.lock_strings().get(s.as_ref()).copied() { return existing; } let mut lock = self.lock_strings_mut(); if let Some(existing) = lock.get(s.as_ref()).copied() { return existing; } let leaked = Self::leak(s); lock.insert(leaked); leaked } fn leak_fields(&self, fields: Vec>) -> &'static [&'static str] { let fields: Box<[_]> = fields .into_iter() .map(|field| self.alloc_string(field)) .collect(); Box::leak(fields) } fn leak_metadata(&self, data: CallSiteData) -> &'static Metadata<'static> { let call_site = Self::new_call_site(); let call_site_id = tracing_core::identify_callsite!(call_site); let fields = FieldSet::new(self.leak_fields(data.fields), call_site_id); let metadata = Metadata::new( self.alloc_string(data.name), self.alloc_string(data.target), data.level.into(), data.file.map(|file| self.alloc_string(file)), data.line, data.module_path.map(|path| self.alloc_string(path)), fields, data.kind.into(), ); let metadata = Box::leak(Box::new(metadata)) as &_; call_site.metadata.set(metadata).unwrap(); metadata } fn lock_metadata(&self) -> impl ops::Deref + '_ { self.metadata.read().unwrap() } fn lock_metadata_mut(&self) -> impl ops::DerefMut + '_ { self.metadata.write().unwrap() } /// Returns the metadata and a flag whether it was allocated in this call. pub(super) fn alloc_metadata(&self, data: CallSiteData) -> (&'static Metadata<'static>, bool) { let hash_value = Self::hash_metadata(&data); let scanned_bucket_len = { let lock = self.lock_metadata(); if let Some(bucket) = lock.get(&hash_value) { for &metadata in bucket { if Self::eq_metadata(&data, metadata) { return (metadata, false); } } bucket.len() } else { 0 } }; let mut lock = self.lock_metadata_mut(); let bucket = lock.entry(hash_value).or_default(); for &metadata in &bucket[scanned_bucket_len..] { if Self::eq_metadata(&data, metadata) { return (metadata, false); } } // Finally, we need to actually leak metadata. let metadata = self.leak_metadata(data); bucket.push(metadata); (metadata, true) } // The returned hash doesn't necessarily match the hash of `Metadata`, but it is the same // for the equivalent `(kind, data)` tuples, which is what we need. fn hash_metadata(data: &CallSiteData) -> u64 { let mut hasher = DefaultHasher::new(); data.hash(&mut hasher); hasher.finish() } fn eq_metadata(data: &CallSiteData, metadata: &Metadata<'_>) -> bool { // number comparisons go first matches!(data.kind, CallSiteKind::Span) == metadata.is_span() && Level::from(data.level) == *metadata.level() && data.line == metadata.line() // ...then, string comparisons && data.name == metadata.name() && data.target == metadata.target() && data.module_path.as_ref().map(Cow::as_ref) == metadata.module_path() && data.file.as_ref().map(Cow::as_ref) == metadata.file() // ...and finally, comparison of fields && data .fields .iter() .map(Cow::as_ref) .eq(metadata.fields().iter().map(|field| field.name())) } } pub(crate) static ARENA: Lazy = Lazy::new(Arena::default); tracing-tunnel-0.1.0/src/receiver/mod.rs000064400000000000000000000504560072674642500163360ustar 00000000000000//! `TracingEvent` receiver. use serde::{Deserialize, Serialize}; use tracing_core::{ dispatcher::{self, Dispatch}, field::{self, FieldSet, Value, ValueSet}, span::{Attributes, Id, Record}, Event, Field, Metadata, }; use std::{ collections::{HashMap, HashSet}, error, fmt, mem, }; mod arena; #[cfg(test)] mod tests; use self::arena::ARENA; use crate::{CallSiteData, MetadataId, RawSpanId, TracedValue, TracedValues, TracingEvent}; enum CowValue<'a> { Borrowed(&'a dyn Value), Owned(Box), } impl fmt::Debug for CowValue<'_> { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Borrowed(_) => formatter.debug_struct("Borrowed").finish_non_exhaustive(), Self::Owned(_) => formatter.debug_struct("Owned").finish_non_exhaustive(), } } } impl<'a> CowValue<'a> { fn as_ref(&self) -> &(dyn Value + 'a) { match self { Self::Borrowed(value) => value, Self::Owned(boxed) => boxed.as_ref(), } } } impl TracedValue { fn as_value(&self) -> CowValue<'_> { CowValue::Borrowed(match self { Self::Bool(value) => value, Self::Int(value) => value, Self::UInt(value) => value, Self::Float(value) => value, Self::String(value) => value, Self::Object(value) => return CowValue::Owned(Box::new(field::debug(value))), Self::Error(err) => { let err = err as &(dyn error::Error + 'static); return CowValue::Owned(Box::new(err)); } }) } } #[derive(Debug, Clone, Serialize, Deserialize)] struct SpanData { metadata_id: MetadataId, #[serde(default, skip_serializing_if = "Option::is_none")] parent_id: Option, ref_count: usize, values: TracedValues, } /// Information about span / event [`Metadata`] that is [serializable] and thus /// can be persisted across multiple [`TracingEventReceiver`] lifetimes. /// /// `PersistedMetadata` logically corresponds to a program executable (e.g., a WASM module), /// not to its particular execution (e.g., a WASM module instance). /// Multiple executions of the same executable can (and optimally should) /// share `PersistedMetadata`. /// /// [serializable]: https://docs.rs/serde/1/serde #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct PersistedMetadata { inner: HashMap, } impl PersistedMetadata { /// Returns the number of metadata entries. pub fn len(&self) -> usize { self.inner.len() } /// Checks whether this metadata collection is empty (i.e., no metadata was recorded yet). pub fn is_empty(&self) -> bool { self.inner.is_empty() } /// Iterates over contained call site metadata together with the corresponding /// [`MetadataId`]s. pub fn iter(&self) -> impl Iterator + '_ { self.inner.iter().map(|(id, data)| (*id, data)) } /// Merges entries from another `PersistedMetadata` instance. pub fn extend(&mut self, other: Self) { self.inner.extend(other.inner); } } /// Information about alive tracing spans for a particular execution that is (de)serializable and /// can be persisted across multiple [`TracingEventReceiver`] lifetimes. /// /// Unlike [`PersistedMetadata`], `PersistedSpans` are specific to an executable invocation /// (e.g., a WASM module instance). Compared to [`LocalSpans`], `PersistedSpans` have /// the lifetime of the execution and not the host [`Subscriber`]. /// /// [`Subscriber`]: tracing_core::Subscriber #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(transparent)] pub struct PersistedSpans { inner: HashMap, } impl PersistedSpans { /// Returns the number of alive spans. pub fn len(&self) -> usize { self.inner.len() } /// Checks whether this span collection is empty (i.e., no spans were recorded yet). pub fn is_empty(&self) -> bool { self.inner.is_empty() } } /// [`Subscriber`]-specific information about tracing spans for a particular execution /// (e.g., a WASM module instance). /// /// Unlike [`PersistedSpans`], this information is not serializable and lives along with /// the host [`Subscriber`]. It is intended to be placed in something like /// (an initially empty) `HashMap`, where `K` denotes the execution ID. /// /// [`Subscriber`]: tracing_core::Subscriber #[derive(Debug, Default)] pub struct LocalSpans { inner: HashMap, } /// Error processing a [`TracingEvent`] by a [`TracingEventReceiver`]. #[derive(Debug)] #[non_exhaustive] pub enum ReceiveError { /// The event contains a reference to an unknown [`Metadata`] ID. UnknownMetadataId(MetadataId), /// The event contains a reference to an unknown span ID. UnknownSpanId(RawSpanId), /// The event contains too many values. TooManyValues { /// Maximum supported number of values per event. max: usize, /// Actual number of values. actual: usize, }, } impl fmt::Display for ReceiveError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UnknownMetadataId(id) => write!(formatter, "unknown metadata ID: {id}"), Self::UnknownSpanId(id) => write!(formatter, "unknown span ID: {id}"), Self::TooManyValues { max, actual } => write!( formatter, "too many values provided ({actual}), should be no more than {max}" ), } } } impl error::Error for ReceiveError {} macro_rules! create_value_set { ($fields:ident, $values:ident, [$($i:expr,)+]) => { match $values.len() { 0 => $fields.value_set(&[]), $( $i => $fields.value_set(<&[_; $i]>::try_from($values).unwrap()), )+ _ => unreachable!(), } }; } /// Container for non-persisted information specific to a particular traced execution. #[derive(Debug, Default)] struct CurrentExecution { uncommitted_span_ids: HashSet, entered_span_ids: HashSet, } impl CurrentExecution { fn remove_span(&mut self, id: RawSpanId) { self.entered_span_ids.remove(&id); self.uncommitted_span_ids.remove(&id); } fn finalize(&mut self, local_spans: &LocalSpans) { for id in mem::take(&mut self.entered_span_ids) { if let Some(local_id) = local_spans.inner.get(&id) { TracingEventReceiver::dispatch(|dispatch| dispatch.exit(local_id)); } } for id in mem::take(&mut self.uncommitted_span_ids) { if let Some(local_id) = local_spans.inner.get(&id) { TracingEventReceiver::dispatch(|dispatch| dispatch.try_close(local_id.clone())); } } } } /// Receiver of [`TracingEvent`]s produced by [`TracingEventSender`] that relays them /// to the tracing infrastructure. /// /// The receiver takes care of persisting [`Metadata`] / spans that can outlive /// the lifetime of the host program (not just the `TracingEventReceiver` instance!). /// As an example, in [the Tardigrade runtime], a consumer instance is created each time /// a workflow is executed. It relays tracing events from the workflow logic (executed in WASM) /// to the host. /// /// In some cases, the execution tracked by `TracingEventReceiver` may finish abnormally. /// (E.g., a WASM module instance panics while it has the `panic = abort` set /// in the compilation options.) In these cases, all entered /// spans are force-exited when the receiver is dropped. Additionally, spans created /// by the execution are closed on drop as long as they are not [persisted](Self::persist()). /// That is, persistence acts as a commitment of the execution results, while the default /// behavior is rollback. /// /// # ⚠ Resource consumption /// /// To fit the API of the [`tracing-core`] crate, the receiver leaks string parts /// of [`CallSiteData`]: we need a `&'static str` when we only have a `String`. Steps are taken /// to limit the amount of leaked memory; we use a `static` string arena which checks whether /// a particular string was already leaked, and reuses the existing `&'static str` if possible. /// Still, this has negative implications regarding both memory consumption and performance, /// so you probably should limit the number of executables to use with a `TracingEventReceiver`. /// The number of *executions* of each executable is not a limiting factor. /// /// # Examples /// /// See [crate-level docs](index.html) for an example of usage. /// /// [`TracingEventSender`]: crate::TracingEventSender /// [the Tardigrade runtime]: https://github.com/slowli/tardigrade /// [`tracing-core`]: https://docs.rs/tracing-core/ #[derive(Debug, Default)] pub struct TracingEventReceiver { metadata: HashMap>, spans: PersistedSpans, local_spans: LocalSpans, current_execution: CurrentExecution, } impl TracingEventReceiver { /// Maximum supported number of values in a span or event. const MAX_VALUES: usize = 32; /// Restores the receiver from the persisted metadata and tracing spans. /// /// A receiver will work fine if `local_spans` information is lost (e.g., reset to the default /// empty value). However, this is likely to result in span leakage /// in the underlying [`Subscriber`]. On the other hand, mismatch between `metadata` / `spans` /// and the execution producing [`TracingEvent`]s is **bad**; it will most likely result /// in errors returned from [`Self::try_receive()`]. /// /// [`Subscriber`]: tracing_core::Subscriber pub fn new( metadata: PersistedMetadata, spans: PersistedSpans, local_spans: LocalSpans, ) -> Self { let mut this = Self { metadata: HashMap::new(), spans, local_spans, current_execution: CurrentExecution::default(), }; for (id, data) in metadata.inner { this.on_new_call_site(id, data); } this } fn dispatch(dispatch_fn: impl FnOnce(&Dispatch) -> T) -> T { dispatch_fn(&dispatcher::get_default(Dispatch::clone)) } fn metadata(&self, id: MetadataId) -> Result<&'static Metadata<'static>, ReceiveError> { self.metadata .get(&id) .copied() .ok_or(ReceiveError::UnknownMetadataId(id)) } fn span(&self, id: RawSpanId) -> Result<&SpanData, ReceiveError> { self.spans .inner .get(&id) .ok_or(ReceiveError::UnknownSpanId(id)) } fn span_mut(&mut self, id: RawSpanId) -> Result<&mut SpanData, ReceiveError> { self.spans .inner .get_mut(&id) .ok_or(ReceiveError::UnknownSpanId(id)) } /// Returns `Ok(None)` if the local span ID is (validly) not set yet, and `Err(_)` /// if it must have been set by this point. fn map_span_id(&self, remote_id: RawSpanId) -> Result, ReceiveError> { match self.local_spans.inner.get(&remote_id) { Some(local_id) => Ok(Some(local_id)), None => { // Check if the the referenced span is alive. if self.spans.inner.contains_key(&remote_id) { Ok(None) } else { Err(ReceiveError::UnknownSpanId(remote_id)) } } } } fn ensure_values_len(values: &TracedValues) -> Result<(), ReceiveError> { if values.len() > Self::MAX_VALUES { return Err(ReceiveError::TooManyValues { actual: values.len(), max: Self::MAX_VALUES, }); } Ok(()) } fn generate_fields<'a>( metadata: &'static Metadata<'static>, values: &'a TracedValues, ) -> Vec<(Field, CowValue<'a>)> { let fields = metadata.fields(); values .iter() .filter_map(|(field_name, value)| { fields .field(field_name) .map(|field| (field, value.as_value())) }) .collect() } fn expand_fields<'a>( values: &'a [(Field, CowValue<'_>)], ) -> Vec<(&'a Field, Option<&'a dyn Value>)> { values .iter() .map(|(field, value)| (field, Some(value.as_ref()))) .collect() } fn create_values<'a>( fields: &'a FieldSet, values: &'a [(&Field, Option<&dyn Value>)], ) -> ValueSet<'a> { create_value_set!( fields, values, [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, ] ) } fn on_new_call_site(&mut self, id: MetadataId, data: CallSiteData) { let (metadata, is_new) = ARENA.alloc_metadata(data); self.metadata.insert(id, metadata); if is_new { Self::dispatch(|dispatch| dispatch.register_callsite(metadata)); } } fn create_local_span(&self, data: &SpanData) -> Result { let metadata = self.metadata(data.metadata_id)?; let local_parent_id = data .parent_id .map(|parent_id| self.map_span_id(parent_id)) .transpose()? .flatten(); let value_set = Self::generate_fields(metadata, &data.values); let value_set = Self::expand_fields(&value_set); let value_set = Self::create_values(metadata.fields(), &value_set); let attributes = if let Some(local_parent_id) = local_parent_id { Attributes::child_of(local_parent_id.clone(), metadata, &value_set) } else { Attributes::new(metadata, &value_set) }; Ok(Self::dispatch(|dispatch| dispatch.new_span(&attributes))) } /// Tries to consume an event and relays it to the tracing infrastructure. /// /// # Errors /// /// Fails if the event contains a bogus reference to a call site or a span, or if it contains /// too many values. In general, an error can mean that the consumer was restored /// from an incorrect persisted state, or that the event generator is bogus (e.g., /// not a [`TracingEventSender`]). /// /// [`TracingEventSender`]: crate::TracingEventSender #[allow(clippy::missing_panics_doc, clippy::map_entry)] // false positive pub fn try_receive(&mut self, event: TracingEvent) -> Result<(), ReceiveError> { match event { TracingEvent::NewCallSite { id, data } => { self.on_new_call_site(id, data); } TracingEvent::NewSpan { id, parent_id, metadata_id, values, } => { Self::ensure_values_len(&values)?; let data = SpanData { metadata_id, parent_id, ref_count: 1, values, }; if !self.local_spans.inner.contains_key(&id) { let local_id = self.create_local_span(&data)?; self.local_spans.inner.insert(id, local_id); } self.spans.inner.insert(id, data); self.current_execution.uncommitted_span_ids.insert(id); } TracingEvent::FollowsFrom { id, follows_from } => { let local_id = self.map_span_id(id)?; let local_follows_from = self.map_span_id(follows_from)?; // TODO: properly handle remaining cases if let (Some(id), Some(follows_from)) = (local_id, local_follows_from) { Self::dispatch(|dispatch| { dispatch.record_follows_from(id, follows_from); }); } } TracingEvent::SpanEntered { id } => { let local_id = if let Some(id) = self.map_span_id(id)? { id.clone() } else { let data = self.span(id)?; let local_id = self.create_local_span(data)?; self.local_spans.inner.insert(id, local_id.clone()); local_id }; self.current_execution.entered_span_ids.insert(id); Self::dispatch(|dispatch| dispatch.enter(&local_id)); } TracingEvent::SpanExited { id } => { if let Some(local_id) = self.map_span_id(id)? { Self::dispatch(|dispatch| dispatch.exit(local_id)); } self.current_execution.entered_span_ids.remove(&id); } TracingEvent::SpanCloned { id } => { let span = self.span_mut(id)?; span.ref_count += 1; // Dispatcher is intentionally not called: we handle ref counting locally. } TracingEvent::SpanDropped { id } => { let span = self.span_mut(id)?; span.ref_count -= 1; if span.ref_count == 0 { self.spans.inner.remove(&id); self.current_execution.remove_span(id); if let Some(local_id) = self.local_spans.inner.remove(&id) { Self::dispatch(|dispatch| dispatch.try_close(local_id.clone())); } } } TracingEvent::ValuesRecorded { id, values } => { Self::ensure_values_len(&values)?; if let Some(local_id) = self.map_span_id(id)? { let metadata = self.metadata(self.spans.inner[&id].metadata_id)?; let values = Self::generate_fields(metadata, &values); let values = Self::expand_fields(&values); let values = Self::create_values(metadata.fields(), &values); let values = Record::new(&values); Self::dispatch(|dispatch| dispatch.record(local_id, &values)); } let span = self.span_mut(id)?; span.values.extend(values); } TracingEvent::NewEvent { metadata_id, parent, values, } => { Self::ensure_values_len(&values)?; let metadata = self.metadata(metadata_id)?; let values = Self::generate_fields(metadata, &values); let values = Self::expand_fields(&values); let values = Self::create_values(metadata.fields(), &values); let parent = parent.map(|id| self.map_span_id(id)).transpose()?.flatten(); let event = if let Some(parent) = parent { Event::new_child_of(parent.clone(), metadata, &values) } else { Event::new(metadata, &values) }; Self::dispatch(|dispatch| dispatch.event(&event)); } } Ok(()) } /// Consumes an event and relays it to the tracing infrastructure. /// /// # Panics /// /// Panics in the same cases when [`Self::try_receive()`] returns an error. pub fn receive(&mut self, event: TracingEvent) { self.try_receive(event) .expect("received bogus tracing event"); } /// Persists [`Metadata`] produced by the previously consumed events. The returned /// metadata should be merged into the metadata provided to [`Self::new()`]. pub fn persist_metadata(&self) -> PersistedMetadata { let inner = self .metadata .iter() .map(|(&id, &metadata)| (id, CallSiteData::from(metadata))) .collect(); PersistedMetadata { inner } } /// Returns persisted and local spans. pub fn persist(mut self) -> (PersistedSpans, LocalSpans) { self.current_execution.uncommitted_span_ids.clear(); let spans = mem::take(&mut self.spans); let local_spans = mem::take(&mut self.local_spans); self.current_execution.finalize(&local_spans); (spans, local_spans) } } impl Drop for TracingEventReceiver { fn drop(&mut self) { self.current_execution.finalize(&self.local_spans); } } tracing-tunnel-0.1.0/src/receiver/tests.rs000064400000000000000000000143000072674642500167050ustar 00000000000000//! Tests for tracing event receiver. use assert_matches::assert_matches; use std::borrow::Cow; use super::*; use crate::{CallSiteKind, TracingLevel}; const CALL_SITE_DATA: CallSiteData = create_call_site(Vec::new()); const fn create_call_site(fields: Vec>) -> CallSiteData { CallSiteData { kind: CallSiteKind::Span, name: Cow::Borrowed("test"), target: Cow::Borrowed("tracing_tunnel"), level: TracingLevel::Error, module_path: Some(Cow::Borrowed("receiver::tests")), file: Some(Cow::Borrowed("tests")), line: Some(42), fields, } } #[test] fn duplicate_call_site_definitions_are_allowed() { let events = [ TracingEvent::NewCallSite { id: 0, data: CALL_SITE_DATA, }, TracingEvent::NewCallSite { id: 0, data: CALL_SITE_DATA, }, ]; let mut receiver = TracingEventReceiver::default(); for event in events { receiver.receive(event); } let metadata = receiver.persist_metadata(); assert_eq!(metadata.inner.len(), 1); } #[test] fn unknown_metadata_error() { let event = TracingEvent::NewSpan { id: 0, parent_id: None, metadata_id: 0, values: TracedValues::new(), }; let mut receiver = TracingEventReceiver::default(); let err = receiver.try_receive(event).unwrap_err(); assert_matches!(err, ReceiveError::UnknownMetadataId(0)); } #[test] fn unknown_span_errors() { let bogus_events = [ TracingEvent::SpanEntered { id: 1 }, TracingEvent::SpanExited { id: 1 }, TracingEvent::SpanDropped { id: 1 }, TracingEvent::NewSpan { id: 42, parent_id: Some(1), metadata_id: 0, values: TracedValues::new(), }, TracingEvent::NewEvent { metadata_id: 0, parent: Some(1), values: TracedValues::new(), }, TracingEvent::ValuesRecorded { id: 1, values: TracedValues::new(), }, ]; let mut receiver = TracingEventReceiver::default(); receiver.receive(TracingEvent::NewCallSite { id: 0, data: CALL_SITE_DATA, }); for bogus_event in bogus_events { let err = receiver.try_receive(bogus_event).unwrap_err(); assert_matches!(err, ReceiveError::UnknownSpanId(1)); } } #[test] fn spans_with_allowed_value_lengths() { for values_len in 0..=32 { println!("values length: {values_len}"); let mut receiver = TracingEventReceiver::default(); let fields = (0..values_len) .map(|i| Cow::Owned(format!("field{i}"))) .collect(); receiver.receive(TracingEvent::NewCallSite { id: 0, data: create_call_site(fields), }); let values = (0..values_len) .map(|i| (format!("field{i}"), TracedValue::Int(i.into()))) .collect(); receiver.receive(TracingEvent::NewSpan { id: 0, parent_id: None, metadata_id: 0, values, }); receiver.receive(TracingEvent::SpanDropped { id: 0 }); } } #[test] fn too_many_values_error() { let mut receiver = TracingEventReceiver::default(); receiver.receive(TracingEvent::NewCallSite { id: 0, data: CALL_SITE_DATA, }); let values = (0..33) .map(|i| (format!("field{i}"), TracedValue::Int(i.into()))) .collect(); let bogus_event = TracingEvent::NewSpan { id: 0, parent_id: None, metadata_id: 0, values, }; let err = receiver.try_receive(bogus_event).unwrap_err(); assert_matches!( err, ReceiveError::TooManyValues { actual: 33, max: 32 } ); } #[test] fn receiver_does_not_panic_on_bogus_field() { let events = [ TracingEvent::NewCallSite { id: 0, data: CALL_SITE_DATA, }, TracingEvent::NewSpan { id: 0, parent_id: None, metadata_id: 0, values: TracedValues::from_iter([("i".to_owned(), TracedValue::from(42_i64))]), }, ]; let mut receiver = TracingEventReceiver::default(); for event in events { receiver.receive(event); } } #[test] fn restoring_spans() { let metadata = PersistedMetadata { inner: HashMap::from_iter([(0, CALL_SITE_DATA)]), }; let spans = PersistedSpans { inner: HashMap::from_iter([( 1, SpanData { metadata_id: 0, parent_id: None, ref_count: 1, values: TracedValues::new(), }, )]), }; let local_spans = LocalSpans::default(); let mut receiver = TracingEventReceiver::new(metadata, spans, local_spans); visit_and_drop_span(&mut receiver); } fn visit_and_drop_span(receiver: &mut TracingEventReceiver) { receiver.receive(TracingEvent::SpanEntered { id: 1 }); assert!(receiver.local_spans.inner.contains_key(&1)); receiver.receive(TracingEvent::SpanExited { id: 1 }); receiver.receive(TracingEvent::SpanDropped { id: 1 }); assert!(!receiver.spans.inner.contains_key(&1)); assert!(!receiver.local_spans.inner.contains_key(&1)); } #[test] fn restoring_span_after_recording_values() { let call_site = create_call_site(vec!["i".into()]); let metadata = PersistedMetadata { inner: HashMap::from_iter([(0, call_site)]), }; let spans = PersistedSpans { inner: HashMap::from_iter([( 1, SpanData { metadata_id: 0, parent_id: None, ref_count: 1, values: TracedValues::new(), }, )]), }; let local_spans = LocalSpans::default(); let mut receiver = TracingEventReceiver::new(metadata, spans, local_spans); receiver.receive(TracingEvent::ValuesRecorded { id: 1, values: TracedValues::from_iter([("i".to_owned(), TracedValue::from(42_i64))]), }); assert_eq!(receiver.spans.inner[&1].values["i"], 42_i64); assert!(!receiver.local_spans.inner.contains_key(&1)); visit_and_drop_span(&mut receiver); } tracing-tunnel-0.1.0/src/sender.rs000064400000000000000000000075600072674642500152310ustar 00000000000000//! Client-side subscriber. use tracing_core::{ span::{Attributes, Id, Record}, Event, Interest, Metadata, Subscriber, }; use core::sync::atomic::{AtomicU32, Ordering}; use crate::{CallSiteData, MetadataId, RawSpanId, TracedValues, TracingEvent}; impl TracingEvent { fn new_span(span: &Attributes<'_>, metadata_id: MetadataId, id: RawSpanId) -> Self { Self::NewSpan { id, parent_id: span.parent().map(Id::into_u64), metadata_id, values: TracedValues::from_values(span.values()), } } fn values_recorded(id: RawSpanId, values: &Record<'_>) -> Self { Self::ValuesRecorded { id, values: TracedValues::from_record(values), } } fn new_event(event: &Event<'_>, metadata_id: MetadataId) -> Self { Self::NewEvent { metadata_id, parent: event.parent().map(Id::into_u64), values: TracedValues::from_event(event), } } } /// Tracing [`Subscriber`] that converts tracing events into (de)serializable [presentation] /// that can be sent elsewhere using a customizable hook. /// /// As an example, this subscriber is used in the [Tardigrade client library] to send /// workflow traces to the host via a WASM import function. /// /// # Examples /// /// See [crate-level docs](index.html) for an example of usage. /// /// [presentation]: TracingEvent /// [Tardigrade client library]: https://github.com/slowli/tardigrade #[derive(Debug)] pub struct TracingEventSender { next_span_id: AtomicU32, on_event: F, } impl TracingEventSender { /// Creates a subscriber with the specified "on event" hook. pub fn new(on_event: F) -> Self { Self { next_span_id: AtomicU32::new(1), // 0 is invalid span ID on_event, } } fn metadata_id(metadata: &'static Metadata<'static>) -> MetadataId { metadata as *const _ as MetadataId } fn send(&self, event: TracingEvent) { (self.on_event)(event); } } impl Subscriber for TracingEventSender { fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest { let id = Self::metadata_id(metadata); self.send(TracingEvent::NewCallSite { id, data: CallSiteData::from(metadata), }); Interest::always() } fn enabled(&self, _metadata: &Metadata<'_>) -> bool { true } fn new_span(&self, span: &Attributes<'_>) -> Id { let metadata_id = Self::metadata_id(span.metadata()); let span_id = u64::from(self.next_span_id.fetch_add(1, Ordering::SeqCst)); self.send(TracingEvent::new_span(span, metadata_id, span_id)); Id::from_u64(span_id) } fn record(&self, span: &Id, values: &Record<'_>) { self.send(TracingEvent::values_recorded(span.into_u64(), values)); } fn record_follows_from(&self, span: &Id, follows: &Id) { self.send(TracingEvent::FollowsFrom { id: span.into_u64(), follows_from: follows.into_u64(), }); } fn event(&self, event: &Event<'_>) { let metadata_id = Self::metadata_id(event.metadata()); self.send(TracingEvent::new_event(event, metadata_id)); } fn enter(&self, span: &Id) { self.send(TracingEvent::SpanEntered { id: span.into_u64(), }); } fn exit(&self, span: &Id) { self.send(TracingEvent::SpanExited { id: span.into_u64(), }); } fn clone_span(&self, span: &Id) -> Id { self.send(TracingEvent::SpanCloned { id: span.into_u64(), }); span.clone() } fn try_close(&self, span: Id) -> bool { self.send(TracingEvent::SpanDropped { id: span.into_u64(), }); false } } tracing-tunnel-0.1.0/src/types.rs000064400000000000000000000131540072674642500151110ustar 00000000000000//! Types to carry tracing events over the WASM client-host boundary. use serde::{Deserialize, Serialize}; use tracing_core::{Level, Metadata}; use core::hash::Hash; use crate::{ alloc::{Cow, String, Vec}, TracedValues, }; /// ID of a tracing [`Metadata`] record as used in [`TracingEvent`]s. pub type MetadataId = u64; /// ID of a tracing span as used in [`TracingEvent`]s. pub type RawSpanId = u64; /// Tracing level defined in [`CallSiteData`]. /// /// This corresponds to [`Level`] from the `tracing-core` library, but is (de)serializable. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TracingLevel { /// "ERROR" level. Error, /// "WARN" level. Warn, /// "INFO" level. Info, /// "DEBUG" level. Debug, /// "TRACE" level. Trace, } impl From for TracingLevel { fn from(level: Level) -> Self { match level { Level::ERROR => Self::Error, Level::WARN => Self::Warn, Level::INFO => Self::Info, Level::DEBUG => Self::Debug, Level::TRACE => Self::Trace, } } } /// Kind of [`CallSiteData`] location: either a span, or an event. #[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CallSiteKind { /// Call site is a span. Span, /// Call site is an event. Event, } /// Data for a single tracing call site: either a span definition, or an event definition. /// /// This corresponds to [`Metadata`] from the `tracing-core` library, but is (de)serializable. #[derive(Debug, Clone, Hash, Serialize, Deserialize)] pub struct CallSiteData { /// Kind of the call site. pub kind: CallSiteKind, /// Name of the call site. pub name: Cow<'static, str>, /// Tracing target. pub target: Cow<'static, str>, /// Tracing level. pub level: TracingLevel, /// Path to the module where this call site is defined. #[serde(default, skip_serializing_if = "Option::is_none")] pub module_path: Option>, /// Path to the file where this call site is defined. #[serde(default, skip_serializing_if = "Option::is_none")] pub file: Option>, /// Line number for this call site. #[serde(default, skip_serializing_if = "Option::is_none")] pub line: Option, /// Fields defined by this call site. pub fields: Vec>, } impl From<&Metadata<'static>> for CallSiteData { fn from(metadata: &Metadata<'static>) -> Self { let kind = if metadata.is_span() { CallSiteKind::Span } else { debug_assert!(metadata.is_event()); CallSiteKind::Event }; let fields = metadata .fields() .iter() .map(|field| Cow::Borrowed(field.name())); Self { kind, name: Cow::Borrowed(metadata.name()), target: Cow::Borrowed(metadata.target()), level: TracingLevel::from(*metadata.level()), module_path: metadata.module_path().map(Cow::Borrowed), file: metadata.file().map(Cow::Borrowed), line: metadata.line(), fields: fields.collect(), } } } /// Event produced during tracing. /// /// These events are emitted by a [`TracingEventSender`] and then consumed /// by a [`TracingEventReceiver`] to pass tracing info across an API boundary. /// /// [`TracingEventSender`]: crate::TracingEventSender /// [`TracingEventReceiver`]: crate::TracingEventReceiver #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum TracingEvent { /// New call site. NewCallSite { /// Unique ID of the call site that will be used to refer to it in the following events. id: MetadataId, /// Information about the call site. #[serde(flatten)] data: CallSiteData, }, /// New tracing span. NewSpan { /// Unique ID of the span that will be used to refer to it in the following events. id: RawSpanId, /// Parent span ID. `None` means using the contextual parent (i.e., the current span). #[serde(default, skip_serializing_if = "Option::is_none")] parent_id: Option, /// ID of the span metadata. metadata_id: MetadataId, /// Values associated with the span. values: TracedValues, }, /// New "follows from" relation between spans. FollowsFrom { /// ID of the follower span. id: RawSpanId, /// ID of the source span. follows_from: RawSpanId, }, /// Span was entered. SpanEntered { /// ID of the span. id: RawSpanId, }, /// Span was exited. SpanExited { /// ID of the span. id: RawSpanId, }, /// Span was cloned. SpanCloned { /// ID of the span. id: RawSpanId, }, /// Span was dropped (aka closed). SpanDropped { /// ID of the span. id: RawSpanId, }, /// New values recorded for a span. ValuesRecorded { /// ID of the span. id: RawSpanId, /// Recorded values. values: TracedValues, }, /// New event. NewEvent { /// ID of the event metadata. metadata_id: MetadataId, /// Parent span ID. `None` means using the contextual parent (i.e., the current span). #[serde(default, skip_serializing_if = "Option::is_none")] parent: Option, /// Values associated with the event. values: TracedValues, }, } tracing-tunnel-0.1.0/src/value.rs000064400000000000000000000175110072674642500150620ustar 00000000000000//! `TracedValue` and closely related types. use serde::{Deserialize, Serialize}; use core::{borrow::Borrow, fmt}; use crate::alloc::{format, String, ToOwned}; #[cfg(feature = "std")] mod error { use serde::{Deserialize, Serialize}; use std::{error, fmt}; /// (De)serializable presentation for an error recorded as a value in a tracing span or event. #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub struct TracedError { /// Error message produced by its [`Display`](fmt::Display) implementation. pub message: String, /// Error [source](error::Error::source()). pub source: Option>, } impl TracedError { pub(super) fn new(err: &(dyn error::Error + 'static)) -> Self { Self { message: err.to_string(), source: err.source().map(|source| Box::new(Self::new(source))), } } } impl fmt::Display for TracedError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.message) } } impl error::Error for TracedError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { self.source .as_ref() .map(|source| source.as_ref() as &(dyn error::Error + 'static)) } } } #[cfg(feature = "std")] pub use self::error::TracedError; /// Opaque wrapper for a [`Debug`](fmt::Debug)gable object recorded as a value /// in a tracing span or event. #[derive(Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct DebugObject(String); impl fmt::Debug for DebugObject { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.0) } } /// Returns the [`Debug`](fmt::Debug) representation of the object. impl AsRef for DebugObject { fn as_ref(&self) -> &str { &self.0 } } /// Value recorded in a tracing span or event. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum TracedValue { /// Boolean value. Bool(bool), /// Signed integer value. Int(i128), /// Unsigned integer value. UInt(u128), /// Floating-point value. Float(f64), /// String value. String(String), /// Opaque object implementing the [`Debug`](fmt::Debug) trait. Object(DebugObject), /// Opaque error. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] Error(TracedError), } impl TracedValue { #[doc(hidden)] // public for testing purposes pub fn debug(object: &dyn fmt::Debug) -> Self { Self::Object(DebugObject(format!("{object:?}"))) } /// Returns value as a Boolean, or `None` if it's not a Boolean value. #[inline] pub fn as_bool(&self) -> Option { bool::from_value(self) } /// Returns value as a signed integer, or `None` if it's not one. #[inline] pub fn as_int(&self) -> Option { i128::from_value(self) } /// Returns value as an unsigned integer, or `None` if it's not one. #[inline] pub fn as_uint(&self) -> Option { u128::from_value(self) } /// Returns value as a floating-point value, or `None` if it's not one. #[inline] pub fn as_float(&self) -> Option { f64::from_value(self) } /// Returns value as a string, or `None` if it's not one. #[inline] pub fn as_str(&self) -> Option<&str> { str::from_value(self) } /// Checks whether this value is a [`DebugObject`] with the same [`Debug`](fmt::Debug) /// output as the provided `object`. pub fn is_debug(&self, object: &dyn fmt::Debug) -> bool { match self { Self::Object(value) => value.0 == format!("{object:?}"), _ => false, } } /// Returns value as a [`Debug`](fmt::Debug) string output, or `None` if this value /// is not [`Self::Object`]. pub fn as_debug_str(&self) -> Option<&str> { match self { Self::Object(value) => Some(&value.0), _ => None, } } #[cfg(feature = "std")] pub(crate) fn error(err: &(dyn std::error::Error + 'static)) -> Self { Self::Error(TracedError::new(err)) } } /// Fallible conversion from a [`TracedValue`] reference. pub trait FromTracedValue<'a> { /// Output of the conversion. type Output: Borrow + 'a; /// Performs the conversion. fn from_value(value: &'a TracedValue) -> Option; } impl<'a> FromTracedValue<'a> for str { type Output = &'a str; fn from_value(value: &'a TracedValue) -> Option { match value { TracedValue::String(value) => Some(value), _ => None, } } } macro_rules! impl_value_conversions { (TracedValue :: $variant:ident ($source:ty)) => { impl From<$source> for TracedValue { fn from(value: $source) -> Self { Self::$variant(value) } } impl PartialEq<$source> for TracedValue { fn eq(&self, other: &$source) -> bool { match self { Self::$variant(value) => value == other, _ => false, } } } impl PartialEq for $source { fn eq(&self, other: &TracedValue) -> bool { other == self } } impl FromTracedValue<'_> for $source { type Output = Self; fn from_value(value: &TracedValue) -> Option { match value { TracedValue::$variant(value) => Some(*value), _ => None, } } } }; (TracedValue :: $variant:ident ($source:ty as $field_ty:ty)) => { impl From<$source> for TracedValue { fn from(value: $source) -> Self { Self::$variant(value.into()) } } impl PartialEq<$source> for TracedValue { fn eq(&self, other: &$source) -> bool { match self { Self::$variant(value) => *value == <$field_ty>::from(*other), _ => false, } } } impl PartialEq for $source { fn eq(&self, other: &TracedValue) -> bool { other == self } } impl FromTracedValue<'_> for $source { type Output = Self; fn from_value(value: &TracedValue) -> Option { match value { TracedValue::$variant(value) => (*value).try_into().ok(), _ => None, } } } }; } impl_value_conversions!(TracedValue::Bool(bool)); impl_value_conversions!(TracedValue::Int(i128)); impl_value_conversions!(TracedValue::Int(i64 as i128)); impl_value_conversions!(TracedValue::UInt(u128)); impl_value_conversions!(TracedValue::UInt(u64 as u128)); impl_value_conversions!(TracedValue::Float(f64)); impl PartialEq for TracedValue { fn eq(&self, other: &str) -> bool { match self { Self::String(value) => value == other, _ => false, } } } impl PartialEq for str { fn eq(&self, other: &TracedValue) -> bool { other == self } } impl From<&str> for TracedValue { fn from(value: &str) -> Self { Self::String(value.to_owned()) } } impl PartialEq<&str> for TracedValue { fn eq(&self, other: &&str) -> bool { match self { Self::String(value) => value == *other, _ => false, } } } impl PartialEq for &str { fn eq(&self, other: &TracedValue) -> bool { other == self } } tracing-tunnel-0.1.0/src/values.rs000064400000000000000000000214560072674642500152500ustar 00000000000000//! `TracedValues` and closely related types. use serde::{ de::{MapAccess, Visitor}, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer, }; use tracing_core::{ field::{Field, ValueSet, Visit}, span::Record, Event, }; use core::{fmt, mem, ops, slice}; use crate::{ alloc::{vec, String, Vec}, TracedValue, }; /// Collection of named [`TracedValue`]s. /// /// Functionally this collection is similar to a `HashMap`, /// with the key difference being that the order of [iteration](Self::iter()) is the insertion order. /// If a value is updated, including via [`Extend`] etc., it preserves its old placement. #[derive(Clone)] pub struct TracedValues { // Using `Vec` for entries is inefficient for random access, but seems acceptable given that // valid value sets have no more than 32 values. We need this (vs using `linked_hash_map`) // for no-std compatibility. inner: Vec<(S, TracedValue)>, } impl Default for TracedValues { fn default() -> Self { Self { inner: Vec::new() } } } impl> fmt::Debug for TracedValues { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let mut map = formatter.debug_map(); for (name, value) in &self.inner { map.entry(&name.as_ref(), value); } map.finish() } } impl + AsRef> TracedValues { /// Creates traced values from the specified value set. pub fn from_values(values: &ValueSet<'_>) -> Self { let mut visitor = TracedValueVisitor { values: Self::default(), }; values.record(&mut visitor); visitor.values } /// Creates traced values from the specified record. pub fn from_record(values: &Record<'_>) -> Self { let mut visitor = TracedValueVisitor { values: Self::default(), }; values.record(&mut visitor); visitor.values } /// Creates traced values from the values in the specified event. pub fn from_event(event: &Event<'_>) -> Self { let mut visitor = TracedValueVisitor { values: Self::default(), }; event.record(&mut visitor); visitor.values } } impl> TracedValues { /// Creates new empty values. pub fn new() -> Self { Self::default() } /// Returns the number of stored values. pub fn len(&self) -> usize { self.inner.len() } /// Checks whether this collection of values is empty. pub fn is_empty(&self) -> bool { self.inner.is_empty() } /// Returns the value with the specified name, or `None` if it not set. pub fn get(&self, name: &str) -> Option<&TracedValue> { self.inner.iter().find_map(|(existing_name, value)| { if existing_name.as_ref() == name { Some(value) } else { None } }) } /// Iterates over the contained name-value pairs. pub fn iter(&self) -> TracedValuesIter<'_, S> { TracedValuesIter { inner: self.inner.iter(), } } /// Inserts a value with the specified name. If a value with the same name was present /// previously, it is overwritten. Returns the previous value with the specified name, /// if any. pub fn insert(&mut self, name: S, value: TracedValue) -> Option { let position = self .inner .iter() .position(|(existing_name, _)| existing_name.as_ref() == name.as_ref()); if let Some(position) = position { let place = &mut self.inner[position].1; Some(mem::replace(place, value)) } else { self.inner.push((name, value)); None } } } impl> ops::Index<&str> for TracedValues { type Output = TracedValue; fn index(&self, index: &str) -> &Self::Output { self.get(index) .unwrap_or_else(|| panic!("value `{index}` is not defined")) } } impl> FromIterator<(S, TracedValue)> for TracedValues { fn from_iter>(iter: I) -> Self { let iter = iter.into_iter(); let mut this = Self::new(); this.extend(iter); this } } impl> Extend<(S, TracedValue)> for TracedValues { fn extend>(&mut self, iter: I) { let iter = iter.into_iter(); self.inner.reserve(iter.size_hint().0); for (name, value) in iter { self.insert(name, value); } } } impl IntoIterator for TracedValues { type Item = (S, TracedValue); type IntoIter = vec::IntoIter<(S, TracedValue)>; fn into_iter(self) -> Self::IntoIter { self.inner.into_iter() } } /// Iterator over name-value references returned from [`TracedValues::iter()`]. #[derive(Debug)] pub struct TracedValuesIter<'a, S> { inner: slice::Iter<'a, (S, TracedValue)>, } impl<'a, S: AsRef> Iterator for TracedValuesIter<'a, S> { type Item = (&'a str, &'a TracedValue); fn next(&mut self) -> Option { self.inner .next() .map(|(name, value)| (name.as_ref(), value)) } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } impl<'a, S: AsRef> DoubleEndedIterator for TracedValuesIter<'a, S> { fn next_back(&mut self) -> Option { self.inner .next_back() .map(|(name, value)| (name.as_ref(), value)) } } impl<'a, S: AsRef> ExactSizeIterator for TracedValuesIter<'a, S> { fn len(&self) -> usize { self.inner.len() } } impl<'a, S: AsRef> IntoIterator for &'a TracedValues { type Item = (&'a str, &'a TracedValue); type IntoIter = TracedValuesIter<'a, S>; fn into_iter(self) -> Self::IntoIter { self.iter() } } impl> Serialize for TracedValues { fn serialize(&self, serializer: Ser) -> Result { let mut map = serializer.serialize_map(Some(self.len()))?; for (name, value) in self { map.serialize_entry(name, value)?; } map.end() } } impl<'de> Deserialize<'de> for TracedValues { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct MapVisitor; impl<'v> Visitor<'v> for MapVisitor { type Value = TracedValues; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("map of name-value entries") } fn visit_map>(self, mut map: A) -> Result { let mut values = TracedValues { inner: Vec::with_capacity(map.size_hint().unwrap_or(0)), }; while let Some((name, value)) = map.next_entry()? { values.insert(name, value); } Ok(values) } } deserializer.deserialize_map(MapVisitor) } } struct TracedValueVisitor { values: TracedValues, } impl> fmt::Debug for TracedValueVisitor { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("ValueVisitor") .field("values", &self.values) .finish() } } impl + AsRef> Visit for TracedValueVisitor { fn record_f64(&mut self, field: &Field, value: f64) { self.values.insert(field.name().into(), value.into()); } fn record_i64(&mut self, field: &Field, value: i64) { self.values.insert(field.name().into(), value.into()); } fn record_u64(&mut self, field: &Field, value: u64) { self.values.insert(field.name().into(), value.into()); } fn record_i128(&mut self, field: &Field, value: i128) { self.values.insert(field.name().into(), value.into()); } fn record_u128(&mut self, field: &Field, value: u128) { self.values.insert(field.name().into(), value.into()); } fn record_bool(&mut self, field: &Field, value: bool) { self.values.insert(field.name().into(), value.into()); } fn record_str(&mut self, field: &Field, value: &str) { self.values.insert(field.name().into(), value.into()); } #[cfg(feature = "std")] fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) { self.values .insert(field.name().into(), TracedValue::error(value)); } fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { self.values .insert(field.name().into(), TracedValue::debug(value)); } } tracing-tunnel-0.1.0/tests/integration/fib.rs000064400000000000000000000033030072674642500173760ustar 00000000000000use tracing::field; use std::{error, fmt, sync::mpsc}; use tracing_tunnel::{TracingEvent, TracingEventSender}; #[derive(Debug)] struct Overflow; impl fmt::Display for Overflow { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "integer overflow") } } impl error::Error for Overflow {} #[tracing::instrument(target = "fib", ret, err)] fn compute(count: usize) -> Result { let (mut x, mut y) = (0_u64, 1_u64); for i in 0..count { tracing::debug!(target: "fib", i, current = x, "performing iteration"); (x, y) = (y, x.checked_add(y).ok_or(Overflow)?); } Ok(x) } const PHI: f64 = 1.618033988749895; // (1 + sqrt(5)) / 2 pub fn fib(count: usize) { let span = tracing::info_span!("fib", approx = field::Empty); let _entered = span.enter(); let approx = PHI.powi(count as i32) / 5.0_f64.sqrt(); let approx = approx.round(); span.record("approx", approx); tracing::warn!(count, "count looks somewhat large"); match compute(count) { Ok(result) => { tracing::info!(result, "computed Fibonacci number"); } Err(err) => { tracing::error!(error = &err as &dyn error::Error, "computation failed"); } } } pub fn record_events(count: usize) -> Vec { let (events_sx, events_rx) = mpsc::sync_channel(256); // ^ The channel capacity should allow for *all* events since we start collecting events // after they all are emitted. let sender = TracingEventSender::new(move |event| { events_sx.send(event).unwrap(); }); tracing::subscriber::with_default(sender, || fib(count)); events_rx.iter().collect() } tracing-tunnel-0.1.0/tests/integration/main.rs000064400000000000000000000255370072674642500175770ustar 00000000000000//! Integration tests for Tardigrade tracing infrastructure. use assert_matches::assert_matches; use once_cell::sync::Lazy; use tracing_core::{Level, Subscriber}; use tracing_subscriber::{registry::LookupSpan, FmtSubscriber}; use std::{ borrow::Cow, collections::{HashMap, HashSet}, iter, thread, }; mod fib; use tracing_tunnel::{ CallSiteKind, LocalSpans, PersistedMetadata, PersistedSpans, TracedValue, TracingEvent, TracingEventReceiver, TracingLevel, }; #[derive(Debug)] struct RecordedEvents { short: Vec, long: Vec, } // **NB.** Tests calling the `fib` module should block on `EVENTS`; otherwise, // the snapshot tests may fail because of the differing ordering of `NewCallSite` events. static EVENTS: Lazy = Lazy::new(|| RecordedEvents { short: fib::record_events(5), long: fib::record_events(80), }); #[cfg(unix)] // The snapshot contains OS-specific path delimiters #[test] fn event_snapshot() { use tracing_tunnel::MetadataId; let mut events = EVENTS.short.clone(); let mut metadata_id_mapping = HashMap::new(); for event in &mut events { match event { TracingEvent::NewCallSite { id, data } => { // Replace metadata ID to be predictable. let new_metadata_id = metadata_id_mapping.len() as MetadataId; metadata_id_mapping.insert(*id, new_metadata_id); *id = new_metadata_id; // Make event data not depend on specific lines, which could easily // change due to refactoring etc. data.line = Some(42); if matches!(data.kind, CallSiteKind::Event) { data.name = Cow::Borrowed("event"); } } TracingEvent::NewSpan { metadata_id, .. } | TracingEvent::NewEvent { metadata_id, .. } => { *metadata_id = metadata_id_mapping[metadata_id]; } _ => { /* No changes */ } } } insta::assert_yaml_snapshot!("events-fib-5", events); } #[test] fn resource_management_for_tracing_events() { assert_span_management(&EVENTS.long); } fn assert_span_management(events: &[TracingEvent]) { let mut alive_spans = HashSet::new(); let mut open_spans = vec![]; for event in events { match event { TracingEvent::NewSpan { id, .. } => { assert!(alive_spans.insert(*id)); } TracingEvent::SpanCloned { .. } => unreachable!(), TracingEvent::SpanDropped { id } => { assert!(!open_spans.contains(id)); assert!(alive_spans.remove(id)); } TracingEvent::SpanEntered { id } => { assert!(alive_spans.contains(id)); assert!(!open_spans.contains(id)); open_spans.push(*id); } TracingEvent::SpanExited { id } => { assert!(alive_spans.contains(id)); let popped_span = open_spans.pop(); assert_eq!(popped_span, Some(*id)); } _ => { /* Do nothing */ } } } assert!(alive_spans.is_empty()); assert!(open_spans.is_empty()); } #[test] fn call_sites_for_tracing_events() { let events = &EVENTS.long; let fields_by_span = events.iter().filter_map(|event| { if let TracingEvent::NewCallSite { data, .. } = event { if matches!(data.kind, CallSiteKind::Span) { let fields: Vec<_> = data.fields.iter().map(Cow::as_ref).collect(); return Some((data.name.as_ref(), fields)); } } None }); let fields_by_span: HashMap<_, _> = fields_by_span.collect(); assert_eq!(fields_by_span.len(), 2); assert_eq!(fields_by_span["fib"], ["approx"]); assert_eq!(fields_by_span["compute"], ["count"]); let mut known_metadata_ids = HashSet::new(); let event_call_sites: Vec<_> = events .iter() .filter_map(|event| { if let TracingEvent::NewCallSite { id, data } = event { assert!(known_metadata_ids.insert(*id)); if matches!(data.kind, CallSiteKind::Event) { return Some(data); } } None }) .collect(); let targets: HashSet<_> = event_call_sites .iter() .map(|site| site.target.as_ref()) .collect(); assert_eq!(targets, HashSet::from_iter(["fib", "integration::fib"])); let mut call_sites_by_level = HashMap::<_, usize>::new(); for site in &event_call_sites { *call_sites_by_level.entry(site.level).or_default() += 1; } assert_eq!(call_sites_by_level[&TracingLevel::Warn], 1); assert_eq!(call_sites_by_level[&TracingLevel::Info], 2); assert_eq!(call_sites_by_level[&TracingLevel::Debug], 1); } #[test] fn event_fields_have_same_order() { let events = &EVENTS.long; let debug_metadata_id = events.iter().find_map(|event| { if let TracingEvent::NewCallSite { id, data } = event { if matches!(data.kind, CallSiteKind::Event) && data.level == TracingLevel::Debug { return Some(*id); } } None }); let debug_metadata_id = debug_metadata_id.unwrap(); let debug_fields = events.iter().filter_map(|event| { if let TracingEvent::NewEvent { metadata_id, values, .. } = event { if *metadata_id == debug_metadata_id { return Some(values); } } None }); for fields in debug_fields { let fields: Vec<_> = fields.iter().collect(); assert_matches!( fields.as_slice(), [ ("message", TracedValue::Object(_)), ("i", TracedValue::UInt(_)), ("current", TracedValue::UInt(_)), ] ); } } fn create_fmt_subscriber() -> impl Subscriber + for<'a> LookupSpan<'a> { FmtSubscriber::builder() .pretty() .with_max_level(Level::TRACE) .with_test_writer() .finish() } /// This test are mostly about the "expected" output of `FmtSubscriber`. /// Their output should be reviewed manually. #[test] fn reproducing_events_on_fmt_subscriber() { let events = &EVENTS.long; let mut consumer = TracingEventReceiver::default(); tracing::subscriber::with_default(create_fmt_subscriber(), || { for event in events { consumer.receive(event.clone()); } }); } #[test] fn persisting_metadata() { let events = &EVENTS.short; let mut receiver = TracingEventReceiver::default(); tracing::subscriber::with_default(create_fmt_subscriber(), || { for event in events { receiver.receive(event.clone()); } }); let metadata = receiver.persist_metadata(); let (spans, local_spans) = receiver.persist(); let names: HashSet<_> = metadata .iter() .map(|(_, data)| data.name.as_ref()) .collect(); assert!(names.contains("fib"), "{names:?}"); assert!(names.contains("compute"), "{names:?}"); // Check that `receiver` can function after restoring `persisted` meta. let mut receiver = TracingEventReceiver::new(metadata, spans, local_spans); tracing::subscriber::with_default(create_fmt_subscriber(), || { for event in events { if !matches!(event, TracingEvent::NewCallSite { .. }) { receiver.receive(event.clone()); } } }); } fn test_persisting_spans(reset_local_spans: bool) { let events = &EVENTS.short; let split_positions = events.iter().enumerate().filter_map(|(i, event)| { if matches!( event, TracingEvent::NewSpan { .. } | TracingEvent::SpanExited { .. } ) { Some(i + 1) } else { None } }); let split_positions: Vec<_> = iter::once(0) .chain(split_positions) .chain([events.len()]) .collect(); let event_chunks = split_positions.windows(2).map(|window| match window { [prev, next] => &events[*prev..*next], _ => unreachable!(), }); let mut metadata = PersistedMetadata::default(); let mut spans = PersistedSpans::default(); let mut local_spans = LocalSpans::default(); tracing::subscriber::with_default(create_fmt_subscriber(), || { for events in event_chunks { if reset_local_spans { local_spans = LocalSpans::default(); } let mut receiver = TracingEventReceiver::new(metadata.clone(), spans, local_spans); for event in events { receiver.receive(event.clone()); } metadata = receiver.persist_metadata(); (spans, local_spans) = receiver.persist(); } }); } #[test] fn persisting_spans() { test_persisting_spans(false); } #[test] fn persisting_spans_with_reset_local_spans() { test_persisting_spans(true); } #[test] #[allow(clippy::needless_collect)] // necessary for threads to be concurrent fn concurrent_senders() { Lazy::force(&EVENTS); let threads: Vec<_> = (5..10) .map(|i| thread::spawn(move || fib::record_events(i))) .collect(); let events_by_thread = threads.into_iter().map(|handle| handle.join().unwrap()); for (idx, events) in events_by_thread.enumerate() { assert_valid_refs(&events); assert_span_management(&events); let idx = idx + 5; let new_events: Vec<_> = events .iter() .filter_map(|event| { if let TracingEvent::NewEvent { values, .. } = event { return values.get("message").and_then(TracedValue::as_debug_str); } None }) .collect(); assert_eq!(new_events.len(), idx + 2, "{new_events:?}"); assert_eq!(new_events[0], "count looks somewhat large"); assert_eq!(new_events[idx + 1], "computed Fibonacci number"); for &new_event in &new_events[1..=idx] { assert_eq!(new_event, "performing iteration"); } } } fn assert_valid_refs(events: &[TracingEvent]) { let mut call_site_ids = HashSet::new(); let mut span_ids = HashSet::new(); for event in events { match event { TracingEvent::NewCallSite { id, .. } => { call_site_ids.insert(*id); // IDs may duplicate provided they reference the same call site. } TracingEvent::NewSpan { id, metadata_id, .. } => { assert!(span_ids.insert(*id)); assert!(call_site_ids.contains(metadata_id)); } TracingEvent::NewEvent { metadata_id, .. } => { assert!(call_site_ids.contains(metadata_id)); } _ => { /* do nothing */ } } } } tracing-tunnel-0.1.0/tests/integration/snapshots/integration__events-fib-5.snap000064400000000000000000000055500072674642500261510ustar 00000000000000--- source: tunnel/tests/integration/main.rs assertion_line: 45 expression: events --- - new_call_site: id: 0 kind: span name: fib target: "integration::fib" level: info module_path: "integration::fib" file: tunnel/tests/integration/fib.rs line: 42 fields: - approx - new_span: id: 1 metadata_id: 0 values: {} - span_entered: id: 1 - values_recorded: id: 1 values: approx: float: 5 - new_call_site: id: 1 kind: event name: event target: "integration::fib" level: warn module_path: "integration::fib" file: tunnel/tests/integration/fib.rs line: 42 fields: - message - count - new_event: metadata_id: 1 values: message: object: count looks somewhat large count: u_int: 5 - new_call_site: id: 2 kind: span name: compute target: fib level: info module_path: "integration::fib" file: tunnel/tests/integration/fib.rs line: 42 fields: - count - new_span: id: 2 metadata_id: 2 values: count: u_int: 5 - span_entered: id: 2 - new_call_site: id: 3 kind: event name: event target: fib level: debug module_path: "integration::fib" file: tunnel/tests/integration/fib.rs line: 42 fields: - message - i - current - new_event: metadata_id: 3 values: message: object: performing iteration i: u_int: 0 current: u_int: 0 - new_event: metadata_id: 3 values: message: object: performing iteration i: u_int: 1 current: u_int: 1 - new_event: metadata_id: 3 values: message: object: performing iteration i: u_int: 2 current: u_int: 1 - new_event: metadata_id: 3 values: message: object: performing iteration i: u_int: 3 current: u_int: 2 - new_event: metadata_id: 3 values: message: object: performing iteration i: u_int: 4 current: u_int: 3 - new_call_site: id: 4 kind: event name: event target: fib level: info module_path: "integration::fib" file: tunnel/tests/integration/fib.rs line: 42 fields: - return - new_event: metadata_id: 4 values: return: object: "5" - span_exited: id: 2 - span_dropped: id: 2 - new_call_site: id: 5 kind: event name: event target: "integration::fib" level: info module_path: "integration::fib" file: tunnel/tests/integration/fib.rs line: 42 fields: - message - result - new_event: metadata_id: 5 values: message: object: computed Fibonacci number result: u_int: 5 - span_exited: id: 1 - span_dropped: id: 1 tracing-tunnel-0.1.0/tests/version_match.rs000064400000000000000000000004000072674642500171470ustar 00000000000000use version_sync::{assert_html_root_url_updated, assert_markdown_deps_updated}; #[test] fn readme_is_in_sync() { assert_markdown_deps_updated!("README.md"); } #[test] fn html_root_url_is_in_sync() { assert_html_root_url_updated!("src/lib.rs"); }