Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No capture from std::thread::spawn or tokio::task::spawn_blocking #23

Open
elonen opened this issue Feb 11, 2023 · 9 comments
Open

No capture from std::thread::spawn or tokio::task::spawn_blocking #23

elonen opened this issue Feb 11, 2023 · 9 comments

Comments

@elonen
Copy link

elonen commented Feb 11, 2023

I'm having difficulties asserting traces from inside an std thread. Is there something I overlooked, or does the capture perhaps generally fail when using std::thread::spawn instead of multi-threading tokio?

The following passes first two tests but fails the third:

use tracing;
use tracing_test::traced_test;

fn hello() {
    tracing::info!("Hello, world!");
}

#[test]
#[traced_test]
fn test_1_singlethread()
{
    tracing::info!("Test thread");
    hello();
    assert!(logs_contain("Test thread"));
    assert!(logs_contain("Hello, world!"));
}

#[tokio::test]
#[traced_test]
async fn test_2_multithread_tokio()
{
    tracing::info!("Test thread");
    tokio::spawn(async { hello(); }).await.unwrap();
    assert!(logs_contain("Test thread"));
    assert!(logs_contain("Hello, world!"));
}

#[test]
#[traced_test]
fn test_3_multithread_std()
{
    tracing::info!("Test thread");
    std::thread::spawn(|| { hello(); }).join().unwrap();
    assert!(logs_contain("Test thread"));
    assert!(logs_contain("Hello, world!"));
}

Results:

[...]
running 3 tests
test test_1_singlethread ... ok
test test_2_multithread_tokio ... ok
test test_3_multithread_std ... FAILED

failures:

---- test_3_multithread_std stdout ----
2023-02-12T08:02:20.047401Z  INFO test_3_multithread_std: r#mod: Test thread
2023-02-12T08:02:20.047625Z  INFO r#mod: Hello, world!
thread 'test_3_multithread_std' panicked at 'assertion failed: logs_contain(\"Hello, world!\")', tests/mod.rs:35:5

Cargo.toml contains no-env-filter:

[package]
name = "tracing-test-problem"
version = "0.1.0"
edition = "2021"

[dependencies]
tracing = "0.1.37"
tokio = { version = "1.25.0", features = ["rt-multi-thread", "macros"] }

[dev-dependencies]
tracing-test = {version="0.2.4", features=["no-env-filter"] }
@elonen elonen changed the title No capture from inside std::thread::spawn No capture from inside std::thread::spawn Feb 11, 2023
@elonen
Copy link
Author

elonen commented Feb 12, 2023

Oh, looks like tokio::task::spawn_blocking also loses the output. Adding this...

#[tokio::test]
#[traced_test]
async fn test_4_tokio_blocking()
{
    tracing::info!("Test thread");
    tokio::task::spawn_blocking(|| hello() ).await.unwrap();
    assert!(logs_contain("Test thread"));
    assert!(logs_contain("Hello, world!"));
}

...results in:

test test_4_tokio_blocking ... FAILED

failures:
---- test_4_tokio_blocking stdout ----
2023-02-12T08:35:33.086166Z  INFO test_4_tokio_blocking: r#mod: Test thread
2023-02-12T08:35:33.086283Z  INFO r#mod: Hello, world!
thread 'test_4_tokio_blocking' panicked at 'assertion failed: logs_contain(\"Hello, world!\")', tests/mod.rs:46:5

@elonen elonen changed the title No capture from inside std::thread::spawn No capture from std::thread::spawn or tokio::task::spawn_blocking Feb 12, 2023
@elonen
Copy link
Author

elonen commented Feb 12, 2023

Asserting inside the thread block (instead of after joining it) works, but this doesn't really help with integration tests, where the tested functionality uses multiple threads internally.

@bgw
Copy link

bgw commented Feb 16, 2023

This is likely a side-effect of #19. There's no good way to automatically associate tracing events across threads or tasks. The documentation gives a usage example that uses tokio::spawn, but I'm skeptical that it would actually work as documented after the bugfix in #19.

