Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tag enhancement #12

Open
wants to merge 21 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
529302d
Merge pull request #2 from datalust/dev
MattMofDoom Jun 16, 2021
a4cf7a4
Merge branch 'datalust:dev' into dev
MattMofDoom Aug 9, 2021
b1b8cbf
Update description to LongText input type
MattMofDoom Aug 9, 2021
64a0eb7
Merge branch 'dev' of https://github.com/MattMofDoom/seq-app-opsgenie…
MattMofDoom Aug 9, 2021
40b40d8
Update Handlebars.NET
MattMofDoom Aug 9, 2021
e5187e9
Merge branch 'datalust:dev' into dev
MattMofDoom Aug 17, 2021
b3d52a8
Add optional suppression timeframe
MattMofDoom Aug 25, 2021
1bcdeb3
Merge branch 'dev' of https://github.com/MattMofDoom/seq-app-opsgenie…
MattMofDoom Aug 25, 2021
7120c43
Improve logging of OpsGenie results, add Seq properties to OpsGenie, …
MattMofDoom Nov 24, 2021
4ae80e2
Fix product name
MattMofDoom Nov 24, 2021
55dcd2b
Merge branch 'BetterLogging' into dev
MattMofDoom Nov 24, 2021
ddff0cb
Merge pull request #3 from MattMofDoom/dev
MattMofDoom Nov 24, 2021
723ca69
Update dependencies
MattMofDoom Jan 31, 2022
0659eaa
Merge branch 'BetterLogging' of https://github.com/MattMofDoom/seq-ap…
MattMofDoom Jan 31, 2022
2b79763
Allow tags to be passed as a comma-delimited string
MattMofDoom Feb 10, 2022
f5a7b88
Update Microsoft.NET.Test.SDK
MattMofDoom Feb 23, 2022
71ea744
Re-work test cases to evaluate OpsGenieAlert
MattMofDoom May 18, 2022
89cb77e
Remove Version tag
MattMofDoom May 18, 2022
5c81208
Reorganise to Api and Client, remove suppression time
MattMofDoom May 19, 2022
308c7c0
Update dependencies
MattMofDoom May 19, 2022
493c101
Update dependencies
MattMofDoom Jul 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions seq-app-opsgenie.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CI/@EntryIndexedValue">CI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IO/@EntryIndexedValue">IO</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=app_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jira/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Opsgenie/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using Seq.Apps;
using Seq.Apps.LogEvents;

namespace Seq.App.Opsgenie
namespace Seq.App.Opsgenie.Classes
{
class HandlebarsTemplate
{
Expand All @@ -26,32 +26,33 @@ public string Render(Event<LogEventData> evt)
if (evt == null) throw new ArgumentNullException(nameof(evt));
return FormatTemplate(_template, evt, _host);
}

static string FormatTemplate(Func<object, string> template, Event<LogEventData> evt, Host host)
{
var properties = (IDictionary<string,object>) ToDynamic(evt.Data.Properties ?? new Dictionary<string, object>());
var properties =
(IDictionary<string, object>) ToDynamic(evt.Data.Properties ?? new Dictionary<string, object>());

var payload = (IDictionary<string,object>) ToDynamic(new Dictionary<string, object>
var payload = (IDictionary<string, object>) ToDynamic(new Dictionary<string, object>
{
{ "$Id", evt.Id },
{ "$UtcTimestamp", evt.TimestampUtc },
{ "$LocalTimestamp", evt.Data.LocalTimestamp },
{ "$Level", evt.Data.Level },
{ "$MessageTemplate", evt.Data.MessageTemplate },
{ "$Message", evt.Data.RenderedMessage },
{ "$Exception", evt.Data.Exception },
{ "$Properties", properties },
{ "$EventType", "$" + evt.EventType.ToString("X8") },
{ "$Instance", host.InstanceName },
{ "$ServerUri", host.BaseUri },
{"$Id", evt.Id},
{"$UtcTimestamp", evt.TimestampUtc},
{"$LocalTimestamp", evt.Data.LocalTimestamp},
{"$Level", evt.Data.Level},
{"$MessageTemplate", evt.Data.MessageTemplate},
{"$Message", evt.Data.RenderedMessage},
{"$Exception", evt.Data.Exception},
{"$Properties", properties},
{"$EventType", "$" + evt.EventType.ToString("X8")},
{"$Instance", host.InstanceName},
{"$ServerUri", host.BaseUri},
// Note, this will only be valid when events are streamed directly to the app, and not when the app is sending an alert notification.
{ "$EventUri", string.Concat(host.BaseUri, "#/events?filter=@Id%20%3D%20'", evt.Id, "'&amp;show=expanded") }
{
"$EventUri",
string.Concat(host.BaseUri, "#/events?filter=@Id%20%3D%20'", evt.Id, "'&amp;show=expanded")
}
});

foreach (var property in properties)
{
payload[property.Key] = property.Value;
}
foreach (var property in properties) payload[property.Key] = property.Value;

return template(payload);
}
Expand All @@ -67,10 +68,7 @@ static object ToDynamic(object o)
return result;
}

if (o is IEnumerable<object> enumerable)
{
return enumerable.Select(ToDynamic).ToArray();
}
if (o is IEnumerable<object> enumerable) return enumerable.Select(ToDynamic).ToArray();

return o;
}
Expand Down
33 changes: 33 additions & 0 deletions src/Seq.App.Opsgenie/Classes/OpsGenieData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global

