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}