Skip to content

Commit 65a7ac3

Browse files
Fluzkojlizen
andauthored
Support inline config for #[dial9_tokio_telemetry::main()] (#256)
* support inline config * pr comments * refactor builder approach * pr comments * fallback config example * restore old config approach and deprecate it * pr comments * create TelemetryRuntime * TelemetryRuntime builds the guard and the runtime * move errors to its corresponding place * wire up build_or_disabled * review docs * review tests * lhf comments * move writer to config builder, collapse Dial9ConfigFallback, reshape error * promote TracedRuntime instead of TelemetryRuntime * always-present TelemetryGuard, inert TelemetryHandle, non-panicking current() * update changelog * docs: address review feedback on config builder, examples, and changelog --------- Co-authored-by: Jess Izen <jlizen@amazon.com>
1 parent 92fe453 commit 65a7ac3

22 files changed

Lines changed: 2558 additions & 971 deletions

CHANGELOG.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Inline config in the macro**: `#[dial9_tokio_telemetry::main]` now accepts a closure, so simple setups no longer need a separate config function.
13+
- **Fluent config builder**: `Dial9Config::builder()` with named setters, `with_tokio`/`with_runtime` closures, and `.enabled(bool)`. The original positional API under `dial9_tokio_telemetry::config` is unchanged.
14+
- **`build_or_disabled()`**: on config validation or writer I/O failure, logs an error and starts a plain tokio runtime instead of crashing. Use `build()` to handle failures explicitly.
15+
16+
All three in action:
17+
18+
```rust
19+
#[dial9_tokio_telemetry::main(config = || {
20+
Dial9Config::builder()
21+
.base_path("/tmp/trace.bin")
22+
.max_file_size(64 * 1024 * 1024)
23+
.max_total_size(256 * 1024 * 1024)
24+
.build_or_disabled()
25+
})]
26+
async fn main() { /* ... */ }
27+
```
28+
29+
### Changed
30+
31+
- `TelemetryHandle::current()` no longer panics off-runtime. It returns an inert handle whose `spawn` falls through to `tokio::spawn`. Use `TelemetryHandle::is_enabled()` to check whether telemetry is live.
32+
1033
## [0.3.6](https://github.com/dial9-rs/dial9-tokio-telemetry/compare/dial9-tokio-telemetry-v0.3.5...dial9-tokio-telemetry-v0.3.6) - 2026-04-30
1134

1235
### Added
@@ -97,13 +120,13 @@ tracing_subscriber::registry()
97120
.init();
98121
```
99122

100-
Tracing support means you can attach a request ID or other context to spans via `#[instrument(fields(request_id = %id))]` and then search for specific requests in the trace. You can also see what's happening inside long polls: if a single poll contains many small operations without yielding, the span breakdown shows exactly where the time went.
123+
Tracing support means you can attach a request ID or other context to spans via `#[instrument(fields(request_id = %id))]` and then search for specific requests in the trace. You can also see what's happening inside long polls: if a single poll contains many small operations without yielding, the span breakdown shows exactly where the time went.
101124

102125
Standard `tracing-subscriber` filtering rules apply. Without a filter, libraries like the AWS SDK will flood the trace with internal spans. The preceding captures only spans from `my_app`.
103126

104127
## [0.3.0](https://github.com/dial9-rs/dial9-tokio-telemetry/compare/dial9-tokio-telemetry-v0.2.0...dial9-tokio-telemetry-v0.3.0) - 2026-04-17
105128

106-
Big release. The setup story is much better, there's support for tracing multiple runtimes, you can emit your own events into the trace, and the viewer is its own crate now.
129+
Big release. The setup story is much better, there's support for tracing multiple runtimes, you can emit your own events into the trace, and the viewer is its own crate now.
107130

108131
### `#[dial9_tokio_telemetry::main]` macro ([#212](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/212))
109132