You can associate the spawned code with the current span using Instrument::in_current_span (for async tasks) or Span::or_current (for synchronous threads) which may resolve your issue.

@elonen
Copy link
Author

elonen commented Feb 16, 2023

Thank you. Test 2 (tokio::spawn) still works like documented with features=["no-env-filter"], but test 4 (tokio::spawn_blocking) fails.

Passing the span manually into a thread with Span::or_current() indeed fixes the asserts for both std::thread::spawn and tokio::spawn_blocking:

#[test]
#[traced_test]
fn test_5_multithread_std_or_current()
{
    tracing::info!("Test thread");

    let thread_span = tracing::debug_span!("thread").or_current();
    std::thread::spawn(|| { 
        let _entered = thread_span.entered();
        hello();
    }).join().unwrap();
    
    assert!(logs_contain("Test thread"));
    assert!(logs_contain("Hello, world!"));
}

#[tokio::test]
#[traced_test]
async fn test_6_tokio_blocking_or_current()
{
    tracing::info!("Test thread");

    let thread_span = tracing::debug_span!("thread").or_current();
    tokio::task::spawn_blocking(|| { 
        let _entered = thread_span.entered();
        hello() 
    }).await.unwrap();

    assert!(logs_contain("Test thread"));
    assert!(logs_contain("Hello, world!"));
}

Results:

test test_5_multithread_std_or_current ... ok
test test_6_tokio_blocking_or_current ... ok

Interesting gotcha: the guard variable has to be named or it gets immediately dropped. Using plain _ instead of _entered fails the test without any compiler warnings:

#[tokio::test]
#[traced_test]
async fn test_7_tokio_blocking_or_current_unnamed_guard()
{
    tracing::info!("Test thread");

    let thread_span = tracing::debug_span!("thread").or_current();
    tokio::task::spawn_blocking(|| { 
        let _ = thread_span.entered();   // this doesn't work, but _entered does
        hello() 
    }).await.unwrap();

    assert!(logs_contain("Test thread"));
    assert!(logs_contain("Hello, world!"));
}

Result:

test test_7_tokio_blocking_or_current_unnamed_guard ... FAILED

Conclusions

Since tracing events were shown in test failure log, like the "Hello, world!" here...

---- test_3_multithread_std stdout ----
2023-02-16T09:50:18.446516Z  INFO test_3_multithread_std: r#mod: Test thread
2023-02-16T09:50:18.447020Z  INFO r#mod: Hello, world!
thread 'test_3_multithread_std' panicked at 'assertion failed: logs_contain(\"Hello, world!\")', tests/mod.rs:37:5

---- test_4_tokio_blocking stdout ----
2023-02-16T09:50:18.446222Z  INFO test_4_tokio_blocking: r#mod: Test thread
2023-02-16T09:50:18.447002Z  INFO r#mod: Hello, world!
thread 'test_4_tokio_blocking' panicked at 'assertion failed: logs_contain(\"Hello, world!\")', tests/mod.rs:48:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

...I expeced logs_contain() to have used whatever method cargo test was using to globally capture everything.
Then again, #19 and the associated issue seem reasonable, too.
How about just adding a short note to docs about threads and the need for .or_current()?

@bgw
Copy link

bgw commented Feb 18, 2023

Interesting gotcha: the guard variable has to be named or it gets immediately dropped. Using plain _ instead of _entered fails the test without any compiler warnings

Wow, TIL. https://medium.com/codechain/rust-underscore-does-not-bind-fec6a18115a8

xfbs added a commit to xfbs-fork/deadpool that referenced this issue Feb 20, 2023
With deadpool-sqlite, the database interactions happen in a sync context
(due to using rusqlite) on a separate thread. The tracing crate can add
conetxt to any logs (events). Due to the database interactions happening
on a separate thread, this context gets lost. Enabling the tracing
feature allows the information to be preserved.

Related: dbrgn/tracing-test#23

