Repository: emit-rs/emit (rust)

Issue:
Consider streamlining the ActiveSpan::start API
We need to specify ambient properties upfront, but could give you a default completion and span properties, letting you override them through methods on the resulting `ActiveSpan`. This would cut the noise down for cases where you want a span, but don’t need to configure everything.

Pull request description:
Add a start_span! macro for easy span creation
Closes #188 

This PR adds a `start_span!` macro that lets you lift a `#[span]` attribute into a `let` binding.

For example, this attribute:

```rust
#[emit::span(guard: span, "Running an example", i)]
fn example(i: i32) {
    let r = i + 1;

    if r == 4 {
        span.complete_with(emit::span::completion::from_fn(|evt| {
            emit::error!(evt, "Running an example failed with {r}");
        }));
    } else {
        span.complete_with(emit::span::completion::from_fn(|evt| {
            emit::info!(evt, "Running an example produced {r}");
        }));
    }
}
```

can also be written as:

```rust
fn example(i: i32) {
    let (span, frame) = emit::start_span!("Running an example", i);

    frame.call(move || {
        let r = i + 1;

        if r == 4 {
            span.complete_with(emit::span::completion::from_fn(|evt| {
                emit::error!(evt, "Running an example failed with {r}");
            }));
        } else {
            span.complete_with(emit::span::completion::from_fn(|evt| {
                emit::info!(evt, "Running an example produced {r}");
            }));
        }
    })
}
```

It's important that the `span` is completed within the `frame`, otherwise it won't have any ambient context (like its trace and span ids).

Patch:
diff --git a/book/src/producing-events/tracing/manual-span-creation.md b/book/src/producing-events/tracing/manual-span-creation.md
index aa9702a..df5db62 100644
--- a/book/src/producing-events/tracing/manual-span-creation.md
+++ b/book/src/producing-events/tracing/manual-span-creation.md
@@ -8,30 +8,7 @@ You can also create `ActiveSpan`s manually if you can't or don't want to use the
 
 ```rust
 # extern crate emit;
-let (span, frame) = emit::span::ActiveSpan::start(
-    // A filter that determines whether the span is active or not
-    emit::filter(),
-    // The context to generate span context from, and store ambient context in
-    emit::ctxt(),
-    // The source of the span's internal timer
-    emit::clock(),
-    // The source of randomness to generate span context from
-    emit::rng(),
-    // 1. What to do with the span when the guard completes
-    //    Typically you'll want to emit it
-    emit::span::completion::default(emit::emitter(), emit::ctxt()),
-    // Any properties to put in the ambient context
-    // This doesn't need to include any span or trace ids unless you want to
-    // override whatever `ActiveSpan` generates for you
-    emit::Empty,
-    // The module that generated the span
-    emit::mdl!(),
-    // 2. The name of the span
-    "manual span",
-    // Any properties to include on the span on completion
-    // These properties won't be added to the ambient context
-    emit::Empty,
-);
+let (span, frame) = emit::start_span!("manual span");
 
 frame.call(move || {
     // Your code goes here
@@ -40,12 +17,11 @@ frame.call(move || {
 })
 ```
 
