# ray-dbg
Rust client library for the [Ray](https://myray.app) debugging app.
> [!Warning]
> with default features (`debug-macros`), all Ray macros are disabled in
non-debug builds. In release builds they return a disabled handle and send
nothing. If you want Ray output in release, disable the `debug-macros` feature
or construct `Ray` manually with an enabled `RayConfig`.
## Quickstart
Add the crate (crates.io, path for this workspace, or git):
```toml
[dependencies]
ray = { version = "0.1", package = "ray-dbg" }
# or
ray = { path = "crates/ray", package = "ray-dbg" }
# or
ray = { git = "https://github.com/bnomei/ray-dbg", package = "ray-dbg" }
```
The published package name is `ray-dbg`, but the crate remains `ray`, so you can
`use ray::...` as usual.
Send a message (fluent chaining is the intended style):
```rust
use ray::ray;
fn main() {
ray!("Hello from Rust").green().label("init");
}
```
## Learning path (Rust)
This sequence mirrors the Ray PHP usage guide and keeps the learning curve gentle.
Start at step 1 and stop when you've reached what you need.
`ray!()` returns a `Ray` handle. `ray!(value, ...)` logs and returns the same handle so
you can keep chaining. For Debug-only types, use `ray!().raw(&value)`.
### 1. Send values to Ray
```rust
use ray::ray;
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u64,
name: &'static str,
}
fn main() {
ray!("a string");
ray!(vec!["an array"]);
ray!(&User { id: 1, name: "Ada" });
}
```
### 2. Multiple arguments
```rust
use ray::ray;
fn main() {
ray!("first", "second", "third");
}
```
### 3. See the caller of a function
```rust
use ray::ray;
fn main() {
ray!().caller();
ray!().trace();
}
```
Enable the `origin-backtrace` feature to capture full stacks; otherwise Ray gets the
callsite frame.
### 4. Pausing execution (stub)
`pause()` is not supported in ray-dbg for parity with PHP Ray; it does not block, wait
for input, or throw.
```rust
use ray::ray;
fn main() {
ray!().pause();
}
```
### 5. Halting the process
Use `die()` to terminate the process. `rd!` is the macro alias for
`ray!(...).die()`.
```rust
use ray::{ray, rd};
fn main() {
ray!("fatal").die();
rd!("fatal");
}
```
`die`/`rd!` only terminate the process when Ray is enabled (e.g. debug builds with
default macros).
### 6. Counting execution times
`count()` increments a counter for the current callsite and updates a single Ray
entry each time. Use `count_named()` to reuse the same counter across callsites,
and `counter_value()` to read it back (named counters only).
```rust
use ray::ray;
fn main() {
for _ in 0..3 {
ray!().count();
}
for _ in 0..2 {
ray!().count_named("first");
}
ray!().count_named("second");
if ray!().counter_value("first") == 2 {
ray!("counter value is two!");
}
}
```
Counters are process-global (the name or callsite fingerprint is the key), so
pick unique names when you want isolated counters.
### 7. Measuring performance and memory usage
```rust
use ray::ray;
use std::thread;
use std::time::Duration;
fn main() {
ray!().measure();
thread::sleep(Duration::from_millis(200));
ray!().measure();
ray!().measure_named("db");
thread::sleep(Duration::from_millis(50));
ray!().measure_named("db");
}
```
Memory usage is best-effort by platform and may be reported as `0` when unavailable.
The Rust API does not accept a callable for `measure`; call it before/after the
work you want to time.
### 8. Display the class name of an object
```rust
use ray::ray;
fn main() {
let value = vec![1, 2, 3];
ray!().class_name(&value);
}
```
### 9. Displaying private properties and methods (not supported)
`invade()` is not implemented in ray-dbg and is not planned (Rust does not expose
private access). See the PHP vanilla compatibility section for parity notes.
### 10. Updating displayed items
```rust
use ray::ray;
use std::thread;
use std::time::Duration;
fn main() {
let mut handle = ray!("counting down");
for n in (1..=3).rev() {
thread::sleep(Duration::from_millis(200));
handle = handle.send(&n);
}
handle.red().large().label("count");
}
```
### 11. Conditionally showing items
PHP Ray uses `showIf`. In Rust, use `when` to decide whether to send at all:
```rust
use ray::ray;
fn main() {
ray!().when(true, |ray| ray.log(&"will be shown"));
ray!().when(false, |ray| ray.log(&"will be hidden"));
}
```
### 12. Conditionally sending items to Ray
PHP Ray has an `if()` chain; ray-dbg only supports the callback style via `when`.
If you want to gate an entire chain, use `if` in Rust or wrap the chain in `when`.
```rust
use ray::ray;
fn main() {
let value = 10;
ray!().when(value < 20, |ray| ray.text(format!("value is less than {value}")));
}
```
### 13. Removing items
```rust
use ray::ray;
use std::thread;
use std::time::Duration;
fn main() {
let handle = ray!("will be removed");
thread::sleep(Duration::from_millis(200));
handle.remove();
ray!("remove when true").remove_when(true);
}
```
### 14. Returning items
```rust
use ray::ray;
fn main() {
let value = ray!().pass("return value");
assert_eq!(value, "return value");
}
```
### 15. Showing Rust info (phpinfo alias)
PHP Ray exposes `phpinfo()`. The Rust version renders a compact HTML table with
runtime and build metadata (aliases: `phpinfo*`).
```rust
use ray::ray;
fn main() {
ray!().rust_info();
ray!().phpinfo_with_env(["RUST_LOG", "PATH"]);
}
```
### 16. Displaying exceptions
```rust
use ray::ray;
fn main() {
let err = std::io::Error::new(std::io::ErrorKind::Other, "boom");
ray!().exception(&err);
}
```
### 17. Callables and handling exceptions
`catch` accepts a callable that returns a `Result`. On error, it logs the error
via `exception` and keeps execution going.
```rust
use ray::ray;
fn main() {
ray!().catch(|| -> Result<(), std::io::Error> {
Err(std::io::Error::new(std::io::ErrorKind::Other, "boom"))
});
}
```
### 18. Showing raw values
```rust
use ray::ray;
fn main() {
let value = vec![1, 2, 3];
ray!().raw(&value);
}
```
To always send raw values, set `RayConfig.always_send_raw_values` or
`RAY_ALWAYS_SEND_RAW_VALUES=true`.
### 19. Creating a new screen
```rust
use ray::ray;
fn main() {
ray!().new_screen("My debug screen");
ray!().separator();
ray!().clear_screen();
}
```
### 20. Clearing everything including history
```rust
use ray::ray;
fn main() {
ray!().clear_all();
}
```
### 21. Showing and hiding the app
```rust
use ray::ray;
fn main() {
ray!().show_app();
ray!().hide_app();
}
```
### 22. Enabling and disabling Ray
```rust
use ray::ray;
fn main() {
let mut handle = ray!("one");
handle = handle.disable();
handle = handle.send(&"two");
handle = handle.enable();
handle.send(&"three");
let enabled = handle.enabled();
let disabled = handle.disabled();
assert!(enabled || disabled);
}
```
### 23. Displaying a notification
```rust
use ray::ray;
fn main() {
ray!().notify("This is my notification");
}
```
### 24. Shooting confetti
```rust
use ray::ray;
fn main() {
ray!().confetti();
}
```
### 25. Putting it together: Ray app workflow
```rust
use ray::ray;
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u64,
name: &'static str,
}
fn main() {
let user = User { id: 1, name: "Ada" };
ray!().new_screen("Checkout flow");
ray!(&user).label("user").green();
ray!().table(&user).label("user table");
ray!().image("https://example.com/avatar.png").label("avatar");
ray!().separator();
ray!().link("https://myray.app").label("Ray docs");
ray!().notify("Checkout complete");
}
```
## Additional helpers (Rust)
### Limiting output (once/limit)
Use `limit(n)` to allow only `n` payloads from a callsite. `once()` is shorthand
for `limit(1)`. `once_send()` sends a value immediately once, and `ray_once!` is
the macro variant.
```rust
use ray::{ray, ray_once};
fn main() {
for _ in 0..3 {
ray!().limit(2).text("only twice");
}
for _ in 0..2 {
ray!().once().text("only once");
}
ray!().once_send("only once (send)");
ray_once!("only once (macro)");
}
```
### Rate limiting
`rate_limiter()` exposes a global limiter for the current client. When the limit
is reached, Ray shows a single "Rate limit" entry and drops further payloads
until you `clear()` or relax the limit.
```rust
use ray::ray;
fn main() {
ray!().rate_limiter().clear().max(10).per_second(5);
ray!("this will send");
ray!().rate_limiter().clear();
}
```
### Display timestamps with `carbon` (UTC)
We keep the `carbon` name for parity with the PHP Ray API; this does not use the PHP Carbon
library and simply formats Rust `SystemTime` values in UTC.
```rust
use ray::ray;
use std::time::SystemTime;
fn main() {
ray!().carbon(SystemTime::now());
ray!().carbon(SystemTime::UNIX_EPOCH);
}
```
### Custom rendering helpers
Use the helper payloads to format values:
```rust
use ray::ray;
use serde::Serialize;
#[derive(Serialize)]
struct User {
name: String,
age: u8,
}
fn main() {
let user = User {
name: "Jane".to_string(),
age: 30,
};
ray!().to_json(&user);
ray!().html("<b>raw html</b>");
ray!().xml("<root />");
ray!().table(&user);
ray!().image("https://example.com/image.png");
ray!().file("Cargo.toml");
ray!().url("https://example.com");
ray!().link("https://example.com");
}
```
`ray_json!` is an explicit alias for JSON logging if you prefer to use a separate macro.
For links, pass an absolute URL or pre-parse with `RayUrl::parse`/`url::Url` and use
`try_url`/`try_link` to surface errors. If the scheme is omitted, `https://` is assumed.
### Send semantics (immediate vs buffered)
By default, every chained call sends a request immediately (best-effort by default).
Chainable methods intentionally ignore transport errors; `try_*` variants surface
serialization issues, and transport errors only when `RayConfig.swallow_errors` is `false`
(or you use `ray_strict!` / `ray_async_strict!`).
Updating a single Ray entry (reuse the same `Ray` handle to keep the UUID stable):
```rust
use ray::ray;
fn main() {
let mut ray = ray!("counting down");
for n in (1..=3).rev() {
ray = ray.log(&n);
}
ray.green().small().label("count");
}
```
If you want to buffer several payloads from the same handle (same UUID) and send
them in a single request, use `batch()`:
```rust
use ray::ray;
fn main() {
ray!()
.batch()
.log(&"hello")
.green()
.label("init")
.commit();
}
```
`batch()` does not create multiple Ray entries; it batches the same payloads you would
otherwise send immediately. This is useful for combining `log` + styling updates (or
reducing HTTP calls). To create separate entries, call `ray!()` again to get a new handle.
## ray_dbg macro (Rust-only)
`ray_dbg!` mirrors `dbg!`: it labels the entry with the expression and returns the value.
It uses `Debug` formatting, so any `Debug` value works.
```rust
use ray::ray_dbg;
fn main() {
let value = ray_dbg!(2 + 2);
assert_eq!(value, 4);
}
```
## Panic hook (Rust-only)
Install a panic hook that sends panic details to Ray (and keeps the default panic output).
```rust
fn main() {
ray::panic_hook();
// ...
}
```
## Async support (feature: transport-reqwest)
Enable the `transport-reqwest` feature to get async send support.
```toml
ray = { path = "crates/ray", package = "ray-dbg", features = ["transport-reqwest"] }
```
Then you can call the async API (requires an async runtime like Tokio):
```rust
use ray::ray_async;
#[tokio::main]
async fn main() {
ray_async!().label("async label").await;
}
```
To surface errors, use the `try_*` variants with `?`:
```rust
use ray::{ray, RayError};
fn main() -> Result<(), RayError> {
ray!().try_log(&"hello")?;
Ok(())
}
```
```rust
use ray::{ray_async, RayError};
#[tokio::main]
async fn main() -> Result<(), RayError> {
ray_async!().try_label("async label").await?;
Ok(())
}
```
To force transport errors to bubble up, set `RAY_SWALLOW_ERRORS=false` or use
`ray_strict!` / `ray_async_strict!` (async requires `transport-reqwest`).
## Tracing integration (feature: tracing)
Enable the `tracing` feature to forward `tracing` events to Ray.
```toml
ray = { path = "crates/ray", package = "ray-dbg", features = ["tracing"] }
```
```rust
fn main() {
ray::tracing::init().unwrap();
tracing::info!("hello from tracing");
}
```
`RUST_LOG` controls the filtering level, just like `tracing-subscriber`.
## Debug-only macros (feature: debug-macros)
`debug-macros` is enabled by default to make the core macros no-op outside debug builds.
If you want the macros to always run, disable the default features and opt into a transport:
```toml
ray = { path = "crates/ray", package = "ray-dbg", default-features = false, features = ["transport-ureq"] }
```
Swap `transport-ureq` for `transport-reqwest` if you want async support.
Enable `transport-ureq-tls` if you need HTTPS with the ureq transport.
## PHP vanilla compatibility (core)
Based on the PHP vanilla reference: https://myray.app/docs/php/vanilla-php/reference
For full parity notes and edge cases, see the notes below.
| Logging and values (ray, text, raw, toJson, json) | Implemented (json multi-arg partial) | `ray!`, `Ray::text`, `Ray::raw`, `Ray::to_json`, `Ray::json` |
| Item updates (send) | Partial | `Ray::send` (alias: `Ray::log`) |
| Rendering and files (html, xml, table, image, file) | Implemented | `Ray::html`, `Ray::xml`, `Ray::table`, `Ray::image`, `Ray::file` |
| Types and exceptions (className, exception) | Implemented | `Ray::class_name`, `Ray::exception` |
| Debugging and stack (caller, trace, backtrace) | Implemented | `Ray::caller`, `Ray::trace`, `Ray::backtrace` |
| Links and URLs (link, url) | Implemented | `Ray::link`, `Ray::url` |
| Screens and layout (newScreen, clearScreen, clearAll, separator) | Implemented | `Ray::new_screen`, `Ray::clear_screen`, `Ray::clear_all`, `Ray::separator` |
| Styling (label, color, size) | Implemented | `Ray::label`, `Ray::color`, `Ray::{small, large}` |
| App UI (notify, confetti, hide, showApp/hideApp, remove) | Implemented | `Ray::notify`, `Ray::confetti`, `Ray::hide`, `Ray::show_app`, `Ray::hide_app`, `Ray::remove` |
| Control flow (if, catch, die/rd, once) | Implemented | `Ray::when`, `Ray::catch`, `Ray::die`, `Ray::rd`, `Ray::once` |
| Counters and rate limiting (count, limit, rateLimiter) | Implemented | `Ray::count`, `Ray::limit`, `Ray::rate_limiter` |
| Diagnostics (measure, carbon, phpinfo, pause) | Partial | `Ray::measure`, `Ray::carbon`, `Ray::rust_info`, `Ray::pause` |
| Private access (invade) | Won't implement | Rust privacy is compile-time only |
Notes on differences:
- json multi-arg: use `.batch().json(...).json(...)`.
- send: `Ray::send` aliases `Ray::log` and reuses UUIDs when you keep the same handle.
- measure: memory usage is best-effort; unsupported platforms report `0`.
- phpinfo/pause/carbon: Rust info table, pause is a no-op, `carbon` is a UTC `SystemTime` formatter (no PHP Carbon dependency).
- invade: not implemented due to Rust privacy rules.
## Examples
Run the usage-focused example (aligned with the Ray PHP usage docs where possible):
```bash
cargo run -p ray-dbg --example usage
```
## Testing
Unit tests use `httpmock` to validate request envelopes.
```bash
cargo test -p ray-dbg
```
Async tests require `transport-reqwest`:
```bash
cargo test -p ray-dbg --features transport-reqwest
```
For live Ray testing on your local setup, enable the `live-tests` feature and opt into ignored tests:
```bash
cargo test -p ray-dbg --features live-tests -- --ignored
```
Async live tests require `transport-reqwest`:
```bash
cargo test -p ray-dbg --features "transport-reqwest,live-tests" -- --ignored
```
## Configuration
Configuration is loaded from (in increasing priority):
1. defaults
2. `ray.json` (searched from the current directory upwards)
3. environment variables
The default `Client`/configuration is cached on first use for performance.
Use `RayConfig::load_strict()` to surface invalid `ray.json` or environment values.
### Environment variables
- `RAY_ENABLED` (default: `true`)
- `RAY_HOST` (default: `localhost`)
- `RAY_PORT` (default: `23517`)
- `RAY_SWALLOW_ERRORS` (default: `true`)
- `RAY_STRICT` (if `true`, forces `RAY_SWALLOW_ERRORS=false`)
- `RAY_PROJECT_NAME` (optional)
- `RAY_TIMEOUT_MS` (default: `250`)
- `RAY_CANONICALIZE_PATHS` (default: `false`)
- `RAY_SEND_META` (default: `true`)
- `RAY_ALWAYS_SEND_RAW_VALUES` (default: `false`)
- `RAY_LOCAL_PATH` (optional)
- `RAY_REMOTE_PATH` (optional)
In strict mode (`RayConfig::load_strict()`), invalid or zero values return a `ConfigError`.
When both local/remote paths are set, callsite file paths are rewritten from the local prefix to the remote prefix.
When `always_send_raw_values` is enabled, `ray!(...)` and `Ray::log` send RAW preformatted JSON instead of log payloads.
### `ray.json`
Keys are optional; `strict` forces `swallow_errors=false` when `true`.
Example:
```json
{
"enabled": true,
"host": "localhost",
"port": 23517,
"swallow_errors": true,
"strict": false,
"project_name": "my-project",
"timeout_ms": 250,
"canonicalize_paths": false,
"send_meta": true,
"always_send_raw_values": false,
"local_path": null,
"remote_path": null
}
```
## Developing with `ray-proxy`
See [permafrost-dev/ray-proxy](https://github.com/permafrost-dev/ray-proxy) for details.
1. In Ray app preferences, set the port to `23516`.
2. Run:
```bash
npx ray-proxy
```
3. Point this crate to the proxy:
```bash
export RAY_PORT=23516
```
Then run any code/tests that use `ray!` or `ray_async!`, or run the example:
```bash
cargo run -p ray-dbg --example usage
```