Note: I don't know if this is something that should rather be
implemented in the `spawn_blocking` method of Tokio.
xfbs added a commit to xfbs-fork/deadpool that referenced this issue Feb 20, 2023
With deadpool-sqlite, the database interactions happen in a sync context
(due to using rusqlite) on a separate thread. The tracing crate can add
conetxt to any logs (events). Due to the database interactions happening
on a separate thread, this context gets lost. Enabling the tracing
feature allows the information to be preserved.

Related: dbrgn/tracing-test#23

Note: I don't know if this is something that should rather be
implemented in the `spawn_blocking` method of Tokio.
bikeshedder pushed a commit to deadpool-rs/deadpool that referenced this issue Sep 6, 2023
With deadpool-sqlite, the database interactions happen in a sync context
(due to using rusqlite) on a separate thread. The tracing crate can add
conetxt to any logs (events). Due to the database interactions happening
on a separate thread, this context gets lost. Enabling the tracing
feature allows the information to be preserved.

Related: dbrgn/tracing-test#23

Note: I don't know if this is something that should rather be
implemented in the `spawn_blocking` method of Tokio.
@odyslam
Copy link

odyslam commented Nov 23, 2023

I am encountering the same issue. Is there a good solution to this?

@landonxjames
Copy link

I ran into this issue today as well and it is fairly easy to reproduce. The following small variation on the example from the documentation fails. Since tokio tests default to running in a single threaded runtime the comment in the example is incorrect and the default example does not include any separate threads.

    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
    #[traced_test]
    async fn test_logs_are_captured() {
        // Local log
        info!("This is being logged on the info level");

        info!("CountMe");
        // Log from a spawned task (which runs in a separate thread)
        tokio::spawn(async {
            warn!("This is being logged on the warn level from a spawned task");
            info!("CountMe");
        })
        .await
        .unwrap();

        // Ensure that `logs_contain` works as intended
        assert!(logs_contain("logged on the info level"));
        assert!(logs_contain("logged on the warn level"));
        assert!(!logs_contain("logged on the error level"));

        // Ensure that `logs_assert` works as intended (with a closure)
        logs_assert(|lines: &[&str]| {
            match lines.iter().filter(|line| line.contains("CountMe")).count() {
                2 => Ok(()),
                n => Err(format!("Count should be 2, but was {}", n)),
            }
        });

        // Ensure that `logs_assert` works as intended (with a function)
        fn assert_fn(lines: &[&str]) -> Result<(), String> {
            match lines.iter().filter(|line| line.contains("CountMe")).count() {
                2 => Ok(()),
                n => Err(format!("Count should be 2, but was {}", n)),
            }
        }
        logs_assert(assert_fn);
    }

fails with:

running 1 test
test meter::tests::test_logs_are_captured ... FAILED

successes:

successes:

failures:

---- meter::tests::test_logs_are_captured stdout ----
2024-11-05T22:09:27.493105Z  INFO test_logs_are_captured: aws_smithy_observability_otel::meter::tests: This is being logged on the info level
2024-11-05T22:09:27.493167Z  INFO test_logs_are_captured: aws_smithy_observability_otel::meter::tests: CountMe
2024-11-05T22:09:27.493352Z  WARN aws_smithy_observability_otel::meter::tests: This is being logged on the warn level from a spawned task
2024-11-05T22:09:27.493368Z  INFO aws_smithy_observability_otel::meter::tests: CountMe
thread 'meter::tests::test_logs_are_captured' panicked at aws-smithy-observability-otel/src/meter.rs:525:9:
assertion failed: logs_contain("logged on the warn level")

@josecelano
Copy link

I think I'm also having problems with nested spawned tasks, but I have not been able to reproduce it with a minimal example yet.

I'm trying to refactor this PR since I realized I was building this crate. However, I can only see logs from the first level of spawned tasks.

@josecelano
Copy link

I've created a test repo to play with different configurations:

https://github.com/josecelano/tracing-test-bug

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants