Skip to main content

rustlavel_telescope/
lib.rs

1//! rustlavel-telescope: the debugging dashboard, enabled with one line.
2//!
3//! ```ignore
4//! App::new()?
5//!     .routes(routes::web::routes)
6//!     .plugin(Telescope::default())
7//!     .serve()
8//!     .await
9//! ```
10//!
11//! Telescope listens on the instrumentation bus core has carried since the
12//! first release, keeps what it hears in a bounded in-memory ring buffer, and
13//! serves a single self-contained page at `/telescope` that shows requests, the
14//! queries and log lines each one produced, and how long everything took.
15//!
16//! # Why a ring buffer and not SQLite
17//!
18//! Laravel Telescope writes every entry to a database. That is the right answer
19//! in PHP, where the process dies at the end of the request and there is
20//! nowhere else to put anything. Rust has a process that lives for weeks, so
21//! this crate makes two promises instead:
22//!
23//! **A debugging tool must never be the reason a request is slow.** Recording
24//! an entry costs a redaction pass, a short mutex and a push onto a
25//! `VecDeque` — no connection, no transaction, no `fsync`, no `await` on the
26//! request's path. Optional persistence is a send into a queue that a writer
27//! thread drains; the request never waits for a disk. The buffer is bounded, so
28//! a server under load a week from now uses exactly the memory it uses today.
29//!
30//! **A debugging tool must never add a dependency to production builds.** The
31//! whole crate depends on `rustlavel-core` and `rustlavel-http` and nothing
32//! else — no embedded database, no serialisation framework, not even an async
33//! runtime of its own. Persistence is JSON lines written with the JSON
34//! implementation core already has: readable with `tail -f`, recoverable after
35//! a `kill -9` at the cost of at most the last line, and compacted at boot so
36//! the file can never outgrow the buffer it feeds.
37//!
38//! # What it records
39//!
40//! Whatever is dispatched. The framework emits `http.request`, `db.query` and
41//! `log` today, and the roadmap promises `ai.call`, `queue.job` and
42//! `mail.sent`; an [`Entry`] therefore stores an open field map rather than a
43//! fixed enum, and every renderer degrades gracefully — a kind Telescope has
44//! never seen still gets a readable summary, a stable colour, a duration and a
45//! full field table.
46//!
47//! # Safety
48//!
49//! The dashboard shows SQL, log messages, and every field a package chose to
50//! emit. So it refuses to mount in production unless someone explicitly says
51//! otherwise (`telescope.enabled`, or [`Telescope::even_in_production`]), and
52//! values under keys that look like credentials are replaced at record time —
53//! before they reach the buffer, the API, or the journal on disk.
54
55pub mod dashboard;
56pub mod entry;
57pub mod journal;
58pub mod plugin;
59pub mod recorder;
60pub mod redact;
61pub mod routes;
62pub mod store;
63
64pub use entry::Entry;
65pub use journal::Journal;
66pub use plugin::Telescope;
67pub use recorder::Recorder;
68pub use store::{Filter, Store};
69
70/// The event bus is process-global and tests run concurrently, so any test that
71/// subscribes has to take this lock first — the same pattern the HTTP crate's
72/// error page tests use for the process-wide debug flag.
73#[cfg(test)]
74pub(crate) mod test_support {
75    use tokio::sync::{Mutex, MutexGuard};
76
77    /// A tokio mutex rather than a std one, because the async tests hold it
78    /// across an `.await` — which a std guard must never do.
79    static BUS: Mutex<()> = Mutex::const_new(());
80
81    /// For a synchronous test. Panics inside a runtime, which is exactly where
82    /// it should not be used.
83    pub fn exclusive() -> MutexGuard<'static, ()> {
84        BUS.blocking_lock()
85    }
86
87    /// For an async test.
88    ///
89    /// Any test that reaches the event bus needs this — including one that only
90    /// makes a request through `TestClient`, because the router dispatches an
91    /// `http.request` event for every request it handles.
92    pub async fn exclusive_async() -> MutexGuard<'static, ()> {
93        BUS.lock().await
94    }
95}