@@ -227,6 +250,7 @@ Uncompressed trace files can be concatenated (`cat trace.0.bin trace.1.bin > com
227250
## [0.2.0](https://github.com/dial9-rs/dial9-tokio-telemetry/compare/dial9-tokio-telemetry-v0.1.1...dial9-tokio-telemetry-v0.2.0) - 2026-03-20
228251

229252
0.2.0 brings two major improvements:
253+
230254
1. Support for publishing traces to S3
231255
2. Migration to the new trace format (dial9-trace-format). This format is self describing, extremely compact, compressible and fast to write. This will set us up to easily add application level telemetry in the future.
232256

@@ -246,15 +270,15 @@ For setting it up in production applications, the new `.install(true/false)` met
246270
- Bring back support for locations in offline symbolization ([#110](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/110))
247271
- stop writing trailing garbage in gzip segments after graceful_shutdown ([#104](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/104))
248272
- Fix worker spin-loop on gzip-compressed and permanently failing segments ([#102](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/102))
249-
- *(trace_viewer)* update format name from TOKIOTRC to D9TF in landing screen ([#103](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/103))
250-
- *(js-decoder)* handle truncated frames gracefully, read symbol frames even if >= MAX_EVENTS ([#98](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/98))
273+
- _(trace_viewer)_ update format name from TOKIOTRC to D9TF in landing screen ([#103](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/103))
274+
- _(js-decoder)_ handle truncated frames gracefully, read symbol frames even if >= MAX_EVENTS ([#98](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/98))
251275
- clarify S3 key layout is the default, not the only option ([#89](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/89))
252276
- add missing crates.io metadata ([#84](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/84))
253277
- thread-local buffer not flushing on drop ([#54](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/54))
254278

255279
### Other
256280

257-
- *(trace-parser)* consolidate per-branch cap checks into early continue ([#116](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/116))
281+
- _(trace-parser)_ consolidate per-branch cap checks into early continue ([#116](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/116))
258282
- fix flaky worker park test ([#117](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/117))
259283
- Harden flush path with ArrayQueue & emit metrics ([#97](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/97))
260284
- Update demo trace to have symbols ([#105](https://github.com/dial9-rs/dial9-tokio-telemetry/pull/105))

dial9-macro/src/lib.rs

Lines changed: 149 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@ use proc_macro::TokenStream;
22
use proc_macro2::TokenStream as TokenStream2;
33
use quote::quote;
44
use syn::parse::{Parse, ParseStream};
5-
use syn::{ItemFn, Path, Token, parse_macro_input};
5+
use syn::{ExprClosure, ItemFn, Path, Token, parse_macro_input};
6+
7+
enum ConfigSource {
8+
Path(Path),
9+
Closure(ExprClosure),
10+
}
611

712
struct MainArgs {
8-
config: Path,
13+
config: ConfigSource,
914
}
1015

11-
const MISSING_CONFIG_HELP: &str = "missing required `config = <fn>` argument, \
12-
e.g. #[dial9_tokio_telemetry::main(config = my_config)]";
16+
const MISSING_CONFIG_HELP: &str = "missing required `config` argument, e.g.\n \
17+
#[dial9_tokio_telemetry::main(config = my_config_fn)]\n\
18+
or with an inline closure:\n \
19+
#[dial9_tokio_telemetry::main(config = || Dial9Config::builder().base_path(...).max_file_size(...).max_total_size(...).build().unwrap())]";
1320

14-
const CONFIG_MUST_BE_ZERO_ARG_HELP: &str = "`config` must be a path to a zero-argument function, \
15-
e.g. #[dial9_tokio_telemetry::main(config = my_config)]";
21+
const CONFIG_MUST_BE_ZERO_ARG_HELP: &str = "`config` must be a zero-argument function path or a zero-argument closure, e.g.\n \
22+
#[dial9_tokio_telemetry::main(config = my_config_fn)]\n\
23+
or with an inline closure:\n \
24+
#[dial9_tokio_telemetry::main(config = || Dial9Config::builder().base_path(...).max_file_size(...).max_total_size(...).build().unwrap())]";
1625
impl Parse for MainArgs {
1726
fn parse(input: ParseStream) -> syn::Result<Self> {
1827
if input.is_empty() {
@@ -23,7 +32,20 @@ impl Parse for MainArgs {
2332
return Err(syn::Error::new(ident.span(), MISSING_CONFIG_HELP));
2433
}
2534
input.parse::<Token![=]>()?;
26-
let config: Path = input.parse()?;
35+
36+
let config = if input.peek(Token![|]) || input.peek(Token![move]) {
37+
let closure: ExprClosure = input.parse()?;
38+
if !closure.inputs.is_empty() {
39+
return Err(syn::Error::new_spanned(
40+
&closure.inputs,
41+
CONFIG_MUST_BE_ZERO_ARG_HELP,
42+
));
43+
}
44+
ConfigSource::Closure(closure)
45+
} else {
46+
ConfigSource::Path(input.parse()?)
47+
};
48+
2749
if !input.is_empty() {
2850
return Err(input.error(CONFIG_MUST_BE_ZERO_ARG_HELP));
2951
}
@@ -60,7 +82,10 @@ fn expand_main(args: MainArgs, input: ItemFn) -> Result<TokenStream2, syn::Error
6082
));
6183
}
6284

63-
let config_fn = &args.config;
85+
let config_call = match &args.config {
86+
ConfigSource::Path(p) => quote! { #p() },
87+
ConfigSource::Closure(c) => quote! { (#c)() },
88+
};
6489
let attrs = &input.attrs;
6590
let vis = &input.vis;
6691
let name = &input.sig.ident;
@@ -70,23 +95,8 @@ fn expand_main(args: MainArgs, input: ItemFn) -> Result<TokenStream2, syn::Error
7095
Ok(quote! {
7196
#(#attrs)*
7297
#vis fn #name() #ret {
73-
let (__tokio_runtime, __maybe_guard) = #config_fn()
74-
.build()
75-
.expect("failed to initialize runtime");
76-
if let Some(__dial9_guard) = __maybe_guard {
77-
let __dial9_handle = __dial9_guard.handle();
78-
__tokio_runtime.block_on(async move {
79-
match __dial9_handle.spawn(async move { #(#body_stmts)* }).await {
80-
Ok(output) => output,
81-
Err(err) if err.is_panic() => {
82-
::std::panic::resume_unwind(err.into_panic())
83-
}
84-
Err(_) => unreachable!("task cannot be cancelled inside block_on"),
85-
}
86-
})
87-
} else {
88-
__tokio_runtime.block_on(async move { #(#body_stmts)* })
89-
}
98+
let __dial9_rt = ::dial9_tokio_telemetry::TracedRuntime::new(#config_call);
99+
__dial9_rt.block_on(async move { #(#body_stmts)* })
90100
}
91101
})
92102
}
@@ -105,18 +115,38 @@ fn expand_main(args: MainArgs, input: ItemFn) -> Result<TokenStream2, syn::Error
105115
///
106116
/// # Arguments
107117
///
108-
/// * `config` — path to a zero-argument function returning [`Dial9Config`].
109-
/// Build one with [`Dial9ConfigBuilder::new`] (telemetry enabled) or
110-
/// [`Dial9ConfigBuilder::disabled`] (plain tokio, no telemetry).
118+
/// * `config` — a zero-argument function path or a zero-argument closure
119+
/// returning any value convertible into a `TracedRuntime`. In
120+
/// practice that means one of:
121+
/// - [`Dial9Config`] from `Dial9Config::builder().build()` (strict):
122+
/// any builder validation or writer-I/O failure surfaces from
123+
/// `.build()` as a `Dial9ConfigBuilderError`; runtime construction
124+
/// under the macro panics on tokio-builder or telemetry-core I/O.
125+
/// - [`Dial9Config`] from `Dial9Config::builder().build_or_disabled()`
126+
/// (lenient): the same `Dial9Config` type, but validation and
127+
/// writer-I/O failures are logged at `error!` and downgraded to a
128+
/// disabled config that still preserves your `with_tokio`
129+
/// configurators.
130+
/// - The deprecated positional `dial9_tokio_telemetry::config::Dial9Config`,
131+
/// kept compatible via a bridge impl.
132+
///
133+
/// Use `.enabled(false)` on the builder to run without telemetry
134+
/// while keeping your `with_tokio` configurators.
135+
///
136+
/// # Examples
111137
///
112-
/// # Example
138+
/// Using a named function:
113139
///
114140
/// ```rust,ignore
115-
/// use dial9_tokio_telemetry::{main, config::{Dial9Config, Dial9ConfigBuilder}, telemetry::TelemetryHandle};
141+
/// use dial9_tokio_telemetry::{main, Dial9Config, telemetry::TelemetryHandle};
116142
///
117143
/// fn my_config() -> Dial9Config {
118-
/// Dial9ConfigBuilder::new("/tmp/trace.bin", 1024 * 1024, 16 * 1024 * 1024)
144+
/// Dial9Config::builder()
145+
/// .base_path("/tmp/trace.bin")
146+
/// .max_file_size(1024 * 1024)
147+
/// .max_total_size(16 * 1024 * 1024)
119148
/// .build()
149+
/// .expect("config build failed")
120150
/// }
121151
///
122152
/// #[dial9_tokio_telemetry::main(config = my_config)]
@@ -128,6 +158,53 @@ fn expand_main(args: MainArgs, input: ItemFn) -> Result<TokenStream2, syn::Error
128158
/// .unwrap();
129159
/// }
130160
/// ```
161+
///
162+
/// Using an inline closure:
163+
///
164+
/// ```rust,ignore
165+
/// #[dial9_tokio_telemetry::main(config = || {
166+
/// Dial9Config::builder()
167+
/// .base_path("/tmp/trace.bin")
168+
/// .max_file_size(1024 * 1024)
169+
/// .max_total_size(16 * 1024 * 1024)
170+
/// .build()
171+
/// .expect("config build failed")
172+
/// })]
173+
/// async fn main() {
174+
/// /* ... */
175+
/// }
176+
/// ```
177+
///
178+
/// Lenient (telemetry is best-effort; falls back to a plain tokio
179+
/// runtime if writer setup fails):
180+
///
181+
/// ```rust,ignore
182+
/// #[dial9_tokio_telemetry::main(config = || {
183+
/// Dial9Config::builder()
184+
/// .base_path("/tmp/trace.bin")
185+
/// .max_file_size(1024 * 1024)
186+
/// .max_total_size(16 * 1024 * 1024)
187+
/// .build_or_disabled()
188+
/// })]
189+
/// async fn main() {
190+
/// /* ... */
191+
/// }
192+
/// ```
193+
///
194+
/// Disabled (no telemetry, plain tokio runtime — useful for toggling
195+
/// dial9 off via a feature flag or env var without removing the macro):
196+
///
197+
/// ```rust,ignore
198+
/// #[dial9_tokio_telemetry::main(config = || {
199+
/// Dial9Config::builder()
200+
/// .enabled(false)
201+
/// .build()
202+
/// .expect("config build failed")
203+
/// })]
204+
/// async fn main() {
205+
/// /* ... */
206+
/// }
207+
/// ```
131208
#[proc_macro_attribute]
132209
pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
133210
let args = parse_macro_input!(attr as MainArgs);
@@ -235,31 +312,63 @@ mod tests {
235312
#[test]
236313
fn error_empty_args() {
237314
let msg = parse_args_err(quote! {});
238-
assert!(msg.contains("config = <fn>"), "unexpected error: {msg}");
315+
assert!(
316+
msg.contains("missing required `config`"),
317+
"unexpected error: {msg}"
318+
);
239319
}
240320

241321
#[test]
242322
fn error_wrong_arg_name() {
243323
let msg = parse_args_err(quote! { foo = bar });
244-
assert!(msg.contains("config = <fn>"), "unexpected error: {msg}");
324+
assert!(
325+
msg.contains("missing required `config`"),
326+
"unexpected error: {msg}"
327+
);
245328
}
246329

247330
#[test]
248331
fn error_config_with_args() {
249332
let msg = parse_args_err(quote! { config = my_config(arg) });
250-
assert!(
251-
msg.contains("zero-argument function"),
252-
"unexpected error: {msg}"
253-
);
333+
assert!(msg.contains("zero-argument"), "unexpected error: {msg}");
254334
}
255335

256336
#[test]
257337
fn error_config_trailing_tokens() {
258338
let msg = parse_args_err(quote! { config = my_config, extra = stuff });
259-
assert!(
260-
msg.contains("zero-argument function"),
261-
"unexpected error: {msg}"
339+
assert!(msg.contains("zero-argument"), "unexpected error: {msg}");
340+
}
341+
342+
#[test]
343+
fn expand_with_inline_closure() {
344+
let output = expand(
345+
quote! { config = || my_config() },
346+
quote! {
347+
async fn main() {
348+
do_work().await;
349+
}
350+
},
262351
);
352+
insta::assert_snapshot!(output);
353+
}
354+
355+
#[test]
356+
fn expand_with_move_closure() {
357+
let output = expand(
358+
quote! { config = move || my_config() },
359+
quote! {
360+
async fn main() {
361+
do_work().await;
362+
}
363+
},
364+
);
365+
insta::assert_snapshot!(output);
366+
}
367+
368+
#[test]
369+
fn error_closure_with_args() {
370+
let msg = parse_args_err(quote! { config = |x| my_config() });
371+
assert!(msg.contains("zero-argument"), "unexpected error: {msg}");
263372
}
264373

265374
#[test]

dial9-macro/src/snapshots/dial9_macro__tests__expand_basic.snap

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,9 @@ source: dial9-macro/src/lib.rs
33
expression: output
44
---
55
fn main() {
6-
let (__tokio_runtime, __maybe_guard) = my_config()
7-
.build()
8-
.expect("failed to initialize runtime");
9-
if let Some(__dial9_guard) = __maybe_guard {
10-
let __dial9_handle = __dial9_guard.handle();
11-
__tokio_runtime
12-
.block_on(async move {
13-
match __dial9_handle
14-
.spawn(async move {
15-
do_work().await;
16-
})
17-
.await
18-
{
19-
Ok(output) => output,
20-
Err(err) if err.is_panic() => {
21-
::std::panic::resume_unwind(err.into_panic())
22-
}
23-
Err(_) => unreachable!("task cannot be cancelled inside block_on"),
24-
}
25-
})
26-
} else {
27-
__tokio_runtime
28-
.block_on(async move {
29-
do_work().await;
30-
})
31-
}
6+
let __dial9_rt = ::dial9_tokio_telemetry::TracedRuntime::new(my_config());
7+
__dial9_rt
8+
.block_on(async move {
9+
do_work().await;
10+
})
3211
}

0 commit comments

Comments
 (0)