Skip to content

Commit e87dac8

Browse files
missymessaCopilot
andcommitted
Modify AlertHookController to create AzDO work items instead of GitHub issues
Rewrites the Grafana alert webhook handler to create DNCENG Task work items in Azure DevOps (area path: .NET Engineering Services\First Responders) instead of opening GitHub issues. Key changes: - AlertHookController now uses IAzureDevOpsClient with Managed Identity auth - Removed Octokit/GitHub dependencies from the alert path - Alert fires: creates work item in Backlog state with 'Active Alert' tag - Alert resolves: moves work item to Done with 'Inactive Alert' tag - Reuses existing work items on recurrence (WIQL search by automation ID) - Added CreateWorkItemAsync, UpdateWorkItemAsync, AddWorkItemCommentAsync, and QueryWorkItemsAsync to IAzureDevOpsClient - New AzureDevOpsAlertOptions config section ('AzureDevOpsAlert') - Updated unit tests to match new controller signature Resolves: https://dnceng.visualstudio.com/internal/_workitems/edit/8579 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 862301c commit e87dac8

11 files changed

Lines changed: 466 additions & 272 deletions

File tree

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
using System;
2-
using System.Reflection;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
34
using AwesomeAssertions;
45
using DotNet.Status.Web.Controllers;
56
using DotNet.Status.Web.Models;
67
using DotNet.Status.Web.Options;
7-
using Microsoft.DotNet.GitHub.Authentication;
8+
using Microsoft.DotNet.Internal.AzureDevOps;
9+
using Microsoft.DotNet.Internal.DependencyInjection;
810
using Microsoft.Extensions.Logging.Abstractions;
911
using Microsoft.Extensions.Options;
1012
using Moq;
1113
using NUnit.Framework;
12-
using Octokit;
1314

1415
namespace DotNet.Status.Web.Tests;
1516