-**Make sure you pass ownership of the returned [`ActiveSpan`](https://docs.rs/emit/0.11.0-alpha.21/emit/span/struct.ActiveSpan.html) into the closure in [`Frame::call`](https://docs.rs/emit/0.11.0-alpha.21/emit/frame/struct.Frame.html#method.call) or async block in [`Frame::in_future`](https://docs.rs/emit/0.11.0-alpha.21/emit/frame/struct.Frame.html#method.in_future)**. If you don't, the span will complete early, without its ambient context.
+The `start_span!` macro returns a tuple of [`ActiveSpan`](https://docs.rs/emit/0.11.0-alpha.21/emit/span/struct.ActiveSpan.html) for completing the span, and [`Frame`](https://docs.rs/emit/0.11.0-alpha.21/emit/frame/struct.Frame.html) for activating the span's ambient trace and span ids for correlation.
 
-The main properties you'll control when creating `ActiveSpan`s are annotated in the example above and are:
+The syntax accepted by the `start_span!` macro is the same as the [`#[span]`](https://docs.rs/emit/0.11.0-alpha.21/emit/attr.span.html) attribute.
 
-1. The [`Completion`](https://docs.rs/emit/0.11.0-alpha.21/emit/span/completion/trait.Completion.html) to use when the span is completed. Usually this will emit the span, but may also set a template or additional properties.
-2. The name of the span.
+**Make sure you pass ownership of the returned [`ActiveSpan`](https://docs.rs/emit/0.11.0-alpha.21/emit/span/struct.ActiveSpan.html) into the closure in [`Frame::call`](https://docs.rs/emit/0.11.0-alpha.21/emit/frame/struct.Frame.html#method.call) or async block in [`Frame::in_future`](https://docs.rs/emit/0.11.0-alpha.21/emit/frame/struct.Frame.html#method.in_future)**. If you don't, the span will complete early, without its ambient context.
 
 Using `ActiveSpan`s is the recommended way to trace code with `emit`. It applies filtering for you, so the span is only created if it matches the configured filter. It also ensures a span is emitted even if the traced code panics or otherwise returns without explicitly completing.
 
diff --git a/examples/common_patterns/span_manual_creation.rs b/examples/common_patterns/span_manual_creation.rs
index 689adb8..27aae42 100644
--- a/examples/common_patterns/span_manual_creation.rs
+++ b/examples/common_patterns/span_manual_creation.rs
@@ -9,22 +9,7 @@ This example differs from `span_manual_creation_full` in still using the same `A
 use std::time::Duration;
 
 fn example(i: i32) {
-    let (span, frame) = emit::span::ActiveSpan::start(
-        emit::filter(),
-        emit::ctxt(),
-        emit::clock(),
-        emit::rng(),
-        // The completion determines what happens to the span when it completes
-        emit::span::completion::default(emit::emitter(), emit::ctxt()),
-        // Any properties to put in ambient context along with trace/span ids
-        emit::props! {},
-        emit::mdl!(),
-        // The name of the span
-        "example",
-        // Any properties to associate with the span itself
-        // These aren't added to the ambient context
-        emit::props! {},
-    );
+    let (span, frame) = emit::start_span!("example");
 
     // Execute our code within the context of the frame
     // If this function was async, then you would use `frame.in_future(..).await`
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index dcc0d08..34ee848 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -469,6 +469,113 @@ pub fn error_span(
     )
 }
 
+/**
+Start a span.
+
+# Syntax
+
+```text
+(control_param),* tpl, (property),*
+```
+
+where
+
+- `control_param`: A Rust field-value with a pre-determined identifier (see below).
+- `tpl`: A template string literal.
+- `property`: A Rust field-value for a property to capture.
+
+# Control parameters
+
+This macro accepts the following optional control parameters:
+
+| name        | type                          | description                                                                                                                                                    |
+| ----------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `rt`        | `impl emit::runtime::Runtime` | The runtime to emit the event through.                                                                                                                         |
+| `mdl`       | `impl Into<emit::Path>`       | The module the event belongs to. If unspecified the current module path is used.                                                                               |
+| `when`      | `impl emit::Filter`           | A filter to use instead of the one configured on the runtime.                                                                                                  |
+| `panic_lvl` | `str` or `emit::Level`        | Detect whether the function panics and use the given level if it does.                                                                                         |
+
+# Template
+
+The template for the event. See the [`macro@tpl`] macro for syntax.
+
+# Properties
+
+Properties that appear within the template or after it are added to the emitted event. The identifier of the property is its key. Property capturing can be adjusted through the `as_*` attribute macros.
+*/
+#[proc_macro]
+pub fn start_span(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    span::expand_start_tokens(span::ExpandStartTokens {
+        level: None,
+        input: TokenStream::from(item),
+    })
+    .unwrap_or_compile_error()
+}
+
+/**
+Start a debug span.
+
+# Syntax
+
+See the [`macro@start_span`] macro for syntax.
+*/
+#[proc_macro]
+pub fn start_debug_span(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    span::expand_start_tokens(span::ExpandStartTokens {
+        level: Some(quote!(emit::Level::Debug)),
+        input: TokenStream::from(item),
+    })
+    .unwrap_or_compile_error()
+}
+
+/**
+Start an info span.
+
+# Syntax
+
+See the [`macro@start_span`] macro for syntax.
+*/
+#[proc_macro]
+pub fn start_info_span(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    span::expand_start_tokens(span::ExpandStartTokens {
+        level: Some(quote!(emit::Level::Info)),
+        input: TokenStream::from(item),
+    })
+    .unwrap_or_compile_error()
+}
+
+/**
+Start a warning span.
+
+# Syntax
+
+See the [`macro@start_span`] macro for syntax.
+*/
+#[proc_macro]
+pub fn start_warn_span(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    span::expand_start_tokens(span::ExpandStartTokens {
+        level: Some(quote!(emit::Level::Warn)),
+        input: TokenStream::from(item),
+    })
+    .unwrap_or_compile_error()
+}
+
+/**
+Start an error span.
+
+# Syntax
+
+See the [`macro@start_span`] macro for syntax.
+*/
+#[proc_macro]
+pub fn start_error_span(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    span::expand_start_tokens(span::ExpandStartTokens {
+        level: Some(quote!(emit::Level::Error)),
+        input: TokenStream::from(item),
+    })
+    .unwrap_or_compile_error()
+}
+
 /**
 Construct a template.
 
diff --git a/macros/src/span.rs b/macros/src/span.rs
index 3faf83f..2e67eae 100644
--- a/macros/src/span.rs
+++ b/macros/src/span.rs
@@ -515,3 +515,94 @@ fn completion(
         default_completion_tokens,
     })
 }
+
+pub struct ExpandStartTokens {
+    pub level: Option<TokenStream>,
+    pub input: TokenStream,
+}
+
+/**
+The `start_span!` macro.
+*/
+pub fn expand_start_tokens(opts: ExpandStartTokens) -> Result<TokenStream, syn::Error> {
+    let span = opts.input.span();
+
+    let (args, template, ctxt_props) =
+        template::parse2::<Args>(opts.input, capture::default_fn_name, true)?;
+
+    let template =
+        template.ok_or_else(|| syn::Error::new(span, "missing template string literal"))?;
+
+    check_evt_props(&ctxt_props)?;
+
+    let Args {
+        rt,
+        mdl,
+        when,
+        guard,
+        setup,
+        ok_lvl,
+        err_lvl,
+        panic_lvl,
+        err,
+    } = args;
+
+    ensure_missing("guard", guard)?;
+    ensure_missing("setup", setup)?;
+    ensure_missing("ok_lvl", ok_lvl)?;
+    ensure_missing("err_lvl", err_lvl)?;
+    ensure_missing("err", err)?;
+
+    let default_lvl_tokens = opts.level;
+    let panic_lvl_tokens = panic_lvl;
+
+    let rt_tokens = rt.to_tokens()?.to_ref_tokens();
+    let mdl_tokens = mdl.to_tokens();
+    let when_tokens = when
+        .to_tokens()
+        .map(|when| when.to_ref_tokens())
+        .to_option_tokens(quote!(&emit::Empty));
+
+    let ctxt_props_tokens = ctxt_props.props_tokens().to_ref_tokens();
+    let template_tokens = template.template_tokens().to_ref_tokens();
+    let span_name_tokens = template.template_literal_tokens();
+
+    let lvl_tokens = default_lvl_tokens
+        .map(|lvl| lvl.to_ref_tokens())
+        .to_option_tokens(quote!(&emit::Level));
+
+    let panic_lvl_tokens = panic_lvl_tokens
+        .map(|lvl| lvl.to_ref_tokens())
+        .to_option_tokens(quote!(&emit::Level));
+
+    Ok(quote!(
+        emit::__private::__private_begin_span(
+            #rt_tokens,
+            #mdl_tokens,
+            #span_name_tokens,
+            #lvl_tokens,
+            #when_tokens,
+            #ctxt_props_tokens,
+            emit::span::completion::from_fn(|span| {
+                emit::__private::__private_complete_span(
+                    #rt_tokens,
+                    span,
+                    #template_tokens,
+                    #lvl_tokens,
+                    #panic_lvl_tokens,
+                )
+            }),
+        )
+    ))
+}
+
+fn ensure_missing(name: &str, value: Option<impl Spanned>) -> Result<(), syn::Error> {
+    if let Some(value) = value {
+        return Err(syn::Error::new(
+            value.span(),
+            format!("the `{name}` control parameter is not supported"),
+        ));
+    }
+
+    Ok(())
+}


Test patch:
diff --git a/test/ui/src/lib.rs b/test/ui/src/lib.rs
index a41583b..72b235c 100644
--- a/test/ui/src/lib.rs
+++ b/test/ui/src/lib.rs
@@ -20,6 +20,7 @@ mod emit;
 mod event;
 mod props;
 mod span;
+mod start_span;
 mod tpl;
 
 #[cfg(feature = "std")]
diff --git a/test/ui/src/start_span.rs b/test/ui/src/start_span.rs
new file mode 100644
index 0000000..300e2ae
--- /dev/null
+++ b/test/ui/src/start_span.rs
@@ -0,0 +1,179 @@
+use std::time::Duration;
+
+use emit::{Emitter, Props};
+
+use crate::util::{simple_runtime, Called};
+
+#[test]
+fn start_span_basic() {
+    for lvl in [
+        Some(emit::Level::Debug),
+        Some(emit::Level::Info),
+        Some(emit::Level::Warn),
+        Some(emit::Level::Error),
+        None,
+    ] {
+        let called = Called::new();
+
+        let rt = simple_runtime(
+            |evt| {
+                assert_eq!("Hello, Rust", evt.msg().to_string());
+                assert_eq!("Hello, {user}", evt.tpl().to_string());
+                assert_eq!(module_path!(), evt.mdl());
+
+                assert!(evt.extent().is_some());
+
+                assert_eq!("Rust", evt.props().pull::<&str, _>("user").unwrap());
+
+                assert_eq!(lvl, evt.props().pull::<emit::Level, _>("lvl"));
+
+                assert!(evt.props().get("trace_id").is_some());
+                assert!(evt.props().get("span_id").is_some());
+
+                called.record();
+            },
+            |_| true,
+        );
+
+        let user = "Rust";
+
+        match lvl {
+            None => {
+                let (guard, frame) = emit::start_span!(rt, "Hello, {user}");
+
+                frame.call(move || {
+                    guard.complete();
+                });
+            }
+            Some(emit::Level::Debug) => {
+                let (guard, frame) = emit::start_debug_span!(rt, "Hello, {user}");
+
+                frame.call(move || {
+                    guard.complete();
+                });
+            }
+            Some(emit::Level::Info) => {
+                let (guard, frame) = emit::start_info_span!(rt, "Hello, {user}");
+
+                frame.call(move || {
+                    guard.complete();
+                });
+            }
+            Some(emit::Level::Warn) => {
+                let (guard, frame) = emit::start_warn_span!(rt, "Hello, {user}");
+
+                frame.call(move || {
+                    guard.complete();
+                });
+            }
+            Some(emit::Level::Error) => {
+                let (guard, frame) = emit::start_error_span!(rt, "Hello, {user}");
+
+                frame.call(move || {
+                    guard.complete();
+                });
+            }
+        }
+
+        rt.emitter().blocking_flush(Duration::from_secs(1));
+
+        assert!(called.was_called());
+    }
+}
+
+#[tokio::test]
+async fn start_span_basic_async() {
+    for lvl in [
+        Some(emit::Level::Debug),
+        Some(emit::Level::Info),
+        Some(emit::Level::Warn),
+        Some(emit::Level::Error),
+        None,
+    ] {
+        let called = Called::new();
+
+        let rt = simple_runtime(
+            |evt| {
+                assert_eq!("Hello, Rust", evt.msg().to_string());
+                assert_eq!("Hello, {user}", evt.tpl().to_string());
+                assert_eq!(module_path!(), evt.mdl());
+
+                assert!(evt.extent().is_some());
+
+                assert_eq!("Rust", evt.props().pull::<&str, _>("user").unwrap());
+
+                assert_eq!(lvl, evt.props().pull::<emit::Level, _>("lvl"));
+
+                assert!(evt.props().get("trace_id").is_some());
+                assert!(evt.props().get("span_id").is_some());
+
+                called.record();
+            },
+            |_| true,
+        );
+
+        let user = "Rust";
+
+        match lvl {
+            None => {
+                let (guard, frame) = emit::start_span!(rt, "Hello, {user}");
+
+                frame
+                    .in_future(async move {
+                        tokio::time::sleep(Duration::from_micros(1)).await;
+
+                        guard.complete();
+                    })
+                    .await;
+            }
+            Some(emit::Level::Debug) => {
+                let (guard, frame) = emit::start_debug_span!(rt, "Hello, {user}");
+
+                frame
+                    .in_future(async move {
+                        tokio::time::sleep(Duration::from_micros(1)).await;
+
+                        guard.complete();
+                    })
+                    .await;
+            }
+            Some(emit::Level::Info) => {
+                let (guard, frame) = emit::start_info_span!(rt, "Hello, {user}");
+
+                frame
+                    .in_future(async move {
+                        tokio::time::sleep(Duration::from_micros(1)).await;
+
+                        guard.complete();
+                    })
+                    .await;
+            }
+            Some(emit::Level::Warn) => {
+                let (guard, frame) = emit::start_warn_span!(rt, "Hello, {user}");
+
+                frame
+                    .in_future(async move {
+                        tokio::time::sleep(Duration::from_micros(1)).await;
+
+                        guard.complete();
+                    })
+                    .await;
+            }
+            Some(emit::Level::Error) => {
+                let (guard, frame) = emit::start_error_span!(rt, "Hello, {user}");
+
+                frame
+                    .in_future(async move {
+                        tokio::time::sleep(Duration::from_micros(1)).await;
+
+                        guard.complete();
+                    })
+                    .await;
+            }
+        }
+
+        rt.emitter().blocking_flush(Duration::from_secs(1));
+
+        assert!(called.was_called());
+    }
+}