// ReSharper disable ClassNeverInstantiated.Global

namespace Seq.App.Opsgenie.Classes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organizing into Classes and Interfaces might not give as much insight into the structure of the codebase as some alternatives - could this one go in Seq.App.Opsgenie.Api, perhaps, since the structure of these types (presumably) matches the requirements of the Opsgenie API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah should be fine. I'll take a look.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reorganised into Api and Client

{
public class OpsGenieData
{
public OpsGenieData()
{
Success = false;
Action = string.Empty;
ProcessedAt = DateTime.Now;
IntegrationId = string.Empty;
IsSuccess = false;
Status = string.Empty;
AlertId = string.Empty;
Alias = string.Empty;
}

public bool Success { get; set; }
public string Action { get; set; }
public DateTime ProcessedAt { get; set; }
public string IntegrationId { get; set; }
public bool IsSuccess { get; set; }
public string Status { get; set; }
public string AlertId { get; set; }
public string Alias { get; set; }
}
}
19 changes: 19 additions & 0 deletions src/Seq.App.Opsgenie/Classes/OpsGenieResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// ReSharper disable UnusedMember.Global

namespace Seq.App.Opsgenie.Classes
{
public class OpsGenieResponse
{
public OpsGenieResponse()
{
Result = string.Empty;
RequestId = string.Empty;
Took = -1;
}

public OpsGenieData Data { get; set; }
public string Result { get; set; }
public string RequestId { get; set; }
public decimal Took { get; set; }
}
}
28 changes: 28 additions & 0 deletions src/Seq.App.Opsgenie/Classes/OpsGenieResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Net.Http;
// ReSharper disable UnusedAutoPropertyAccessor.Global

// ReSharper disable NotAccessedField.Global

