diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..dc0be9b
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+ enable
+ enable
+ latest
+ true
+
+
diff --git a/README.md b/README.md
index 9ca0bcf..55c91bb 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,13 @@ An app for [Seq](https://datalust.co/seq) that forwards messages to [Slack](http
### Getting started
- 1. Install the app into Seq through the Seq UI: _Settings_ > _Apps_ > _Install from Nuget_. The package id is _Seq.App.Slack_.
- 2. In Slack, select _Manage apps_ > _Search App Directory_ > _"Incoming WebHooks"_
- 3. Add a new incoming webhook configuration and copy the _Webhook URL_
- 4. Back in Seq, under _Settings_ > _Apps_, select _Add Instance_ next to the Slack app icon
- 5. Configure the app instance, providing the webhook URL
+ 1. Install the app into Seq through the Seq UI: _Settings_ > _Apps_ > _Install from NuGet_; the package id is _Seq.App.Slack_
+ 2. In Slack, select _Admin_ > _Apps and Workflows_ > _Build_ > _Create new App_ > _From Scratch_
+ 3. In the app registration, choose _Incoming WebHooks_ (this is the new endpoint, not the legacy one)
+ 4. Add a new incoming webhook configuration and copy the _Webhook URL_
+ 5. Back in Seq, under _Settings_ > _Apps_, select _Add Instance_ next to the Slack app icon
+ 6. Configure the app instance, providing the webhook URL
Consult the Seq documentation for further information about [installing Seq apps](https://docs.datalust.co/docs/installing-seq-apps).
-For more information see [Notifying with Slack](https://docs.datalust.co/docs/slack-notifications).
\ No newline at end of file
+For more information see [Notifying with Slack](https://docs.datalust.co/docs/slack-notifications).
diff --git a/Seq.App.Slack.sln b/Seq.App.Slack.sln
index 91eba8e..1016df2 100644
--- a/Seq.App.Slack.sln
+++ b/Seq.App.Slack.sln
@@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{225F20AD
README.md = README.md
Build.ps1 = Build.ps1
Run.ps1 = Run.ps1
+ Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Seq.App.Slack.Tests", "test\Seq.App.Slack.Tests\Seq.App.Slack.Tests.csproj", "{143E44C0-C8B0-473D-9522-B31BCAA81A80}"
diff --git a/src/Seq.App.Slack/Api/ISlackApi.cs b/src/Seq.App.Slack/Api/ISlackApi.cs
index 992d057..f70a0ba 100644
--- a/src/Seq.App.Slack/Api/ISlackApi.cs
+++ b/src/Seq.App.Slack/Api/ISlackApi.cs
@@ -1,9 +1,8 @@
using System.Threading.Tasks;
-namespace Seq.App.Slack.Api
+namespace Seq.App.Slack.Api;
+
+public interface ISlackApi
{
- public interface ISlackApi
- {
- Task SendMessageAsync(string webhookUrl, SlackMessage message);
- }
-}
+ Task SendMessageAsync(string webhookUrl, SlackMessage message);
+}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Api/SlackApi.cs b/src/Seq.App.Slack/Api/SlackApi.cs
index 8675022..1ef5695 100644
--- a/src/Seq.App.Slack/Api/SlackApi.cs
+++ b/src/Seq.App.Slack/Api/SlackApi.cs
@@ -4,52 +4,51 @@
using System.Threading.Tasks;
using Newtonsoft.Json;
-namespace Seq.App.Slack.Api
+namespace Seq.App.Slack.Api;
+
+class SlackApi : ISlackApi
{
- class SlackApi : ISlackApi
+ static SlackApi()
{
- static SlackApi()
- {
- // Enable TLS 1.2 before any connection to the Slack API is made.
- ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
- }
+ // Enable TLS 1.2 before any connection to the Slack API is made.
+ ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
+ }
- private readonly HttpClient _httpClient;
- private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
- {
- NullValueHandling = NullValueHandling.Ignore
- };
+ private readonly HttpClient _httpClient;
+ private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
+ {
+ NullValueHandling = NullValueHandling.Ignore
+ };
- public SlackApi(string proxyServer)
+ public SlackApi(string? proxyServer)
+ {
+ if (!string.IsNullOrWhiteSpace(proxyServer))
{
- if (!string.IsNullOrWhiteSpace(proxyServer))
+ var proxy = new WebProxy(proxyServer, false)
{
- var proxy = new WebProxy(proxyServer, false)
- {
- UseDefaultCredentials = true
- };
- var httpClientHandler = new HttpClientHandler()
- {
- Proxy = proxy,
- PreAuthenticate = true,
- UseDefaultCredentials = true,
- };
- _httpClient = new HttpClient(handler: httpClientHandler);
- }
- else
+ UseDefaultCredentials = true
+ };
+ var httpClientHandler = new HttpClientHandler()
{
- _httpClient = new HttpClient();
- }
+ Proxy = proxy,
+ PreAuthenticate = true,
+ UseDefaultCredentials = true,
+ };
+ _httpClient = new HttpClient(handler: httpClientHandler);
+ }
+ else
+ {
+ _httpClient = new HttpClient();
}
+ }
- public async Task SendMessageAsync(string webhookUrl, SlackMessage message)
+ public async Task SendMessageAsync(string webhookUrl, SlackMessage message)
+ {
+ var json = JsonConvert.SerializeObject(message, JsonSettings);
+ using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
{
- var json = JsonConvert.SerializeObject(message, JsonSettings);
- using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
- {
- var resp = await _httpClient.PostAsync(webhookUrl, content);
- resp.EnsureSuccessStatusCode();
- }
+ var resp = await _httpClient.PostAsync(webhookUrl, content);
+ resp.EnsureSuccessStatusCode();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Api/SlackMessage.cs b/src/Seq.App.Slack/Api/SlackMessage.cs
index f4eb8bf..c57cac0 100644
--- a/src/Seq.App.Slack/Api/SlackMessage.cs
+++ b/src/Seq.App.Slack/Api/SlackMessage.cs
@@ -1,36 +1,35 @@
using System.Collections.Generic;
using Newtonsoft.Json;
-namespace Seq.App.Slack.Api
+namespace Seq.App.Slack.Api;
+
+public class SlackMessage
{
- public class SlackMessage
- {
- [JsonProperty("fallback")]
- public string Fallback { get; }
+ [JsonProperty("fallback")]
+ public string Fallback { get; }
- [JsonProperty("text")]
- public string Text { get; }
+ [JsonProperty("text")]
+ public string Text { get; }
- [JsonProperty("attachments")]
- public List Attachments { get; }
+ [JsonProperty("attachments")]
+ public List Attachments { get; }
- [JsonProperty("username")]
- public string Username { get; }
+ [JsonProperty("username")]
+ public string Username { get; }
- [JsonProperty("icon_url")]
- public string IconUrl { get; }
+ [JsonProperty("icon_url")]
+ public string IconUrl { get; }
- [JsonProperty("channel")]
- public string Channel { get; }
+ [JsonProperty("channel")]
+ public string? Channel { get; }
- public SlackMessage(string fallback, string text, string username, string iconUrl, string channel)
- {
- this.Fallback = fallback;
- this.Text = text;
- this.Attachments = new List();
- this.Username = username;
- this.IconUrl = iconUrl;
- this.Channel = channel;
- }
+ public SlackMessage(string fallback, string text, string username, string iconUrl, string? channel)
+ {
+ this.Fallback = fallback;
+ this.Text = text;
+ this.Attachments = new List();
+ this.Username = username;
+ this.IconUrl = iconUrl;
+ this.Channel = channel;
}
}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Api/SlackMessageAttachment.cs b/src/Seq.App.Slack/Api/SlackMessageAttachment.cs
index dd9f010..eab7953 100644
--- a/src/Seq.App.Slack/Api/SlackMessageAttachment.cs
+++ b/src/Seq.App.Slack/Api/SlackMessageAttachment.cs
@@ -1,35 +1,34 @@
using System.Collections.Generic;
using Newtonsoft.Json;
-namespace Seq.App.Slack.Api
+namespace Seq.App.Slack.Api;
+
+public class SlackMessageAttachment
{
- public class SlackMessageAttachment
- {
- [JsonProperty("color")]
- public string Color { get; }
+ [JsonProperty("color")]
+ public string Color { get; }
- [JsonProperty("text")]
- public string Text { get; }
+ [JsonProperty("text")]
+ public string? Text { get; }
- [JsonProperty("title")]
- public string Title { get; }
+ [JsonProperty("title")]
+ public string? Title { get; }
- [JsonProperty("fields")]
- public List Fields { get; }
+ [JsonProperty("fields")]
+ public List Fields { get; }
- [JsonProperty("mrkdwn_in")]
- public List MarkdownIn { get; }
+ [JsonProperty("mrkdwn_in")]
+ public List MarkdownIn { get; }
- public SlackMessageAttachment(string color, string text = null, string title = null, bool textIsMarkdown = false)
- {
- this.Color = color;
- this.Text = text;
- this.Title = title;
- this.Fields = new List();
- this.MarkdownIn = new List();
+ public SlackMessageAttachment(string color, string? text = null, string? title = null, bool textIsMarkdown = false)
+ {
+ this.Color = color;
+ this.Text = text;
+ this.Title = title;
+ this.Fields = new List();
+ this.MarkdownIn = new List();
- if (textIsMarkdown)
- this.MarkdownIn.Add("text");
- }
+ if (textIsMarkdown)
+ this.MarkdownIn.Add("text");
}
-}
+}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Api/SlackMessageAttachmentField.cs b/src/Seq.App.Slack/Api/SlackMessageAttachmentField.cs
index d2ad399..63926f8 100644
--- a/src/Seq.App.Slack/Api/SlackMessageAttachmentField.cs
+++ b/src/Seq.App.Slack/Api/SlackMessageAttachmentField.cs
@@ -1,23 +1,22 @@
using Newtonsoft.Json;
-namespace Seq.App.Slack.Api
+namespace Seq.App.Slack.Api;
+
+public class SlackMessageAttachmentField
{
- public class SlackMessageAttachmentField
- {
- [JsonProperty("title")]
- public string Title { get; }
+ [JsonProperty("title")]
+ public string Title { get; }
- [JsonProperty("value")]
- public string Value { get; }
+ [JsonProperty("value")]
+ public string Value { get; }
- [JsonProperty("short")]
- public bool Short { get; }
+ [JsonProperty("short")]
+ public bool Short { get; }
- public SlackMessageAttachmentField(string title, string value, bool @short)
- {
- Title = title;
- Value = value;
- Short = @short;
- }
+ public SlackMessageAttachmentField(string title, string value, bool @short)
+ {
+ Title = title;
+ Value = value;
+ Short = @short;
}
}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Formatting/EventFormatting.cs b/src/Seq.App.Slack/Formatting/EventFormatting.cs
index 607e023..9aa5033 100644
--- a/src/Seq.App.Slack/Formatting/EventFormatting.cs
+++ b/src/Seq.App.Slack/Formatting/EventFormatting.cs
@@ -1,110 +1,106 @@
-using System;
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
+using System.Text.RegularExpressions;
using Seq.Apps;
using Seq.Apps.LogEvents;
using Serilog;
-namespace Seq.App.Slack.Formatting
+namespace Seq.App.Slack.Formatting;
+
+static class EventFormatting
{
- static class EventFormatting
+ private static readonly Regex PlaceholdersRegex = new(@"(\[(?[^\[\]]+?)(\:(?[^\[\]]+?))?\])", RegexOptions.CultureInvariant | RegexOptions.Compiled);
+
+ private static readonly IReadOnlyDictionary LevelColorMap = new Dictionary
+ {
+ [LogEventLevel.Verbose] = "#D3D3D3",
+ [LogEventLevel.Debug] = "#D3D3D3",
+ [LogEventLevel.Information] = "#00A000",
+ [LogEventLevel.Warning] = "#f9c019",
+ [LogEventLevel.Error] = "#e03836",
+ [LogEventLevel.Fatal] = "#e03836",
+ };
+
+ public static string LevelToColor(LogEventLevel level)
{
- private static readonly Regex PlaceholdersRegex = new Regex(@"(\[(?[^\[\]]+?)(\:(?[^\[\]]+?))?\])", RegexOptions.CultureInvariant | RegexOptions.Compiled);
+ return LevelColorMap[level];
+ }
- private static readonly IReadOnlyDictionary LevelColorMap = new Dictionary
- {
- [LogEventLevel.Verbose] = "#D3D3D3",
- [LogEventLevel.Debug] = "#D3D3D3",
- [LogEventLevel.Information] = "#00A000",
- [LogEventLevel.Warning] = "#f9c019",
- [LogEventLevel.Error] = "#e03836",
- [LogEventLevel.Fatal] = "#e03836",
- };
-
- public static string LevelToColor(LogEventLevel level)
- {
- return LevelColorMap[level];
- }
+ public static string SafeGetProperty(Event evt, string propertyPath, bool raw = false)
+ {
+ var path = new Queue(propertyPath.Split('.'));
+ var root = evt.Data.Properties;
- public static string SafeGetProperty(Event evt, string propertyPath, bool raw = false)
+ while(root != null)
{
- var path = new Queue(propertyPath.Split('.'));
- var root = evt.Data.Properties;
+ var step = path.Dequeue();
+ if (!root.TryGetValue(step, out var next))
+ return "";
- while(root != null)
+ if (path.Count == 0)
{
- var step = path.Dequeue();
- if (!root.TryGetValue(step, out var next))
- return "";
-
- if (path.Count == 0)
- {
- if (next == null) return "`null`";
- return raw ? next.ToString() : SlackSyntax.Escape(next.ToString());
- }
-
- root = next as IReadOnlyDictionary;
+ if (next == null) return "`null`";
+ return raw ? next.ToString() ?? "" : SlackSyntax.Escape(next.ToString() ?? "");
}
- return "";
+ root = next as IReadOnlyDictionary;
}
- public static string SubstitutePlaceholders(string messageTemplateToUse, Event evt, bool addLogData = true)
- {
- var data = evt.Data;
- var eventType = evt.EventType;
- var level = data.Level;
+ return "";
+ }
+
+ public static string SubstitutePlaceholders(string messageTemplateToUse, Event evt, bool addLogData = true)
+ {
+ var data = evt.Data;
+ var eventType = evt.EventType;
+ var level = data.Level;
- var placeholders = new Dictionary();
- if (data.Properties != null)
- foreach (var kvp in data.Properties)
- placeholders[kvp.Key.ToLower()] = kvp.Value;
+ var placeholders = new Dictionary();
+ if (data.Properties != null)
+ foreach (var kvp in data.Properties)
+ placeholders[kvp.Key.ToLower()] = kvp.Value;
- if (addLogData)
- {
- AddValueIfKeyDoesNotExist(placeholders, "Level", level);
- AddValueIfKeyDoesNotExist(placeholders, "EventType", eventType);
- AddValueIfKeyDoesNotExist(placeholders, "RenderedMessage", data.RenderedMessage);
- AddValueIfKeyDoesNotExist(placeholders, "Exception", data.Exception);
- }
- return PlaceholdersRegex.Replace(messageTemplateToUse, m =>
- {
- var key = m.Groups["key"].Value.ToLower();
- var format = m.Groups["format"].Value;
- return placeholders.ContainsKey(key) ? FormatValue(placeholders[key], format) : m.Value;
- });
+ if (addLogData)
+ {
+ AddValueIfKeyDoesNotExist(placeholders, "Level", level);
+ AddValueIfKeyDoesNotExist(placeholders, "EventType", eventType);
+ AddValueIfKeyDoesNotExist(placeholders, "RenderedMessage", data.RenderedMessage);
+ AddValueIfKeyDoesNotExist(placeholders, "Exception", data.Exception);
}
-
- private static string FormatValue(object value, string format)
+ return PlaceholdersRegex.Replace(messageTemplateToUse, m =>
{
- var rawValue = value?.ToString() ?? SlackSyntax.Code("null");
-
- if (string.IsNullOrWhiteSpace(format))
- return rawValue;
+ var key = m.Groups["key"].Value.ToLower();
+ var format = m.Groups["format"].Value;
+ return placeholders.TryGetValue(key, out var placeholder) ? FormatValue(placeholder, format) : m.Value;
+ });
+ }
- try
- {
- // Field values can contain formatting.
- return SlackSyntax.Escape(string.Format(format, rawValue));
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not format Slack message: {Value} {Format}", value, format);
- }
+ private static string FormatValue(object? value, string format)
+ {
+ var rawValue = value?.ToString() ?? SlackSyntax.Code("null");
+ if (string.IsNullOrWhiteSpace(format))
return rawValue;
- }
- private static void AddValueIfKeyDoesNotExist(IDictionary placeholders, string key, object value)
+ try
{
- var loweredKey = key.ToLower();
- if (!placeholders.ContainsKey(loweredKey))
- placeholders.Add(loweredKey, value);
+ // Field values can contain formatting.
+ return SlackSyntax.Escape(string.Format(format, rawValue));
}
-
- public static string LinkToId(Host host, string eventId)
+ catch (Exception ex)
{
- return $"{host.BaseUri.TrimEnd('/')}/#/events?filter=@Id%20%3D%3D%20%22{eventId}%22&show=expanded";
+ Log.Error(ex, "Could not format Slack message: {Value} {Format}", value, format);
}
+
+ return rawValue;
+ }
+
+ private static void AddValueIfKeyDoesNotExist(IDictionary placeholders, string key, object value)
+ {
+ var loweredKey = key.ToLower();
+ placeholders.TryAdd(loweredKey, value);
+ }
+
+ public static string LinkToId(Host host, string eventId)
+ {
+ return $"{host.BaseUri.TrimEnd('/')}/#/events?filter=@Id%20%3D%3D%20%22{eventId}%22&show=expanded";
}
-}
+}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Formatting/PropertyValueFormatter.cs b/src/Seq.App.Slack/Formatting/PropertyValueFormatter.cs
index fdbcd91..78e3130 100644
--- a/src/Seq.App.Slack/Formatting/PropertyValueFormatter.cs
+++ b/src/Seq.App.Slack/Formatting/PropertyValueFormatter.cs
@@ -1,49 +1,38 @@
-using System;
-using System.Collections.Generic;
-using Newtonsoft.Json;
+using Newtonsoft.Json;
-namespace Seq.App.Slack.Formatting
+namespace Seq.App.Slack.Formatting;
+
+class PropertyValueFormatter
{
- class PropertyValueFormatter
- {
- private readonly int? _maxPropertyLength;
+ private readonly int? _maxPropertyLength;
- private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
- {
- NullValueHandling = NullValueHandling.Ignore
- };
+ private static readonly JsonSerializerSettings JsonSettings = new()
+ {
+ NullValueHandling = NullValueHandling.Ignore
+ };
- public PropertyValueFormatter(int? maxPropertyLength)
- {
- _maxPropertyLength = maxPropertyLength;
- }
+ public PropertyValueFormatter(int? maxPropertyLength)
+ {
+ _maxPropertyLength = maxPropertyLength;
+ }
- public string ConvertPropertyValueToString(object propertyValue)
- {
- if (propertyValue == null)
- return string.Empty;
+ public string ConvertPropertyValueToString(object? propertyValue)
+ {
+ if (propertyValue == null)
+ return string.Empty;
- string result;
- Type t = propertyValue.GetType();
- bool isDict = t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>);
- if (isDict)
- {
- result = JsonConvert.SerializeObject(propertyValue, JsonSettings);
- }
- else
- {
- result = propertyValue.ToString();
- }
+ var t = propertyValue.GetType();
+ var isDict = t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>);
+ var result = isDict ? JsonConvert.SerializeObject(propertyValue, JsonSettings) : propertyValue.ToString();
- if (_maxPropertyLength.HasValue)
+ if (_maxPropertyLength.HasValue)
+ {
+ if (result?.Length > _maxPropertyLength)
{
- if (result.Length > _maxPropertyLength)
- {
- result = result.Substring(0, _maxPropertyLength.Value) + "...";
- }
+ result = result[.._maxPropertyLength.Value] + "...";
}
-
- return result;
}
+
+ return result ?? "";
}
}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Formatting/SlackSyntax.cs b/src/Seq.App.Slack/Formatting/SlackSyntax.cs
index e81753d..1f58bd4 100644
--- a/src/Seq.App.Slack/Formatting/SlackSyntax.cs
+++ b/src/Seq.App.Slack/Formatting/SlackSyntax.cs
@@ -1,35 +1,34 @@
using System;
-namespace Seq.App.Slack.Formatting
+namespace Seq.App.Slack.Formatting;
+
+static class SlackSyntax
{
- static class SlackSyntax
+ public static string Escape(string s)
{
- public static string Escape(string s)
- {
- if (s == null) throw new ArgumentNullException(nameof(s));
- return s
- .Replace("<", "<")
- .Replace(">", ">")
- .Replace("&", "&");
- }
+ if (s == null) throw new ArgumentNullException(nameof(s));
+ return s
+ .Replace("<", "<")
+ .Replace(">", ">")
+ .Replace("&", "&");
+ }
- public static string Hyperlink(string url, string caption)
- {
- if (url == null) throw new ArgumentNullException(nameof(url));
- if (caption == null) throw new ArgumentNullException(nameof(caption));
- return $"<{url}|{caption}>";
- }
+ public static string Hyperlink(string url, string caption)
+ {
+ if (url == null) throw new ArgumentNullException(nameof(url));
+ if (caption == null) throw new ArgumentNullException(nameof(caption));
+ return $"<{url}|{caption}>";
+ }
- public static string Preformatted(string s)
- {
- if (s == null) throw new ArgumentNullException(nameof(s));
- return $"```\n{s.Replace("\r", "")}\n```";
- }
+ public static string Preformatted(string s)
+ {
+ if (s == null) throw new ArgumentNullException(nameof(s));
+ return $"```\n{s.Replace("\r", "")}\n```";
+ }
- public static string Code(string s)
- {
- if (s == null) throw new ArgumentNullException(nameof(s));
- return $"`{s}`";
- }
+ public static string Code(string s)
+ {
+ if (s == null) throw new ArgumentNullException(nameof(s));
+ return $"`{s}`";
}
-}
+}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Messages/AlertV1MessageBuilder.cs b/src/Seq.App.Slack/Messages/AlertV1MessageBuilder.cs
index cad5515..ba9e65d 100644
--- a/src/Seq.App.Slack/Messages/AlertV1MessageBuilder.cs
+++ b/src/Seq.App.Slack/Messages/AlertV1MessageBuilder.cs
@@ -3,44 +3,43 @@
using Seq.Apps;
using Seq.Apps.LogEvents;
-namespace Seq.App.Slack.Messages
+namespace Seq.App.Slack.Messages;
+
+class AlertV1MessageBuilder : SlackMessageBuilder
{
- class AlertV1MessageBuilder : SlackMessageBuilder
+ private readonly string? _messageTemplate;
+
+ public AlertV1MessageBuilder(Apps.App app, string? channel, string? username, string? messageTemplate, string? iconUrl, bool excludeOptionalAttachments)
+ : base(app, channel, username, iconUrl, excludeOptionalAttachments)
{
- private readonly string _messageTemplate;
+ _messageTemplate = messageTemplate;
+ }
- public AlertV1MessageBuilder(Apps.App app, string channel, string username, string messageTemplate, string iconUrl, bool excludeOptionalAttachments)
- : base(app, channel, username, iconUrl, excludeOptionalAttachments)
+ protected override string GenerateMessageText(Event evt)
+ {
+ var dashboardUrl = EventFormatting.SafeGetProperty(evt, "DashboardUrl");
+ var condition = EventFormatting.SafeGetProperty(evt, "Condition", raw: true);
+ var dashboardTitle = EventFormatting.SafeGetProperty(evt, "DashboardTitle");
+ var chartTitle = EventFormatting.SafeGetProperty(evt, "ChartTitle");
+ var ownerNamespace = "";
+ if (evt.Data.Properties.TryGetValue("OwnerUsername", out var ownerUsernameProperty) && ownerUsernameProperty is string ownerUsername)
{
- _messageTemplate = messageTemplate;
+ if (!string.IsNullOrEmpty(ownerUsername))
+ ownerNamespace = SlackSyntax.Escape(ownerUsername) + "/";
}
+ return $"Alert condition {SlackSyntax.Code(condition)} detected on {SlackSyntax.Hyperlink(dashboardUrl, $"{ownerNamespace}{dashboardTitle}/{chartTitle}")}.";
+ }
- protected override string GenerateMessageText(Event evt)
- {
- var dashboardUrl = EventFormatting.SafeGetProperty(evt, "DashboardUrl");
- var condition = EventFormatting.SafeGetProperty(evt, "Condition", raw: true);
- var dashboardTitle = EventFormatting.SafeGetProperty(evt, "DashboardTitle");
- var chartTitle = EventFormatting.SafeGetProperty(evt, "ChartTitle");
- var ownerNamespace = "";
- if (evt.Data.Properties.TryGetValue("OwnerUsername", out var ownerUsernameProperty) && ownerUsernameProperty is string ownerUsername)
- {
- if (!string.IsNullOrEmpty(ownerUsername))
- ownerNamespace = SlackSyntax.Escape(ownerUsername) + "/";
- }
- return $"Alert condition {SlackSyntax.Code(condition)} detected on {SlackSyntax.Hyperlink(dashboardUrl, $"{ownerNamespace}{dashboardTitle}/{chartTitle}")}.";
- }
+ protected override void AddNecessaryAttachments(SlackMessage message, Event evt, string color)
+ {
+ var resultsUrl = EventFormatting.SafeGetProperty(evt, "ResultsUrl");
+ var resultsText = SlackSyntax.Hyperlink(resultsUrl, "Explore detected results in Seq");
+ var results = new SlackMessageAttachment(color, resultsText);
+ message.Attachments.Add(results);
- protected override void AddNecessaryAttachments(SlackMessage message, Event evt, string color)
+ if (_messageTemplate != null)
{
- var resultsUrl = EventFormatting.SafeGetProperty(evt, "ResultsUrl");
- var resultsText = SlackSyntax.Hyperlink(resultsUrl, "Explore detected results in Seq");
- var results = new SlackMessageAttachment(color, resultsText);
- message.Attachments.Add(results);
-
- if (_messageTemplate != null)
- {
- message.Attachments.Add(new SlackMessageAttachment(color, _messageTemplate, null, true));
- }
+ message.Attachments.Add(new SlackMessageAttachment(color, _messageTemplate, null, true));
}
}
}
\ No newline at end of file
diff --git a/src/Seq.App.Slack/Messages/AlertV2MessageBuilder.cs b/src/Seq.App.Slack/Messages/AlertV2MessageBuilder.cs
index 9b29797..b00c4f6 100644
--- a/src/Seq.App.Slack/Messages/AlertV2MessageBuilder.cs
+++ b/src/Seq.App.Slack/Messages/AlertV2MessageBuilder.cs
@@ -1,95 +1,145 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System.Text;
using Seq.App.Slack.Api;
using Seq.App.Slack.Formatting;
using Seq.Apps;
using Seq.Apps.LogEvents;
// ReSharper disable PossibleMultipleEnumeration
-namespace Seq.App.Slack.Messages
+namespace Seq.App.Slack.Messages;
+
+class AlertV2MessageBuilder : SlackMessageBuilder
{
- class AlertV2MessageBuilder : SlackMessageBuilder
+ private readonly Host _host;
+ private readonly PropertyValueFormatter _propertyValueFormatter;
+ private readonly string? _messageTemplate;
+ private static readonly HashSet SpecialProperties = ["NamespacedAlertTitle", "Alert", "Source", "SuppressedUntil", "Failures"];
+
+ public AlertV2MessageBuilder(Host host, Apps.App app, PropertyValueFormatter propertyValueFormatter, string? channel, string? username, string? messageTemplate, string? iconUrl, bool excludeOptionalAttachments)
+ : base(app, channel, username, iconUrl, excludeOptionalAttachments)
+ {
+ _host = host;
+ _propertyValueFormatter = propertyValueFormatter ?? throw new ArgumentNullException(nameof(propertyValueFormatter));
+ _messageTemplate = messageTemplate;
+ }
+
+ protected override string GenerateMessageText(Event evt)
+ {
+ var namespacedAlertTitle = EventFormatting.SafeGetProperty(evt, "NamespacedAlertTitle");
+ var alertUrl = EventFormatting.SafeGetProperty(evt, "Alert.Url");
+ return $"Alert condition triggered by {SlackSyntax.Hyperlink(alertUrl, namespacedAlertTitle)}";
+ }
+
+ protected override void AddNecessaryAttachments(SlackMessage message, Event evt, string color)
{
- private readonly Host _host;
- private readonly PropertyValueFormatter _propertyValueFormatter;
- private readonly string _messageTemplate;
- private static readonly HashSet SpecialProperties = new HashSet(new[] { "NamespacedAlertTitle", "Alert", "Source", "SuppressedUntil", "Failures" });
+ var resultsUrl = EventFormatting.SafeGetProperty(evt, "Source.ResultsUrl");
+ var contributingEventsUrl = EventFormatting.SafeGetProperty(evt, "Source.ContributingEventsUrl");
- public AlertV2MessageBuilder(Host host, Apps.App app, PropertyValueFormatter propertyValueFormatter, string channel, string username, string messageTemplate, string iconUrl, bool excludeOptionalAttachments)
- : base(app, channel, username, iconUrl, excludeOptionalAttachments)
+ if (!string.IsNullOrWhiteSpace(contributingEventsUrl))
{
- _host = host;
- _propertyValueFormatter = propertyValueFormatter ?? throw new ArgumentNullException(nameof(propertyValueFormatter));
- _messageTemplate = messageTemplate;
+ // 2026.1+
+ var exploreText =
+ "Explore " +
+ SlackSyntax.Hyperlink(resultsUrl, "detected results") +
+ " and " +
+ SlackSyntax.Hyperlink(contributingEventsUrl, "contributing events") +
+ " in Seq";
+ message.Attachments.Add(new SlackMessageAttachment(color, exploreText));
}
-
- protected override string GenerateMessageText(Event evt)
+ else
{
- var namespacedAlertTitle = EventFormatting.SafeGetProperty(evt, "NamespacedAlertTitle");
- var alertUrl = EventFormatting.SafeGetProperty(evt, "Alert.Url");
- return $"Alert condition triggered by {SlackSyntax.Hyperlink(alertUrl, namespacedAlertTitle)}";
+ // 2025.2 and earlier
+ var resultsText = SlackSyntax.Hyperlink(resultsUrl, "Explore detected results in Seq");
+ message.Attachments.Add(new SlackMessageAttachment(color, resultsText));
}
- protected override void AddNecessaryAttachments(SlackMessage message, Event evt, string color)
+ if (_messageTemplate != null)
{
- var resultsUrl = EventFormatting.SafeGetProperty(evt, "Source.ResultsUrl");
- var resultsText = SlackSyntax.Hyperlink(resultsUrl, "Explore detected results in Seq");
- var results = new SlackMessageAttachment(color, resultsText);
- message.Attachments.Add(results);
+ message.Attachments.Add(new SlackMessageAttachment(color, _messageTemplate, null, true));
+ }
- if (_messageTemplate != null)
+ if (evt.Data.Properties.TryGetValue("Failures", out var f) &&
+ f is IEnumerable