Skip to content

Commit

Permalink
Merge pull request #4 from MumrichNET/improve-logging
Browse files Browse the repository at this point in the history
improve logging.
  • Loading branch information
mumrich authored May 1, 2024
2 parents fee49dc + 279ccf6 commit 50df2ae
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 189 deletions.
4 changes: 2 additions & 2 deletions Mumrich.DDD/Mumrich.DDD.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Akka.DependencyInjection" Version="1.5.14" />
<PackageReference Include="Akka.Persistence" Version="1.5.14" />
<PackageReference Include="Akka.DependencyInjection" Version="1.5.20" />
<PackageReference Include="Akka.Persistence" Version="1.5.20" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions Mumrich.SpaDevMiddleware.Domain/Models/SpaSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Generic;

using Mumrich.SpaDevMiddleware.Domain.Types;

namespace Mumrich.SpaDevMiddleware.Domain.Models
Expand Down Expand Up @@ -76,7 +75,8 @@ public class SpaSettings
/// The RegExp for detecting SPA-Assets requests.
/// </summary>
//language=regexp
public string SpaAssetsExpression { get; set; } = "^(src|node_modules|favicon.+|@[a-zA-Z]+|.*vite.*|.*\\.json|.*\\.js|.*\\.css)$";
public string SpaAssetsExpression { get; set; } =
"^(src|node_modules|favicon.+|@[a-zA-Z]+|.*vite.*|.*\\.json|.*\\.js|.*\\.css|__devtools__.*)$";

/// <summary>
/// The RegExp for detecting SPA-Root requests.
Expand All @@ -90,4 +90,4 @@ public class SpaSettings
/// </summary>
public string SpaRootPath { get; set; }
}
}
}
260 changes: 136 additions & 124 deletions Mumrich.SpaDevMiddleware/Actors/ProcessRunnerActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,176 +2,188 @@
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;

using Akka.Actor;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using Mumrich.SpaDevMiddleware.Domain.Contracts;
using Mumrich.SpaDevMiddleware.Domain.Models;
using Mumrich.SpaDevMiddleware.Extensions;
using Mumrich.SpaDevMiddleware.Utils;

using Newtonsoft.Json;

namespace Mumrich.SpaDevMiddleware.Actors
namespace Mumrich.SpaDevMiddleware.Actors;

public record StartProcessCommand;

