opentelemetry_auto_span/
lib.rs1mod dig;
2mod handle_sqlx;
3mod line;
4mod utils;
5
6use darling::ast::NestedMeta;
7use darling::{Error, FromMeta};
8use proc_macro2::{Ident, Span, TokenStream};
9use quote::{quote, quote_spanned};
10use syn::{
11 parse_macro_input, spanned::Spanned, visit_mut::VisitMut, Expr, ExprAwait, ExprClosure,
12 ExprTry, ItemFn, Signature,
13};
14
15use crate::{dig::find_source_path, line::LineAccess};
16
17#[derive(Default, FromMeta)]
18#[darling(default)]
19struct Opt {
20 pub debug: bool,
21}
22
23#[proc_macro_attribute]
24pub fn auto_span(
25 attr: proc_macro::TokenStream,
26 item: proc_macro::TokenStream,
27) -> proc_macro::TokenStream {
28 let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
29 Ok(v) => v,
30 Err(e) => {
31 return proc_macro::TokenStream::from(Error::from(e).write_errors());
32 }
33 };
34 let opt = match Opt::from_list(&attr_args) {
35 Ok(v) => v,
36 Err(e) => {
37 return proc_macro::TokenStream::from(e.write_errors());
38 }
39 };
40
41 let mut input = parse_macro_input!(item as ItemFn);
42
43 let mut dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
44 dir.push("src");
45 let line_access = find_source_path(dir, &input).map(LineAccess::new);
46 let mut visitor = AutoSpanVisitor::new(line_access);
47 visitor.visit_item_fn_mut(&mut input);
48
49 insert_function_span(&mut input);
50 let token = quote! {#input};
51
52 if opt.debug {
53 let mut target = std::path::PathBuf::from(
54 std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "/tmp".to_owned()),
55 );
56 target.push("auto-span");
57 std::fs::create_dir_all(&target).unwrap();
58 target.push(format!("{}.rs", input.sig.ident));
59 std::fs::write(&target, format!("{}", token)).unwrap();
60 }
61
62 token.into()
63}
64
65fn insert_function_span(i: &mut ItemFn) {
66 let def_tracer = quote! {
67 let __otel_auto_tracer = ::opentelemetry::global::tracer("");
68 };
69 let span_ident = Ident::new("span", Span::call_site());
70 let start_tracer = otel_start_tracer_token(&format!("fn:{}", i.sig.ident));
71 let ctx = otel_ctx_token(&span_ident);
72 let stmts = &i.block.stmts;
73 let tokens = if i.sig.asyncness.is_some() {
74 quote! {
75 #def_tracer
76 ::opentelemetry::trace::FutureExt::with_context(
77 async {#(#stmts)*},
78 {
79 let #span_ident = #start_tracer;
80 #ctx
81 }
82 ).await
83 }
84 } else {
85 quote! {
86 #def_tracer
87 let #span_ident = #start_tracer;
88 let __otel_auto_ctx = #ctx;
89 let __otel_auto_guard = __otel_auto_ctx.clone().attach();
90 #(#stmts)*
91 }
92 };
93 let body: Expr = syn::parse2(quote! {{#tokens}}).unwrap();
94 match body {
95 Expr::Block(block) => {
96 i.block.stmts = block.block.stmts;
97 }
98 _ => unreachable!(),
99 }
100}
101
102fn otel_start_tracer_token(name: &str) -> TokenStream {
103 quote! {
104 ::opentelemetry::trace::Tracer::start(&__otel_auto_tracer, #name)
105 }
106}
107
108fn otel_ctx_token(span_ident: &Ident) -> TokenStream {
109 quote! {
110 <::opentelemetry::Context as ::opentelemetry::trace::TraceContextExt>::current_with_span(#span_ident)
111 }
112}
113
114struct AutoSpanVisitor {
115 line_access: Option<LineAccess>,
116 context: Vec<ReturnTypeContext>,
117}
118
119#[derive(Copy, Clone)]
120enum ReturnTypeContext {
121 Unknown,
122 Result,
123 Option,
124}
125
126impl AutoSpanVisitor {
127 fn new(line_access: Option<LineAccess>) -> AutoSpanVisitor {
128 AutoSpanVisitor {
129 line_access,
130 context: Vec::new(),
131 }
132 }
133
134 fn push_fn_context(&mut self, sig: &Signature) {
135 let rt = match &sig.output {
136 syn::ReturnType::Default => ReturnTypeContext::Unknown,
137 syn::ReturnType::Type(_, ty) => match ty.as_ref() {
138 syn::Type::Path(path) => {
139 let name = path.path.segments.last().unwrap().ident.to_string();
140 if name.contains("Result") {
141 ReturnTypeContext::Result
142 } else if name.contains("Option") {
143 ReturnTypeContext::Option
144 } else {
145 ReturnTypeContext::Unknown
146 }
147 }
148 _ => ReturnTypeContext::Unknown,
149 },
150 };
151 self.context.push(rt);
152 }
153
154 pub fn push_closure_context(&mut self) {
155 self.context.push(ReturnTypeContext::Unknown);
156 }
157
158 pub fn pop_context(&mut self) {
159 self.context.pop();
160 }
161
162 pub fn current_context(&self) -> ReturnTypeContext {
163 *self.context.last().unwrap()
164 }
165
166 fn handle_sqlx(&self, expr_await: &mut ExprAwait) -> bool {
167 let mut visitor = handle_sqlx::SqlxVisitor::new();
168 visitor.visit_expr_await_mut(expr_await);
169 visitor.is_mutate()
170 }
171
172 fn get_line_info(&self, span: Span) -> Option<(i64, String)> {
173 self.line_access.as_ref().and_then(|la| la.span(span))
174 }
175
176 fn span_ident(&self) -> Ident {
177 Ident::new("__otel_auto_span", Span::call_site())
178 }
179}
180
181fn add_line_info(tokens: &mut TokenStream, span_ident: &Ident, line_info: Option<(i64, String)>) {
182 if let Some((lineno, line)) = line_info {
183 tokens.extend(quote! {
184 #span_ident.set_attribute(::opentelemetry::KeyValue::new("code.lineno", #lineno));
185 #span_ident.set_attribute(::opentelemetry::KeyValue::new("code.line", #line));
186 });
187 }
188}
189
190impl VisitMut for AutoSpanVisitor {
191 fn visit_expr_mut(&mut self, i: &mut Expr) {
192 let span = i.span();
193
194 let span_ident = self.span_ident();
195 let new_span = |name, line_info, expr| {
196 let start_tracer = otel_start_tracer_token(name);
197 let current_with_span = otel_ctx_token(&span_ident);
198 let mut tokens = quote! {
199 #[allow(unused_import)]
200 use ::opentelemetry::trace::{Span as _};
201 #[allow(unused_mut)]
202 let mut #span_ident = #start_tracer;
203 };
204 add_line_info(&mut tokens, &span_ident, line_info);
205 let tokens = quote_spanned! {
206 span => {
207 ::opentelemetry::trace::FutureExt::with_context(
208 async { #expr },
209 {
210 #tokens
211 #current_with_span
212 }
213 ).await
214 }
215 };
216 syn::parse2(tokens).unwrap()
217 };
218
219 match i {
220 Expr::Await(expr) => {
221 if self.handle_sqlx(expr) {
222 *i = new_span("db", self.get_line_info(span), expr);
223 } else {
224 syn::visit_mut::visit_expr_await_mut(self, expr);
225 }
226 }
227 _ => syn::visit_mut::visit_expr_mut(self, i),
228 };
229 }
230
231 fn visit_expr_closure_mut(&mut self, i: &mut ExprClosure) {
232 self.push_closure_context();
233 syn::visit_mut::visit_expr_closure_mut(self, i);
234 self.pop_context();
235 }
236
237 fn visit_expr_try_mut(&mut self, i: &mut ExprTry) {
238 syn::visit_mut::visit_expr_try_mut(self, i);
239
240 if let ReturnTypeContext::Result = self.current_context() {
241 let span_ident = self.span_ident();
242 let span = i.expr.span();
243 let inner = i.expr.as_ref();
244 let mut tokens = quote! {
245 #span_ident.set_status(::opentelemetry::trace::Status::error(format!("{}", e)));
246 };
247 add_line_info(&mut tokens, &span_ident, self.get_line_info(span));
248 i.expr = Box::new(
249 syn::parse2(quote_spanned! {
250 span => #inner.map_err(|e| {
251 ::opentelemetry::trace::get_active_span(|__otel_auto_span| {
252 #tokens
253 });
254 e
255 })
256 })
257 .unwrap(),
258 );
259 }
260 }
261
262 fn visit_item_fn_mut(&mut self, i: &mut ItemFn) {
263 self.push_fn_context(&i.sig);
264 if self.context.len() == 1 {
265 syn::visit_mut::visit_item_fn_mut(self, i);
267 }
268 self.pop_context();
269 }
270}