Skip to content

Commit 6530ddc

Browse files
Adds execution summary plugin. Closes #152 (#193)
1 parent b9b194f commit 6530ddc

File tree

2 files changed

+399
-0
lines changed

2 files changed

+399
-0
lines changed
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Graph.DeveloperProxy.Abstractions;
6+
using System.CommandLine;
7+
using System.CommandLine.Invocation;
8+
using System.Text.Json.Serialization;
9+
using System.Text.RegularExpressions;
10+
11+
namespace Microsoft.Graph.DeveloperProxy.Plugins.RequestLogs;
12+
13+
internal enum SummaryGroupBy
14+
{
15+
[JsonPropertyName("url")]
16+
Url,
17+
[JsonPropertyName("messageType")]
18+
MessageType
19+
}
20+
21+
internal class ExecutionSummaryPluginConfiguration
22+
{
23+
public string FilePath { get; set; } = "";
24+
public SummaryGroupBy GroupBy { get; set; } = SummaryGroupBy.Url;
25+
}
26+
27+
public class ExecutionSummaryPlugin : BaseProxyPlugin
28+
{
29+
public override string Name => nameof(ExecutionSummaryPlugin);
30+
private ExecutionSummaryPluginConfiguration _configuration = new();
31+
private readonly Option<string?> _filePath;
32+
private readonly Option<SummaryGroupBy?> _groupBy;
33+
private const string _requestsInterceptedMessage = "Requests intercepted";
34+
private const string _requestsPassedThroughMessage = "Requests passed through";
35+
36+
public ExecutionSummaryPlugin()
37+
{
38+
_filePath = new Option<string?>("--summary-file-path", "Path to the file where the summary should be saved. If not specified, the summary will be printed to the console. Path can be absolute or relative to the current working directory.");
39+
_filePath.ArgumentHelpName = "summary-file-path";
40+
_filePath.AddValidator(input =>
41+
{
42+
var outputFilePath = input.Tokens.First().Value;
43+
if (string.IsNullOrEmpty(outputFilePath))
44+
{
45+
return;
46+
}
47+
48+
var outputDir = Path.GetFullPath(Path.GetDirectoryName(outputFilePath));
49+
if (!Directory.Exists(outputDir))
50+
{
51+
input.ErrorMessage = $"The directory {outputDir} does not exist.";
52+
}
53+
});
54+
55+
_groupBy = new Option<SummaryGroupBy?>("--summary-group-by", "Specifies how the information should be grouped in the summary. Available options: `url` (default), `messageType`.");
56+
_groupBy.ArgumentHelpName = "summary-group-by";
57+
_groupBy.AddValidator(input =>
58+
{
59+
if (!Enum.TryParse<SummaryGroupBy>(input.Tokens.First().Value, true, out var groupBy))
60+
{
61+
input.ErrorMessage = $"{input.Tokens.First().Value} is not a valid option to group by. Allowed values are: {string.Join(", ", Enum.GetNames(typeof(SummaryGroupBy)))}";
62+
}
63+
});
64+
}
65+
66+
public override void Register(IPluginEvents pluginEvents,
67+
IProxyContext context,
68+
ISet<Regex> urlsToWatch,
69+
IConfigurationSection? configSection = null)
70+
{
71+
base.Register(pluginEvents, context, urlsToWatch, configSection);
72+
73+
configSection?.Bind(_configuration);
74+
75+
pluginEvents.Init += OnInit;
76+
pluginEvents.OptionsLoaded += OnOptionsLoaded;
77+
pluginEvents.AfterRecordingStop += AfterRecordingStop;
78+
}
79+
80+
private void OnInit(object? sender, InitArgs e)
81+
{
82+
e.RootCommand.AddOption(_filePath);
83+
e.RootCommand.AddOption(_groupBy);
84+
}
85+
86+
private void OnOptionsLoaded(object? sender, OptionsLoadedArgs e)
87+
{
88+
InvocationContext context = e.Context;
89+
90+
var filePath = context.ParseResult.GetValueForOption(_filePath);
91+
if (filePath is not null)
92+
{
93+
_configuration.FilePath = filePath;
94+
}
95+
96+
var groupBy = context.ParseResult.GetValueForOption(_groupBy);
97+
if (groupBy is not null)
98+
{
99+
_configuration.GroupBy = groupBy.Value;
100+
}
101+
}
102+
103+
private void AfterRecordingStop(object? sender, RecordingArgs e)
104+
{
105+
if (!e.RequestLogs.Any())
106+
{
107+
return;
108+
}
109+
110+
var report = _configuration.GroupBy switch
111+
{
112+
SummaryGroupBy.Url => GetGroupedByUrlReport(e.RequestLogs),
113+
SummaryGroupBy.MessageType => GetGroupedByMessageTypeReport(e.RequestLogs),
114+
_ => throw new NotImplementedException()
115+
};
116+
117+
if (string.IsNullOrEmpty(_configuration.FilePath))
118+
{
119+
_logger?.LogInfo(string.Join(Environment.NewLine, report));
120+
}
121+
else
122+
{
123+
File.WriteAllLines(_configuration.FilePath, report);
124+
}
125+
}
126+
127+
private string[] GetGroupedByUrlReport(IEnumerable<RequestLog> requestLogs)
128+
{
129+
var report = new List<string>();
130+
report.AddRange(GetReportTitle());
131+
report.Add("## Requests");
132+
133+
var data = GetGroupedByUrlData(requestLogs);
134+
135+
var sortedMethodAndUrls = data.Keys.OrderBy(k => k);
136+
foreach (var methodAndUrl in sortedMethodAndUrls)
137+
{
138+
report.AddRange(new[] {
139+
"",
140+
$"### {methodAndUrl}",
141+
});
142+
143+
var sortedMessageTypes = data[methodAndUrl].Keys.OrderBy(k => k);
144+
foreach (var messageType in sortedMessageTypes)
145+
{
146+
report.AddRange(new [] {
147+
"",
148+
$"#### {messageType}",
149+
""
150+
});
151+
152+
var sortedMessages = data[methodAndUrl][messageType].Keys.OrderBy(k => k);
153+
foreach (var message in sortedMessages)
154+
{
155+
report.Add($"- ({data[methodAndUrl][messageType][message]}) {message}");
156+
}
157+
}
158+
}
159+
160+
report.AddRange(GetSummary(requestLogs));
161+
162+
return report.ToArray();
163+
}
164+
165+
private string[] GetGroupedByMessageTypeReport(IEnumerable<RequestLog> requestLogs)
166+
{
167+
var report = new List<string>();
168+
report.AddRange(GetReportTitle());
169+
report.Add("## Message types");
170+
171+
var data = GetGroupedByMessageTypeData(requestLogs);
172+
173+
var sortedMessageTypes = data.Keys.OrderBy(k => k);
174+
foreach (var messageType in sortedMessageTypes)
175+
{
176+
report.AddRange(new[] {
177+
"",
178+
$"### {messageType}"
179+
});
180+
181+
if (messageType == _requestsInterceptedMessage ||
182+
messageType == _requestsPassedThroughMessage)
183+
{
184+
report.Add("");
185+
186+
var sortedMethodAndUrls = data[messageType][messageType].Keys.OrderBy(k => k);
187+
foreach (var methodAndUrl in sortedMethodAndUrls)
188+
{
189+
report.Add($"- ({data[messageType][messageType][methodAndUrl]}) {methodAndUrl}");
190+
}
191+
}
192+
else
193+
{
194+
var sortedMessages = data[messageType].Keys.OrderBy(k => k);
195+
foreach (var message in sortedMessages)
196+
{
197+
report.AddRange(new[] {
198+
"",
199+
$"#### {message}",
200+
""
201+
});
202+
203+
var sortedMethodAndUrls = data[messageType][message].Keys.OrderBy(k => k);
204+
foreach (var methodAndUrl in sortedMethodAndUrls)
205+
{
206+
report.Add($"- ({data[messageType][message][methodAndUrl]}) {methodAndUrl}");
207+
}
208+
}
209+
}
210+
}
211+
212+
report.AddRange(GetSummary(requestLogs));
213+
214+
return report.ToArray();
215+
}
216+
217+
private string[] GetReportTitle()
218+
{
219+
return new string[]
220+
{
221+
"# Microsoft Graph Developer Proxy execution summary",
222+
"",
223+
$"Date: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}",
224+
""
225+
};
226+
}
227+
228+
// in this method we're producing the follow data structure
229+
// request > message type > (count) message
230+
private Dictionary<string, Dictionary<string, Dictionary<string, int>>> GetGroupedByUrlData(IEnumerable<RequestLog> requestLogs)
231+
{
232+
var data = new Dictionary<string, Dictionary<string, Dictionary<string, int>>>();
233+
234+
foreach (var log in requestLogs)
235+
{
236+
var message = GetRequestMessage(log);
237+
if (log.MessageType == MessageType.InterceptedRequest)
238+
{
239+
var request = message;
240+
if (!data.ContainsKey(request))
241+
{
242+
data.Add(request, new Dictionary<string, Dictionary<string, int>>());
243+
}
244+
245+
continue;
246+
}
247+
248+
// last line of the message is the method and URL of the request
249+
var methodAndUrl = GetMethodAndUrl(log);
250+
var readableMessageType = GetReadableMessageTypeForSummary(log.MessageType);
251+
if (!data[methodAndUrl].ContainsKey(readableMessageType))
252+
{
253+
data[methodAndUrl].Add(readableMessageType, new Dictionary<string, int>());
254+
}
255+
256+
if (data[methodAndUrl][readableMessageType].ContainsKey(message))
257+
{
258+
data[methodAndUrl][readableMessageType][message]++;
259+
}
260+
else
261+
{
262+
data[methodAndUrl][readableMessageType].Add(message, 1);
263+
}
264+
}
265+
266+
return data;
267+
}
268+
269+
// in this method we're producing the follow data structure
270+
// message type > message > (count) request
271+
private Dictionary<string, Dictionary<string, Dictionary<string, int>>> GetGroupedByMessageTypeData(IEnumerable<RequestLog> requestLogs)
272+
{
273+
var data = new Dictionary<string, Dictionary<string, Dictionary<string, int>>>();
274+
275+
foreach (var log in requestLogs)
276+
{
277+
var readableMessageType = GetReadableMessageTypeForSummary(log.MessageType);
278+
if (!data.ContainsKey(readableMessageType))
279+
{
280+
data.Add(readableMessageType, new Dictionary<string, Dictionary<string, int>>());
281+
282+
if (log.MessageType == MessageType.InterceptedRequest ||
283+
log.MessageType == MessageType.PassedThrough)
284+
{
285+
// intercepted and passed through requests don't have
286+
// a sub-grouping so let's repeat the message type
287+
// to keep the same data shape
288+
data[readableMessageType].Add(readableMessageType, new Dictionary<string, int>());
289+
}
290+
}
291+
292+
var message = GetRequestMessage(log);
293+
if (log.MessageType == MessageType.InterceptedRequest ||
294+
log.MessageType == MessageType.PassedThrough)
295+
{
296+
// for passed through requests we need to log the URL rather than the
297+
// fixed message
298+
if (log.MessageType == MessageType.PassedThrough) {
299+
message = GetMethodAndUrl(log);
300+
}
301+
302+
if (!data[readableMessageType][readableMessageType].ContainsKey(message))
303+
{
304+
data[readableMessageType][readableMessageType].Add(message, 1);
305+
}
306+
else
307+
{
308+
data[readableMessageType][readableMessageType][message]++;
309+
}
310+
continue;
311+
}
312+
313+
if (!data[readableMessageType].ContainsKey(message))
314+
{
315+
data[readableMessageType].Add(message, new Dictionary<string, int>());
316+
}
317+
var methodAndUrl = GetMethodAndUrl(log);
318+
if (data[readableMessageType][message].ContainsKey(methodAndUrl))
319+
{
320+
data[readableMessageType][message][methodAndUrl]++;
321+
}
322+
else
323+
{
324+
data[readableMessageType][message].Add(methodAndUrl, 1);
325+
}
326+
}
327+
328+
return data;
329+
}
330+
331+
private string GetRequestMessage(RequestLog requestLog)
332+
{
333+
return String.Join(' ', requestLog.Message);
334+
}
335+
336+
private string GetMethodAndUrl(RequestLog requestLog)
337+
{
338+
if (requestLog.MessageType == MessageType.InterceptedRequest)
339+
{
340+
return requestLog.Message.First();
341+
}
342+
else
343+
{
344+
if (requestLog.Context is not null)
345+
{
346+
return $"{requestLog.Context.Session.HttpClient.Request.Method} {requestLog.Context.Session.HttpClient.Request.RequestUri}";
347+
}
348+
else
349+
{
350+
return "Undefined";
351+
}
352+
}
353+
}
354+
355+
private string[] GetSummary(IEnumerable<RequestLog> requestLogs)
356+
{
357+
var data = requestLogs
358+
.Select(log => GetReadableMessageTypeForSummary(log.MessageType))
359+
.OrderBy(log => log)
360+
.GroupBy(log => log)
361+
.ToDictionary(group => group.Key, group => group.Count());
362+
363+
var summary = new List<string> {
364+
"",
365+
"## Summary",
366+
"",
367+
"Category|Count",
368+
"--------|----:"
369+
};
370+
371+
foreach (var messageType in data.Keys)
372+
{
373+
summary.Add($"{messageType}|{data[messageType]}");
374+
}
375+
376+
return summary.ToArray();
377+
}
378+
379+
private string GetReadableMessageTypeForSummary(MessageType messageType) => messageType switch
380+
{
381+
MessageType.Chaos => "Requests with chaos",
382+
MessageType.Failed => "Failures",
383+
MessageType.InterceptedRequest => _requestsInterceptedMessage,
384+
MessageType.Mocked => "Requests mocked",
385+
MessageType.PassedThrough => _requestsPassedThroughMessage,
386+
MessageType.Tip => "Tips",
387+
MessageType.Warning => "Warnings",
388+
_ => "Unknown"
389+
};
390+
}

0 commit comments

Comments
 (0)