diff --git a/Cargo.toml b/Cargo.toml index 0e371bf..66de038 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tracing-layer-slack" -version = "0.5.1" +version = "0.6.0" edition = "2018" license = "Apache-2.0" description = "Send filtered tracing events to Slack" @@ -17,13 +17,19 @@ path = "src/lib.rs" [[example]] name = "simple" +[features] +default = ["blocks", "rustls", "gzip"] +blocks = [] +gzip = [ "reqwest/gzip" ] +native-tls = [ "reqwest/default-tls" ] +rustls = [ "reqwest/rustls-tls" ] + [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1", default-features = false, features = ["test-util", "sync", "macros", "rt-multi-thread"] } -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.11", default-features = false } tracing = { version = "0.1", features = ["log"] } -tracing-futures = "0.2" tracing-subscriber = "0.3" tracing-bunyan-formatter = { default-features = false, version = "0.3" } regex = "1" diff --git a/README.md b/README.md index 7af79b8..51fef3a 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ Configure the dependencies and pull directly from GitHub: [dependencies] tokio = "1.0" tracing = "0.1" -tracing-futures = "0.2" -tracing-layer-slack = "0.5" +tracing-layer-slack = "0.6" ``` ## Examples @@ -32,9 +31,19 @@ In this simple example, a layer is created using Slack configuration in the envi #### Slack Messages -This screenshots shows the first three Slack messages sent while running this example. More messages are sent but were truncated from this image. +This screenshots shows the first three Slack messages sent while running this example. More messages are sent but were truncated from these images. -Screenshot demonstrating the current formatter implementation for events sent as Slack messages +##### Slack Blocks + +By default, messages are sent using [Slack Blocks](https://api.slack.com/block-kit). Here's an example: + +Screenshot demonstrating the current formatter implementation for events sent as Slack messages + +##### Slack Text + +By disabling the default features of this crate (and therefore disabling the `blocks` feature), you can revert to the older format which does not use the block kit. + +Screenshot demonstrating the current formatter implementation for events sent as Slack messages #### Code example @@ -78,7 +87,7 @@ async fn main() { let target_to_filter: EventFilters = Regex::new("simple").unwrap().into(); // Initialize the layer and an async background task for sending our Slack messages. - let (slack_layer, background_worker) = SlackLayer::builder(target_to_filter).build(); + let (slack_layer, background_worker) = SlackLayer::builder("my-app-name".to_string(), target_to_filter).build(); // Initialize the global default subscriber for tracing events. let subscriber = Registry::default().with(slack_layer); tracing::subscriber::set_global_default(subscriber).unwrap(); diff --git a/examples/exclude_fields_from_messages.rs b/examples/exclude_fields_from_messages.rs index 92a7e57..72ee465 100644 --- a/examples/exclude_fields_from_messages.rs +++ b/examples/exclude_fields_from_messages.rs @@ -22,7 +22,7 @@ async fn main() { Regex::new(".*password.*").unwrap(), Regex::new("command").unwrap(), ]; - let (slack_layer, background_worker) = SlackLayer::builder(targets_to_filter) + let (slack_layer, background_worker) = SlackLayer::builder("test-app".to_string(), targets_to_filter) .field_exclusion_filters(fields_to_exclude) .build(); let subscriber = Registry::default().with(slack_layer); diff --git a/examples/exclude_messages_below_level.rs b/examples/exclude_messages_below_level.rs index d1c621b..3bc98af 100644 --- a/examples/exclude_messages_below_level.rs +++ b/examples/exclude_messages_below_level.rs @@ -13,7 +13,7 @@ pub async fn handler() { #[tokio::main] async fn main() { let targets_to_filter: EventFilters = Regex::new("exclude_messages_below_level").unwrap().into(); - let (slack_layer, background_worker) = SlackLayer::builder(targets_to_filter) + let (slack_layer, background_worker) = SlackLayer::builder("test-app".to_string(), targets_to_filter) .level_filters("info".to_string()) .build(); let subscriber = Registry::default().with(slack_layer); diff --git a/examples/exclude_messages_by_content.rs b/examples/exclude_messages_by_content.rs index de13e4f..e7b29f3 100644 --- a/examples/exclude_messages_by_content.rs +++ b/examples/exclude_messages_by_content.rs @@ -14,7 +14,7 @@ pub async fn handler() { async fn main() { let targets_to_filter: EventFilters = (None, None).into(); let messages_to_exclude = vec![Regex::new("the message we want to exclude").unwrap()]; - let (slack_layer, background_worker) = SlackLayer::builder(targets_to_filter) + let (slack_layer, background_worker) = SlackLayer::builder("test-app".to_string(), targets_to_filter) .message_filters((Vec::new(), messages_to_exclude).into()) .build(); let subscriber = Registry::default().with(slack_layer); diff --git a/examples/filter_records_by_fields.rs b/examples/filter_records_by_fields.rs index ef7037a..bf5757d 100644 --- a/examples/filter_records_by_fields.rs +++ b/examples/filter_records_by_fields.rs @@ -18,7 +18,7 @@ pub async fn handler() { async fn main() { let targets_to_filter: EventFilters = Regex::new("filter_records_by_fields").unwrap().into(); let event_fields_to_filter: EventFilters = Regex::new("password").unwrap().into(); - let (slack_layer, background_worker) = SlackLayer::builder(targets_to_filter) + let (slack_layer, background_worker) = SlackLayer::builder("test-app".to_string(), targets_to_filter) .event_by_field_filters(event_fields_to_filter) .build(); let subscriber = Registry::default().with(slack_layer); diff --git a/examples/simple.rs b/examples/simple.rs index acdf7b6..54ff35b 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -25,7 +25,7 @@ async fn main() { // Only show events from where this example code is the target. let target_to_filter: EventFilters = Regex::new("simple").unwrap().into(); - let (slack_layer, background_worker) = SlackLayer::builder(target_to_filter).build(); + let (slack_layer, background_worker) = SlackLayer::builder("test-app".to_string(), target_to_filter).build(); let subscriber = Registry::default().with(slack_layer); tracing::subscriber::set_global_default(subscriber).unwrap(); diff --git a/src/filters.rs b/src/filters.rs index e0e73ea..ff1ece0 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -2,7 +2,6 @@ use core::option::Option; use core::option::Option::Some; use core::result::Result; use core::result::Result::{Err, Ok}; -use std::io::Error; use regex::Regex; diff --git a/src/layer.rs b/src/layer.rs index abe00c2..0e461ae 100644 --- a/src/layer.rs +++ b/src/layer.rs @@ -6,6 +6,7 @@ use tracing_bunyan_formatter::JsonStorage; use tracing_subscriber::{layer::Context, Layer}; use crate::filters::{EventFilters, Filter, FilterError}; +use crate::message::PayloadMessageType; use crate::worker::{SlackBackgroundWorker, WorkerMessage}; use crate::{config::SlackConfig, message::SlackPayload, worker::worker, ChannelSender}; use std::collections::HashMap; @@ -44,6 +45,9 @@ pub struct SlackLayer { /// Filter events by their level. level_filter: Option, + #[cfg(feature = "blocks")] + app_name: String, + /// Configure the layer's connection to the Slack Webhook API. config: SlackConfig, @@ -61,6 +65,7 @@ impl SlackLayer { /// used to shutdown the background worker, and a future to spawn as a task on a tokio runtime /// to initialize the worker's processing and sending of HTTP requests to the Slack API. pub(crate) fn new( + app_name: String, target_filters: EventFilters, message_filters: Option, event_by_field_filters: Option, @@ -75,6 +80,7 @@ impl SlackLayer { field_exclusion_filters, event_by_field_filters, level_filter, + app_name, config, slack_sender: tx.clone(), }; @@ -86,8 +92,8 @@ impl SlackLayer { } /// Create a new builder for SlackLayer. - pub fn builder(target_filters: EventFilters) -> SlackLayerBuilder { - SlackLayerBuilder::new(target_filters) + pub fn builder(app_name: String, target_filters: EventFilters) -> SlackLayerBuilder { + SlackLayerBuilder::new(app_name, target_filters) } } @@ -99,6 +105,7 @@ impl SlackLayer { /// Several methods expose initialization of optional filtering mechanisms, along with Slack /// configuration that defaults to searching in the local environment variables. pub struct SlackLayerBuilder { + app_name: String, target_filters: EventFilters, message_filters: Option, event_by_field_filters: Option, @@ -108,8 +115,9 @@ pub struct SlackLayerBuilder { } impl SlackLayerBuilder { - pub(crate) fn new(target_filters: EventFilters) -> Self { + pub(crate) fn new(app_name: String, target_filters: EventFilters) -> Self { Self { + app_name, target_filters, message_filters: None, event_by_field_filters: None, @@ -163,6 +171,7 @@ impl SlackLayerBuilder { /// Create a SlackLayer and its corresponding background worker to (async) send the messages. pub fn build(self) -> (SlackLayer, SlackBackgroundWorker) { SlackLayer::new( + self.app_name, self.target_filters, self.message_filters, self.event_by_field_filters, @@ -192,27 +201,22 @@ where let message = event_visitor .values() .get("message") - .map(|v| match v { + .and_then(|v| match v { Value::String(s) => Some(s.as_str()), _ => None, }) - .flatten() .or_else(|| { - event_visitor - .values() - .get("error") - .map(|v| match v { - Value::String(s) => Some(s.as_str()), - _ => None, - }) - .flatten() + event_visitor.values().get("error").and_then(|v| match v { + Value::String(s) => Some(s.as_str()), + _ => None, + }) }) - .unwrap_or_else(|| "No message"); + .unwrap_or("No message"); self.message_filters.process(message)?; if let Some(level_filters) = &self.level_filter { let message_level = { - LevelFilter::from_str(event.metadata().level().to_string().as_str()) + LevelFilter::from_str(event.metadata().level().as_str()) .map_err(|e| FilterError::IoError(Box::new(e)))? }; let level_threshold = @@ -249,38 +253,26 @@ where let span = match ¤t_span { Some(span) => span.metadata().name(), - None => "None".into(), + None => "", }; let metadata = { - let data: HashMap = serde_json::from_slice(metadata_buffer.as_slice()).unwrap(); + let data: HashMap = + serde_json::from_slice(metadata_buffer.as_slice()).unwrap(); serde_json::to_string_pretty(&data).unwrap() }; - let message = format!( - concat!( - "*Event [{}]*: \"{}\"\n", - "*Span*: _{}_\n", - "*Target*: _{}_\n", - "*Source*: _{}#L{}_\n", - "*Metadata*:\n", - "```", - "{}", - "```", - ), - event.metadata().level().to_string(), + Ok(Self::format_payload( + self.app_name.as_str(), message, - span, + event, target, - event.metadata().file().unwrap_or("Unknown"), - event.metadata().line().unwrap_or(0), - metadata - ); - - Ok(message) + span, + metadata, + )) }; - let result: Result = format(); + let result: Result = format(); if let Ok(formatted) = result { let payload = SlackPayload::new(formatted, self.config.webhook_url.clone()); if let Err(e) = self.slack_sender.send(WorkerMessage::Data(payload)) { @@ -289,3 +281,102 @@ where } } } + +impl SlackLayer { + #[cfg(feature = "blocks")] + fn format_payload( + app_name: &str, + message: &str, + event: &Event, + target: &str, + span: &str, + metadata: String, + ) -> PayloadMessageType { + let event_level = event.metadata().level(); + let event_level_emoji = match *event_level { + tracing::Level::TRACE => ":mag:", + tracing::Level::DEBUG => ":bug:", + tracing::Level::INFO => ":information_source:", + tracing::Level::WARN => ":warning:", + tracing::Level::ERROR => ":x:", + }; + let source_file = event.metadata().file().unwrap_or("Unknown"); + let source_line = event.metadata().line().unwrap_or(0); + let blocks = serde_json::json!([ + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": format!("{} - {} *{}*", app_name, event_level_emoji, event_level), + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": format!("\"_{}_\"", message), + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": format!("*Target Span*\n{}::{}", target, span) + }, + { + "type": "mrkdwn", + "text": format!("*Source*\n{}#L{}", source_file, source_line) + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Metadata:*" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": format!("```\n{}\n```", metadata) + } + } + ]); + let blocks_json = blocks.to_string(); + PayloadMessageType::Blocks(blocks_json) + } + + #[cfg(not(feature = "blocks"))] + fn format_payload( + app_name: &str, + message: &str, + event: &Event, + target: &str, + span: &str, + metadata: String, + ) -> PayloadMessageType { + let event_level = event.metadata().level().as_str(); + let source_file = event.metadata().file().unwrap_or("Unknown"); + let source_line = event.metadata().line().unwrap_or(0); + let payload = format!( + concat!( + "*Trace from {}*\n", + "*Event [{}]*: \"{}\"\n", + "*Target*: _{}_\n", + "*Span*: _{}_\n", + "*Metadata*:\n", + "```", + "{}", + "```\n", + "*Source*: _{}#L{}_", + ), + app_name, event_level, message, span, target, metadata, source_file, source_line, + ); + PayloadMessageType::Text(payload) + } +} diff --git a/src/message.rs b/src/message.rs index 0ff9f17..c72f994 100644 --- a/src/message.rs +++ b/src/message.rs @@ -4,14 +4,39 @@ use serde::Serialize; /// converted into this format. #[derive(Debug, Clone, Serialize)] pub(crate) struct SlackPayload { - text: String, + #[serde(skip_serializing_if = "Option::is_none")] + text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + blocks: Option, #[serde(skip_serializing)] webhook_url: String, } +#[allow(dead_code)] +pub(crate) enum PayloadMessageType { + Text(String), + Blocks(String), +} + impl SlackPayload { - pub(crate) fn new(text: String, webhook_url: String) -> Self { - Self { text, webhook_url } + pub(crate) fn new(payload: PayloadMessageType, webhook_url: String) -> Self { + let text; + let blocks; + match payload { + PayloadMessageType::Text(t) => { + text = Some(t); + blocks = None; + } + PayloadMessageType::Blocks(b) => { + text = None; + blocks = Some(b); + } + } + Self { + text, + blocks, + webhook_url, + } } }