Skip to content

Commit b9b194f

Browse files
Adds support for recording. Closes #93 (#184)
1 parent 1aa3ac7 commit b9b194f

File tree

8 files changed

+147
-10
lines changed

8 files changed

+147
-10
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"args": [],
1515
"cwd": "${workspaceFolder}/msgraph-developer-proxy",
1616
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17-
"console": "internalConsole",
17+
"console": "integratedTerminal",
1818
"stopAtEntry": false,
1919
"launchSettingsProfile": "Default"
2020
},

msgraph-developer-proxy-abstractions/PluginEvents.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ public OptionsLoadedArgs(InvocationContext context) {
5555
public InvocationContext Context { get; set; }
5656
}
5757

58+
public class RequestLog {
59+
public string[] Message { get; set; }
60+
public MessageType MessageType { get; set; }
61+
public LoggingContext? Context { get; set; }
62+
63+
public RequestLog(string[] message, MessageType messageType, LoggingContext? context)
64+
{
65+
Message = message ?? throw new ArgumentNullException(nameof(message));
66+
MessageType = messageType;
67+
Context = context;
68+
}
69+
}
70+
71+
public class RecordingArgs {
72+
public RecordingArgs(IEnumerable<RequestLog> requestLogs) {
73+
RequestLogs = requestLogs ?? throw new ArgumentNullException(nameof(requestLogs));
74+
}
75+
public IEnumerable<RequestLog> RequestLogs { get; set; }
76+
}
77+
78+
public class RequestLogArgs {
79+
public RequestLogArgs(RequestLog requestLog) {
80+
RequestLog = requestLog ?? throw new ArgumentNullException(nameof(requestLog));
81+
}
82+
public RequestLog RequestLog { get; set; }
83+
}
84+
5885
public interface IPluginEvents {
5986
/// <summary>
6087
/// Raised while starting the proxy, allows plugins to register command line options
@@ -81,6 +108,14 @@ public interface IPluginEvents {
81108
/// Raised for all responses
82109
/// </summary>
83110
event EventHandler<ProxyResponseArgs>? AfterResponse;
111+
/// <summary>
112+
/// Raised after request message has been logged.
113+
/// </summary>
114+
event EventHandler<RequestLogArgs>? AfterRequestLog;
115+
/// <summary>
116+
/// Raised after recording request logs has stopped.
117+
/// </summary>
118+
event EventHandler<RecordingArgs>? AfterRecordingStop;
84119
}
85120

86121
public class PluginEvents : IPluginEvents {
@@ -94,6 +129,10 @@ public class PluginEvents : IPluginEvents {
94129
public event EventHandler<ProxyResponseArgs>? BeforeResponse;
95130
/// <inheritdoc />
96131
public event EventHandler<ProxyResponseArgs>? AfterResponse;
132+
/// <inheritdoc />
133+
public event EventHandler<RequestLogArgs>? AfterRequestLog;
134+
/// <inheritdoc />
135+
public event EventHandler<RecordingArgs>? AfterRecordingStop;
97136

98137
public void RaiseInit(InitArgs args) {
99138
Init?.Invoke(this, args);
@@ -114,4 +153,12 @@ public void RaiseProxyBeforeResponse(ProxyResponseArgs args) {
114153
public void RaiseProxyAfterResponse(ProxyResponseArgs args) {
115154
AfterResponse?.Invoke(this, args);
116155
}
156+
157+
public void RaiseRequestLogged(RequestLogArgs args) {
158+
AfterRequestLog?.Invoke(this, args);
159+
}
160+
161+
public void RaiseRecordingStopped(RecordingArgs args) {
162+
AfterRecordingStop?.Invoke(this, args);
163+
}
117164
}

msgraph-developer-proxy/ConsoleLogger.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ namespace Microsoft.Graph.DeveloperProxy;
88
public class ConsoleLogger : ILogger {
99
private readonly ConsoleColor _color;
1010
private readonly LabelMode _labelMode;
11+
private readonly PluginEvents _pluginEvents;
1112
private readonly string _boxTopLeft = "\u250c ";
1213
private readonly string _boxLeft = "\u2502 ";
1314
private readonly string _boxBottomLeft = "\u2514 ";
1415
// used to align single-line messages
1516
private readonly string _boxSpacing = " ";
1617

17-
private static readonly object lockObject = new object();
18+
public static readonly object ConsoleLock = new object();
1819

1920
public LogLevel LogLevel { get; set; }
2021

21-
public ConsoleLogger(ProxyConfiguration configuration) {
22+
public ConsoleLogger(ProxyConfiguration configuration, PluginEvents pluginEvents) {
2223
_color = Console.ForegroundColor;
2324
_labelMode = configuration.LabelMode;
25+
_pluginEvents = pluginEvents;
2426
LogLevel = configuration.LogLevel;
2527
}
2628

@@ -72,7 +74,7 @@ public void LogRequest(string[] message, MessageType messageType, LoggingContext
7274
messageLines.Add($"{context.Session.HttpClient.Request.Method} {context.Session.HttpClient.Request.Url}");
7375
}
7476

75-
lock (lockObject) {
77+
lock (ConsoleLock) {
7678
switch (_labelMode) {
7779
case LabelMode.Text:
7880
WriteBoxedWithInvertedLabels(messageLines.ToArray(), messageType);
@@ -85,6 +87,8 @@ public void LogRequest(string[] message, MessageType messageType, LoggingContext
8587
break;
8688
}
8789
}
90+
91+
_pluginEvents.RaiseRequestLogged(new RequestLogArgs(new RequestLog(message, messageType, context)));
8892
}
8993

9094
public void WriteBoxedWithInvertedLabels(string[] message, MessageType messageType) {

msgraph-developer-proxy/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
using Microsoft.Graph.DeveloperProxy.Abstractions;
66
using System.CommandLine;
77

8-
ILogger logger = new ConsoleLogger(ProxyCommandHandler.Configuration);
8+
PluginEvents pluginEvents = new PluginEvents();
9+
ILogger logger = new ConsoleLogger(ProxyCommandHandler.Configuration, pluginEvents);
910
IProxyContext context = new ProxyContext(logger);
1011
ProxyHost proxyHost = new();
1112
RootCommand rootCommand = proxyHost.GetRootCommand();
12-
PluginEvents pluginEvents = new PluginEvents();
1313
PluginLoaderResult loaderResults = new PluginLoader(logger).LoadPlugins(pluginEvents, context);
1414

1515
// have all the plugins init and provide any command line options

msgraph-developer-proxy/ProxyCommandHandler.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@ namespace Microsoft.Graph.DeveloperProxy;
1212
public class ProxyCommandHandler : ICommandHandler {
1313
public Option<int?> Port { get; set; }
1414
public Option<LogLevel?> LogLevel { get; set; }
15+
public Option<bool?> Record { get; set; }
1516

1617
private readonly PluginEvents _pluginEvents;
1718
private readonly ISet<Regex> _urlsToWatch;
1819
private readonly ILogger _logger;
1920

2021
public ProxyCommandHandler(Option<int?> port,
2122
Option<LogLevel?> logLevel,
23+
Option<bool?> record,
2224
PluginEvents pluginEvents,
2325
ISet<Regex> urlsToWatch,
2426
ILogger logger) {
2527
Port = port ?? throw new ArgumentNullException(nameof(port));
2628
LogLevel = logLevel ?? throw new ArgumentNullException(nameof(logLevel));
29+
Record = record ?? throw new ArgumentNullException(nameof(record));
2730
_pluginEvents = pluginEvents ?? throw new ArgumentNullException(nameof(pluginEvents));
2831
_urlsToWatch = urlsToWatch ?? throw new ArgumentNullException(nameof(urlsToWatch));
2932
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -42,6 +45,10 @@ public async Task<int> InvokeAsync(InvocationContext context) {
4245
if (logLevel is not null) {
4346
_logger.LogLevel = logLevel.Value;
4447
}
48+
var record = context.ParseResult.GetValueForOption(Record);
49+
if (record is not null) {
50+
Configuration.Record = record.Value;
51+
}
4552

4653
CancellationToken? cancellationToken = (CancellationToken?)context.BindingContext.GetService(typeof(CancellationToken?));
4754

msgraph-developer-proxy/ProxyConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public class ProxyConfiguration {
2222
[JsonPropertyName("labelMode")]
2323
[JsonConverter(typeof(JsonStringEnumConverter))]
2424
public LabelMode LabelMode { get; set; } = LabelMode.Text;
25+
[JsonPropertyName("record")]
26+
public bool Record { get; set; } = false;
2527
[JsonPropertyName("logLevel")]
2628
[JsonConverter(typeof(JsonStringEnumConverter))]
2729
public LogLevel LogLevel { get; set; } = LogLevel.Info;

msgraph-developer-proxy/ProxyEngine.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public static string ProductVersion {
3838
}
3939
}
4040

41+
private bool _isRecording = false;
42+
private List<RequestLog> _requestLogs = new List<RequestLog>();
43+
4144
public ProxyEngine(ProxyConfiguration config, ISet<Regex> urlsToWatch, PluginEvents pluginEvents, ILogger logger) {
4245
_config = config ?? throw new ArgumentNullException(nameof(config));
4346
_urlsToWatch = urlsToWatch ?? throw new ArgumentNullException(nameof(urlsToWatch));
@@ -95,11 +98,80 @@ public async Task Run(CancellationToken? cancellationToken) {
9598
_logger.LogInfo("Press CTRL+C to stop the Microsoft Graph Developer Proxy");
9699
_logger.LogInfo("");
97100
Console.CancelKeyPress += Console_CancelKeyPress;
98-
// wait for the proxy to stop
99-
Console.ReadLine();
101+
102+
if (_config.Record) {
103+
StartRecording();
104+
}
105+
_pluginEvents.AfterRequestLog += AfterRequestLog;
106+
107+
// we need this check or proxy will fail with an exception
108+
// when run for example in VSCode's integrated terminal
109+
if (!Console.IsInputRedirected) {
110+
ReadKeys();
111+
}
100112
while (_proxyServer.ProxyRunning) { Thread.Sleep(10); }
101113
}
102114

115+
private void AfterRequestLog(object? sender, RequestLogArgs e) {
116+
if (!_isRecording)
117+
{
118+
return;
119+
}
120+
121+
_requestLogs.Add(e.RequestLog);
122+
}
123+
124+
private void ReadKeys() {
125+
ConsoleKey key;
126+
do {
127+
key = Console.ReadKey(true).Key;
128+
if (key == ConsoleKey.R) {
129+
StartRecording();
130+
}
131+
else if (key == ConsoleKey.S) {
132+
StopRecording();
133+
}
134+
} while (key != ConsoleKey.Escape);
135+
}
136+
137+
private void StartRecording() {
138+
if (_isRecording) {
139+
return;
140+
}
141+
142+
_isRecording = true;
143+
PrintRecordingIndicator();
144+
}
145+
146+
private void StopRecording() {
147+
if (!_isRecording) {
148+
return;
149+
}
150+
151+
_isRecording = false;
152+
PrintRecordingIndicator();
153+
// clone the list so that we can clear the original
154+
// list in case a new recording is started, and
155+
// we let plugins handle previously recorded requests
156+
var clonedLogs = _requestLogs.ToArray();
157+
_requestLogs.Clear();
158+
_pluginEvents.RaiseRecordingStopped(new RecordingArgs(clonedLogs));
159+
}
160+
161+
private void PrintRecordingIndicator() {
162+
lock (ConsoleLogger.ConsoleLock) {
163+
if (_isRecording) {
164+
Console.ForegroundColor = ConsoleColor.Red;
165+
Console.Error.Write("◉");
166+
Console.ResetColor();
167+
Console.Error.WriteLine(" Recording... ");
168+
}
169+
else {
170+
Console.Error.WriteLine("○ Stopped recording");
171+
}
172+
}
173+
}
174+
103175
// Convert strings from config to regexes.
104176
// From the list of URLs, extract host names and convert them to regexes.
105177
// We need this because before we decrypt a request, we only have access
@@ -129,6 +201,7 @@ private void LoadHostNamesFromUrls() {
129201
}
130202

131203
private void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
204+
StopRecording();
132205
StopProxy();
133206
}
134207

msgraph-developer-proxy/ProxyHost.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Microsoft.Graph.DeveloperProxy;
1010
internal class ProxyHost {
1111
private Option<int?> _portOption;
1212
private Option<LogLevel?> _logLevelOption;
13+
private Option<bool?> _recordOption;
1314

1415
public ProxyHost() {
1516
_portOption = new Option<int?>("--port", "The port for the proxy server to listen on");
@@ -23,18 +24,21 @@ public ProxyHost() {
2324
input.ErrorMessage = $"{input.Tokens.First().Value} is not a valid log level. Allowed values are: {string.Join(", ", Enum.GetNames(typeof(LogLevel)))}";
2425
}
2526
});
27+
28+
_recordOption = new Option<bool?>("--record", "Use this option to record all request logs");
2629
}
2730

2831
public RootCommand GetRootCommand() {
2932
var command = new RootCommand {
3033
_portOption,
31-
_logLevelOption
34+
_logLevelOption,
35+
_recordOption
3236
};
3337
command.Description = "Microsoft Graph Developer Proxy is a command line tool that simulates real world behaviors of Microsoft Graph and other APIs, locally.";
3438

3539
return command;
3640
}
3741

38-
public ProxyCommandHandler GetCommandHandler(PluginEvents pluginEvents, ISet<Regex> urlsToWatch, ILogger logger) => new ProxyCommandHandler(_portOption, _logLevelOption, pluginEvents, urlsToWatch, logger);
42+
public ProxyCommandHandler GetCommandHandler(PluginEvents pluginEvents, ISet<Regex> urlsToWatch, ILogger logger) => new ProxyCommandHandler(_portOption, _logLevelOption, _recordOption, pluginEvents, urlsToWatch, logger);
3943
}
4044

0 commit comments

Comments
 (0)