Skip to content

Commit

Permalink
feat: allow formatting blocks with either block-kit or plaintext
Browse files Browse the repository at this point in the history
By default, this formatting option is enabled through the default "blocks" feature.

Also, includes new features for rustls vs native-tls and using gzip with
requests.

Signed-off-by: Sean Pianka <[email protected]>
  • Loading branch information
seanpianka committed May 17, 2023
1 parent ba6c980 commit 639f722
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 54 deletions.
12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

<img src="https://i.imgur.com/vefquEK.png" width="350" title="hover text" alt="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:

<img src="https://i.imgur.com/76V50Gr.png" title="hover text" alt="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.

<img src="https://i.imgur.com/vefquEK.png" width="450" title="hover text" alt="Screenshot demonstrating the current formatter implementation for events sent as Slack messages">

#### Code example

Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion examples/exclude_fields_from_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion examples/exclude_messages_below_level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion examples/exclude_messages_by_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion examples/filter_records_by_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion examples/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
1 change: 0 additions & 1 deletion src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
165 changes: 128 additions & 37 deletions src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,6 +45,9 @@ pub struct SlackLayer {
/// Filter events by their level.
level_filter: Option<String>,

#[cfg(feature = "blocks")]
app_name: String,

/// Configure the layer's connection to the Slack Webhook API.
config: SlackConfig,

Expand All @@ -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<EventFilters>,
event_by_field_filters: Option<EventFilters>,
Expand All @@ -75,6 +80,7 @@ impl SlackLayer {
field_exclusion_filters,
event_by_field_filters,
level_filter,
app_name,
config,
slack_sender: tx.clone(),
};
Expand All @@ -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)
}
}

Expand All @@ -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<EventFilters>,
event_by_field_filters: Option<EventFilters>,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -249,38 +253,26 @@ where

let span = match &current_span {
Some(span) => span.metadata().name(),
None => "None".into(),
None => "",
};

let metadata = {
let data: HashMap<String, Value> = serde_json::from_slice(metadata_buffer.as_slice()).unwrap();
let data: HashMap<String, serde_json::Value> =
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<String, crate::filters::FilterError> = format();
let result: Result<PayloadMessageType, crate::filters::FilterError> = 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)) {
Expand All @@ -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)
}
}
Loading

0 comments on commit 639f722

Please sign in to comment.