public class ProcessRunnerActor : ReceiveActor
{
public record StartProcessCommand;
private const string DefaultRegex = "running at";
private static readonly Regex AnsiColorRegex =
new("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromMinutes(5);
private readonly ILogger<ProcessRunnerActor> _logger;

public class ProcessRunnerActor : ReceiveActor
public ProcessRunnerActor(IServiceProvider serviceProvider, SpaSettings spaSettings)
{
private const string DefaultRegex = "running at";
private static readonly Regex AnsiColorRegex = new("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromMinutes(5);
private readonly ILogger<ProcessRunnerActor> _logger;
var serviceProviderScope = serviceProvider.CreateScope();
var spaDevServerSettings =
serviceProviderScope.ServiceProvider.GetService<ISpaDevServerSettings>();
_logger = serviceProviderScope.ServiceProvider.GetService<ILogger<ProcessRunnerActor>>();

public ProcessRunnerActor(IServiceProvider serviceProvider, SpaSettings spaSettings)
{
var serviceProviderScope = serviceProvider.CreateScope();
var spaDevServerSettings = serviceProviderScope.ServiceProvider.GetService<ISpaDevServerSettings>();
_logger = serviceProviderScope.ServiceProvider.GetService<ILogger<ProcessRunnerActor>>();

var regex = spaSettings.Regex;
var regex = spaSettings.Regex;

_logger.LogInformation("SpaSettings: {}", JsonConvert.SerializeObject(spaSettings, Formatting.Indented));
_logger.LogInformation(
"SpaSettings: {spaSettings}",
JsonConvert.SerializeObject(spaSettings, Formatting.Indented)
);

var processStartInfo = spaSettings.GetProcessStartInfo(spaDevServerSettings);
var processStartInfo = spaSettings.GetProcessStartInfo(spaDevServerSettings);

_logger.LogInformation("{}: {} {} (cwd: '{}')", nameof(processStartInfo), processStartInfo.FileName, processStartInfo.Arguments, processStartInfo.WorkingDirectory);
_logger.LogInformation(
"{processStartInfo}: {FileName} {Arguments} (cwd: '{WorkingDirectory}')",
nameof(processStartInfo),
processStartInfo.FileName,
processStartInfo.Arguments,
processStartInfo.WorkingDirectory
);

ReceiveAsync<StartProcessCommand>(async _ =>
{
RunnerProcess = LaunchNodeProcess(processStartInfo);
ReceiveAsync<StartProcessCommand>(async _ =>
{
RunnerProcess = LaunchNodeProcess(processStartInfo);

StdOut = new EventedStreamReader(RunnerProcess.StandardOutput);
StdErr = new EventedStreamReader(RunnerProcess.StandardError);
StdOut = new EventedStreamReader(RunnerProcess.StandardOutput);
StdErr = new EventedStreamReader(RunnerProcess.StandardError);

AttachToLogger();
AttachToLogger();

using var stdErrReader = new EventedStreamStringReader(StdErr);
using var stdErrReader = new EventedStreamStringReader(StdErr);

try
{
// Although the Vue dev server may eventually tell us the URL it's listening on,
// it doesn't do so until it's finished compiling, and even then only if there were
// no compiler warnings. So instead of waiting for that, consider it ready as soon
// as it starts listening for requests.
await StdOut.WaitForMatch(new Regex(
try
{
// Although the Vue dev server may eventually tell us the URL it's listening on,
// it doesn't do so until it's finished compiling, and even then only if there were
// no compiler warnings. So instead of waiting for that, consider it ready as soon
// as it starts listening for requests.
await StdOut.WaitForMatch(
new Regex(
!string.IsNullOrWhiteSpace(regex) ? regex : DefaultRegex,
RegexOptions.None,
RegexMatchTimeout));
}
catch (EndOfStreamException ex)
{
throw new InvalidOperationException(
$"The Command '{spaSettings.NodeStartScript}' exited without indicating that the " +
"server was listening for requests. The error output was: " +
$"{stdErrReader.ReadAsString()}", ex);
}
});

Self.Tell(new StartProcessCommand());
}
RegexMatchTimeout
)
);
}
catch (EndOfStreamException ex)
{
throw new InvalidOperationException(
$"The Command '{spaSettings.NodeStartScript}' exited without indicating that the "
+ "server was listening for requests. The error output was: "
+ $"{stdErrReader.ReadAsString()}",
ex
);
}
});

private Process RunnerProcess { get; set; }
Self.Tell(new StartProcessCommand());
}

private Process RunnerProcess { get; set; }

private EventedStreamReader StdErr { get; set; }

private EventedStreamReader StdOut { get; set; }

protected override void PostStop()
{
RunnerProcess?.Kill();
}

private EventedStreamReader StdErr { get; set; }
private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
{
try
{
var process = Process.Start(startInfo);

private EventedStreamReader StdOut { get; set; }
if (process != null)
{
process.EnableRaisingEvents = true;
}

protected override void PostStop()
return process;
}
catch (Exception ex)
{
RunnerProcess?.Kill();
var message =
$"Failed to start '{startInfo.FileName}'. To resolve this:.\n\n"
+ $"[1] Ensure that '{startInfo.FileName}' is installed and can be found in one of the PATH directories.\n"
+ $" Current PATH enviroment variable is: {Environment.GetEnvironmentVariable("PATH")}\n"
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n"
+ "[2] See the InnerException for further details of the cause.";
throw new InvalidOperationException(message, ex);
}
}

private static string StripAnsiColors(string line) => AnsiColorRegex.Replace(line, string.Empty);

private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
private void AttachToLogger()
{
void StdErrOnReceivedLine(string line)
{
try
if (string.IsNullOrWhiteSpace(line))
{
var process = Process.Start(startInfo);
return;
}

if (process != null)
{
process.EnableRaisingEvents = true;
}
// NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward
// those to loggers (because a logger isn't necessarily any kind of terminal)
// making this console for debug purpose
if (line.StartsWith("<s>"))
{
line = line[3..];
}

return process;
if (_logger == null)
{
Console.Error.WriteLine(line);
}
catch (Exception ex)
else
{
var message = $"Failed to start '{startInfo.FileName}'. To resolve this:.\n\n"
+ $"[1] Ensure that '{startInfo.FileName}' is installed and can be found in one of the PATH directories.\n"
+ $" Current PATH enviroment variable is: {Environment.GetEnvironmentVariable("PATH")}\n"
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n"
+ "[2] See the InnerException for further details of the cause.";
throw new InvalidOperationException(message, ex);
var effectiveLine = StripAnsiColors(line).TrimEnd('\n');
_logger.LogError("{effectiveLine}", effectiveLine);
}
}

private static string StripAnsiColors(string line) => AnsiColorRegex.Replace(line, string.Empty);

private void AttachToLogger()
void StdOutOnReceivedLine(string line)
{
void StdErrOnReceivedLine(string line)
if (_logger == null)
{
Console.WriteLine(line);
}
else
{
if (string.IsNullOrWhiteSpace(line))
{
return;
}

// NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward
// those to loggers (because a logger isn't necessarily any kind of terminal)
// making this console for debug purpose
if (line.StartsWith("<s>"))
{
line = line[3..];
}

if (_logger == null)
{
Console.Error.WriteLine(line);
}
else
{
var effectiveLine = StripAnsiColors(line).TrimEnd('\n');
_logger.LogError("{}", effectiveLine);
}
var effectiveLine = StripAnsiColors(line).TrimEnd('\n');
_logger.LogInformation("{effectiveLine}", effectiveLine);
}
}

// When the NPM task emits complete lines, pass them through to the real logger
StdOut.OnReceivedLine += StdOutOnReceivedLine;
StdErr.OnReceivedLine += StdErrOnReceivedLine;

void StdOutOnReceivedLine(string line)
// But when it emits incomplete lines, assume this is progress information and
// hence just pass it through to StdOut regardless of logger config.
StdErr.OnReceivedChunk += chunk =>
{
if (chunk.Array == null)
{
if (_logger == null)
{
Console.WriteLine(line);
}
else
{
var effectiveLine = StripAnsiColors(line).TrimEnd('\n');
_logger.LogInformation("{}", effectiveLine);
}
return;
}

// When the NPM task emits complete lines, pass them through to the real logger
StdOut.OnReceivedLine += StdOutOnReceivedLine;
StdErr.OnReceivedLine += StdErrOnReceivedLine;
var containsNewline = Array.IndexOf(chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;

// But when it emits incomplete lines, assume this is progress information and
// hence just pass it through to StdOut regardless of logger config.
StdErr.OnReceivedChunk += chunk =>
if (!containsNewline)
{
if (chunk.Array == null)
{
return;
}

var containsNewline = Array.IndexOf(chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;

if (!containsNewline)
{
_logger.LogInformation("{}", new string(chunk.Array));
}
};
}
_logger.LogInformation("{chunk}", new string(chunk.Array));
}
};
}
}
}
19 changes: 8 additions & 11 deletions Mumrich.SpaDevMiddleware/Actors/SpaBuilderActor.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
using System.IO;

using Akka.Actor;

using Mumrich.AkkaExt.Extensions;
using Mumrich.SpaDevMiddleware.Domain.Contracts;
using Mumrich.SpaDevMiddleware.Domain.Models;

namespace Mumrich.SpaDevMiddleware.Actors
namespace Mumrich.SpaDevMiddleware.Actors;

public class SpaBuilderActor : ReceiveActor
{
public class SpaBuilderActor : ReceiveActor
public SpaBuilderActor(ISpaDevServerSettings spaDevServerSettings)
{
public SpaBuilderActor(ISpaDevServerSettings spaDevServerSettings)
foreach ((string _, SpaSettings value) in spaDevServerSettings.SinglePageApps)
{
foreach ((string _, SpaSettings value) in spaDevServerSettings.SinglePageApps)
{
DirectoryInfo directoryInfo = new(value.SpaRootPath);
Context.ActorOfWithNameAndArgs<ProcessRunnerActor>(directoryInfo.Name, value);
}
DirectoryInfo directoryInfo = new(value.SpaRootPath);
Context.ActorOfWithNameAndArgs<ProcessRunnerActor>(directoryInfo.Name, value);
}
}
}
}
Loading

0 comments on commit 50df2ae

Please sign in to comment.