diff --git a/.gitignore b/.gitignore index 105fd95..d530300 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Cargo.lock example/target/ *.so *.a +src/gen.rs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0506791 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "example/telegraf-plugin-go-glue"] + path = example/telegraf-plugin-go-glue + url = https://github.com/StarryInternet/telegraf-plugin-go-glue +[submodule "telegraf-plugin-go-glue"] + path = telegraf-plugin-go-glue + url = https://github.com/StarryInternet/telegraf-plugin-go-glue diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..2227519 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,20 @@ +# line length +error_on_line_overflow = true +max_width = 100 + +# comments +comment_width = 100 +wrap_comments = false # rustfmt is broken currently and doesn't softwrap +normalize_comments = true + +# commas +trailing_comma = "Never" +match_block_trailing_comma = true + +# code +use_try_shorthand = true +binop_separator = "Back" +format_strings = false # rustfmt string handling is horribly busted + +# imports +reorder_imports = true diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2793fdf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "telegraf-plugin" +version = "0.1.0" +authors = ["Timidger "] +edition = "2018" +build = "build.rs" + +[lib] +proc-macro = true + +[build-dependencies] +bindgen = "0.49.*" + +[dependencies] +chrono = "0.4" +libc = "0.2" +syn = { version = "0.15", features = ["full", "fold"] } +quote = "0.6" +proc-macro2 = "0.4" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..c124584 --- /dev/null +++ b/build.rs @@ -0,0 +1,15 @@ +// Copyright 2019. Starry, Inc. All Rights Reserved. +fn main() { + let builder = bindgen::builder() + .derive_debug(true) + .derive_default(true) + .derive_copy(true) + .generate_comments(true) + .blacklist_function("sample_config") + .blacklist_function("description") + .blacklist_function("gather") + .header("telegraf-plugin-go-glue/c_api.h") + .ctypes_prefix("libc"); + let generated = builder.generate().unwrap(); + generated.write_to_file("src/gen.rs").unwrap(); +} diff --git a/example/Cargo.toml b/example/Cargo.toml new file mode 100644 index 0000000..b2a3f49 --- /dev/null +++ b/example/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "telegraf-plugin-example" +version = "0.1.0" +authors = ["Timidger "] +edition = "2018" + +[lib] +crate_type = ["staticlib"] + +[dependencies] +libc = "0.2" +telegraf-plugin = { path = "../" } diff --git a/example/makefile-template.toml b/example/makefile-template.toml new file mode 100644 index 0000000..9f48ed9 --- /dev/null +++ b/example/makefile-template.toml @@ -0,0 +1,41 @@ +[tasks.rust_build] +command = "cargo" +args = ["build"] + +[tasks.copy_go_files] +script = [ +''' +cp target/debug/libtelegraf_plugin_example.a ./plugin.a +cp telegraf-plugin-go-glue/lib.go ./ +cp telegraf-plugin-go-glue/c_api.h ./ +''' +] + +[tasks.go_build] +command = "go" +args = ["build", "-buildmode=plugin", "-o=plugin.so"] +dependencies = ["rust_build", "copy_go_files"] + +[tasks.cleanup] +script = [ +''' +rm plugin.a +rm lib.go +rm c_api.h +if [ -f plugin.so ]; then + chmod a+x plugin.so +fi +''' +] + +# This must run after the Rust build, +# so that it can link the Rust artifact to it. +[tasks.build] +dependencies = [ + "rust_build", + "go_build", + "cleanup" +] + +[config] +on_error_task = "cleanup" diff --git a/example/src/lib.rs b/example/src/lib.rs new file mode 100644 index 0000000..a64c4c4 --- /dev/null +++ b/example/src/lib.rs @@ -0,0 +1,25 @@ +// Copyright 2019. Starry, Inc. All Rights Reserved. +use telegraf_plugin::link_to_go; + +macro_rules! map( + { $($key:expr => $value:expr),* } => { + { + #[allow(unused_mut)] + let mut m = ::std::collections::HashMap::new(); + $( + m.insert($key, $value); + )* + m + } + }; +); + +#[link_to_go("Description of the plugin", "")] +fn collect_metric() { + AddField( + "rust-metric".into(), + map! {"rust_key".into() => "rust_value".into()}, + map! {"rust_field".into() => 100000000.into()}, + None + ) +} diff --git a/example/telegraf-plugin-go-glue b/example/telegraf-plugin-go-glue new file mode 120000 index 0000000..f5fe791 --- /dev/null +++ b/example/telegraf-plugin-go-glue @@ -0,0 +1 @@ +../telegraf-plugin-go-glue/ \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..705a55b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,173 @@ +// Copyright 2019. Starry, Inc. All Rights Reserved. +#![recursion_limit = "264"] + +extern crate proc_macro; + +use std::ffi::CString; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{ + parse::{self, Parse, ParseStream}, + parse_macro_input, parse_quote, + punctuated::Punctuated, + Expr, ExprBlock, ItemFn, LitByteStr, LitStr, Token +}; + +#[allow( + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + unused_variables, + unused_imports, + dead_code +)] + +struct Args { + description: CString, + sample_config: CString +} + +impl Parse for Args { + fn parse(input: ParseStream) -> parse::Result { + let mut vars = Punctuated::::parse_terminated(input)?.into_iter(); + let description = vars + .next() + .map(|v| CString::new(v.value()).expect("Invalid CString")) + .expect("Need two arguments: description and sample_config"); + let sample_config = vars + .next() + .as_ref() + .map(|v| CString::new(v.value()).expect("Invalid CString")) + .expect("Need two arguments: description and sample_config"); + Ok(Args { + description, + sample_config + }) + } +} + +#[proc_macro_attribute] +pub fn link_to_go(args: TokenStream, input: TokenStream) -> TokenStream { + let fn_gather_user = parse_macro_input!(input as ItemFn); + let args = parse_macro_input!(args as Args); + + let description_string = + LitByteStr::new(args.description.as_bytes_with_nul(), Span::call_site()); + let fn_description: ItemFn = parse_quote! { + #[no_mangle] + unsafe extern "C" fn description() -> *const libc::c_char { + #description_string as *const _ as *const libc::c_char + } + }; + + let sample_config_string = + LitByteStr::new(args.sample_config.as_bytes_with_nul(), Span::call_site()); + let fn_sample_config: ItemFn = parse_quote! { + #[no_mangle] + unsafe extern "C" fn sample_config() -> *const libc::c_char { + #sample_config_string as *const _ as *const libc::c_char + } + }; + + let call_user_code = Expr::Block(ExprBlock { + attrs: vec![], + label: None, + block: (*fn_gather_user.block).clone() + }); + let fn_gather: ItemFn = parse_quote! { + #[no_mangle] + extern "C" fn gather() { + #call_user_code + } + }; + + // TODO(timidger): This should all be generated automatically + let telegraf_api_decl: proc_macro2::TokenStream = parse_quote! { + #[repr(C)] + enum Type { + TYPE_INT = 0 + } + + #[repr(C)] + union Value { + int_: libc::c_int + } + + impl From for go_value { + fn from(num: i32) -> Self { + go_value { + type_: go_value_TYPE_INT, + value: go_value__bindgen_ty_2 { + int_: i32::from(num) + } + } + } + } + + fn AddField( + management: String, + tags: std::collections::HashMap, + fields: std::collections::HashMap, + timestamp: Option + ) { + let management = std::ffi::CString::new(management).unwrap(); + + let tags = tags + .into_iter() + .map(|(k, v)| { + ( + std::ffi::CString::new(k).expect("Not a C string"), + std::ffi::CString::new(v).expect("Not a C string") + ) + }) + .collect::>(); + let mut tags_list = Vec::with_capacity(tags.len()); + for (key, value) in tags.iter() { + tags_list.push(tag { + key: key.as_ptr() as *mut _, + value: value.as_ptr() as *mut _ + }) + } + + let fields = fields + .into_iter() + .map(|(k, v)| (std::ffi::CString::new(k).expect("Not a C string"), v)) + .collect::>(); + let mut fields_list = Vec::with_capacity(fields.len()); + for (key, &value) in fields.iter() { + fields_list.push(field { + key: key.as_ptr() as *mut _, + value + }) + } + + let unix_time = timestamp + .map(|timestamp| timestamp.duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards")) + .unwrap_or_else(|| std::time::Duration::new(0, 0)); + + unsafe { + add_field( + management.as_ptr() as *mut _, + tags_list.as_mut_ptr(), + tags_list.len() as i32, + fields_list.as_mut_ptr(), + fields_list.len() as i32, + unix_time.as_secs() as i64, + unix_time.as_nanos() as i64 + ) + } + } + }; + let c_prelude: proc_macro2::TokenStream = syn::parse_str(include_str!("gen.rs")).unwrap(); + + TokenStream::from(quote! { + #c_prelude + #fn_gather + #telegraf_api_decl + #fn_description + #fn_sample_config + }) +} diff --git a/telegraf-plugin-go-glue b/telegraf-plugin-go-glue new file mode 160000 index 0000000..11ece80 --- /dev/null +++ b/telegraf-plugin-go-glue @@ -0,0 +1 @@ +Subproject commit 11ece802906f38819b265bc00b8d02bc31f196a3