tracing_layer_discord/
lib.rs1#![doc = include_str!("../README.md")]
2
3use serde::Serialize;
4use serde_json::Value;
5pub use tracing_layer_core::filters::EventFilters;
6pub use tracing_layer_core::layer::WebhookLayer;
7use tracing_layer_core::layer::WebhookLayerBuilder;
8pub use tracing_layer_core::BackgroundWorker;
9use tracing_layer_core::{Config, WebhookMessage, WebhookMessageFactory, WebhookMessageInputs};
10
11pub struct DiscordLayer;
12
13impl DiscordLayer {
14 pub fn builder(app_name: String, target_filters: EventFilters) -> WebhookLayerBuilder<DiscordConfig, Self> {
15 WebhookLayer::builder(app_name, target_filters)
16 }
17}
18
19impl WebhookMessageFactory for DiscordLayer {
20 fn create(inputs: WebhookMessageInputs) -> impl WebhookMessage {
21 let target = inputs.target;
22 let span = inputs.span;
23 let metadata = inputs.metadata;
24 let message = inputs.message;
25 let app_name = inputs.app_name;
26 let source_file = inputs.source_file;
27 let source_line = inputs.source_line;
28 let event_level = inputs.event_level;
29
30 #[cfg(feature = "embed")]
31 {
32 let event_level_emoji = match event_level {
33 tracing::Level::TRACE => ":mag:",
34 tracing::Level::DEBUG => ":bug:",
35 tracing::Level::INFO => ":information_source:",
36 tracing::Level::WARN => ":warning:",
37 tracing::Level::ERROR => ":x:",
38 };
39 let event_level_color = match event_level {
40 tracing::Level::TRACE => 1752220,
41 tracing::Level::DEBUG => 1752220,
42 tracing::Level::INFO => 5763719,
43 tracing::Level::WARN => 15105570,
44 tracing::Level::ERROR => 15548997,
45 };
46
47 const MAX_FIELD_VALUE_CHARS: usize = 1024 - 15;
49 const MAX_ERROR_MESSAGE_CHARS: usize = 2048 - 15;
50
51 let mut truncated_message = String::new();
53 if message.chars().count() > MAX_ERROR_MESSAGE_CHARS {
54 #[cfg(feature = "log-errors")]
55 eprintln!(
56 "WARN: Truncating message to {} characters, original: {}",
57 MAX_ERROR_MESSAGE_CHARS, message
58 );
59 let mut char_count = 0;
60 for c in message.chars() {
61 char_count += 1;
62 if char_count > MAX_ERROR_MESSAGE_CHARS {
63 break;
64 }
65 truncated_message.push(c);
66 }
67 }
68 let message = if truncated_message.is_empty() {
69 message
70 } else {
71 truncated_message
72 };
73
74 let mut discord_embed = serde_json::json!({
75 "title": format!("{} - {} {}", app_name, event_level_emoji, event_level),
76 "description": format!("```rust\n{}\n```", message),
77 "fields": [
78 {
79 "name": "Target Span",
80 "value": format!("`{}::{}`", target, span),
81 "inline": true
82 },
83 {
84 "name": "Source",
85 "value": format!("`{}#L{}`", source_file, source_line),
86 "inline": true
87 },
88 ],
89 "footer": {
90 "text": app_name
91 },
92 "color": event_level_color, "thumbnail": {
94 "url": "https://example.com/error-thumbnail.png"
95 }
96 });
97
98 if metadata.len() <= MAX_FIELD_VALUE_CHARS {
100 discord_embed["fields"].as_array_mut().unwrap().push(serde_json::json!({
102 "name": "Metadata",
103 "value": format!("```json\n{}\n```", metadata),
104 "inline": false
105 }));
106 } else {
107 let mut remaining_metadata = metadata;
109 let mut chunk_number = 1;
110 while !remaining_metadata.is_empty() {
111 let chunk = remaining_metadata
112 .chars()
113 .take(MAX_FIELD_VALUE_CHARS)
114 .collect::<String>();
115
116 remaining_metadata = remaining_metadata.chars().skip(MAX_FIELD_VALUE_CHARS).collect();
117
118 discord_embed["fields"].as_array_mut().unwrap().push(serde_json::json!({
119 "name": format!("Metadata ({})", chunk_number),
120 "value": format!("```json\n{}\n```", chunk),
121 "inline": false
122 }));
123
124 chunk_number += 1;
125 }
126 }
127
128 DiscordMessagePayload {
129 content: None,
130 embeds: Some(vec![discord_embed]),
131 webhook_url: inputs.webhook_url,
132 }
133 }
134 #[cfg(not(feature = "embed"))]
135 {
136 let event_level = event.metadata().level().as_str();
137 let source_file = event.metadata().file().unwrap_or("Unknown");
138 let source_line = event.metadata().line().unwrap_or(0);
139 let payload = format!(
140 concat!(
141 "*Trace from {}*\n",
142 "*Event [{}]*: \"{}\"\n",
143 "*Target*: _{}_\n",
144 "*Span*: _{}_\n",
145 "*Metadata*:\n",
146 "```",
147 "{}",
148 "```\n",
149 "*Source*: _{}#L{}_",
150 ),
151 app_name, event_level, message, span, target, metadata, source_file, source_line,
152 );
153 DiscordMessagePayload {
154 content: Some(payload),
155 embeds: None,
156 webhook_url: inputs.webhook_url,
157 }
158 }
159 }
160}
161
162pub struct DiscordConfig {
164 pub(crate) webhook_url: String,
165}
166
167impl DiscordConfig {
168 pub fn new(webhook_url: String) -> Self {
169 Self { webhook_url }
170 }
171
172 pub fn new_from_env() -> Self {
178 Self::new(std::env::var("DISCORD_WEBHOOK_URL").expect("discord webhook url in env"))
179 }
180}
181
182impl Default for DiscordConfig {
183 fn default() -> Self {
184 Self::new_from_env()
185 }
186}
187
188impl Config for DiscordConfig {
189 fn webhook_url(&self) -> &str {
190 &self.webhook_url
191 }
192
193 fn new_from_env() -> Self
194 where
195 Self: Sized,
196 {
197 Self::new_from_env()
198 }
199}
200
201#[derive(Debug, Clone, Serialize)]
204pub(crate) struct DiscordMessagePayload {
205 #[serde(skip_serializing_if = "Option::is_none")]
206 content: Option<String>,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 embeds: Option<Vec<Value>>,
209 #[serde(skip_serializing)]
210 webhook_url: String,
211}
212
213impl WebhookMessage for DiscordMessagePayload {
214 fn webhook_url(&self) -> &str {
215 self.webhook_url.as_str()
216 }
217
218 fn serialize(&self) -> String {
219 serde_json::to_string(self).expect("failed to serialize discord message")
220 }
221}
222
223#[cfg(test)]
224mod tests {}