grafana_plugin_sdk_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3
4use darling::{FromDeriveInput, FromMeta};
5use quote::{quote, quote_spanned, ToTokens};
6use syn::{meta::ParseNestedMeta, parenthesized, parse::ParseBuffer, parse_macro_input, Lit};
7
8#[derive(Debug, FromMeta)]
9enum PluginType {
10    Datasource,
11    App,
12}
13
14#[derive(Debug, FromDeriveInput)]
15#[darling(attributes(grafana_plugin))]
16struct GrafanaPluginOpts {
17    ident: syn::Ident,
18    plugin_type: PluginType,
19    json_data: Option<syn::Path>,
20    secure_json_data: Option<syn::Path>,
21}
22
23#[doc(hidden)]
24#[allow(missing_docs)]
25#[proc_macro_derive(GrafanaPlugin, attributes(grafana_plugin))]
26pub fn derive(input: TokenStream) -> TokenStream {
27    let ast: syn::DeriveInput = syn::parse(input).expect("Couldn't parse item");
28    let GrafanaPluginOpts {
29        ident,
30        plugin_type,
31        json_data,
32        secure_json_data,
33    } = match GrafanaPluginOpts::from_derive_input(&ast) {
34        Ok(x) => x,
35        Err(e) => return e.flatten().write_errors().into(),
36    };
37    let ptype = match plugin_type {
38        PluginType::App => {
39            quote! { ::grafana_plugin_sdk::backend::AppPlugin<Self::JsonData, Self::SecureJsonData> }
40        }
41        PluginType::Datasource => {
42            quote! { ::grafana_plugin_sdk::backend::DataSourcePlugin<Self::JsonData, Self::SecureJsonData> }
43        }
44    };
45    let json_data =
46        json_data.unwrap_or_else(|| syn::parse_quote!(::grafana_plugin_sdk::serde_json::Value));
47    let secure_json_data = secure_json_data
48        .unwrap_or_else(|| syn::parse_quote!(::grafana_plugin_sdk::serde_json::Value));
49    quote! {
50        impl ::grafana_plugin_sdk::backend::GrafanaPlugin for #ident {
51
52            type PluginType = #ptype;
53            type JsonData = #json_data;
54            type SecureJsonData = #secure_json_data;
55        }
56    }
57    .into()
58}
59
60fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream {
61    tokens.extend(TokenStream::from(error.into_compile_error()));
62    tokens
63}
64
65#[derive(Default)]
66struct Configuration {
67    services: Option<Services>,
68    init_subscriber: Option<bool>,
69    shutdown_handler: Option<String>,
70}
71
72impl Configuration {
73    fn set_services(&mut self, meta: ParseNestedMeta) -> Result<(), syn::Error> {
74        if self.services.is_some() {
75            return Err(meta.error("`services` set multiple times."));
76        }
77        let mut cfg_services = Services::default();
78        meta.parse_nested_meta(|meta| {
79            if meta.path.is_ident("data") {
80                if cfg_services.data {
81                    return Err(meta.error("`data` set multiple times."));
82                }
83                cfg_services.data = true;
84                Ok(())
85            } else if meta.path.is_ident("diagnostics") {
86                if cfg_services.diagnostics {
87                    return Err(meta.error("`diagnostics` set multiple times."));
88                }
89                cfg_services.diagnostics = true;
90                Ok(())
91            } else if meta.path.is_ident("resource") {
92                if cfg_services.resource {
93                    return Err(meta.error("`resource` set multiple times."));
94                }
95                cfg_services.resource = true;
96                Ok(())
97            } else if meta.path.is_ident("stream") {
98                if cfg_services.stream {
99                    return Err(meta.error("`stream` set multiple times."));
100                }
101                cfg_services.stream = true;
102                Ok(())
103            } else {
104                Err(meta.error(
105                    "Unknown service. Only `data`, `diagnostics`, `resource` and `stream` are supported.",
106                ))
107            }
108        })?;
109        if !cfg_services.data
110            && !cfg_services.diagnostics
111            && !cfg_services.resource
112            && !cfg_services.stream
113        {
114            return Err(meta.error("At least one service must be specified in `services`."));
115        }
116
117        self.services = Some(cfg_services);
118        Ok(())
119    }
120
121    fn get_accidental_nested_meta(input: &ParseBuffer) -> Result<Lit, syn::Error> {
122        let content;
123        parenthesized!(content in input);
124        let x: Lit = content.parse()?;
125        Ok(x)
126    }
127
128    fn set_init_subscriber(&mut self, init_subscriber: ParseNestedMeta) -> Result<(), syn::Error> {
129        if self.init_subscriber.is_some() {
130            return Err(init_subscriber.error("`init_subscriber` set multiple times."));
131        }
132        let value = init_subscriber.value().map_err(|_| {
133            init_subscriber.error(format!(
134                "`init_subscriber` should be specified as `init_subscriber = {}`",
135                Self::get_accidental_nested_meta(init_subscriber.input)
136                    .ok()
137                    .and_then(|x| if let Lit::Bool(b) = x {
138                        Some(b.value)
139                    } else {
140                        None
141                    })
142                    .unwrap_or(true)
143            ))
144        })?;
145        let s: syn::LitBool = value
146            .parse()
147            .map_err(|e| syn::Error::new(e.span(), "`init_subscriber` must be a bool literal."))?;
148        self.init_subscriber = Some(s.value);
149        Ok(())
150    }
151
152    fn set_shutdown_handler(
153        &mut self,
154        shutdown_handler: ParseNestedMeta,
155    ) -> Result<(), syn::Error> {
156        if self.shutdown_handler.is_some() {
157            return Err(shutdown_handler.error("`shutdown_handler` set multiple times."));
158        }
159        let value = shutdown_handler.value().map_err(|_| {
160            let address = Self::get_accidental_nested_meta(shutdown_handler.input)
161                .ok()
162                .and_then(|x| {
163                    if let Lit::Str(s) = x {
164                        Some(s.value())
165                    } else {
166                        None
167                    }
168                })
169                .unwrap_or_else(|| "<address>".to_string());
170            shutdown_handler.error(format!(
171                r#"`shutdown_handler` should be specified as `shutdown_handler = "{}""#,
172                address
173            ))
174        })?;
175        let s: syn::LitStr = value.parse().map_err(|e| {
176            syn::Error::new(e.span(), "`shutdown_handler` must be a string literal.")
177        })?;
178        self.shutdown_handler = Some(s.value());
179        Ok(())
180    }
181
182    fn parse(&mut self, meta: ParseNestedMeta) -> Result<(), syn::Error> {
183        if meta.path.is_ident("init_subscriber") {
184            self.set_init_subscriber(meta)?;
185        } else if meta.path.is_ident("shutdown_handler") {
186            self.set_shutdown_handler(meta)?;
187        } else if meta.path.is_ident("services") {
188            self.set_services(meta)?;
189        } else {
190            return Err(meta.error("Unknown attribute. Only `services`, `init_subscriber` and `shutdown_handler` are supported."));
191        }
192        Ok(())
193    }
194
195    fn build(self) -> FinalConfig {
196        FinalConfig {
197            services: self.services.unwrap(),
198            init_subscriber: self.init_subscriber.unwrap_or_default(),
199            shutdown_handler: self.shutdown_handler,
200        }
201    }
202}
203
204#[derive(Default)]
205struct Services {
206    stream: bool,
207    data: bool,
208    diagnostics: bool,
209    resource: bool,
210}
211
212#[derive(Default)]
213struct FinalConfig {
214    services: Services,
215    init_subscriber: bool,
216    shutdown_handler: Option<String>,
217}
218
219/// Config used in case of the attribute not being able to build a valid config
220const DEFAULT_ERROR_CONFIG: FinalConfig = FinalConfig {
221    services: Services {
222        stream: false,
223        data: false,
224        diagnostics: false,
225        resource: false,
226    },
227    init_subscriber: false,
228    shutdown_handler: None,
229};
230
231fn parse_knobs(input: syn::ItemFn, config: FinalConfig) -> TokenStream {
232    // If type mismatch occurs, the current rustc points to the last statement.
233    let (last_stmt_start_span, _) = {
234        let mut last_stmt = input
235            .block
236            .stmts
237            .last()
238            .map(ToTokens::into_token_stream)
239            .unwrap_or_default()
240            .into_iter();
241        // `Span` on stable Rust has a limitation that only points to the first
242        // token, not the whole tokens. We can work around this limitation by
243        // using the first/last span of the tokens like
244        // `syn::Error::new_spanned` does.
245        let start = last_stmt.next().map_or_else(Span::call_site, |t| t.span());
246        let end = last_stmt.last().map_or(start, |t| t.span());
247        (start, end)
248    };
249
250    let body = input.block;
251
252    let mut plugin = quote_spanned! {last_stmt_start_span=>
253        ::grafana_plugin_sdk::backend::Plugin::new()
254    };
255    if config.services.data {
256        plugin = quote! { #plugin.data_service(std::clone::Clone::clone(&service)) };
257    }
258    if config.services.diagnostics {
259        plugin = quote! { #plugin.diagnostics_service(std::clone::Clone::clone(&service)) };
260    }
261    if config.services.resource {
262        plugin = quote! { #plugin.resource_service(std::clone::Clone::clone(&service)) };
263    }
264    if config.services.stream {
265        plugin = quote! { #plugin.stream_service(std::clone::Clone::clone(&service)) };
266    }
267    let init_subscriber = config.init_subscriber;
268    if init_subscriber {
269        plugin = quote! { #plugin.init_subscriber(#init_subscriber) };
270    }
271    if let Some(x) = config.shutdown_handler {
272        let shutdown_handler =
273            quote! { #x.parse().expect("could not parse shutdown handler as SocketAddr") };
274        plugin = quote! { #plugin.shutdown_handler(#shutdown_handler) };
275    }
276
277    let expanded = quote! {
278        fn main() -> Result<(), Box<dyn std::error::Error>> {
279            let fut = async {
280                let listener = ::grafana_plugin_sdk::backend::initialize().await?;
281                let service = #body;
282                #plugin
283                    .start(listener)
284                    .await?;
285                Ok::<_, Box<dyn std::error::Error>>(())
286            };
287            ::grafana_plugin_sdk::async_main(fut)?;
288            Ok(())
289        }
290    };
291    TokenStream::from(expanded)
292}
293
294/**
295Generates a `main` function that starts a [`Plugin`](../grafana_plugin_sdk/backend/struct.Plugin.html) with the returned service struct.
296
297When applied to a function that returns a struct implementing one or more of the various
298`Service` traits, `#[main]` will create an async runtime and a [`Plugin`](../grafana_plugin_sdk/backend/struct.Plugin.html),
299then attach the desired services
300
301The returned struct _must_ be `Clone` so that it can be used to handle multiple
302services.
303
304# Attributes
305
306## `services`
307
308The `services` attribute takes a list of services that the plugin should expose.
309At least one service must be specified. Possible options are:
310
311- `data` (registers a [`DataService`](../grafana_plugin_sdk/backend/trait.DataService.html) using [`Plugin::data_service`](../grafana_plugin_sdk/backend/struct.Plugin.html#method.data_service))
312- `diagnostics` (registers a [`DiagnosticsService`](../grafana_plugin_sdk/backend/trait.DiagnosticsService.html) using [`Plugin::data_service`](../grafana_plugin_sdk/backend/struct.Plugin.html#method.diagnostics_service))
313- `resource` (registers a [`ResourceService`](../grafana_plugin_sdk/backend/trait.ResourceService.html) using [`Plugin::data_service`](../grafana_plugin_sdk/backend/struct.Plugin.html#method.resource_service))
314- `stream` (registers a [`StreamService`](../grafana_plugin_sdk/backend/trait.StreamService.html) using [`Plugin::data_service`](../grafana_plugin_sdk/backend/struct.Plugin.html#method.stream_service))
315
316### Example:
317
318```rust
319# use std::sync::Arc;
320#
321# use grafana_plugin_sdk::{backend, data, prelude::*};
322# use serde::Deserialize;
323# use thiserror::Error;
324#
325# #[derive(Clone, GrafanaPlugin)]
326# #[grafana_plugin(plugin_type = "app")]
327# struct Plugin;
328#
329# #[derive(Debug, Deserialize)]
330# struct Query {
331#     pub expression: String,
332#     pub other_user_input: u64,
333# }
334#
335# #[derive(Debug)]
336# struct QueryError {
337#     source: data::Error,
338#     ref_id: String,
339# }
340#
341# impl std::fmt::Display for QueryError {
342#     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
343#         write!(f, "Error in query {}: {}", self.ref_id, self.source)
344#     }
345# }
346#
347# impl std::error::Error for QueryError {}
348#
349# impl backend::DataQueryError for QueryError {
350#     fn ref_id(self) -> String {
351#         self.ref_id
352#     }
353# }
354#
355# #[backend::async_trait]
356# impl backend::DataService for Plugin {
357#     type Query = Query;
358#     type QueryError = QueryError;
359#     type Stream = backend::BoxDataResponseStream<Self::QueryError>;
360#     async fn query_data(&self, request: backend::QueryDataRequest<Self::Query, Self>) -> Self::Stream {
361#         todo!()
362#     }
363# }
364#
365# #[backend::async_trait]
366# impl backend::DiagnosticsService for Plugin {
367#     type CheckHealthError = std::convert::Infallible;
368#
369#     async fn check_health(
370#         &self,
371#         request: backend::CheckHealthRequest<Self>,
372#     ) -> Result<backend::CheckHealthResponse, Self::CheckHealthError> {
373#         todo!()
374#     }
375#
376#     type CollectMetricsError = Arc<dyn std::error::Error + Send + Sync>;
377#
378#     async fn collect_metrics(
379#         &self,
380#         request: backend::CollectMetricsRequest<Self>,
381#     ) -> Result<backend::CollectMetricsResponse, Self::CollectMetricsError> {
382#         todo!()
383#     }
384# }
385#
386# #[derive(Debug, Error)]
387# enum ResourceError {
388#     #[error("HTTP error: {0}")]
389#     Http(#[from] http::Error),
390#
391#     #[error("Path not found")]
392#     NotFound,
393# }
394#
395# impl backend::ErrIntoHttpResponse for ResourceError {}
396#
397# #[backend::async_trait]
398# impl backend::ResourceService for Plugin {
399#     type Error = ResourceError;
400#     type InitialResponse = Vec<u8>;
401#     type Stream = backend::BoxResourceStream<Self::Error>;
402#     async fn call_resource(&self, r: backend::CallResourceRequest<Self>) -> Result<(Self::InitialResponse, Self::Stream), Self::Error> {
403#         todo!()
404#     }
405# }
406#
407# #[backend::async_trait]
408# impl backend::StreamService for Plugin {
409#     type JsonValue = ();
410#     async fn subscribe_stream(
411#         &self,
412#         request: backend::SubscribeStreamRequest<Self>,
413#     ) -> Result<backend::SubscribeStreamResponse, Self::Error> {
414#         todo!()
415#     }
416#     type Error = Arc<dyn std::error::Error>;
417#     type Stream = backend::BoxRunStream<Self::Error>;
418#     async fn run_stream(&self, _request: backend::RunStreamRequest<Self>) -> Result<Self::Stream, Self::Error> {
419#         todo!()
420#     }
421#     async fn publish_stream(
422#         &self,
423#         _request: backend::PublishStreamRequest<Self>,
424#     ) -> Result<backend::PublishStreamResponse, Self::Error> {
425#         todo!()
426#     }
427# }
428#[grafana_plugin_sdk::main(
429    services(data, diagnostics, resource, stream),
430)]
431async fn plugin() -> Plugin {
432    Plugin
433}
434```
435
436## `init_subscriber`
437
438The `init_subscriber` attribute indicates whether a tracing subscriber should be
439initialized automatically using
440[`Plugin::init_subscriber`](../grafana_plugin_sdk/backend/struct.Plugin.html#method.init_subscriber).
441Unless this is being done in the annotated plugin function, this should
442generally be set to `true`.
443
444This must be a boolean.
445
446### Example
447
448```
449# use std::sync::Arc;
450#
451# use grafana_plugin_sdk::{backend, prelude::*};
452# use thiserror::Error;
453#
454# #[derive(Clone, GrafanaPlugin)]
455# #[grafana_plugin(plugin_type = "app")]
456# struct Plugin;
457#
458# #[derive(Debug, Error)]
459# enum ResourceError {
460#     #[error("HTTP error: {0}")]
461#     Http(#[from] http::Error),
462#
463#     #[error("Path not found")]
464#     NotFound,
465# }
466#
467# impl backend::ErrIntoHttpResponse for ResourceError {}
468#
469# #[backend::async_trait]
470# impl backend::ResourceService for Plugin {
471#     type Error = ResourceError;
472#     type InitialResponse = Vec<u8>;
473#     type Stream = backend::BoxResourceStream<Self::Error>;
474#     async fn call_resource(&self, r: backend::CallResourceRequest<Self>) -> Result<(Self::InitialResponse, Self::Stream), Self::Error> {
475#         todo!()
476#     }
477# }
478#
479#[grafana_plugin_sdk::main(
480    services(resource),
481    init_subscriber = true,
482)]
483async fn plugin() -> Plugin {
484    Plugin
485}
486```
487
488## `shutdown_handler`
489
490The `shutdown_handler` attribute indicates that a shutdown handler should be exposed using
491[`Plugin::shutdown_handler`](../grafana_plugin_sdk/backend/struct.Plugin.html#method.shutdown_handler)
492
493This must be a string which can be parsed as a [`SocketAddr`][std::net::SocketAddr] using `SocketAddr::parse`.
494
495### Example
496
497```
498# use std::sync::Arc;
499#
500# use grafana_plugin_sdk::{backend, prelude::*};
501# use thiserror::Error;
502#
503# #[derive(Clone, GrafanaPlugin)]
504# #[grafana_plugin(plugin_type = "app")]
505# struct Plugin;
506#
507# #[derive(Debug, Error)]
508# enum ResourceError {
509#     #[error("HTTP error: {0}")]
510#     Http(#[from] http::Error),
511#
512#     #[error("Path not found")]
513#     NotFound,
514# }
515#
516# impl backend::ErrIntoHttpResponse for ResourceError {}
517#
518# #[backend::async_trait]
519# impl backend::ResourceService for Plugin {
520#     type Error = ResourceError;
521#     type InitialResponse = Vec<u8>;
522#     type Stream = backend::BoxResourceStream<Self::Error>;
523#     async fn call_resource(&self, r: backend::CallResourceRequest<Self>) -> Result<(Self::InitialResponse, Self::Stream), Self::Error> {
524#         todo!()
525#     }
526# }
527#
528#[grafana_plugin_sdk::main(
529    services(resource),
530    shutdown_handler = "127.0.0.1:10001",
531)]
532async fn plugin() -> Plugin {
533    Plugin
534}
535```
536
537# Macro expansion
538
539The following example shows what the `#[main]` macro expands to:
540
541```rust
542use std::sync::Arc;
543
544use grafana_plugin_sdk::{backend, prelude::*};
545use thiserror::Error;
546
547# #[derive(Clone, GrafanaPlugin)]
548# #[grafana_plugin(plugin_type = "app")]
549struct Plugin;
550
551#[derive(Debug, Error)]
552enum ResourceError {
553    #[error("HTTP error: {0}")]
554    Http(#[from] http::Error),
555
556    #[error("Path not found")]
557    NotFound,
558}
559
560impl backend::ErrIntoHttpResponse for ResourceError {}
561
562#[backend::async_trait]
563impl backend::ResourceService for Plugin {
564    type Error = ResourceError;
565    type InitialResponse = Vec<u8>;
566    type Stream = backend::BoxResourceStream<Self::Error>;
567    async fn call_resource(&self, r: backend::CallResourceRequest<Self>) -> Result<(Self::InitialResponse, Self::Stream), Self::Error> {
568        todo!()
569    }
570}
571
572#[grafana_plugin_sdk::main(
573    services(resource),
574    init_subscriber = true,
575    shutdown_handler = "127.0.0.1:10001",
576)]
577async fn plugin() -> Plugin {
578    Plugin
579}
580```
581
582expands to:
583
584```rust
585# use std::sync::Arc;
586#
587# use grafana_plugin_sdk::{backend, prelude::*};
588# use thiserror::Error;
589#
590# #[derive(Clone, GrafanaPlugin)]
591# #[grafana_plugin(plugin_type = "app")]
592# struct Plugin;
593#
594# #[derive(Debug, Error)]
595# enum ResourceError {
596#     #[error("HTTP error: {0}")]
597#     Http(#[from] http::Error),
598#
599#     #[error("Path not found")]
600#     NotFound,
601# }
602#
603# impl backend::ErrIntoHttpResponse for ResourceError {}
604#
605# #[backend::async_trait]
606# impl backend::ResourceService for Plugin {
607#     type Error = ResourceError;
608#     type InitialResponse = Vec<u8>;
609#     type Stream = backend::BoxResourceStream<Self::Error>;
610#     async fn call_resource(&self, r: backend::CallResourceRequest<Self>) -> Result<(Self::InitialResponse, Self::Stream), Self::Error> {
611#         todo!()
612#     }
613# }
614
615fn main() -> Result<(), Box<dyn std::error::Error>> {
616    let fut = async {
617        let listener = ::grafana_plugin_sdk::backend::initialize().await?;
618        let service = Plugin;
619        ::grafana_plugin_sdk::backend::Plugin::new()
620            .resource_service(service.clone())
621            .init_subscriber(true)
622            .shutdown_handler("127.0.0.1:10001".parse().expect("could not parse shutdown handler as SocketAddr"))
623            .start(listener)
624            .await?;
625        Ok::<_, Box<dyn std::error::Error>>(())
626    };
627    # if false {
628    tokio::runtime::Builder::new_multi_thread()
629        .thread_name("grafana-plugin-worker-thread")
630        .enable_all()
631        .build()
632        .expect("create tokio runtime")
633        .block_on(fut)?;
634    # }
635    Ok(())
636}
637```
638*/
639#[proc_macro_attribute]
640pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
641    // If any of the steps for this macro fail, we still want to expand to an item that is as close
642    // to the expected output as possible. This helps out IDEs such that completions and other
643    // related features keep working.
644    let input: syn::ItemFn = match syn::parse(item.clone()) {
645        Ok(it) => it,
646        Err(e) => return token_stream_with_error(item, e),
647    };
648    if args.is_empty() {
649        let span = Span::call_site();
650        let source_text = span
651            .source_text()
652            .unwrap_or_else(|| "grafana_plugin_sdk::main".to_string());
653        return token_stream_with_error(
654            parse_knobs(input, DEFAULT_ERROR_CONFIG),
655            syn::Error::new(
656                span,
657                format!(
658                    "at least one service must be provided, e.g. `#[{}(services(data))]`",
659                    source_text.trim_matches(&['[', ']', '#'] as &[char])
660                ),
661            ),
662        );
663    }
664
665    let res = if input.sig.ident != "plugin" {
666        let msg = "the plugin function must be named 'plugin'";
667        Err(syn::Error::new_spanned(&input.sig.ident, msg))
668    } else if !input.sig.inputs.is_empty() {
669        let msg = "the plugin function cannot accept arguments";
670        Err(syn::Error::new_spanned(&input.sig.inputs, msg))
671    } else if input.sig.asyncness.is_none() {
672        let msg = "the `async` keyword is missing from the function declaration";
673        Err(syn::Error::new_spanned(input.sig.fn_token, msg))
674    } else {
675        let mut config = Configuration::default();
676        let config_parser = syn::meta::parser(|meta| config.parse(meta));
677        parse_macro_input!(args with config_parser);
678        Ok(config)
679    };
680
681    match res {
682        Ok(c) => parse_knobs(input, c.build()),
683        Err(e) => token_stream_with_error(parse_knobs(input, DEFAULT_ERROR_CONFIG), e),
684    }
685}