micromegas_proc_macros/
lib.rs1use proc_macro2::TokenStream;
7use quote::quote;
8use syn::{Expr, ExprLit, ItemFn, Lit, Meta};
9
10#[proc_macro_attribute]
67pub fn micromegas_main(
68 args: proc_macro::TokenStream,
69 input: proc_macro::TokenStream,
70) -> proc_macro::TokenStream {
71 expand_micromegas_main(args.into(), input.into())
72 .unwrap_or_else(|err| err.to_compile_error())
73 .into()
74}
75
76fn expand_micromegas_main(
77 args: TokenStream,
78 input: TokenStream,
79) -> Result<TokenStream, syn::Error> {
80 use syn::parse::Parser;
81
82 let args: Vec<Meta> = syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated
83 .parse2(args)?
84 .into_iter()
85 .collect();
86
87 let function: ItemFn = syn::parse2(input)?;
88
89 if function.sig.asyncness.is_none() {
90 return Err(syn::Error::new_spanned(
91 function.sig.fn_token,
92 "micromegas_main can only be applied to async functions",
93 ));
94 }
95
96 if function.sig.ident != "main" {
97 return Err(syn::Error::new_spanned(
98 &function.sig.ident,
99 "micromegas_main can only be applied to the main function",
100 ));
101 }
102
103 let mut interop_max_level: Option<syn::LitStr> = None;
104 let mut max_level_override: Option<syn::LitStr> = None;
105 let mut ctrlc_handling: bool = true;
106 let mut local_sink_enabled: bool = true;
107 let mut local_sink_max_level: Option<syn::LitStr> = None;
108 let mut install_log_capture: bool = false;
109 let mut system_metrics: bool = true;
110 let mut telemetry_url: Option<String> = None;
111 let mut api_key: Option<String> = None;
112
113 for arg in args {
114 match arg {
115 Meta::NameValue(nv) if nv.path.is_ident("interop_max_level") => {
116 if let Expr::Lit(ExprLit {
117 lit: Lit::Str(lit_str),
118 ..
119 }) = &nv.value
120 {
121 interop_max_level = Some(lit_str.clone());
122 } else {
123 return Err(syn::Error::new_spanned(
124 &nv.value,
125 "interop_max_level must be a string literal",
126 ));
127 }
128 }
129 Meta::NameValue(nv) if nv.path.is_ident("max_level_override") => {
130 if let Expr::Lit(ExprLit {
131 lit: Lit::Str(lit_str),
132 ..
133 }) = &nv.value
134 {
135 max_level_override = Some(lit_str.clone());
136 } else {
137 return Err(syn::Error::new_spanned(
138 &nv.value,
139 "max_level_override must be a string literal",
140 ));
141 }
142 }
143 Meta::NameValue(nv) if nv.path.is_ident("ctrlc_handling") => {
144 if let Expr::Lit(ExprLit {
145 lit: Lit::Bool(lit_bool),
146 ..
147 }) = &nv.value
148 {
149 ctrlc_handling = lit_bool.value();
150 } else {
151 return Err(syn::Error::new_spanned(
152 &nv.value,
153 "ctrlc_handling must be a bool literal",
154 ));
155 }
156 }
157 Meta::NameValue(nv) if nv.path.is_ident("local_sink_enabled") => {
158 if let Expr::Lit(ExprLit {
159 lit: Lit::Bool(lit_bool),
160 ..
161 }) = &nv.value
162 {
163 local_sink_enabled = lit_bool.value();
164 } else {
165 return Err(syn::Error::new_spanned(
166 &nv.value,
167 "local_sink_enabled must be a bool literal",
168 ));
169 }
170 }
171 Meta::NameValue(nv) if nv.path.is_ident("local_sink_max_level") => {
172 if let Expr::Lit(ExprLit {
173 lit: Lit::Str(lit_str),
174 ..
175 }) = &nv.value
176 {
177 local_sink_max_level = Some(lit_str.clone());
178 } else {
179 return Err(syn::Error::new_spanned(
180 &nv.value,
181 "local_sink_max_level must be a string literal",
182 ));
183 }
184 }
185 Meta::NameValue(nv) if nv.path.is_ident("install_log_capture") => {
186 if let Expr::Lit(ExprLit {
187 lit: Lit::Bool(lit_bool),
188 ..
189 }) = &nv.value
190 {
191 install_log_capture = lit_bool.value();
192 } else {
193 return Err(syn::Error::new_spanned(
194 &nv.value,
195 "install_log_capture must be a bool literal",
196 ));
197 }
198 }
199 Meta::NameValue(nv) if nv.path.is_ident("system_metrics") => {
200 if let Expr::Lit(ExprLit {
201 lit: Lit::Bool(lit_bool),
202 ..
203 }) = &nv.value
204 {
205 system_metrics = lit_bool.value();
206 } else {
207 return Err(syn::Error::new_spanned(
208 &nv.value,
209 "system_metrics must be a bool literal",
210 ));
211 }
212 }
213 Meta::NameValue(nv) if nv.path.is_ident("telemetry_url") => {
214 if let Expr::Lit(ExprLit {
215 lit: Lit::Str(lit_str),
216 ..
217 }) = &nv.value
218 {
219 telemetry_url = Some(lit_str.value());
220 } else {
221 return Err(syn::Error::new_spanned(
222 &nv.value,
223 "telemetry_url must be a string literal",
224 ));
225 }
226 }
227 Meta::NameValue(nv) if nv.path.is_ident("api_key") => {
228 if let Expr::Lit(ExprLit {
229 lit: Lit::Str(lit_str),
230 ..
231 }) = &nv.value
232 {
233 api_key = Some(lit_str.value());
234 } else {
235 return Err(syn::Error::new_spanned(
236 &nv.value,
237 "api_key must be a string literal",
238 ));
239 }
240 }
241 other => {
242 return Err(syn::Error::new_spanned(
243 &other,
244 "Unsupported attribute argument. Supported: api_key, ctrlc_handling, install_log_capture, interop_max_level, local_sink_enabled, local_sink_max_level, max_level_override, system_metrics, telemetry_url",
245 ));
246 }
247 }
248 }
249
250 let original_block = &function.block;
251 let return_type = &function.sig.output;
252
253 let level_to_filter = |lit: &syn::LitStr| -> Result<TokenStream, syn::Error> {
254 Ok(match lit.value().to_lowercase().as_str() {
255 "trace" => quote! { micromegas::tracing::levels::LevelFilter::Trace },
256 "debug" => quote! { micromegas::tracing::levels::LevelFilter::Debug },
257 "info" => quote! { micromegas::tracing::levels::LevelFilter::Info },
258 "warn" => quote! { micromegas::tracing::levels::LevelFilter::Warn },
259 "error" => quote! { micromegas::tracing::levels::LevelFilter::Error },
260 "off" => quote! { micromegas::tracing::levels::LevelFilter::Off },
261 _ => {
262 return Err(syn::Error::new_spanned(
263 lit,
264 "Invalid level value. Must be one of: trace, debug, info, warn, error, off",
265 ));
266 }
267 })
268 };
269
270 let mut builder_calls = vec![quote! {
271 .with_process_property("version".to_string(), env!("CARGO_PKG_VERSION").to_string())
272 }];
273
274 if ctrlc_handling {
275 builder_calls.push(quote! { .with_ctrlc_handling() });
276 }
277
278 if !local_sink_enabled {
279 builder_calls.push(quote! { .with_local_sink_enabled(false) });
280 }
281
282 {
283 let level_filter = match &local_sink_max_level {
284 Some(lit) => level_to_filter(lit)?,
285 None => quote! { micromegas::tracing::levels::LevelFilter::Debug },
286 };
287 builder_calls.push(quote! { .with_local_sink_max_level(#level_filter) });
288 }
289
290 if install_log_capture {
291 builder_calls.push(quote! { .with_install_log_capture(true) });
292 }
293
294 if !system_metrics {
295 builder_calls.push(quote! { .with_system_metrics_enabled(false) });
296 }
297
298 if let Some(url) = telemetry_url {
299 builder_calls.push(quote! { .with_telemetry_sink_url(#url.to_string()) });
300 }
301
302 if let Some(key) = api_key {
303 builder_calls.push(quote! {
304 .with_request_decorator(std::boxed::Box::new(move || std::sync::Arc::new(
305 micromegas::telemetry_sink::api_key_decorator::ApiKeyRequestDecorator::new(#key.to_string())
306 )))
307 });
308 } else {
309 builder_calls.push(quote! { .with_auth_from_env() });
310 }
311
312 if let Some(lit) = &max_level_override {
313 let level_filter = level_to_filter(lit)?;
314 builder_calls.push(quote! { .with_max_level_override(#level_filter) });
315 }
316
317 if let Some(lit) = &interop_max_level {
318 let level_filter = level_to_filter(lit)?;
319 builder_calls.push(quote! { .with_interop_max_level_override(#level_filter) });
320 }
321
322 let telemetry_guard_builder = quote! {
323 micromegas::telemetry_sink::TelemetryGuardBuilder::default()
324 #(#builder_calls)*
325 .build()
326 };
327
328 Ok(quote! {
329 fn main() #return_type {
330 let cpu_tracing_enabled = std::env::var("MICROMEGAS_ENABLE_CPU_TRACING")
331 .map(|v| v == "true")
332 .unwrap_or(false);
333
334 let _telemetry_guard = #telemetry_guard_builder;
335
336 let runtime = {
337 use micromegas::tracing::runtime::TracingRuntimeExt;
338 let mut builder = tokio::runtime::Builder::new_multi_thread();
339 builder.enable_all();
340 builder.thread_name(env!("CARGO_PKG_NAME"));
341 if cpu_tracing_enabled {
342 builder.with_tracing_callbacks();
343 }
344 builder.build().expect("Failed to build tokio runtime")
345 };
346
347 runtime.block_on(async move {
348 #original_block
349 })
350 }
351 })
352}
353
354#[cfg(test)]
355mod tests {
356 use super::expand_micromegas_main;
357 use quote::quote;
358
359 fn expand(args: proc_macro2::TokenStream) -> String {
360 let input = quote! { async fn main() {} };
361 expand_micromegas_main(args, input)
362 .expect("expansion should succeed")
363 .to_string()
364 }
365
366 #[test]
367 fn default_produces_standard_calls() {
368 let out = expand(quote! {});
369 assert!(out.contains("with_auth_from_env"));
370 assert!(out.contains("with_ctrlc_handling"));
371 assert!(out.contains("with_local_sink_max_level"));
372 }
373
374 #[test]
375 fn api_key_replaces_env_auth() {
376 let out = expand(quote! { api_key = "secret" });
377 assert!(out.contains("ApiKeyRequestDecorator"));
378 assert!(!out.contains("with_auth_from_env"));
379 }
380
381 #[test]
382 fn ctrlc_handling_false_omits_call() {
383 let out = expand(quote! { ctrlc_handling = false });
384 assert!(!out.contains("with_ctrlc_handling"));
385 }
386
387 #[test]
388 fn telemetry_url_emits_call() {
389 let out = expand(quote! { telemetry_url = "http://localhost:9000" });
390 assert!(out.contains("with_telemetry_sink_url"));
391 }
392
393 #[test]
394 fn local_sink_disabled_emits_call() {
395 let out = expand(quote! { local_sink_enabled = false });
396 assert!(out.contains("with_local_sink_enabled"));
397 }
398
399 #[test]
400 fn system_metrics_false_emits_call() {
401 let out = expand(quote! { system_metrics = false });
402 assert!(out.contains("with_system_metrics_enabled"));
403 }
404
405 #[test]
406 fn install_log_capture_true_emits_call() {
407 let out = expand(quote! { install_log_capture = true });
408 assert!(out.contains("with_install_log_capture"));
409 }
410
411 #[test]
412 fn local_sink_max_level_custom_emits_correct_filter() {
413 let out = expand(quote! { local_sink_max_level = "info" });
414 assert!(out.contains("LevelFilter :: Info"));
415 }
416
417 fn expand_err(args: proc_macro2::TokenStream) -> syn::Error {
418 let input = quote! { async fn main() {} };
419 expand_micromegas_main(args, input).expect_err("expansion should fail")
420 }
421
422 #[test]
423 fn bad_ctrlc_type_is_error() {
424 let err = expand_err(quote! { ctrlc_handling = "not_a_bool" });
425 assert_eq!(err.to_string(), "ctrlc_handling must be a bool literal");
426 }
427
428 #[test]
429 fn unknown_arg_is_error() {
430 let err = expand_err(quote! { unknown_arg = true });
431 assert!(err.to_string().contains("Unsupported attribute argument"));
432 }
433
434 #[test]
435 fn invalid_level_is_error() {
436 let err = expand_err(quote! { max_level_override = "verbose" });
437 assert!(err.to_string().contains("Invalid level value"));
438 }
439
440 #[test]
441 fn non_async_fn_is_error() {
442 let err = expand_micromegas_main(quote! {}, quote! { fn main() {} })
443 .expect_err("non-async main should fail");
444 assert!(err.to_string().contains("async functions"));
445 }
446
447 #[test]
448 fn malformed_args_is_error() {
449 let err = expand_micromegas_main(quote! { = = = }, quote! { async fn main() {} })
451 .expect_err("malformed args should fail");
452 assert!(!err.to_string().is_empty());
454 }
455}