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.
-
+##### Slack Blocks
+
+By default, messages are sent using [Slack Blocks](https://api.slack.com/block-kit). Here's an example:
+
+
+
+##### 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.
+
+
#### 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,
+ }
}
}