namespace Seq.App.Opsgenie.Classes
{
public class OpsGenieResult
{
public OpsGenieResult()
{
Ok = false;
StatusCode = -1;
HttpResponse = null;
Response = null;
Error = null;
ResponseBody = string.Empty;
}

public bool Ok { get; set; }
public int StatusCode { get; set; }
public HttpResponseMessage HttpResponse { get; set; }
public OpsGenieResponse Response { get; set; }
public Exception Error { get; set; }
public string ResponseBody { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,17 @@
// Properties on this type are serialized into the JSON payload sent to OpsGenie.
// ReSharper disable MemberCanBePrivate.Global, UnusedAutoPropertyAccessor.Global

namespace Seq.App.Opsgenie
namespace Seq.App.Opsgenie.Classes
{
class OpsgenieAlert
{
public string Message { get; }
public string Alias { get; }
public string Description { get; }
public string Priority { get; }
public List<Responder> Responders { get; }
public string Source { get; }
public string[] Tags { get; }

public OpsgenieAlert(
string message,
string alias,
string description,
string priority,
List<Responder> responders,
Dictionary<string, string> details,
string source,
string[] tags)
{
Expand All @@ -29,8 +22,18 @@ public OpsgenieAlert(
Description = description;
Priority = priority;
Responders = responders;
Details = details;
Source = source;
Tags = tags;
}
}

public string Message { get; }
public string Alias { get; }
public string Description { get; }
public string Priority { get; }
public List<Responder> Responders { get; }
public Dictionary<string, string> Details { get; }
public string Source { get; }
public string[] Tags { get; }
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Values of this type are populated from user-provided configuration.
// ReSharper disable UnusedMember.Global

namespace Seq.App.Opsgenie
namespace Seq.App.Opsgenie.Classes
{
public enum Priority
{
Expand All @@ -11,4 +11,4 @@ public enum Priority
P4 = 4,
P5 = 5
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System.Text.Json.Serialization;
// ReSharper disable UnusedAutoPropertyAccessor.Global

namespace Seq.App.Opsgenie
namespace Seq.App.Opsgenie.Classes
{
class Responder
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Username { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Name { get; set; }

public ResponderType Type { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Used to maintain the (JSON) OpsGenie API contract.
// ReSharper disable UnusedMember.Global

namespace Seq.App.Opsgenie
namespace Seq.App.Opsgenie.Classes
{
enum ResponderType
{
Expand All @@ -10,4 +10,4 @@ enum ResponderType
Escalation,
Schedule
}
}
}
10 changes: 0 additions & 10 deletions src/Seq.App.Opsgenie/IOpsgenieApiClient.cs

This file was deleted.

10 changes: 10 additions & 0 deletions src/Seq.App.Opsgenie/Interfaces/IOpsgenieApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Seq.App.Opsgenie.Classes;

namespace Seq.App.Opsgenie.Interfaces
{
interface IOpsgenieApiClient
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one might go in Seq.App.Opsgenie.Client?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reorganised into Api and Client

{
Task<OpsGenieResult> CreateAsync(OpsgenieAlert alert);
}
}
60 changes: 39 additions & 21 deletions src/Seq.App.Opsgenie/OpsgenieApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Seq.Apps;
using Seq.App.Opsgenie.Classes;
using Seq.App.Opsgenie.Interfaces;

namespace Seq.App.Opsgenie
{
class OpsgenieApiClient : IOpsgenieApiClient, IDisposable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type could also move to Seq.App.Opsgenie.Client

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reorganised into Api and Client

{
const string OpsgenieCreateAlertUrl = "https://api.opsgenie.com/v2/alerts";
readonly HttpClient _httpClient = new HttpClient();
readonly Encoding _utf8Encoding = new UTF8Encoding(false);

static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Expand All @@ -24,23 +24,21 @@ class OpsgenieApiClient : IOpsgenieApiClient, IDisposable
}
};

public static string Serialize(IEnumerable list)
{
return JsonSerializer.Serialize(list, SerializerOptions);
}

public static string Serialize(OpsgenieAlert alert)
{
return JsonSerializer.Serialize(alert, SerializerOptions);
}
readonly HttpClient _httpClient = new HttpClient();
readonly Encoding _utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

public OpsgenieApiClient(string apiKey)
{
if (apiKey == null) throw new ArgumentNullException(nameof(apiKey));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("GenieKey", apiKey);
}

public async Task<HttpResponseMessage> CreateAsync(OpsgenieAlert alert)
public void Dispose()
{
_httpClient.Dispose();
}

public async Task<OpsGenieResult> CreateAsync(OpsgenieAlert alert)
{
if (alert == null) throw new ArgumentNullException(nameof(alert));

Expand All @@ -50,21 +48,41 @@ public async Task<HttpResponseMessage> CreateAsync(OpsgenieAlert alert)
"application/json");

var response = await _httpClient.PostAsync(OpsgenieCreateAlertUrl, content);
var responseBody = await response.Content.ReadAsStringAsync();
var result = new OpsGenieResult
{
StatusCode = (int) response.StatusCode,
Ok = response.IsSuccessStatusCode,
HttpResponse = response,
ResponseBody = responseBody
};

try
{
var opsGenieResponse =
JsonSerializer.Deserialize<OpsGenieResponse>(responseBody,
new JsonSerializerOptions {PropertyNameCaseInsensitive = true});

if (!response.IsSuccessStatusCode)
result.Response = opsGenieResponse;
}
catch (Exception ex)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to just let the exception bubble up, here, and be handled by the hosting environment? In general, Seq apps shouldn't customize error handling, since the app host does this consistently for all hosted apps and in future we might make improvements up at that layer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the perspective was to allow a consistent approach to error handling based on a signal targeted to the specific appid or appinstanceid (or instance name) that included the context/properties around what the alert was. So for example if we fail to connect to OpsGenie to send the alert, we can send an email or Teams or JIRA alert that preserves the context of what we were alerting, by having an error event that preserves this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still inclined to have this caught for the reasons above. It's fairly serious if an alert doesn't go, and if we can catch what the alert was, then we have opportunity to send an alert via other alerting apps. Overall this speaks to Seq reliability - being able to do fallbacks etc. Unfortunately the hosting environment doesn't preserve the event data ... I suppose it could still throw after this?

{
var responseBody = await response.Content.ReadAsStringAsync();
var fragment = responseBody.Substring(0, Math.Min(1024, responseBody.Length));
throw new SeqAppException(
$"Opsgenie alert creation failed ({response.StatusCode}/{response.ReasonPhrase}): {fragment}");
result.Ok = false;
result.Error = ex;
}

return response;

return result;
}

public void Dispose()
public static string Serialize(IEnumerable list)
{
_httpClient.Dispose();
return JsonSerializer.Serialize(list, SerializerOptions);
}

public static string Serialize(OpsgenieAlert alert)
{
return JsonSerializer.Serialize(alert, SerializerOptions);
}
}
}
Loading