1617
[TestFixture]
1718
public class AlertHookControllerTests
1819
{
1920
[Test]
20-
public void GenerateNewIssue_WithMissingEvalMatchesAndNotificationTargets_DoesNotThrow()
21+
public void GenerateDescription_WithMissingEvalMatches_DoesNotThrow()
2122
{
22-
AlertHookController controller = CreateController(null);
23+
AlertHookController controller = CreateController();
2324
GrafanaNotification notification = new GrafanaNotification
2425
{
2526
Title = "Alert title",
@@ -29,19 +30,19 @@ public void GenerateNewIssue_WithMissingEvalMatchesAndNotificationTargets_DoesNo
2930
EvalMatches = null,
3031
};
3132

32-
Action action = () => InvokeGenerateNewIssue(controller, notification);
33+
Action action = () => controller.GenerateDescription(notification);
3334

3435
action.Should().NotThrow();
3536

36-
NewIssue issue = InvokeGenerateNewIssue(controller, notification);
37-
issue.Body.Should().Contain("Please investigate");
38-
issue.Body.Should().NotContain(", please investigate");
37+
string description = controller.GenerateDescription(notification);
38+
description.Should().Contain("Supplemental text");
39+
description.Should().Contain("Grafana-Automated-Alert-Id-");
3940
}
4041

4142
[Test]
42-
public void GenerateNewNotificationComment_WithMissingEvalMatches_DoesNotThrow()
43+
public void GenerateComment_WithMissingEvalMatches_DoesNotThrow()
4344
{
44-
AlertHookController controller = CreateController(Array.Empty<string>());
45+
AlertHookController controller = CreateController();
4546
GrafanaNotification notification = new GrafanaNotification
4647
{
4748
Title = "Alert title",
@@ -51,58 +52,76 @@ public void GenerateNewNotificationComment_WithMissingEvalMatches_DoesNotThrow()
5152
EvalMatches = null,
5253
};
5354

54-
Action action = () => InvokeGenerateNewNotificationComment(controller, notification);
55+
Action action = () => controller.GenerateComment(notification);
5556

5657
action.Should().NotThrow();
5758

58-
string comment = InvokeGenerateNewNotificationComment(controller, notification);
59-
comment.Should().Contain("Metric state changed to *alerting*");
59+
string comment = controller.GenerateComment(notification);
60+
comment.Should().Contain("Metric state changed to");
61+
comment.Should().Contain("alerting");
6062
}
6163

62-
private static AlertHookController CreateController(string[] notificationTargets)
64+
[Test]
65+
public void GenerateTitle_WithPrefix_PrependsPrefixToTitle()
6366
{
64-
Mock<IGitHubTokenProvider> tokenProvider = new(MockBehavior.Strict);
65-
IOptions<GitHubConnectionOptions> githubOptions = Microsoft.Extensions.Options.Options.Create(new GitHubConnectionOptions
67+
AlertHookController controller = CreateController();
68+
GrafanaNotification notification = new GrafanaNotification
6669
{
67-
Organization = "dotnet",
68-
Repository = "dnceng",
69-
NotificationTargets = notificationTargets,
70-
AlertLabels = Array.Empty<string>(),
71-
EnvironmentLabels = Array.Empty<string>(),
70+
Title = "CPU High",
71+
State = "alerting",
72+
};
73+
74+
string title = controller.GenerateTitle(notification);
75+
76+
title.Should().Be("[test] CPU High");
77+
}
78+
79+
[Test]
80+
public void GenerateDescription_WithEvalMatches_IncludesMetrics()
81+
{
82+
AlertHookController controller = CreateController();
83+
GrafanaNotification notification = new GrafanaNotification
84+
{
85+
Title = "Alert title",
86+
State = "alerting",
87+
Message = "High CPU",
88+
RuleUrl = "https://example/rule",
89+
EvalMatches = new List<GrafanaNotificationMatch>
90+
{
91+
new GrafanaNotificationMatch { Metric = "cpu_usage", Value = 95.5 },
92+
}.ToImmutableList(),
93+
};
94+
95+
string description = controller.GenerateDescription(notification);
96+
97+
description.Should().Contain("cpu_usage");
98+
description.Should().Contain("95.5");
99+
}
100+
101+
private static AlertHookController CreateController()
102+
{
103+
Mock<IAzureDevOpsClient> azureDevOpsClient = new(MockBehavior.Strict);
104+
Mock<IClientFactory<IAzureDevOpsClient>> clientFactory = new(MockBehavior.Strict);
105+
IOptions<AzureDevOpsAlertOptions> alertOptions = Microsoft.Extensions.Options.Options.Create(new AzureDevOpsAlertOptions
106+
{
107+
Organization = "dnceng",
108+
Project = "internal",
109+
AreaPath = @"internal\.NET Engineering Services\First Responders",
110+
WorkItemType = "DNCENG Task",
72111
TitlePrefix = "[test] ",
73112
SupplementalBodyText = "Supplemental text",
74113
});
75-
IOptions<GitHubClientOptions> clientOptions = Microsoft.Extensions.Options.Options.Create(new GitHubClientOptions
76-
{
77-
ProductHeader = new ProductHeaderValue("DotNetStatusWebTests"),
78-
});
79114

80115
IOptions<GrafanaOptions> grafanaOptions = Microsoft.Extensions.Options.Options.Create(new GrafanaOptions
81116
{
82117
WebhookSecret = "test-secret",
83118
});
84119

85120
return new AlertHookController(
86-
tokenProvider.Object,
87-
githubOptions,
88-
clientOptions,
121+
clientFactory.Object,
122+
alertOptions,
89123
grafanaOptions,
90124
NullLogger<AlertHookController>.Instance);
91125
}
92-
93-
private static NewIssue InvokeGenerateNewIssue(AlertHookController controller, GrafanaNotification notification)
94-
{
95-
MethodInfo method = typeof(AlertHookController).GetMethod("GenerateNewIssue", BindingFlags.Instance | BindingFlags.NonPublic);
96-
method.Should().NotBeNull();
97-
98-
return (NewIssue)method.Invoke(controller, new object[] { notification });
99-
}
100-
101-
private static string InvokeGenerateNewNotificationComment(AlertHookController controller, GrafanaNotification notification)
102-
{
103-
MethodInfo method = typeof(AlertHookController).GetMethod("GenerateNewNotificationComment", BindingFlags.Instance | BindingFlags.NonPublic);
104-
method.Should().NotBeNull();
105-
106-
return (string)method.Invoke(controller, new object[] { notification });
107-
}
108126
}
127+

src/DotNet.Status.Web/DotNet.Status.Web/.config/settings.Production.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"EnvironmentLabels": [ "Production" ],
1313
"TitlePrefix": "Production - "
1414
},
15+
"AzureDevOpsAlert": {
16+
"TitlePrefix": "Production - ",
17+
"SupplementalBodyText": "This alert was auto-generated by Grafana monitoring in the Production environment."
18+
},
1519
"DataProtection": {
1620
"KeyBlobUri": "https://dotnetengstatusprod.blob.core.windows.net/site/keys.xml",
1721
"DataProtectionKeyUri": "https://dotneteng-status-prod.vault.azure.net/keys/dotnet-status-data-protection/"

src/DotNet.Status.Web/DotNet.Status.Web/.config/settings.Staging.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
"EnvironmentLabels": [ "Staging" ],
1212
"TitlePrefix": "Staging - "
1313
},
14+
"AzureDevOpsAlert": {
15+
"TitlePrefix": "Staging - ",
16+
"SupplementalBodyText": "This alert was auto-generated by Grafana monitoring in the Staging environment."
17+
},
1418
"DataProtection": {
1519
"KeyBlobUri": "https://dotnetengstatusstaging.blob.core.windows.net/site/keys.xml",
1620
"DataProtectionKeyUri": "https://dotneteng-status-staging.vault.azure.net/keys/dotnet-status-data-protection/"

src/DotNet.Status.Web/DotNet.Status.Web/.config/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,14 @@
285285
"RcaRequestedLabels": [ "RCA Requested" ],
286286
"RcaLabel": "RCA"
287287
},
288+
"AzureDevOpsAlert": {
289+
"Organization": "dnceng",
290+
"Project": "internal",
291+
"AreaPath": "internal\\.NET Engineering Services\\First Responders",
292+
"WorkItemType": "DNCENG Task",
293+
"TitlePrefix": "",
294+
"SupplementalBodyText": ""
295+
},
288296
"AzureTableTokenStore": {
289297
"TableName": "tokens"
290298
},

0 commit comments

Comments
 (0)