SIGN IN SIGN UP
FuelLabs / fuel-core UNCLAIMED

Rust full node implementation of the Fuel v2 protocol.

0 0 15 Rust

Enable debug logging in predicates and scripts using ECAL (#3059)

## Linked Issues/PRs

VM changes done in https://github.com/FuelLabs/fuel-vm/pull/963 ,
waiting for the next `fuel-vm` release.

See
https://github.com/FuelLabs/fuel-vm/pull/945#issuecomment-2829271997.
Replaces https://github.com/FuelLabs/fuel-core/pull/2973.

## Description

Accomodates https://github.com/FuelLabs/fuel-vm/pull/963. Allows setting
`--allow-syscall` flag, that in turn allows logging from scripts and
predicates using `__dbg!` macro in Sway, which uses the new "ecal
syscall api" defined here.

Currently the API looks like this: `ECAL a b c d` where:
* `a`: Syscall number, set to `1000` for debug log output
* `b`: File descriptor, set to `1` for stdout
* `c`: Address of (pointer to) the message to log
* `d`: Length of the message to log.

### Open questions

* Should we print receipts or at least indicate if the tx reverts in the
console output?

### Resolved 

* Should we test the log output somehow? I attempted to use the
[tracing-test crate](https://crates.io/crates/tracing-test), but it
cannot read logs from the fuel-core. [See this
issue](https://github.com/dbrgn/tracing-test/issues/22). I tried to
implement my own, but it turns out it's complicated. Code below.
  * Done for now using a separate process and reading stderr.

<details>

<summary>How to capture logs</summary>

Doesn't work since global subscriber cannot be added for multiple
simultaneous tests. Would need to force single thread and filter based
on thread id, or somehow inject per-test unique span to fuel-core.

```rust
use std::{
    array, io, sync::{
        Arc,
        Mutex,
    }
};
use tracing::Dispatch;
use tracing_subscriber::{
    FmtSubscriber,
    fmt::MakeWriter,
};


/// A struct to capture tracing logs for testing purposes.
/// Inspired by the `tracing-test` crate, which turned out to
/// be insufficient for this.
#[derive(Clone)]
pub struct CaptureLogs(Arc<Mutex<String>>);

impl CaptureLogs {
    pub fn init() -> Self {
        let buffer = Arc::new(Mutex::new(String::new()));
        let this = Self(buffer.clone());
        let subscriber: Dispatch = FmtSubscriber::builder()
            .with_writer(this.clone())
            .with_level(true)
            .with_ansi(false)
            .into();
        tracing::dispatcher::set_global_default(subscriber)
            .expect("Could not set global tracing subscriber");
        this
    }

    /// Finds expected part of logs and discards everything before it. Panics if the expected part is not found.
    pub fn expect(&self, expected: &str) {
        // Just reuse the regex logic
        self.expect_regex(&regex::escape(expected))
    }

    /// Like `expect`, but the next part must follow immediately after the previous one.
    pub fn expect_follows(&self, expected: &str) {
        // Again, reuse the regex logic
        self.expect_regex(&format!("^{}", regex::escape(expected)))
    }

    /// Finds expected part of logs and discards everything before it. Panics if the expected part is not found.
    pub fn expect_regex(&self, regex: &str) {
        let mut data = self.0.lock().expect("Mutex poisoned");
        let Some(m) = regex::Regex::new(regex)
            .expect("Failed to compile regex")
            .find(&data) else {
            println!("Captured logs: {}", data);
            panic!(
                "Expected pattern `{}` not found in logs.",
                regex,
            );
        };
        *data = data.split_at(m.end()).1.to_owned();
    }
}

impl io::Write for CaptureLogs {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let mut target = self.0.lock().expect("Mutex poisoned");
        target.push_str(&String::from_utf8_lossy(buf));
        Ok(buf.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

impl MakeWriter<'_> for CaptureLogs {
    type Writer = Self;

    fn make_writer(&self) -> Self::Writer {
        self.clone()
    }
}
```
</details>


## Checklist
- [ ] Breaking changes are clearly marked as such in the PR description
and changelog
- [x] New behavior is reflected in tests - only partially, see
discussion above
- [x] [The specification](https://github.com/FuelLabs/fuel-specs/)
matches the implemented behavior (link update PR if changes are needed)

### Before requesting review
- [x] I have reviewed the code myself
- [x] I have created follow-up issues caused by this PR and linked them
here

### After merging, notify other teams

[Add or remove entries as needed]

- [ ] [Rust SDK](https://github.com/FuelLabs/fuels-rs/)
- [ ] [Sway compiler](https://github.com/FuelLabs/sway/)
- [ ] [Platform
documentation](https://github.com/FuelLabs/devrel-requests/issues/new?assignees=&labels=new+request&projects=&template=NEW-REQUEST.yml&title=%5BRequest%5D%3A+)
(for out-of-organization contributors, the person merging the PR will do
this)
- [ ] Someone else?

---------

Co-authored-by: Mitchell Turner <james.mitchell.turner@gmail.com>
Co-authored-by: Green Baneling <XgreenX9999@gmail.com>
H
Hannes Karppila committed
68d263ce84cff5e5076251921b20e10a9b25cc2a
Parent: baa805f
Committed by GitHub <noreply@github.com> on 9/18/2025, 2:02:20 PM