diff --git a/seqcli.sln.DotSettings b/seqcli.sln.DotSettings index 6c5c917e..6c218398 100644 --- a/seqcli.sln.DotSettings +++ b/seqcli.sln.DotSettings @@ -1,4 +1,6 @@  + IO + IP MS True True @@ -16,6 +18,7 @@ True True True + True True True True @@ -32,4 +35,5 @@ True True True + True True \ No newline at end of file diff --git a/src/Roastery/Fake/Person.cs b/src/Roastery/Fake/Person.cs index 53394561..56c0ae0c 100644 --- a/src/Roastery/Fake/Person.cs +++ b/src/Roastery/Fake/Person.cs @@ -14,7 +14,7 @@ public Person(string? name, string? address) } static readonly string[] Forenames = - { + [ "Akeem", "Alice", "Alok", @@ -40,10 +40,10 @@ public Person(string? name, string? address) "Yoshi", "Zach", "Zeynep" - }; + ]; static readonly string[] Surnames = - { + [ "Anderson", "Alvarez", "Brookes", @@ -60,10 +60,10 @@ public Person(string? name, string? address) "Smith", "Xia", "Zheng" - }; + ]; static readonly string[] Streets = - { + [ "Lilac Road", "Lilly Street", "Carnation Street", @@ -78,7 +78,7 @@ public Person(string? name, string? address) "Trillium Creek Parkway", "Grevillea Street", "Kurrajong Street" - }; + ]; public static Person Generate() { diff --git a/src/Roastery/Web/FaultInjectionMiddleware.cs b/src/Roastery/Web/FaultInjectionMiddleware.cs index 27dae6d8..0c6f472e 100644 --- a/src/Roastery/Web/FaultInjectionMiddleware.cs +++ b/src/Roastery/Web/FaultInjectionMiddleware.cs @@ -18,15 +18,15 @@ public FaultInjectionMiddleware(ILogger logger, HttpServer next) { _logger = logger.ForContext(); _next = next; - _faults = new Func>[] - { + _faults = + [ Unauthorized, Unauthorized, Unauthorized, Timeout, Timeout, Disposed - }; + ]; } Task Unauthorized(HttpRequest request) diff --git a/src/Roastery/Web/Router.cs b/src/Roastery/Web/Router.cs index 5df8d7ed..0c9f7be9 100644 --- a/src/Roastery/Web/Router.cs +++ b/src/Roastery/Web/Router.cs @@ -63,7 +63,7 @@ public Router(IEnumerable controllers, ILogger logger) { using var _ = LogContext.PushProperty("Controller", controllerName); using var __ = LogContext.PushProperty("Action", actionName); - return (Task) method.Invoke(controller, new object[] {r})!; + return (Task) method.Invoke(controller, [r])!; }); _logger.Debug("Binding route HTTP {HttpMethod} {RouteTemplate} to action method {Controller}.{Action}()", diff --git a/src/SeqCli/Apps/Definitions/AppMetadataReader.cs b/src/SeqCli/Apps/Definitions/AppMetadataReader.cs index 8abd7be0..58269661 100644 --- a/src/SeqCli/Apps/Definitions/AppMetadataReader.cs +++ b/src/SeqCli/Apps/Definitions/AppMetadataReader.cs @@ -79,21 +79,15 @@ static Dictionary GetAvailableSettings(Type mainRe }); } - static readonly HashSet IntegerTypes = new() - { + static readonly HashSet IntegerTypes = + [ typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong) - }; + ]; - static readonly HashSet DecimalTypes = new() - { - typeof(float), typeof(double), typeof(decimal) - }; + static readonly HashSet DecimalTypes = [typeof(float), typeof(double), typeof(decimal)]; - static readonly HashSet BooleanTypes = new() - { - typeof(bool) - }; + static readonly HashSet BooleanTypes = [typeof(bool)]; internal static AppSettingType GetSettingType(Type type) { diff --git a/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs index d10beb63..8935f178 100644 --- a/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/ApiKey/RemoveCommand.cs @@ -48,8 +48,8 @@ protected override async Task Run() var connection = _connectionFactory.Connect(_connection); - var toRemove = _entityIdentity.Id != null ? - new[] {await connection.ApiKeys.FindAsync(_entityIdentity.Id)} : + var toRemove = _entityIdentity.Id != null ? [await connection.ApiKeys.FindAsync(_entityIdentity.Id)] + : (await connection.ApiKeys.ListAsync()) .Where(ak => _entityIdentity.Title == ak.Title) .ToArray(); diff --git a/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs b/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs index 76e90915..376f7354 100644 --- a/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/AppInstance/RemoveCommand.cs @@ -34,8 +34,8 @@ protected override async Task Run() var connection = _connectionFactory.Connect(_connection); - var toRemove = _entityIdentity.Id != null ? - new[] {await connection.AppInstances.FindAsync(_entityIdentity.Id)} : + var toRemove = _entityIdentity.Id != null ? [await connection.AppInstances.FindAsync(_entityIdentity.Id)] + : (await connection.AppInstances.ListAsync()) .Where(ak => _entityIdentity.Title == ak.Title) .ToArray(); diff --git a/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs index 64e45bbf..70d9a78e 100644 --- a/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Dashboard/RemoveCommand.cs @@ -50,8 +50,8 @@ protected override async Task Run() var connection = _connectionFactory.Connect(_connection); - var toRemove = _entityIdentity.Id != null ? - new[] { await connection.Dashboards.FindAsync(_entityIdentity.Id) } : + var toRemove = _entityIdentity.Id != null ? [await connection.Dashboards.FindAsync(_entityIdentity.Id)] + : (await connection.Dashboards.ListAsync(ownerId: _entityOwner.OwnerId, shared: _entityOwner.IncludeShared)) .Where(dashboard => _entityIdentity.Title == dashboard.Title) .ToArray(); diff --git a/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs index 4f0ce4b4..c706fe3e 100644 --- a/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Feed/RemoveCommand.cs @@ -58,8 +58,8 @@ protected override async Task Run() var connection = _connectionFactory.Connect(_connection); - var toRemove = _id != null ? - new[] {await connection.Feeds.FindAsync(_id)} : + var toRemove = _id != null ? [await connection.Feeds.FindAsync(_id)] + : (await connection.Feeds.ListAsync()) .Where(f => _name == f.Name) .ToArray(); diff --git a/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs index 4c39c38b..e743f174 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/RunCommand.cs @@ -12,98 +12,98 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Autofac; -using Seq.Forwarder.Config; -using Serilog; -using Serilog.Events; -using Serilog.Formatting.Compact; using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; -using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; +using Autofac; using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -using Seq.Forwarder.Util; -using Seq.Forwarder.Web.Host; -using SeqCli.Cli; using SeqCli.Cli.Features; +using SeqCli.Config; +using SeqCli.Config.Forwarder; +using SeqCli.Forwarder; +using SeqCli.Forwarder.Util; +using SeqCli.Forwarder.Web.Host; +using Serilog; using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting.Compact; // ReSharper disable UnusedType.Global -namespace Seq.Forwarder.Cli.Commands +namespace SeqCli.Cli.Commands.Forwarder; + +[Command("forwarder", "run", "Run the server interactively")] +class RunCommand : Command { - [Command("forwarder", "run", "Run the server interactively")] - class RunCommand : Command - { - readonly StoragePathFeature _storagePath; - readonly ListenUriFeature _listenUri; + readonly StoragePathFeature _storagePath; + readonly ListenUriFeature _listenUri; - bool _noLogo; + bool _noLogo; - public RunCommand() - { - Options.Add("nologo", _ => _noLogo = true); - _storagePath = Enable(); - _listenUri = Enable(); - } + public RunCommand() + { + Options.Add("nologo", _ => _noLogo = true); + _storagePath = Enable(); + _listenUri = Enable(); + } - protected override async Task Run(string[] unrecognized) + protected override async Task Run(string[] unrecognized) + { + if (Environment.UserInteractive) { - if (Environment.UserInteractive) + if (!_noLogo) { - if (!_noLogo) - { - WriteBanner(); - Console.WriteLine(); - } - - Console.WriteLine("Running as server; press Ctrl+C to exit."); + WriteBanner(); Console.WriteLine(); } - SeqForwarderConfig config; + Console.WriteLine("Running as server; press Ctrl+C to exit."); + Console.WriteLine(); + } - try - { - config = SeqForwarderConfig.ReadOrInit(_storagePath.ConfigFilePath); - } - catch (Exception ex) - { - await using var logger = CreateLogger( - LogEventLevel.Information, - SeqForwarderDiagnosticConfig.GetDefaultInternalLogPath()); + SeqCliConfig config; - logger.Fatal(ex, "Failed to load configuration from {ConfigFilePath}", _storagePath.ConfigFilePath); - return 1; - } + try + { + config = SeqCliConfig.Read(); // _storagePath.ConfigFilePath); + } + catch (Exception ex) + { + await using var logger = CreateLogger( + LogEventLevel.Information, + ForwarderDiagnosticConfig.GetDefaultInternalLogPath()); - Log.Logger = CreateLogger( - config.Diagnostics.InternalLoggingLevel, - config.Diagnostics.InternalLogPath, - config.Diagnostics.InternalLogServerUri, - config.Diagnostics.InternalLogServerApiKey); + logger.Fatal(ex, "Failed to load configuration from {ConfigFilePath}", _storagePath.ConfigFilePath); + return 1; + } - var listenUri = _listenUri.ListenUri ?? config.Api.ListenUri; + Log.Logger = CreateLogger( + config.Forwarder.Diagnostics.InternalLoggingLevel, + config.Forwarder.Diagnostics.InternalLogPath, + config.Forwarder.Diagnostics.InternalLogServerUri, + config.Forwarder.Diagnostics.InternalLogServerApiKey); - try - { - ILifetimeScope? container = null; - using var host = new HostBuilder() - .UseSerilog() - .UseServiceProviderFactory(new AutofacServiceProviderFactory()) - .ConfigureContainer(builder => - { - builder.RegisterBuildCallback(ls => container = ls); - builder.RegisterModule(new SeqForwarderModule(_storagePath.BufferPath, config)); - }) - .ConfigureWebHostDefaults(web => - { - web.UseStartup(); - web.UseKestrel(options => + var listenUri = _listenUri.ListenUri ?? config.Forwarder.Api.ListenUri; + + try + { + ILifetimeScope? container = null; + using var host = new HostBuilder() + .UseSerilog() + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureContainer(builder => + { + builder.RegisterBuildCallback(ls => container = ls); + builder.RegisterModule(new ForwarderModule(_storagePath.BufferPath, config)); + }) + .ConfigureWebHostDefaults(web => + { + web.UseStartup(); + web.UseKestrel(options => { options.AddServerHeader = false; options.AllowSynchronousIO = true; @@ -138,124 +138,123 @@ protected override async Task Run(string[] unrecognized) options.Listen(ipAddress, apiListenUri.Port); } }); - }) - .Build(); + }) + .Build(); - if (container == null) throw new Exception("Host did not build container."); + if (container == null) throw new Exception("Host did not build container."); - var service = container.Resolve( - new TypedParameter(typeof(IHost), host), - new NamedParameter("listenUri", listenUri)); + var service = container.Resolve( + new TypedParameter(typeof(IHost), host), + new NamedParameter("listenUri", listenUri)); - var exit = ExecutionEnvironment.SupportsStandardIO - ? RunStandardIO(service, Console.Out) - : RunService(service); + var exit = ExecutionEnvironment.SupportsStandardIO + ? RunStandardIO(service, Console.Out) + : RunService(service); - return exit; - } - catch (Exception ex) - { - Log.Fatal(ex, "Unhandled exception"); - return -1; - } - finally - { - await Log.CloseAndFlushAsync(); - } + return exit; } - - static Logger CreateLogger( - LogEventLevel internalLoggingLevel, - string internalLogPath, - string? internalLogServerUri = null, - string? internalLogServerApiKey = null) + catch (Exception ex) { - var loggerConfiguration = new LoggerConfiguration() - .Enrich.FromLogContext() - .Enrich.WithProperty("MachineName", Environment.MachineName) - .Enrich.WithProperty("Application", "Seq Forwarder") - .MinimumLevel.Is(internalLoggingLevel) - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .WriteTo.File( - new RenderedCompactJsonFormatter(), - GetRollingLogFilePathFormat(internalLogPath), - rollingInterval: RollingInterval.Day, - fileSizeLimitBytes: 1024 * 1024); - - if (Environment.UserInteractive) - loggerConfiguration.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information); - - if (!string.IsNullOrWhiteSpace(internalLogServerUri)) - loggerConfiguration.WriteTo.Seq( - internalLogServerUri, - apiKey: internalLogServerApiKey); - - return loggerConfiguration.CreateLogger(); + Log.Fatal(ex, "Unhandled exception"); + return -1; } - - static string GetRollingLogFilePathFormat(string internalLogPath) + finally { - if (internalLogPath == null) throw new ArgumentNullException(nameof(internalLogPath)); - - return Path.Combine(internalLogPath, "seq-forwarder-.log"); + await Log.CloseAndFlushAsync(); } + } - [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] - static int RunService(ServerService service) - { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + static int RunService(ServerService service) + { #if WINDOWS System.ServiceProcess.ServiceBase.Run([ new ServiceProcess.SeqForwarderWindowsService(service) ]); return 0; #else - throw new NotSupportedException("Windows services are not supported on this platform."); + throw new NotSupportedException("Windows services are not supported on this platform."); #endif - } + } - static int RunStandardIO(ServerService service, TextWriter cout) + static int RunStandardIO(ServerService service, TextWriter cout) + { + service.Start(); + + try { - service.Start(); + Console.TreatControlCAsInput = true; + var k = Console.ReadKey(true); + while (k.Key != ConsoleKey.C || !k.Modifiers.HasFlag(ConsoleModifiers.Control)) + k = Console.ReadKey(true); - try - { - Console.TreatControlCAsInput = true; - var k = Console.ReadKey(true); - while (k.Key != ConsoleKey.C || !k.Modifiers.HasFlag(ConsoleModifiers.Control)) - k = Console.ReadKey(true); + cout.WriteLine("Ctrl+C pressed; stopping..."); + Console.TreatControlCAsInput = false; + } + catch (Exception ex) + { + Log.Debug(ex, "Console not attached, waiting for any input"); + Console.Read(); + } - cout.WriteLine("Ctrl+C pressed; stopping..."); - Console.TreatControlCAsInput = false; - } - catch (Exception ex) - { - Log.Debug(ex, "Console not attached, waiting for any input"); - Console.Read(); - } + service.Stop(); - service.Stop(); + return 0; + } - return 0; - } + static void WriteBanner() + { + Write("─", ConsoleColor.DarkGray, 47); + Console.WriteLine(); + Write(" Seq Forwarder", ConsoleColor.White); + Write(" ──", ConsoleColor.DarkGray); + Write(" © 2024 Datalust Pty Ltd", ConsoleColor.Gray); + Console.WriteLine(); + Write("─", ConsoleColor.DarkGray, 47); + Console.WriteLine(); + } - static void WriteBanner() - { - Write("─", ConsoleColor.DarkGray, 47); - Console.WriteLine(); - Write(" Seq Forwarder", ConsoleColor.White); - Write(" ──", ConsoleColor.DarkGray); - Write(" © 2024 Datalust Pty Ltd", ConsoleColor.Gray); - Console.WriteLine(); - Write("─", ConsoleColor.DarkGray, 47); - Console.WriteLine(); - } + static void Write(string s, ConsoleColor color, int repeats = 1) + { + Console.ForegroundColor = color; + for (var i = 0; i < repeats; ++i) + Console.Write(s); + Console.ResetColor(); + } + + static Logger CreateLogger( + LogEventLevel internalLoggingLevel, + string internalLogPath, + string? internalLogServerUri = null, + string? internalLogServerApiKey = null) + { + var loggerConfiguration = new LoggerConfiguration() + .Enrich.FromLogContext() + .Enrich.WithProperty("MachineName", Environment.MachineName) + .Enrich.WithProperty("Application", "Seq Forwarder") + .MinimumLevel.Is(internalLoggingLevel) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .WriteTo.File( + new RenderedCompactJsonFormatter(), + GetRollingLogFilePathFormat(internalLogPath), + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: 1024 * 1024); - static void Write(string s, ConsoleColor color, int repeats = 1) - { - Console.ForegroundColor = color; - for (var i = 0; i < repeats; ++i) - Console.Write(s); - Console.ResetColor(); - } + if (Environment.UserInteractive) + loggerConfiguration.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information); + + if (!string.IsNullOrWhiteSpace(internalLogServerUri)) + loggerConfiguration.WriteTo.Seq( + internalLogServerUri, + apiKey: internalLogServerApiKey); + + return loggerConfiguration.CreateLogger(); + } + + static string GetRollingLogFilePathFormat(string internalLogPath) + { + if (internalLogPath == null) throw new ArgumentNullException(nameof(internalLogPath)); + + return Path.Combine(internalLogPath, "seq-forwarder-.log"); } -} +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs b/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs index baf0320c..df5130da 100644 --- a/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs +++ b/src/SeqCli/Cli/Commands/Forwarder/TruncateCommand.cs @@ -13,39 +13,36 @@ // limitations under the License. using System; -using System.IO; using System.Threading.Tasks; -using Seq.Forwarder.Multiplexing; -using SeqCli.Cli; using SeqCli.Cli.Features; +using SeqCli.Forwarder.Multiplexing; using Serilog; -namespace Seq.Forwarder.Cli.Commands +namespace SeqCli.Cli.Commands.Forwarder; + +[Command("forwarder", "truncate", "Clear the log buffer contents")] +class TruncateCommand : Command { - [Command("forwarder", "truncate", "Clear the log buffer contents")] - class TruncateCommand : Command + readonly StoragePathFeature _storagePath; + + public TruncateCommand() { - readonly StoragePathFeature _storagePath; + _storagePath = Enable(); + } - public TruncateCommand() + protected override async Task Run(string[] args) + { + try { - _storagePath = Enable(); + ActiveLogBufferMap.Truncate(_storagePath.BufferPath); + return 0; } - - protected override async Task Run(string[] args) + catch (Exception ex) { - try - { - ActiveLogBufferMap.Truncate(_storagePath.BufferPath); - return 0; - } - catch (Exception ex) - { - await using var logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); + await using var logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); - logger.Fatal(ex, "Could not truncate log buffer"); - return 1; - } + logger.Fatal(ex, "Could not truncate log buffer"); + return 1; } } -} +} \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs b/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs index 57702a93..710ae634 100644 --- a/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/RetentionPolicy/ListCommand.cs @@ -49,8 +49,8 @@ protected override async Task Run() { var connection = _connectionFactory.Connect(_connection); - var list = _id != null ? - new[] { await connection.RetentionPolicies.FindAsync(_id) } : + var list = _id != null ? [await connection.RetentionPolicies.FindAsync(_id)] + : (await connection.RetentionPolicies.ListAsync()) .ToArray(); diff --git a/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs index cf1b890e..35f3a951 100644 --- a/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Signal/RemoveCommand.cs @@ -50,8 +50,8 @@ protected override async Task Run() var connection = _connectionFactory.Connect(_connection); - var toRemove = _entityIdentity.Id != null ? - new[] { await connection.Signals.FindAsync(_entityIdentity.Id) } : + var toRemove = _entityIdentity.Id != null ? [await connection.Signals.FindAsync(_entityIdentity.Id)] + : (await connection.Signals.ListAsync(ownerId: _entityOwner.OwnerId, shared: _entityOwner.IncludeShared)) .Where(signal => _entityIdentity.Title == signal.Title) .ToArray(); diff --git a/src/SeqCli/Cli/Commands/User/RemoveCommand.cs b/src/SeqCli/Cli/Commands/User/RemoveCommand.cs index 51b6127c..782f90d9 100644 --- a/src/SeqCli/Cli/Commands/User/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/User/RemoveCommand.cs @@ -48,8 +48,8 @@ protected override async Task Run() var connection = _connectionFactory.Connect(_connection); - var toRemove = _userIdentity.Id != null ? - new[] {await connection.Users.FindAsync(_userIdentity.Id)} : + var toRemove = _userIdentity.Id != null ? [await connection.Users.FindAsync(_userIdentity.Id)] + : (await connection.Users.ListAsync()) .Where(u => _userIdentity.Name == u.Username) .ToArray(); diff --git a/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs index a4990edd..470e719c 100644 --- a/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Workspace/RemoveCommand.cs @@ -36,8 +36,8 @@ protected override async Task Run() var connection = _connectionFactory.Connect(_connection); - var toRemove = _entityIdentity.Id != null ? - new[] { await connection.Workspaces.FindAsync(_entityIdentity.Id) } : + var toRemove = _entityIdentity.Id != null ? [await connection.Workspaces.FindAsync(_entityIdentity.Id)] + : (await connection.Workspaces.ListAsync(ownerId: _entityOwner.OwnerId, shared: _entityOwner.IncludeShared)) .Where(workspace => _entityIdentity.Title == workspace.Title) .ToArray(); diff --git a/src/SeqCli/Cli/Options.cs b/src/SeqCli/Cli/Options.cs index 5678ac29..beedaad4 100644 --- a/src/SeqCli/Cli/Options.cs +++ b/src/SeqCli/Cli/Options.cs @@ -150,1218 +150,1216 @@ #if NDESK_OPTIONS namespace NDesk.Options #else -namespace SeqCli.Cli +namespace SeqCli.Cli; #endif -{ - delegate U Converter(T t); - static class StringCoda { +delegate U Converter(T t); - public static IEnumerable WrappedLines (string self, params int[] widths) - { - IEnumerable w = widths; - return WrappedLines (self, w); - } +static class StringCoda { - public static IEnumerable WrappedLines (string self, IEnumerable widths) - { - if (widths == null) - throw new ArgumentNullException (nameof(widths)); - return CreateWrappedLinesIterator (self, widths); - } + public static IEnumerable WrappedLines (string self, params int[] widths) + { + IEnumerable w = widths; + return WrappedLines (self, w); + } - private static IEnumerable CreateWrappedLinesIterator (string self, IEnumerable widths) - { - if (string.IsNullOrEmpty (self)) { - yield return string.Empty; - yield break; - } - using (IEnumerator ewidths = widths.GetEnumerator ()) { - bool? hw = null; - int width = GetNextWidth (ewidths, int.MaxValue, ref hw); - int start = 0, end; - do { - end = GetLineEnd (start, width, self); - char c = self [end-1]; - if (char.IsWhiteSpace (c)) - --end; - bool needContinuation = end != self.Length && !IsEolChar (c); - string continuation = ""; - if (needContinuation) { - --end; - continuation = "-"; - } - string line = self.Substring (start, end - start) + continuation; - yield return line; - start = end; - if (char.IsWhiteSpace (c)) - ++start; - width = GetNextWidth (ewidths, width, ref hw); - } while (start < self.Length); - } - } + public static IEnumerable WrappedLines (string self, IEnumerable widths) + { + if (widths == null) + throw new ArgumentNullException (nameof(widths)); + return CreateWrappedLinesIterator (self, widths); + } - private static int GetNextWidth (IEnumerator ewidths, int curWidth, ref bool? eValid) - { - if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) { - curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth; - // '.' is any character, - is for a continuation - const string minWidth = ".-"; - if (curWidth < minWidth.Length) - throw new ArgumentOutOfRangeException ("widths", - string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth)); - return curWidth; - } - // no more elements, use the last element. - return curWidth; + private static IEnumerable CreateWrappedLinesIterator (string self, IEnumerable widths) + { + if (string.IsNullOrEmpty (self)) { + yield return string.Empty; + yield break; + } + using (IEnumerator ewidths = widths.GetEnumerator ()) { + bool? hw = null; + int width = GetNextWidth (ewidths, int.MaxValue, ref hw); + int start = 0, end; + do { + end = GetLineEnd (start, width, self); + char c = self [end-1]; + if (char.IsWhiteSpace (c)) + --end; + bool needContinuation = end != self.Length && !IsEolChar (c); + string continuation = ""; + if (needContinuation) { + --end; + continuation = "-"; + } + string line = self.Substring (start, end - start) + continuation; + yield return line; + start = end; + if (char.IsWhiteSpace (c)) + ++start; + width = GetNextWidth (ewidths, width, ref hw); + } while (start < self.Length); } + } - private static bool IsEolChar (char c) - { - return !char.IsLetterOrDigit (c); + private static int GetNextWidth (IEnumerator ewidths, int curWidth, ref bool? eValid) + { + if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) { + curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth; + // '.' is any character, - is for a continuation + const string minWidth = ".-"; + if (curWidth < minWidth.Length) + throw new ArgumentOutOfRangeException ("widths", + string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth)); + return curWidth; } + // no more elements, use the last element. + return curWidth; + } - private static int GetLineEnd (int start, int length, string description) - { - int end = System.Math.Min (start + length, description.Length); - int sep = -1; - for (int i = start; i < end; ++i) { - if (description [i] == '\n') - return i+1; - if (IsEolChar (description [i])) - sep = i+1; - } - if (sep == -1 || end == description.Length) - return end; - return sep; - } + private static bool IsEolChar (char c) + { + return !char.IsLetterOrDigit (c); } - class OptionValueCollection : IList, IList { + private static int GetLineEnd (int start, int length, string description) + { + int end = System.Math.Min (start + length, description.Length); + int sep = -1; + for (int i = start; i < end; ++i) { + if (description [i] == '\n') + return i+1; + if (IsEolChar (description [i])) + sep = i+1; + } + if (sep == -1 || end == description.Length) + return end; + return sep; + } +} - List values = new List (); - OptionContext c; +class OptionValueCollection : IList, IList { - internal OptionValueCollection (OptionContext c) - { - this.c = c; - } + List values = []; + OptionContext c; - #region ICollection - void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);} - bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}} - object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}} - #endregion - - #region ICollection - public void Add (string item) {values.Add (item);} - public void Clear () {values.Clear ();} - public bool Contains (string item) {return values.Contains (item);} - public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);} - public bool Remove (string item) {return values.Remove (item);} - public int Count {get {return values.Count;}} - public bool IsReadOnly {get {return false;}} - #endregion - - #region IEnumerable - IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();} - #endregion - - #region IEnumerable - public IEnumerator GetEnumerator () {return values.GetEnumerator ();} - #endregion - - #region IList - int IList.Add (object value) {return (values as IList).Add (value);} - bool IList.Contains (object value) {return (values as IList).Contains (value);} - int IList.IndexOf (object value) {return (values as IList).IndexOf (value);} - void IList.Insert (int index, object value) {(values as IList).Insert (index, value);} - void IList.Remove (object value) {(values as IList).Remove (value);} - void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);} - bool IList.IsFixedSize {get {return false;}} - object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}} - #endregion - - #region IList - public int IndexOf (string item) {return values.IndexOf (item);} - public void Insert (int index, string item) {values.Insert (index, item);} - public void RemoveAt (int index) {values.RemoveAt (index);} - - private void AssertValid (int index) - { - if (c.Option == null) - throw new InvalidOperationException ("OptionContext.Option is null."); - if (index >= c.Option.MaxValueCount) - throw new ArgumentOutOfRangeException (nameof(index)); - if (c.Option.OptionValueType == OptionValueType.Required && - index >= values.Count) - throw new OptionException (string.Format ( - c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName), - c.OptionName); - } + internal OptionValueCollection (OptionContext c) + { + this.c = c; + } - public string this [int index] { - get { - AssertValid (index); - return index >= values.Count ? null : values [index]; - } - set { - values [index] = value; - } - } - #endregion + #region ICollection + void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);} + bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}} + object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}} + #endregion + + #region ICollection + public void Add (string item) {values.Add (item);} + public void Clear () {values.Clear ();} + public bool Contains (string item) {return values.Contains (item);} + public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);} + public bool Remove (string item) {return values.Remove (item);} + public int Count {get {return values.Count;}} + public bool IsReadOnly {get {return false;}} + #endregion + + #region IEnumerable + IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IEnumerable + public IEnumerator GetEnumerator () {return values.GetEnumerator ();} + #endregion + + #region IList + int IList.Add (object value) {return (values as IList).Add (value);} + bool IList.Contains (object value) {return (values as IList).Contains (value);} + int IList.IndexOf (object value) {return (values as IList).IndexOf (value);} + void IList.Insert (int index, object value) {(values as IList).Insert (index, value);} + void IList.Remove (object value) {(values as IList).Remove (value);} + void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);} + bool IList.IsFixedSize {get {return false;}} + object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}} + #endregion + + #region IList + public int IndexOf (string item) {return values.IndexOf (item);} + public void Insert (int index, string item) {values.Insert (index, item);} + public void RemoveAt (int index) {values.RemoveAt (index);} + + private void AssertValid (int index) + { + if (c.Option == null) + throw new InvalidOperationException ("OptionContext.Option is null."); + if (index >= c.Option.MaxValueCount) + throw new ArgumentOutOfRangeException (nameof(index)); + if (c.Option.OptionValueType == OptionValueType.Required && + index >= values.Count) + throw new OptionException (string.Format ( + c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName), + c.OptionName); + } - public List ToList () - { - return new List (values); + public string this [int index] { + get { + AssertValid (index); + return index >= values.Count ? null : values [index]; } - - public string[] ToArray () - { - return values.ToArray (); + set { + values [index] = value; } + } + #endregion - public override string ToString () - { - return string.Join (", ", values.ToArray ()); - } + public List ToList () + { + return [..values]; } - class OptionContext { - private Option option; - private string name; - private int index; - private OptionSet set; - private OptionValueCollection c; + public string[] ToArray () + { + return values.ToArray (); + } - public OptionContext (OptionSet set) - { - this.set = set; - this.c = new OptionValueCollection (this); - } + public override string ToString () + { + return string.Join (", ", values.ToArray ()); + } +} - public Option Option { - get {return option;} - set {option = value;} - } +class OptionContext { + private Option option; + private string name; + private int index; + private OptionSet set; + private OptionValueCollection c; - public string OptionName { - get {return name;} - set {name = value;} - } + public OptionContext (OptionSet set) + { + this.set = set; + this.c = new OptionValueCollection (this); + } - public int OptionIndex { - get {return index;} - set {index = value;} - } + public Option Option { + get {return option;} + set {option = value;} + } - public OptionSet OptionSet { - get {return set;} - } + public string OptionName { + get {return name;} + set {name = value;} + } - public OptionValueCollection OptionValues { - get {return c;} - } + public int OptionIndex { + get {return index;} + set {index = value;} } - enum OptionValueType { - None, - Optional, - Required, + public OptionSet OptionSet { + get {return set;} } - abstract class Option { - string prototype, description; - string[] names; - OptionValueType type; - int count; - string[] separators; - bool hidden; + public OptionValueCollection OptionValues { + get {return c;} + } +} - protected Option (string prototype, string description) - : this (prototype, description, 1, false) - { - } +enum OptionValueType { + None, + Optional, + Required, +} - protected Option (string prototype, string description, int maxValueCount) - : this (prototype, description, maxValueCount, false) - { - } +abstract class Option { + string prototype, description; + string[] names; + OptionValueType type; + int count; + string[] separators; + bool hidden; - protected Option (string prototype, string description, int maxValueCount, bool hidden) - { - if (prototype == null) - throw new ArgumentNullException (nameof(prototype)); - if (prototype.Length == 0) - throw new ArgumentException ("Cannot be the empty string.", nameof(prototype)); - if (maxValueCount < 0) - throw new ArgumentOutOfRangeException (nameof(maxValueCount)); - - this.prototype = prototype; - this.description = description; - this.count = maxValueCount; - this.names = (this is OptionSet.Category) - // append GetHashCode() so that "duplicate" categories have distinct - // names, e.g. adding multiple "" categories should be valid. - ? new[]{prototype + this.GetHashCode ()} - : prototype.Split ('|'); - - if (this is OptionSet.Category) - return; - - this.type = ParsePrototype (); - this.hidden = hidden; - - if (this.count == 0 && type != OptionValueType.None) - throw new ArgumentException ( - "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + - "OptionValueType.Optional.", - nameof(maxValueCount)); - if (this.type == OptionValueType.None && maxValueCount > 1) - throw new ArgumentException ( - string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), - nameof(maxValueCount)); - if (Array.IndexOf (names, "<>") >= 0 && - ((names.Length == 1 && this.type != OptionValueType.None) || - (names.Length > 1 && this.MaxValueCount > 1))) - throw new ArgumentException ( - "The default option handler '<>' cannot require values.", - nameof(prototype)); - } + protected Option (string prototype, string description) + : this (prototype, description, 1, false) + { + } - public string Prototype {get {return prototype;}} - public string Description {get {return description;}} - public OptionValueType OptionValueType {get {return type;}} - public int MaxValueCount {get {return count;}} - public bool Hidden {get {return hidden;}} + protected Option (string prototype, string description, int maxValueCount) + : this (prototype, description, maxValueCount, false) + { + } - public string[] GetNames () - { - return (string[]) names.Clone (); - } + protected Option (string prototype, string description, int maxValueCount, bool hidden) + { + if (prototype == null) + throw new ArgumentNullException (nameof(prototype)); + if (prototype.Length == 0) + throw new ArgumentException ("Cannot be the empty string.", nameof(prototype)); + if (maxValueCount < 0) + throw new ArgumentOutOfRangeException (nameof(maxValueCount)); + + this.prototype = prototype; + this.description = description; + this.count = maxValueCount; + this.names = (this is OptionSet.Category) + // append GetHashCode() so that "duplicate" categories have distinct + // names, e.g. adding multiple "" categories should be valid. + ? [prototype + this.GetHashCode ()] + : prototype.Split ('|'); + + if (this is OptionSet.Category) + return; + + this.type = ParsePrototype (); + this.hidden = hidden; + + if (this.count == 0 && type != OptionValueType.None) + throw new ArgumentException ( + "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + + "OptionValueType.Optional.", + nameof(maxValueCount)); + if (this.type == OptionValueType.None && maxValueCount > 1) + throw new ArgumentException ( + string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), + nameof(maxValueCount)); + if (Array.IndexOf (names, "<>") >= 0 && + ((names.Length == 1 && this.type != OptionValueType.None) || + (names.Length > 1 && this.MaxValueCount > 1))) + throw new ArgumentException ( + "The default option handler '<>' cannot require values.", + nameof(prototype)); + } - public string[] GetValueSeparators () - { - if (separators == null) - return new string [0]; - return (string[]) separators.Clone (); - } + public string Prototype {get {return prototype;}} + public string Description {get {return description;}} + public OptionValueType OptionValueType {get {return type;}} + public int MaxValueCount {get {return count;}} + public bool Hidden {get {return hidden;}} - protected static T Parse (string value, OptionContext c) - { - var tt = typeof (T).GetTypeInfo(); - bool nullable = tt.IsValueType && tt.IsGenericType && - !tt.IsGenericTypeDefinition && - tt.GetGenericTypeDefinition () == typeof (Nullable<>); - Type targetType = nullable ? tt.GetGenericArguments () [0] : typeof (T); - T t = default (T); - try { - if (value != null) - t = (T) Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); - } - catch (Exception e) { - throw new OptionException ( - string.Format ( - c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."), - value, targetType.Name, c.OptionName), - c.OptionName, e); - } - return t; - } + public string[] GetNames () + { + return (string[]) names.Clone (); + } - internal string[] Names {get {return names;}} - internal string[] ValueSeparators {get {return separators;}} + public string[] GetValueSeparators () + { + if (separators == null) + return new string [0]; + return (string[]) separators.Clone (); + } - static readonly char[] NameTerminator = new char[]{'=', ':'}; + protected static T Parse (string value, OptionContext c) + { + var tt = typeof (T).GetTypeInfo(); + bool nullable = tt.IsValueType && tt.IsGenericType && + !tt.IsGenericTypeDefinition && + tt.GetGenericTypeDefinition () == typeof (Nullable<>); + Type targetType = nullable ? tt.GetGenericArguments () [0] : typeof (T); + T t = default (T); + try { + if (value != null) + t = (T) Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); + } + catch (Exception e) { + throw new OptionException ( + string.Format ( + c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."), + value, targetType.Name, c.OptionName), + c.OptionName, e); + } + return t; + } - private OptionValueType ParsePrototype () - { - char type = '\0'; - List seps = new List (); - for (int i = 0; i < names.Length; ++i) { - string name = names [i]; - if (name.Length == 0) - throw new ArgumentException ("Empty option names are not supported.", "prototype"); - - int end = name.IndexOfAny (NameTerminator); - if (end == -1) - continue; - names [i] = name.Substring (0, end); - if (type == '\0' || type == name [end]) - type = name [end]; - else - throw new ArgumentException ( - string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), - "prototype"); - AddSeparators (name, end, seps); - } + internal string[] Names {get {return names;}} + internal string[] ValueSeparators {get {return separators;}} - if (type == '\0') - return OptionValueType.None; + static readonly char[] NameTerminator = ['=', ':']; - if (count <= 1 && seps.Count != 0) + private OptionValueType ParsePrototype () + { + char type = '\0'; + List seps = []; + for (int i = 0; i < names.Length; ++i) { + string name = names [i]; + if (name.Length == 0) + throw new ArgumentException ("Empty option names are not supported.", "prototype"); + + int end = name.IndexOfAny (NameTerminator); + if (end == -1) + continue; + names [i] = name.Substring (0, end); + if (type == '\0' || type == name [end]) + type = name [end]; + else throw new ArgumentException ( - string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count), - "prototype"); - if (count > 1) { - if (seps.Count == 0) - this.separators = new string[]{":", "="}; - else if (seps.Count == 1 && seps [0].Length == 0) - this.separators = null; - else - this.separators = seps.ToArray (); - } - - return type == '=' ? OptionValueType.Required : OptionValueType.Optional; + string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]), + "prototype"); + AddSeparators (name, end, seps); + } + + if (type == '\0') + return OptionValueType.None; + + if (count <= 1 && seps.Count != 0) + throw new ArgumentException ( + string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count), + "prototype"); + if (count > 1) { + if (seps.Count == 0) + this.separators = [":", "="]; + else if (seps.Count == 1 && seps [0].Length == 0) + this.separators = null; + else + this.separators = seps.ToArray (); } - private static void AddSeparators (string name, int end, ICollection seps) - { - int start = -1; - for (int i = end+1; i < name.Length; ++i) { - switch (name [i]) { - case '{': - if (start != -1) - throw new ArgumentException ( - string.Format ("Ill-formed name/value separator found in \"{0}\".", name), - "prototype"); - start = i+1; - break; - case '}': - if (start == -1) - throw new ArgumentException ( - string.Format ("Ill-formed name/value separator found in \"{0}\".", name), - "prototype"); - seps.Add (name.Substring (start, i-start)); - start = -1; - break; - default: - if (start == -1) - seps.Add (name [i].ToString ()); - break; - } + return type == '=' ? OptionValueType.Required : OptionValueType.Optional; + } + + private static void AddSeparators (string name, int end, ICollection seps) + { + int start = -1; + for (int i = end+1; i < name.Length; ++i) { + switch (name [i]) { + case '{': + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + start = i+1; + break; + case '}': + if (start == -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + seps.Add (name.Substring (start, i-start)); + start = -1; + break; + default: + if (start == -1) + seps.Add (name [i].ToString ()); + break; } - if (start != -1) - throw new ArgumentException ( - string.Format ("Ill-formed name/value separator found in \"{0}\".", name), - "prototype"); } + if (start != -1) + throw new ArgumentException ( + string.Format ("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + } - public void Invoke (OptionContext c) - { - OnParseComplete (c); - c.OptionName = null; - c.Option = null; - c.OptionValues.Clear (); - } + public void Invoke (OptionContext c) + { + OnParseComplete (c); + c.OptionName = null; + c.Option = null; + c.OptionValues.Clear (); + } - protected abstract void OnParseComplete (OptionContext c); + protected abstract void OnParseComplete (OptionContext c); - public override string ToString () - { - return Prototype; - } + public override string ToString () + { + return Prototype; } +} - abstract class ArgumentSource { +abstract class ArgumentSource { - protected ArgumentSource () - { - } + protected ArgumentSource () + { + } - public abstract string[] GetNames (); - public abstract string Description { get; } - public abstract bool GetArguments (string value, out IEnumerable replacement); + public abstract string[] GetNames (); + public abstract string Description { get; } + public abstract bool GetArguments (string value, out IEnumerable replacement); - public static IEnumerable GetArgumentsFromFile (string file) - { - return GetArguments (File.OpenText (file), true); - } + public static IEnumerable GetArgumentsFromFile (string file) + { + return GetArguments (File.OpenText (file), true); + } - public static IEnumerable GetArguments (TextReader reader) - { - return GetArguments (reader, false); - } + public static IEnumerable GetArguments (TextReader reader) + { + return GetArguments (reader, false); + } - // Cribbed from mcs/driver.cs:LoadArgs(string) - static IEnumerable GetArguments (TextReader reader, bool close) - { - try { - StringBuilder arg = new StringBuilder (); + // Cribbed from mcs/driver.cs:LoadArgs(string) + static IEnumerable GetArguments (TextReader reader, bool close) + { + try { + StringBuilder arg = new StringBuilder (); - string line; - while ((line = reader.ReadLine ()) != null) { - int t = line.Length; + string line; + while ((line = reader.ReadLine ()) != null) { + int t = line.Length; - for (int i = 0; i < t; i++) { - char c = line [i]; + for (int i = 0; i < t; i++) { + char c = line [i]; - if (c == '"' || c == '\'') { - char end = c; + if (c == '"' || c == '\'') { + char end = c; - for (i++; i < t; i++){ - c = line [i]; - - if (c == end) - break; - arg.Append (c); - } - } else if (c == ' ') { - if (arg.Length > 0) { - yield return arg.ToString (); - arg.Length = 0; - } - } else + for (i++; i < t; i++){ + c = line [i]; + + if (c == end) + break; arg.Append (c); - } - if (arg.Length > 0) { - yield return arg.ToString (); - arg.Length = 0; - } + } + } else if (c == ' ') { + if (arg.Length > 0) { + yield return arg.ToString (); + arg.Length = 0; + } + } else + arg.Append (c); + } + if (arg.Length > 0) { + yield return arg.ToString (); + arg.Length = 0; } } - finally { - if (close) - reader.Dispose(); - } + } + finally { + if (close) + reader.Dispose(); } } +} - class ResponseFileSource : ArgumentSource { +class ResponseFileSource : ArgumentSource { - public override string[] GetNames () - { - return new string[]{"@file"}; - } + public override string[] GetNames () + { + return ["@file"]; + } - public override string Description { - get {return "Read response file for more options.";} - } + public override string Description { + get {return "Read response file for more options.";} + } - public override bool GetArguments (string value, out IEnumerable replacement) - { - if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) { - replacement = null; - return false; - } - replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1)); - return true; + public override bool GetArguments (string value, out IEnumerable replacement) + { + if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) { + replacement = null; + return false; } + replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1)); + return true; } +} - class OptionException : Exception { - private string option; +class OptionException : Exception { + private string option; - public OptionException () - { - } + public OptionException () + { + } - public OptionException (string message, string optionName) - : base (message) - { - this.option = optionName; - } + public OptionException (string message, string optionName) + : base (message) + { + this.option = optionName; + } - public OptionException (string message, string optionName, Exception innerException) - : base (message, innerException) - { - this.option = optionName; - } + public OptionException (string message, string optionName, Exception innerException) + : base (message, innerException) + { + this.option = optionName; + } - public string OptionName { - get {return this.option;} - } + public string OptionName { + get {return this.option;} } +} - delegate void OptionAction (TKey key, TValue value); +delegate void OptionAction (TKey key, TValue value); - class OptionSet : KeyedCollection +class OptionSet : KeyedCollection +{ + public OptionSet () + : this (delegate (string f) {return f;}) { - public OptionSet () - : this (delegate (string f) {return f;}) - { - } + } - public OptionSet (Converter localizer) - { - this.localizer = localizer; - this.roSources = new ReadOnlyCollection(sources); - } + public OptionSet (Converter localizer) + { + this.localizer = localizer; + this.roSources = new ReadOnlyCollection(sources); + } - Converter localizer; + Converter localizer; - public Converter MessageLocalizer { - get {return localizer;} - } + public Converter MessageLocalizer { + get {return localizer;} + } + + List sources = []; + ReadOnlyCollection roSources; - List sources = new List (); - ReadOnlyCollection roSources; + public ReadOnlyCollection ArgumentSources { + get {return roSources;} + } - public ReadOnlyCollection ArgumentSources { - get {return roSources;} + + protected override string GetKeyForItem (Option item) + { + if (item == null) + throw new ArgumentNullException ("option"); + if (item.Names != null && item.Names.Length > 0) + return item.Names [0]; + // This should never happen, as it's invalid for Option to be + // constructed w/o any names. + throw new InvalidOperationException ("Option has no names!"); + } + + [Obsolete ("Use KeyedCollection.this[string]")] + protected Option GetOptionForName (string option) + { + if (option == null) + throw new ArgumentNullException (nameof(option)); + try { + return base [option]; + } + catch (KeyNotFoundException) { + return null; } + } + protected override void InsertItem (int index, Option item) + { + base.InsertItem (index, item); + AddImpl (item); + } - protected override string GetKeyForItem (Option item) - { - if (item == null) - throw new ArgumentNullException ("option"); - if (item.Names != null && item.Names.Length > 0) - return item.Names [0]; - // This should never happen, as it's invalid for Option to be - // constructed w/o any names. - throw new InvalidOperationException ("Option has no names!"); + protected override void RemoveItem (int index) + { + Option p = Items [index]; + base.RemoveItem (index); + // KeyedCollection.RemoveItem() handles the 0th item + for (int i = 1; i < p.Names.Length; ++i) { + Dictionary.Remove (p.Names [i]); } + } - [Obsolete ("Use KeyedCollection.this[string]")] - protected Option GetOptionForName (string option) - { - if (option == null) - throw new ArgumentNullException (nameof(option)); - try { - return base [option]; - } - catch (KeyNotFoundException) { - return null; + protected override void SetItem (int index, Option item) + { + base.SetItem (index, item); + AddImpl (item); + } + + private void AddImpl (Option option) + { + if (option == null) + throw new ArgumentNullException (nameof(option)); + List added = new List (option.Names.Length); + try { + // KeyedCollection.InsertItem/SetItem handle the 0th name. + for (int i = 1; i < option.Names.Length; ++i) { + Dictionary.Add (option.Names [i], option); + added.Add (option.Names [i]); } } + catch (Exception) { + foreach (string name in added) + Dictionary.Remove (name); + throw; + } + } + + public OptionSet Add (string header) + { + if (header == null) + throw new ArgumentNullException (nameof(header)); + Add (new Category (header)); + return this; + } - protected override void InsertItem (int index, Option item) + internal sealed class Category : Option { + + // Prototype starts with '=' because this is an invalid prototype + // (see Option.ParsePrototype(), and thus it'll prevent Category + // instances from being accidentally used as normal options. + public Category (string description) + : base ("=:Category:= " + description, description) { - base.InsertItem (index, item); - AddImpl (item); } - protected override void RemoveItem (int index) + protected override void OnParseComplete (OptionContext c) { - Option p = Items [index]; - base.RemoveItem (index); - // KeyedCollection.RemoveItem() handles the 0th item - for (int i = 1; i < p.Names.Length; ++i) { - Dictionary.Remove (p.Names [i]); - } + throw new NotSupportedException ("Category.OnParseComplete should not be invoked."); } + } - protected override void SetItem (int index, Option item) + + public new OptionSet Add (Option option) + { + base.Add (option); + return this; + } + + sealed class ActionOption : Option { + Action action; + + public ActionOption (string prototype, string description, int count, Action action) + : this (prototype, description, count, action, false) { - base.SetItem (index, item); - AddImpl (item); } - private void AddImpl (Option option) + public ActionOption (string prototype, string description, int count, Action action, bool hidden) + : base (prototype, description, count, hidden) { - if (option == null) - throw new ArgumentNullException (nameof(option)); - List added = new List (option.Names.Length); - try { - // KeyedCollection.InsertItem/SetItem handle the 0th name. - for (int i = 1; i < option.Names.Length; ++i) { - Dictionary.Add (option.Names [i], option); - added.Add (option.Names [i]); - } - } - catch (Exception) { - foreach (string name in added) - Dictionary.Remove (name); - throw; - } + this.action = action ?? throw new ArgumentNullException (nameof(action)); } - public OptionSet Add (string header) + protected override void OnParseComplete (OptionContext c) { - if (header == null) - throw new ArgumentNullException (nameof(header)); - Add (new Category (header)); - return this; + action (c.OptionValues); } + } - internal sealed class Category : Option { + public OptionSet Add (string prototype, Action action) + { + return Add (prototype, null, action); + } - // Prototype starts with '=' because this is an invalid prototype - // (see Option.ParsePrototype(), and thus it'll prevent Category - // instances from being accidentally used as normal options. - public Category (string description) - : base ("=:Category:= " + description, description) - { - } + public OptionSet Add (string prototype, string description, Action action) + { + return Add (prototype, description, action, false); + } - protected override void OnParseComplete (OptionContext c) + public OptionSet Add (string prototype, string description, Action action, bool hidden) + { + if (action == null) + throw new ArgumentNullException (nameof(action)); + Option p = new ActionOption (prototype, description, 1, + delegate (OptionValueCollection v) { - throw new NotSupportedException ("Category.OnParseComplete should not be invoked."); - } - } - - - public new OptionSet Add (Option option) - { - base.Add (option); - return this; - } + var v0 = v[0]; + if (!string.IsNullOrWhiteSpace(v0)) + { + action(v0); + } + }, hidden); + base.Add (p); + return this; + } - sealed class ActionOption : Option { - Action action; + public OptionSet Add (string prototype, OptionAction action) + { + return Add (prototype, null, action); + } - public ActionOption (string prototype, string description, int count, Action action) - : this (prototype, description, count, action, false) - { - } + public OptionSet Add (string prototype, string description, OptionAction action) + { + return Add (prototype, description, action, false); + } - public ActionOption (string prototype, string description, int count, Action action, bool hidden) - : base (prototype, description, count, hidden) - { - this.action = action ?? throw new ArgumentNullException (nameof(action)); - } + public OptionSet Add (string prototype, string description, OptionAction action, bool hidden) { + if (action == null) + throw new ArgumentNullException (nameof(action)); + Option p = new ActionOption (prototype, description, 2, + delegate (OptionValueCollection v) {action (v [0], v [1]);}, hidden); + base.Add (p); + return this; + } - protected override void OnParseComplete (OptionContext c) - { - action (c.OptionValues); - } - } + sealed class ActionOption : Option { + Action action; - public OptionSet Add (string prototype, Action action) + public ActionOption (string prototype, string description, Action action) + : base (prototype, description, 1) { - return Add (prototype, null, action); + this.action = action ?? throw new ArgumentNullException (nameof(action)); } - public OptionSet Add (string prototype, string description, Action action) + protected override void OnParseComplete (OptionContext c) { - return Add (prototype, description, action, false); + action (Parse (c.OptionValues [0], c)); } + } - public OptionSet Add (string prototype, string description, Action action, bool hidden) - { - if (action == null) - throw new ArgumentNullException (nameof(action)); - Option p = new ActionOption (prototype, description, 1, - delegate (OptionValueCollection v) - { - var v0 = v[0]; - if (!string.IsNullOrWhiteSpace(v0)) - { - action(v0); - } - }, hidden); - base.Add (p); - return this; - } + sealed class ActionOption : Option { + OptionAction action; - public OptionSet Add (string prototype, OptionAction action) + public ActionOption (string prototype, string description, OptionAction action) + : base (prototype, description, 2) { - return Add (prototype, null, action); + this.action = action ?? throw new ArgumentNullException (nameof(action)); } - public OptionSet Add (string prototype, string description, OptionAction action) + protected override void OnParseComplete (OptionContext c) { - return Add (prototype, description, action, false); + action ( + Parse (c.OptionValues [0], c), + Parse (c.OptionValues [1], c)); } + } - public OptionSet Add (string prototype, string description, OptionAction action, bool hidden) { - if (action == null) - throw new ArgumentNullException (nameof(action)); - Option p = new ActionOption (prototype, description, 2, - delegate (OptionValueCollection v) {action (v [0], v [1]);}, hidden); - base.Add (p); - return this; - } + public OptionSet Add (string prototype, Action action) + { + return Add (prototype, null, action); + } - sealed class ActionOption : Option { - Action action; + public OptionSet Add (string prototype, string description, Action action) + { + return Add (new ActionOption (prototype, description, action)); + } - public ActionOption (string prototype, string description, Action action) - : base (prototype, description, 1) - { - this.action = action ?? throw new ArgumentNullException (nameof(action)); - } + public OptionSet Add (string prototype, OptionAction action) + { + return Add (prototype, null, action); + } - protected override void OnParseComplete (OptionContext c) - { - action (Parse (c.OptionValues [0], c)); - } - } + public OptionSet Add (string prototype, string description, OptionAction action) + { + return Add (new ActionOption (prototype, description, action)); + } - sealed class ActionOption : Option { - OptionAction action; + public OptionSet Add (ArgumentSource source) + { + if (source == null) + throw new ArgumentNullException (nameof(source)); + sources.Add (source); + return this; + } - public ActionOption (string prototype, string description, OptionAction action) - : base (prototype, description, 2) - { - this.action = action ?? throw new ArgumentNullException (nameof(action)); - } + protected virtual OptionContext CreateOptionContext () + { + return new OptionContext (this); + } - protected override void OnParseComplete (OptionContext c) - { - action ( - Parse (c.OptionValues [0], c), - Parse (c.OptionValues [1], c)); + public List Parse (IEnumerable arguments) + { + if (arguments == null) + throw new ArgumentNullException (nameof(arguments)); + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + bool process = true; + List unprocessed = []; + Option def = Contains ("<>") ? this ["<>"] : null; + ArgumentEnumerator ae = new ArgumentEnumerator (arguments); + foreach (string argument in ae) { + ++c.OptionIndex; + if (argument == "--") { + process = false; + continue; } + if (!process) { + Unprocessed (unprocessed, def, c, argument); + continue; + } + if (AddSource (ae, argument)) + continue; + if (!Parse (argument, c)) + Unprocessed (unprocessed, def, c, argument); } + if (c.Option != null) + c.Option.Invoke (c); + return unprocessed; + } - public OptionSet Add (string prototype, Action action) - { - return Add (prototype, null, action); - } + class ArgumentEnumerator : IEnumerable { + List> sources = []; - public OptionSet Add (string prototype, string description, Action action) + public ArgumentEnumerator (IEnumerable arguments) { - return Add (new ActionOption (prototype, description, action)); + sources.Add (arguments.GetEnumerator ()); } - public OptionSet Add (string prototype, OptionAction action) + public void Add (IEnumerable arguments) { - return Add (prototype, null, action); + sources.Add (arguments.GetEnumerator ()); } - public OptionSet Add (string prototype, string description, OptionAction action) + public IEnumerator GetEnumerator () { - return Add (new ActionOption (prototype, description, action)); + do { + IEnumerator c = sources [sources.Count-1]; + if (c.MoveNext ()) + yield return c.Current; + else { + c.Dispose (); + sources.RemoveAt (sources.Count-1); + } + } while (sources.Count > 0); } - public OptionSet Add (ArgumentSource source) + IEnumerator IEnumerable.GetEnumerator () { - if (source == null) - throw new ArgumentNullException (nameof(source)); - sources.Add (source); - return this; + return GetEnumerator (); } + } - protected virtual OptionContext CreateOptionContext () - { - return new OptionContext (this); + bool AddSource (ArgumentEnumerator ae, string argument) + { + foreach (ArgumentSource source in sources) { + IEnumerable replacement; + if (!source.GetArguments (argument, out replacement)) + continue; + ae.Add (replacement); + return true; } + return false; + } - public List Parse (IEnumerable arguments) - { - if (arguments == null) - throw new ArgumentNullException (nameof(arguments)); - OptionContext c = CreateOptionContext (); - c.OptionIndex = -1; - bool process = true; - List unprocessed = new List (); - Option def = Contains ("<>") ? this ["<>"] : null; - ArgumentEnumerator ae = new ArgumentEnumerator (arguments); - foreach (string argument in ae) { - ++c.OptionIndex; - if (argument == "--") { - process = false; - continue; - } - if (!process) { - Unprocessed (unprocessed, def, c, argument); - continue; - } - if (AddSource (ae, argument)) - continue; - if (!Parse (argument, c)) - Unprocessed (unprocessed, def, c, argument); - } - if (c.Option != null) - c.Option.Invoke (c); - return unprocessed; + private static bool Unprocessed (ICollection extra, Option def, OptionContext c, string argument) + { + if (def == null) { + extra.Add (argument); + return false; } + c.OptionValues.Add (argument); + c.Option = def; + c.Option.Invoke (c); + return false; + } - class ArgumentEnumerator : IEnumerable { - List> sources = new List> (); - - public ArgumentEnumerator (IEnumerable arguments) - { - sources.Add (arguments.GetEnumerator ()); - } - - public void Add (IEnumerable arguments) - { - sources.Add (arguments.GetEnumerator ()); - } - - public IEnumerator GetEnumerator () - { - do { - IEnumerator c = sources [sources.Count-1]; - if (c.MoveNext ()) - yield return c.Current; - else { - c.Dispose (); - sources.RemoveAt (sources.Count-1); - } - } while (sources.Count > 0); - } + private readonly Regex ValueOption = new( + @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); - IEnumerator IEnumerable.GetEnumerator () - { - return GetEnumerator (); - } - } + protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) + { + if (argument == null) + throw new ArgumentNullException (nameof(argument)); - bool AddSource (ArgumentEnumerator ae, string argument) - { - foreach (ArgumentSource source in sources) { - IEnumerable replacement; - if (!source.GetArguments (argument, out replacement)) - continue; - ae.Add (replacement); - return true; - } + flag = name = sep = value = null; + Match m = ValueOption.Match (argument); + if (!m.Success) { return false; } - - private static bool Unprocessed (ICollection extra, Option def, OptionContext c, string argument) - { - if (def == null) { - extra.Add (argument); - return false; - } - c.OptionValues.Add (argument); - c.Option = def; - c.Option.Invoke (c); - return false; + flag = m.Groups ["flag"].Value; + name = m.Groups ["name"].Value; + if (m.Groups ["sep"].Success && m.Groups ["value"].Success) { + sep = m.Groups ["sep"].Value; + value = m.Groups ["value"].Value; } + return true; + } - private readonly Regex ValueOption = new Regex ( - @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); + protected virtual bool Parse (string argument, OptionContext c) + { + if (c.Option != null) { + ParseValue (argument, c); + return true; + } - protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) - { - if (argument == null) - throw new ArgumentNullException (nameof(argument)); + string f, n, s, v; + if (!GetOptionParts (argument, out f, out n, out s, out v)) + return false; - flag = name = sep = value = null; - Match m = ValueOption.Match (argument); - if (!m.Success) { - return false; - } - flag = m.Groups ["flag"].Value; - name = m.Groups ["name"].Value; - if (m.Groups ["sep"].Success && m.Groups ["value"].Success) { - sep = m.Groups ["sep"].Value; - value = m.Groups ["value"].Value; + Option p; + if (Contains (n)) { + p = this [n]; + c.OptionName = f + n; + c.Option = p; + switch (p.OptionValueType) { + case OptionValueType.None: + c.OptionValues.Add (n); + c.Option.Invoke (c); + break; + case OptionValueType.Optional: + case OptionValueType.Required: + ParseValue (v, c); + break; } return true; } + // no match; is it a bool option? + if (ParseBool (argument, n, c)) + return true; + // is it a bundled option? + if (ParseBundledValue (f, string.Concat (n + s + v), c)) + return true; - protected virtual bool Parse (string argument, OptionContext c) - { - if (c.Option != null) { - ParseValue (argument, c); - return true; - } - - string f, n, s, v; - if (!GetOptionParts (argument, out f, out n, out s, out v)) - return false; + return false; + } - Option p; - if (Contains (n)) { - p = this [n]; - c.OptionName = f + n; - c.Option = p; - switch (p.OptionValueType) { - case OptionValueType.None: - c.OptionValues.Add (n); - c.Option.Invoke (c); - break; - case OptionValueType.Optional: - case OptionValueType.Required: - ParseValue (v, c); - break; - } - return true; + private void ParseValue (string option, OptionContext c) + { + if (option != null) + foreach (string o in c.Option.ValueSeparators != null + ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None) + : [option]) { + c.OptionValues.Add (o); } - // no match; is it a bool option? - if (ParseBool (argument, n, c)) - return true; - // is it a bundled option? - if (ParseBundledValue (f, string.Concat (n + s + v), c)) - return true; - - return false; + if (c.OptionValues.Count == c.Option.MaxValueCount || + c.Option.OptionValueType == OptionValueType.Optional) + c.Option.Invoke (c); + else if (c.OptionValues.Count > c.Option.MaxValueCount) { + throw new OptionException (localizer (string.Format ( + "Error: Found {0} option values when expecting {1}.", + c.OptionValues.Count, c.Option.MaxValueCount)), + c.OptionName); } + } - private void ParseValue (string option, OptionContext c) - { - if (option != null) - foreach (string o in c.Option.ValueSeparators != null - ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None) - : new string[]{option}) { - c.OptionValues.Add (o); - } - if (c.OptionValues.Count == c.Option.MaxValueCount || - c.Option.OptionValueType == OptionValueType.Optional) - c.Option.Invoke (c); - else if (c.OptionValues.Count > c.Option.MaxValueCount) { - throw new OptionException (localizer (string.Format ( - "Error: Found {0} option values when expecting {1}.", - c.OptionValues.Count, c.Option.MaxValueCount)), - c.OptionName); - } + private bool ParseBool (string option, string n, OptionContext c) + { + Option p; + string rn; + if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') && + Contains ((rn = n.Substring (0, n.Length-1)))) { + p = this [rn]; + string v = n [n.Length-1] == '+' ? option : null; + c.OptionName = option; + c.Option = p; + c.OptionValues.Add (v); + p.Invoke (c); + return true; } + return false; + } - private bool ParseBool (string option, string n, OptionContext c) - { + private bool ParseBundledValue (string f, string n, OptionContext c) + { + if (f != "-") + return false; + for (int i = 0; i < n.Length; ++i) { Option p; - string rn; - if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') && - Contains ((rn = n.Substring (0, n.Length-1)))) { - p = this [rn]; - string v = n [n.Length-1] == '+' ? option : null; - c.OptionName = option; - c.Option = p; - c.OptionValues.Add (v); - p.Invoke (c); - return true; + string opt = f + n [i].ToString (); + string rn = n [i].ToString (); + if (!Contains (rn)) { + if (i == 0) + return false; + throw new OptionException (string.Format (localizer ( + "Cannot bundle unregistered option '{0}'."), opt), opt); } - return false; - } - - private bool ParseBundledValue (string f, string n, OptionContext c) - { - if (f != "-") - return false; - for (int i = 0; i < n.Length; ++i) { - Option p; - string opt = f + n [i].ToString (); - string rn = n [i].ToString (); - if (!Contains (rn)) { - if (i == 0) - return false; - throw new OptionException (string.Format (localizer ( - "Cannot bundle unregistered option '{0}'."), opt), opt); - } - p = this [rn]; - switch (p.OptionValueType) { - case OptionValueType.None: - Invoke (c, opt, n, p); - break; - case OptionValueType.Optional: - case OptionValueType.Required: { - string v = n.Substring (i+1); - c.Option = p; - c.OptionName = opt; - ParseValue (v.Length != 0 ? v : null, c); - return true; - } - default: - throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType); + p = this [rn]; + switch (p.OptionValueType) { + case OptionValueType.None: + Invoke (c, opt, n, p); + break; + case OptionValueType.Optional: + case OptionValueType.Required: { + string v = n.Substring (i+1); + c.Option = p; + c.OptionName = opt; + ParseValue (v.Length != 0 ? v : null, c); + return true; } + default: + throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType); } - return true; - } - - private static void Invoke (OptionContext c, string name, string value, Option option) - { - c.OptionName = name; - c.Option = option; - c.OptionValues.Add (value); - option.Invoke (c); } + return true; + } - private const int OptionWidth = 29; - private const int Description_FirstWidth = 80 - OptionWidth; - private const int Description_RemWidth = 80 - OptionWidth - 2; + private static void Invoke (OptionContext c, string name, string value, Option option) + { + c.OptionName = name; + c.Option = option; + c.OptionValues.Add (value); + option.Invoke (c); + } - public void WriteOptionDescriptions (TextWriter o) - { - foreach (Option p in this) { - int written = 0; + private const int OptionWidth = 29; + private const int Description_FirstWidth = 80 - OptionWidth; + private const int Description_RemWidth = 80 - OptionWidth - 2; - if (p.Hidden) - continue; + public void WriteOptionDescriptions (TextWriter o) + { + foreach (Option p in this) { + int written = 0; - Category c = p as Category; - if (c != null) { - WriteDescription (o, p.Description, "", 80, 80); - continue; - } + if (p.Hidden) + continue; - if (!WriteOptionPrototype (o, p, ref written)) - continue; + Category c = p as Category; + if (c != null) { + WriteDescription (o, p.Description, "", 80, 80); + continue; + } - if (written < OptionWidth) - o.Write (new string (' ', OptionWidth - written)); - else { - o.WriteLine (); - o.Write (new string (' ', OptionWidth)); - } + if (!WriteOptionPrototype (o, p, ref written)) + continue; - WriteDescription (o, p.Description, new string (' ', OptionWidth+2), - Description_FirstWidth -1, Description_RemWidth - 2); + if (written < OptionWidth) + o.Write (new string (' ', OptionWidth - written)); + else { + o.WriteLine (); + o.Write (new string (' ', OptionWidth)); } - foreach (ArgumentSource s in sources) { - string[] names = s.GetNames (); - if (names == null || names.Length == 0) - continue; + WriteDescription (o, p.Description, new string (' ', OptionWidth+2), + Description_FirstWidth -1, Description_RemWidth - 2); + } - int written = 0; + foreach (ArgumentSource s in sources) { + string[] names = s.GetNames (); + if (names == null || names.Length == 0) + continue; - Write (o, ref written, " "); - Write (o, ref written, names [0]); - for (int i = 1; i < names.Length; ++i) { - Write (o, ref written, ", "); - Write (o, ref written, names [i]); - } + int written = 0; - if (written < OptionWidth) - o.Write (new string (' ', OptionWidth - written)); - else { - o.WriteLine (); - o.Write (new string (' ', OptionWidth)); - } - - WriteDescription (o, s.Description, new string (' ', OptionWidth+2), - Description_FirstWidth, Description_RemWidth); + Write (o, ref written, " "); + Write (o, ref written, names [0]); + for (int i = 1; i < names.Length; ++i) { + Write (o, ref written, ", "); + Write (o, ref written, names [i]); } - } - void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth) - { - bool indent = false; - foreach (string line in GetLines (localizer (GetDescription (value)), firstWidth, remWidth)) { - if (indent) - o.Write (prefix); - o.WriteLine (line); - indent = true; + if (written < OptionWidth) + o.Write (new string (' ', OptionWidth - written)); + else { + o.WriteLine (); + o.Write (new string (' ', OptionWidth)); } + + WriteDescription (o, s.Description, new string (' ', OptionWidth+2), + Description_FirstWidth, Description_RemWidth); } + } - public bool WriteOptionPrototype (TextWriter o, Option p, ref int written, bool markdown = false) - { - string[] names = p.Names; - - int i = GetNextOptionIndex (names, 0); - if (i == names.Length) - return false; - - if (names [i].Length == 1) { - if (markdown) - Write(o, ref written, "`-"); - else - Write(o, ref written, " -"); - Write(o, ref written, names [0]); - } - else { - if (markdown) - Write(o, ref written, " `--"); - else - Write (o, ref written, " --"); - Write (o, ref written, names[0]); - } - - for ( i = GetNextOptionIndex (names, i+1); - i < names.Length; i = GetNextOptionIndex (names, i+1)) { - if (markdown) - Write(o, ref written, "`, `"); - else - Write(o, ref written, ", "); - Write (o, ref written, names [i].Length == 1 ? "-" : "--"); - Write (o, ref written, names [i]); - } + void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth) + { + bool indent = false; + foreach (string line in GetLines (localizer (GetDescription (value)), firstWidth, remWidth)) { + if (indent) + o.Write (prefix); + o.WriteLine (line); + indent = true; + } + } - if (p.OptionValueType == OptionValueType.Optional || - p.OptionValueType == OptionValueType.Required) { - if (p.OptionValueType == OptionValueType.Optional) { - Write (o, ref written, localizer ("[")); - } - Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description))); - string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 - ? p.ValueSeparators [0] - : " "; - for (int c = 1; c < p.MaxValueCount; ++c) { - Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description))); - } - if (p.OptionValueType == OptionValueType.Optional) { - Write (o, ref written, localizer ("]")); - } - } + public bool WriteOptionPrototype (TextWriter o, Option p, ref int written, bool markdown = false) + { + string[] names = p.Names; - if (markdown) - Write(o, ref written, "`"); + int i = GetNextOptionIndex (names, 0); + if (i == names.Length) + return false; - return true; + if (names [i].Length == 1) { + if (markdown) + Write(o, ref written, "`-"); + else + Write(o, ref written, " -"); + Write(o, ref written, names [0]); + } + else { + if (markdown) + Write(o, ref written, " `--"); + else + Write (o, ref written, " --"); + Write (o, ref written, names[0]); + } + + for ( i = GetNextOptionIndex (names, i+1); + i < names.Length; i = GetNextOptionIndex (names, i+1)) { + if (markdown) + Write(o, ref written, "`, `"); + else + Write(o, ref written, ", "); + Write (o, ref written, names [i].Length == 1 ? "-" : "--"); + Write (o, ref written, names [i]); } - static int GetNextOptionIndex (string[] names, int i) - { - while (i < names.Length && names [i] == "<>") { - ++i; + if (p.OptionValueType == OptionValueType.Optional || + p.OptionValueType == OptionValueType.Required) { + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("[")); + } + Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description))); + string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 + ? p.ValueSeparators [0] + : " "; + for (int c = 1; c < p.MaxValueCount; ++c) { + Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description))); + } + if (p.OptionValueType == OptionValueType.Optional) { + Write (o, ref written, localizer ("]")); } - return i; } - static void Write (TextWriter o, ref int n, string s) - { - n += s.Length; - o.Write (s); + if (markdown) + Write(o, ref written, "`"); + + return true; + } + + static int GetNextOptionIndex (string[] names, int i) + { + while (i < names.Length && names [i] == "<>") { + ++i; } + return i; + } - private static string GetArgumentName (int index, int maxIndex, string description) - { - if (description == null) - return maxIndex == 1 ? "VALUE" : index == 0 ? "NAME" : "VALUE"; - string[] nameStart; - if (maxIndex == 1) - nameStart = new string[]{"{0:", "{"}; - else - nameStart = new string[]{"{" + index + ":"}; - for (int i = 0; i < nameStart.Length; ++i) { - int start, j = 0; - do { - start = description.IndexOf (nameStart [i], j); - } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false); - if (start == -1) - continue; - int end = description.IndexOf ("}", start); - if (end == -1) - continue; - return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length); - } + static void Write (TextWriter o, ref int n, string s) + { + n += s.Length; + o.Write (s); + } + + private static string GetArgumentName (int index, int maxIndex, string description) + { + if (description == null) return maxIndex == 1 ? "VALUE" : index == 0 ? "NAME" : "VALUE"; - } + string[] nameStart; + if (maxIndex == 1) + nameStart = ["{0:", "{"]; + else + nameStart = ["{" + index + ":"]; + for (int i = 0; i < nameStart.Length; ++i) { + int start, j = 0; + do { + start = description.IndexOf (nameStart [i], j); + } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false); + if (start == -1) + continue; + int end = description.IndexOf ("}", start); + if (end == -1) + continue; + return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length); + } + return maxIndex == 1 ? "VALUE" : index == 0 ? "NAME" : "VALUE"; + } - private static string GetDescription (string description) - { - if (description == null) - return string.Empty; - StringBuilder sb = new StringBuilder (description.Length); - int start = -1; - for (int i = 0; i < description.Length; ++i) { - switch (description [i]) { - case '{': - if (i == start) { - sb.Append ('{'); - start = -1; - } - else if (start < 0) - start = i + 1; - break; - case '}': - if (start < 0) { - if ((i+1) == description.Length || description [i+1] != '}') - throw new InvalidOperationException ("Invalid option description: " + description); - ++i; - sb.Append ("}"); - } - else { - sb.Append (description.Substring (start, i - start)); - start = -1; - } - break; - case ':': - if (start < 0) - goto default; + private static string GetDescription (string description) + { + if (description == null) + return string.Empty; + StringBuilder sb = new StringBuilder (description.Length); + int start = -1; + for (int i = 0; i < description.Length; ++i) { + switch (description [i]) { + case '{': + if (i == start) { + sb.Append ('{'); + start = -1; + } + else if (start < 0) start = i + 1; - break; - default: - if (start < 0) - sb.Append (description [i]); - break; - } + break; + case '}': + if (start < 0) { + if ((i+1) == description.Length || description [i+1] != '}') + throw new InvalidOperationException ("Invalid option description: " + description); + ++i; + sb.Append ("}"); + } + else { + sb.Append (description.Substring (start, i - start)); + start = -1; + } + break; + case ':': + if (start < 0) + goto default; + start = i + 1; + break; + default: + if (start < 0) + sb.Append (description [i]); + break; } - return sb.ToString (); - } - - private static IEnumerable GetLines (string description, int firstWidth, int remWidth) - { - return StringCoda.WrappedLines (description, firstWidth, remWidth); } + return sb.ToString (); } -} + private static IEnumerable GetLines (string description, int firstWidth, int remWidth) + { + return StringCoda.WrappedLines (description, firstWidth, remWidth); + } +} \ No newline at end of file diff --git a/src/SeqCli/Config/ConnectionConfig.cs b/src/SeqCli/Config/ConnectionConfig.cs index 57b6fd50..821b6ad6 100644 --- a/src/SeqCli/Config/ConnectionConfig.cs +++ b/src/SeqCli/Config/ConnectionConfig.cs @@ -14,11 +14,12 @@ using System; using Newtonsoft.Json; +using SeqCli.Forwarder.Cryptography; using SeqCli.Util; namespace SeqCli.Config; -class ConnectionConfig +public class ConnectionConfig { const string ProtectedDataPrefix = "pd."; @@ -57,4 +58,13 @@ public string? ApiKey EncodedApiKey = value; } } + + public string? GetApiKey(IStringDataProtector dataProtector) + { + throw new NotImplementedException(); + } + + public uint? PooledConnectionLifetimeMilliseconds { get; set; } = null; + public ulong EventBodyLimitBytes { get; set; } = 256 * 1024; + public ulong PayloadLimitBytes { get; set; } = 10 * 1024 * 1024; } \ No newline at end of file diff --git a/src/SeqCli/Config/Forwarder/ForwarderApiConfig.cs b/src/SeqCli/Config/Forwarder/ForwarderApiConfig.cs index 321e8017..c0c27c19 100644 --- a/src/SeqCli/Config/Forwarder/ForwarderApiConfig.cs +++ b/src/SeqCli/Config/Forwarder/ForwarderApiConfig.cs @@ -1,6 +1,8 @@ -namespace SeqCli.Config; +namespace SeqCli.Config.Forwarder; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global class ForwarderApiConfig { - public string ListenUri { get; set; } = "http://localhost:15341"; + public string ListenUri { get; set; } = "http://127.0.0.1:15341"; } \ No newline at end of file diff --git a/src/SeqCli/Config/Forwarder/ForwarderConfig.cs b/src/SeqCli/Config/Forwarder/ForwarderConfig.cs index 621072cc..cbb2e16f 100644 --- a/src/SeqCli/Config/Forwarder/ForwarderConfig.cs +++ b/src/SeqCli/Config/Forwarder/ForwarderConfig.cs @@ -1,11 +1,9 @@ -namespace SeqCli.Config; +namespace SeqCli.Config.Forwarder; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global class ForwarderConfig { - public uint? PooledConnectionLifetimeMilliseconds { get; set; } = null; - public ulong EventBodyLimitBytes { get; set; } = 256 * 1024; - public ulong PayloadLimitBytes { get; set; } = 10 * 1024 * 1024; - public ForwarderStorageConfig Storage { get; set; } = new(); public ForwarderDiagnosticConfig Diagnostics { get; set; } = new(); public ForwarderApiConfig Api { get; set; } = new(); diff --git a/src/SeqCli/Config/Forwarder/ForwarderDiagnosticConfig.cs b/src/SeqCli/Config/Forwarder/ForwarderDiagnosticConfig.cs index 8366a6e4..3a63d685 100644 --- a/src/SeqCli/Config/Forwarder/ForwarderDiagnosticConfig.cs +++ b/src/SeqCli/Config/Forwarder/ForwarderDiagnosticConfig.cs @@ -1,8 +1,10 @@ using System; using System.IO; using Serilog.Events; +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global -namespace SeqCli.Config; +namespace SeqCli.Config.Forwarder; public class ForwarderDiagnosticConfig { diff --git a/src/SeqCli/Config/Forwarder/ForwarderStorageConfig.cs b/src/SeqCli/Config/Forwarder/ForwarderStorageConfig.cs index f2143eaa..5bc58044 100644 --- a/src/SeqCli/Config/Forwarder/ForwarderStorageConfig.cs +++ b/src/SeqCli/Config/Forwarder/ForwarderStorageConfig.cs @@ -1,6 +1,8 @@ -namespace SeqCli.Config; +namespace SeqCli.Config.Forwarder; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global public class ForwarderStorageConfig { - public int BufferSizeBytes { get; set; } = 67_108_864; + public ulong BufferSizeBytes { get; set; } = 67_108_864; } \ No newline at end of file diff --git a/src/SeqCli/Config/OutputConfig.cs b/src/SeqCli/Config/OutputConfig.cs index 8727c2b3..b52a73b6 100644 --- a/src/SeqCli/Config/OutputConfig.cs +++ b/src/SeqCli/Config/OutputConfig.cs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + namespace SeqCli.Config; public class OutputConfig diff --git a/src/SeqCli/Config/SeqCliConfig.cs b/src/SeqCli/Config/SeqCliConfig.cs index 8ff11a08..88ac1b08 100644 --- a/src/SeqCli/Config/SeqCliConfig.cs +++ b/src/SeqCli/Config/SeqCliConfig.cs @@ -18,6 +18,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; +using SeqCli.Config.Forwarder; +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global namespace SeqCli.Config; @@ -26,7 +29,7 @@ class SeqCliConfig static readonly string DefaultConfigFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "SeqCli.json"); - static JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings + static JsonSerializerSettings SerializerSettings { get; } = new() { ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = @@ -51,11 +54,10 @@ public static void Write(SeqCliConfig data) File.WriteAllText(DefaultConfigFilename, content); } - public ConnectionConfig Connection { get; set; } = new ConnectionConfig(); + public ConnectionConfig Connection { get; set; } = new(); public OutputConfig Output { get; set; } = new(); public ForwarderConfig Forwarder { get; set; } = new(); public SeqCliEncryptionProviderConfig EncryptionProviderProvider { get; set; } = new SeqCliEncryptionProviderConfig(); - public Dictionary Profiles { get; } = - new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Profiles { get; } = new(StringComparer.OrdinalIgnoreCase); } \ No newline at end of file diff --git a/src/SeqCli/Csv/CsvTokenizer.cs b/src/SeqCli/Csv/CsvTokenizer.cs index aca5796a..2f3529a4 100644 --- a/src/SeqCli/Csv/CsvTokenizer.cs +++ b/src/SeqCli/Csv/CsvTokenizer.cs @@ -42,7 +42,7 @@ protected override IEnumerable> Tokenize(TextSpan span) if (next.Value != '"') { - yield return Result.Empty(next.Location, new[] {"double-quote"}); + yield return Result.Empty(next.Location, ["double-quote"]); yield break; } @@ -79,13 +79,13 @@ protected override IEnumerable> Tokenize(TextSpan span) } else { - yield return Result.Empty(next.Location, new[] {"comma", "newline"}); + yield return Result.Empty(next.Location, ["comma", "newline"]); yield break; } } else { - yield return Result.Empty(next.Location, new[] {"double-quote"}); + yield return Result.Empty(next.Location, ["double-quote"]); yield break; } diff --git a/src/SeqCli/Forwarder/Config/SeqForwarderApiConfig.cs b/src/SeqCli/Forwarder/Config/SeqForwarderApiConfig.cs deleted file mode 100644 index d2e0aaaa..00000000 --- a/src/SeqCli/Forwarder/Config/SeqForwarderApiConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2016-2017 Datalust Pty Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Seq.Forwarder.Config -{ - class SeqForwarderApiConfig - { - public string ListenUri { get; set; } = "http://localhost:15341"; - } -} diff --git a/src/SeqCli/Forwarder/Config/SeqForwarderConfig.cs b/src/SeqCli/Forwarder/Config/SeqForwarderConfig.cs deleted file mode 100644 index d39abb59..00000000 --- a/src/SeqCli/Forwarder/Config/SeqForwarderConfig.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2016-2017 Datalust Pty Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; - -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global - -namespace Seq.Forwarder.Config -{ - class SeqForwarderConfig - { - static JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - Converters = - { - new StringEnumConverter() - } - }; - - public static SeqForwarderConfig ReadOrInit(string filename, bool includeEnvironmentVariables = true) - { - if (filename == null) throw new ArgumentNullException(nameof(filename)); - - if (!File.Exists(filename)) - { - var config = new SeqForwarderConfig(); - Write(filename, config); - return config; - } - - var content = File.ReadAllText(filename); - var combinedConfig = JsonConvert.DeserializeObject(content, SerializerSettings) - ?? throw new ArgumentException("Configuration content is null."); - - if (includeEnvironmentVariables) - { - // Any Environment Variables overwrite those in the Config File - var envVarConfig = new ConfigurationBuilder().AddEnvironmentVariables("FORWARDER_").Build(); - foreach (var sectionProperty in typeof(SeqForwarderConfig).GetTypeInfo().DeclaredProperties - .Where(p => p.GetMethod != null && p.GetMethod.IsPublic && !p.GetMethod.IsStatic)) - { - foreach (var subGroupProperty in sectionProperty.PropertyType.GetTypeInfo().DeclaredProperties - .Where(p => p.GetMethod != null && p.GetMethod.IsPublic && p.SetMethod != null && p.SetMethod.IsPublic && !p.GetMethod.IsStatic)) - { - var envVarName = sectionProperty.Name.ToUpper() + "_" + subGroupProperty.Name.ToUpper(); - var envVarVal = envVarConfig.GetValue(subGroupProperty.PropertyType, envVarName); - if (envVarVal != null) - { - subGroupProperty.SetValue(sectionProperty.GetValue(combinedConfig), envVarVal); - } - } - } - } - - return combinedConfig; - } - - public static void Write(string filename, SeqForwarderConfig data) - { - if (filename == null) throw new ArgumentNullException(nameof(filename)); - if (data == null) throw new ArgumentNullException(nameof(data)); - - var dir = Path.GetDirectoryName(filename); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir!); - - var content = JsonConvert.SerializeObject(data, Formatting.Indented, SerializerSettings); - File.WriteAllText(filename, content); - } - - public SeqForwarderDiagnosticConfig Diagnostics { get; set; } = new SeqForwarderDiagnosticConfig(); - public SeqForwarderOutputConfig Output { get; set; } = new SeqForwarderOutputConfig(); - public SeqForwarderStorageConfig Storage { get; set; } = new SeqForwarderStorageConfig(); - public SeqForwarderApiConfig Api { get; set; } = new SeqForwarderApiConfig(); - } -} diff --git a/src/SeqCli/Forwarder/Config/SeqForwarderDiagnosticConfig.cs b/src/SeqCli/Forwarder/Config/SeqForwarderDiagnosticConfig.cs deleted file mode 100644 index d1bca9f3..00000000 --- a/src/SeqCli/Forwarder/Config/SeqForwarderDiagnosticConfig.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2016-2017 Datalust Pty Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.IO; -using Serilog.Events; - -namespace Seq.Forwarder.Config -{ - public class SeqForwarderDiagnosticConfig - { - public string InternalLogPath { get; set; } = GetDefaultInternalLogPath(); - public LogEventLevel InternalLoggingLevel { get; set; } = LogEventLevel.Information; - public string? InternalLogServerUri { get; set; } - public string? InternalLogServerApiKey { get; set; } - public bool IngestionLogShowDetail { get; set; } - - public static string GetDefaultInternalLogPath() - { - return Path.Combine( -#if WINDOWS - // Common, here, because the service may run as Local Service, which has no obvious home - // directory. - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), -#else - // Specific to and writable by the current user. - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), -#endif - @"Seq", - "Logs"); - } - } -} diff --git a/src/SeqCli/Forwarder/Config/SeqForwarderOutputConfig.cs b/src/SeqCli/Forwarder/Config/SeqForwarderOutputConfig.cs deleted file mode 100644 index a48bdf76..00000000 --- a/src/SeqCli/Forwarder/Config/SeqForwarderOutputConfig.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright Datalust Pty Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using Newtonsoft.Json; -using Seq.Forwarder.Cryptography; - -// ReSharper disable UnusedMember.Global, AutoPropertyCanBeMadeGetOnly.Global - -namespace Seq.Forwarder.Config -{ - public class SeqForwarderOutputConfig - { - public string ServerUrl { get; set; } = "http://localhost:5341"; - public ulong EventBodyLimitBytes { get; set; } = 256 * 1024; - public ulong RawPayloadLimitBytes { get; set; } = 10 * 1024 * 1024; - public uint? PooledConnectionLifetimeMilliseconds { get; set; } = null; - - const string ProtectedDataPrefix = "pd."; - - public string? ApiKey { get; set; } - - public string? GetApiKey(IStringDataProtector dataProtector) - { - if (string.IsNullOrWhiteSpace(ApiKey)) - return null; - - if (!ApiKey.StartsWith(ProtectedDataPrefix)) - return ApiKey; - - return dataProtector.Unprotect(ApiKey.Substring(ProtectedDataPrefix.Length)); - } - - public void SetApiKey(string? apiKey, IStringDataProtector dataProtector) - { - if (string.IsNullOrWhiteSpace(apiKey)) - { - ApiKey = null; - return; - } - - ApiKey = $"{ProtectedDataPrefix}{dataProtector.Protect(apiKey)}"; - } - } -} diff --git a/src/SeqCli/Forwarder/Config/SeqForwarderStorageConfig.cs b/src/SeqCli/Forwarder/Config/SeqForwarderStorageConfig.cs deleted file mode 100644 index 2f713b7d..00000000 --- a/src/SeqCli/Forwarder/Config/SeqForwarderStorageConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2016-2017 Datalust Pty Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Seq.Forwarder.Config -{ - public class SeqForwarderStorageConfig - { - public ulong BufferSizeBytes { get; set; } = 64 * 1024 * 1024; - } -} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Cryptography/IStringDataProtector.cs b/src/SeqCli/Forwarder/Cryptography/IStringDataProtector.cs index 24ef61b0..cdc930c1 100644 --- a/src/SeqCli/Forwarder/Cryptography/IStringDataProtector.cs +++ b/src/SeqCli/Forwarder/Cryptography/IStringDataProtector.cs @@ -1,8 +1,7 @@ -namespace Seq.Forwarder.Cryptography +namespace SeqCli.Forwarder.Cryptography; + +public interface IStringDataProtector { - public interface IStringDataProtector - { - string Protect(string value); - string Unprotect(string @protected); - } -} + string Protect(string value); + string Unprotect(string @protected); +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Cryptography/StringDataProtector.cs b/src/SeqCli/Forwarder/Cryptography/StringDataProtector.cs index 64ef755c..e35ef0b7 100644 --- a/src/SeqCli/Forwarder/Cryptography/StringDataProtector.cs +++ b/src/SeqCli/Forwarder/Cryptography/StringDataProtector.cs @@ -1,14 +1,13 @@ -namespace Seq.Forwarder.Cryptography +namespace SeqCli.Forwarder.Cryptography; + +static class StringDataProtector { - static class StringDataProtector + public static IStringDataProtector CreatePlatformDefault() { - public static IStringDataProtector CreatePlatformDefault() - { #if WINDOWS return new DpapiMachineScopeDataProtect(); #else - return new UnprotectedStringData(); + return new UnprotectedStringData(); #endif - } } } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Cryptography/UnprotectedStringData.cs b/src/SeqCli/Forwarder/Cryptography/UnprotectedStringData.cs index b5213375..6148081e 100644 --- a/src/SeqCli/Forwarder/Cryptography/UnprotectedStringData.cs +++ b/src/SeqCli/Forwarder/Cryptography/UnprotectedStringData.cs @@ -2,20 +2,19 @@ using Serilog; -namespace Seq.Forwarder.Cryptography +namespace SeqCli.Forwarder.Cryptography; + +public class UnprotectedStringData : IStringDataProtector { - public class UnprotectedStringData : IStringDataProtector + public string Protect(string value) { - public string Protect(string value) - { - Log.Warning("Data protection is not available on this platform; sensitive values will be stored in plain text"); - return value; - } + Log.Warning("Data protection is not available on this platform; sensitive values will be stored in plain text"); + return value; + } - public string Unprotect(string @protected) - { - return @protected; - } + public string Unprotect(string @protected) + { + return @protected; } } diff --git a/src/SeqCli/Forwarder/Diagnostics/InMemorySink.cs b/src/SeqCli/Forwarder/Diagnostics/InMemorySink.cs index 00797ec0..b5eea21f 100644 --- a/src/SeqCli/Forwarder/Diagnostics/InMemorySink.cs +++ b/src/SeqCli/Forwarder/Diagnostics/InMemorySink.cs @@ -18,32 +18,31 @@ using Serilog.Core; using Serilog.Events; -namespace Seq.Forwarder.Diagnostics +namespace SeqCli.Forwarder.Diagnostics; + +public class InMemorySink : ILogEventSink { - public class InMemorySink : ILogEventSink - { - readonly int _queueLength; - readonly ConcurrentQueue _queue = new ConcurrentQueue(); + readonly int _queueLength; + readonly ConcurrentQueue _queue = new(); - public InMemorySink(int queueLength) - { - _queueLength = queueLength; - } + public InMemorySink(int queueLength) + { + _queueLength = queueLength; + } - public IEnumerable Read() - { - return _queue.ToArray(); - } + public IEnumerable Read() + { + return _queue.ToArray(); + } - public void Emit(LogEvent logEvent) - { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - _queue.Enqueue(logEvent); + public void Emit(LogEvent logEvent) + { + if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + _queue.Enqueue(logEvent); - while (_queue.Count > _queueLength) - { - _queue.TryDequeue(out _); - } + while (_queue.Count > _queueLength) + { + _queue.TryDequeue(out _); } } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Diagnostics/IngestionLog.cs b/src/SeqCli/Forwarder/Diagnostics/IngestionLog.cs index 52e5af13..5e54a5bf 100644 --- a/src/SeqCli/Forwarder/Diagnostics/IngestionLog.cs +++ b/src/SeqCli/Forwarder/Diagnostics/IngestionLog.cs @@ -18,48 +18,47 @@ using Serilog; using Serilog.Events; -namespace Seq.Forwarder.Diagnostics +namespace SeqCli.Forwarder.Diagnostics; + +static class IngestionLog { - static class IngestionLog - { - const int Capacity = 100; + const int Capacity = 100; - static readonly InMemorySink Sink = new InMemorySink(Capacity); + static readonly InMemorySink Sink = new(Capacity); - public static ILogger Log { get; } + public static ILogger Log { get; } - static IngestionLog() - { - Log = new LoggerConfiguration() - .MinimumLevel.Verbose() - .WriteTo.Sink(Sink) - .WriteTo.Logger(Serilog.Log.Logger) - .CreateLogger(); - } + static IngestionLog() + { + Log = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Sink(Sink) + .WriteTo.Logger(Serilog.Log.Logger) + .CreateLogger(); + } - public static IEnumerable Read() - { - return Sink.Read(); - } + public static IEnumerable Read() + { + return Sink.Read(); + } - public static ILogger ForClient(IPAddress clientHostIP) - { - return Log.ForContext("ClientHostIP", clientHostIP); - } + public static ILogger ForClient(IPAddress clientHostIP) + { + return Log.ForContext("ClientHostIP", clientHostIP); + } - public static ILogger ForPayload(IPAddress clientHostIP, string payload) - { - var prefix = CapturePrefix(payload); - return ForClient(clientHostIP) - .ForContext("StartToLog", prefix.Length) - .ForContext("DocumentStart", prefix); - } + public static ILogger ForPayload(IPAddress clientHostIP, string payload) + { + var prefix = CapturePrefix(payload); + return ForClient(clientHostIP) + .ForContext("StartToLog", prefix.Length) + .ForContext("DocumentStart", prefix); + } - static string CapturePrefix(string line) - { - if (line == null) throw new ArgumentNullException(nameof(line)); - var startToLog = Math.Min(line.Length, 1024); - return line.Substring(0, startToLog); - } + static string CapturePrefix(string line) + { + if (line == null) throw new ArgumentNullException(nameof(line)); + var startToLog = Math.Min(line.Length, 1024); + return line.Substring(0, startToLog); } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/ForwarderModule.cs b/src/SeqCli/Forwarder/ForwarderModule.cs new file mode 100644 index 00000000..2a5005ab --- /dev/null +++ b/src/SeqCli/Forwarder/ForwarderModule.cs @@ -0,0 +1,84 @@ +// Copyright Datalust Pty Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Net.Http; +using System.Threading; +using Autofac; +using SeqCli.Config; +using SeqCli.Forwarder.Cryptography; +using SeqCli.Forwarder.Multiplexing; +using SeqCli.Forwarder.Web.Host; + +namespace SeqCli.Forwarder; + +class ForwarderModule : Module +{ + readonly string _bufferPath; + readonly SeqCliConfig _config; + + public ForwarderModule(string bufferPath, SeqCliConfig config) + { + _bufferPath = bufferPath ?? throw new ArgumentNullException(nameof(bufferPath)); + _config = config ?? throw new ArgumentNullException(nameof(config)); + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().SingleInstance(); + builder.RegisterType() + .WithParameter("bufferPath", _bufferPath) + .SingleInstance(); + + builder.RegisterType().As(); + builder.RegisterType().SingleInstance(); + + builder.Register(c => + { + var outputConfig = c.Resolve(); + var baseUri = outputConfig.ServerUrl; + if (string.IsNullOrWhiteSpace(baseUri)) + throw new ArgumentException("The destination Seq server URL must be configured in SeqForwarder.json."); + + if (!baseUri.EndsWith("/")) + baseUri += "/"; + + // additional configuration options that require the use of SocketsHttpHandler should be added to + // this expression, using an "or" operator. + + var hasSocketHandlerOption = + outputConfig.PooledConnectionLifetimeMilliseconds.HasValue; + + if (hasSocketHandlerOption) + { + var httpMessageHandler = new SocketsHttpHandler + { + PooledConnectionLifetime = outputConfig.PooledConnectionLifetimeMilliseconds.HasValue ? TimeSpan.FromMilliseconds(outputConfig.PooledConnectionLifetimeMilliseconds.Value) : Timeout.InfiniteTimeSpan, + }; + + return new HttpClient(httpMessageHandler) { BaseAddress = new Uri(baseUri) }; + } + + return new HttpClient { BaseAddress = new Uri(baseUri) }; + + }).SingleInstance(); + + builder.RegisterInstance(StringDataProtector.CreatePlatformDefault()); + + builder.RegisterInstance(_config); + builder.RegisterInstance(_config.Forwarder.Api); + builder.RegisterInstance(_config.Forwarder.Diagnostics); + builder.RegisterInstance(_config.Forwarder.Storage); + } +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Multiplexing/ActiveLogBuffer.cs b/src/SeqCli/Forwarder/Multiplexing/ActiveLogBuffer.cs index 637f4c09..52fd743b 100644 --- a/src/SeqCli/Forwarder/Multiplexing/ActiveLogBuffer.cs +++ b/src/SeqCli/Forwarder/Multiplexing/ActiveLogBuffer.cs @@ -13,26 +13,25 @@ // limitations under the License. using System; -using Seq.Forwarder.Shipper; -using Seq.Forwarder.Storage; +using SeqCli.Forwarder.Shipper; +using SeqCli.Forwarder.Storage; -namespace Seq.Forwarder.Multiplexing +namespace SeqCli.Forwarder.Multiplexing; + +sealed class ActiveLogBuffer : IDisposable { - sealed class ActiveLogBuffer : IDisposable - { - public LogShipper Shipper { get; } - public LogBuffer Buffer { get; } + public LogShipper Shipper { get; } + public LogBuffer Buffer { get; } - public ActiveLogBuffer(LogBuffer logBuffer, LogShipper logShipper) - { - Buffer = logBuffer ?? throw new ArgumentNullException(nameof(logBuffer)); - Shipper = logShipper ?? throw new ArgumentNullException(nameof(logShipper)); - } + public ActiveLogBuffer(LogBuffer logBuffer, LogShipper logShipper) + { + Buffer = logBuffer ?? throw new ArgumentNullException(nameof(logBuffer)); + Shipper = logShipper ?? throw new ArgumentNullException(nameof(logShipper)); + } - public void Dispose() - { - Shipper.Dispose(); - Buffer.Dispose(); - } + public void Dispose() + { + Shipper.Dispose(); + Buffer.Dispose(); } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Multiplexing/ActiveLogBufferMap.cs b/src/SeqCli/Forwarder/Multiplexing/ActiveLogBufferMap.cs index 13779570..bcd09c65 100644 --- a/src/SeqCli/Forwarder/Multiplexing/ActiveLogBufferMap.cs +++ b/src/SeqCli/Forwarder/Multiplexing/ActiveLogBufferMap.cs @@ -16,220 +16,207 @@ using System.Collections.Generic; using System.IO; using System.Net; -using Seq.Forwarder.Config; -using Seq.Forwarder.Cryptography; -using Seq.Forwarder.Storage; -using Seq.Forwarder.Web; +using SeqCli.Config; +using SeqCli.Config.Forwarder; +using SeqCli.Forwarder.Cryptography; +using SeqCli.Forwarder.Storage; +using SeqCli.Forwarder.Web; using Serilog; -namespace Seq.Forwarder.Multiplexing +namespace SeqCli.Forwarder.Multiplexing; + +public class ActiveLogBufferMap : IDisposable { - public class ActiveLogBufferMap : IDisposable + const string DataFileName = "data.mdb", LockFileName = "lock.mdb", ApiKeyFileName = ".apikey"; + + readonly ulong _bufferSizeBytes; + readonly ConnectionConfig _connectionConfig; + readonly ILogShipperFactory _shipperFactory; + readonly IStringDataProtector _dataProtector; + readonly string _bufferPath; + readonly ILogger _log = Log.ForContext(); + + readonly object _sync = new(); + bool _loaded; + ActiveLogBuffer? _noApiKeyLogBuffer; + readonly Dictionary _buffersByApiKey = new(); + + public ActiveLogBufferMap( + string bufferPath, + ForwarderStorageConfig storageConfig, + ConnectionConfig outputConfig, + ILogShipperFactory logShipperFactory, + IStringDataProtector dataProtector) { - const string DataFileName = "data.mdb", LockFileName = "lock.mdb", ApiKeyFileName = ".apikey"; - - readonly ulong _bufferSizeBytes; - readonly SeqForwarderOutputConfig _outputConfig; - readonly ILogShipperFactory _shipperFactory; - readonly IStringDataProtector _dataProtector; - readonly string _bufferPath; - readonly ILogger _log = Log.ForContext(); - - readonly object _sync = new object(); - bool _loaded; - ActiveLogBuffer? _noApiKeyLogBuffer; - readonly Dictionary _buffersByApiKey = new Dictionary(); - - public ActiveLogBufferMap( - string bufferPath, - SeqForwarderStorageConfig storageConfig, - SeqForwarderOutputConfig outputConfig, - ILogShipperFactory logShipperFactory, - IStringDataProtector dataProtector) - { - _bufferSizeBytes = storageConfig.BufferSizeBytes; - _outputConfig = outputConfig ?? throw new ArgumentNullException(nameof(outputConfig)); - _shipperFactory = logShipperFactory ?? throw new ArgumentNullException(nameof(logShipperFactory)); - _dataProtector = dataProtector ?? throw new ArgumentNullException(nameof(dataProtector)); - _bufferPath = bufferPath ?? throw new ArgumentNullException(nameof(bufferPath)); - } + _bufferSizeBytes = storageConfig.BufferSizeBytes; + _connectionConfig = outputConfig ?? throw new ArgumentNullException(nameof(outputConfig)); + _shipperFactory = logShipperFactory ?? throw new ArgumentNullException(nameof(logShipperFactory)); + _dataProtector = dataProtector ?? throw new ArgumentNullException(nameof(dataProtector)); + _bufferPath = bufferPath ?? throw new ArgumentNullException(nameof(bufferPath)); + } - // The odd three-stage initialization improves our chances of correctly tearing down the `LightningEnvironment`s within - // `LogBuffer`s in the event of a failure during start-up. See: https://github.com/CoreyKaylor/Lightning.NET/blob/master/src/LightningDB/LightningEnvironment.cs#L252 - public void Load() - { - // At startup, we look for buffers and either delete them if they're empty, or load them - // up if they're not. This garbage collection at start-up is a simplification, - // we might try cleaning up in the background if the gains are worthwhile, although more synchronization - // would be required. + // The odd three-stage initialization improves our chances of correctly tearing down the `LightningEnvironment`s within + // `LogBuffer`s in the event of a failure during start-up. See: https://github.com/CoreyKaylor/Lightning.NET/blob/master/src/LightningDB/LightningEnvironment.cs#L252 + public void Load() + { + // At startup, we look for buffers and either delete them if they're empty, or load them + // up if they're not. This garbage collection at start-up is a simplification, + // we might try cleaning up in the background if the gains are worthwhile, although more synchronization + // would be required. - lock (_sync) - { - if (_loaded) throw new InvalidOperationException("The log buffer map is already loaded."); + lock (_sync) + { + if (_loaded) throw new InvalidOperationException("The log buffer map is already loaded."); - Directory.CreateDirectory(_bufferPath); + Directory.CreateDirectory(_bufferPath); - var defaultDataFilePath = Path.Combine(_bufferPath, DataFileName); - if (File.Exists(defaultDataFilePath)) + var defaultDataFilePath = Path.Combine(_bufferPath, DataFileName); + if (File.Exists(defaultDataFilePath)) + { + _log.Information("Loading the default log buffer in {Path}", _bufferPath); + var buffer = new LogBuffer(_bufferPath, _bufferSizeBytes); + if (buffer.Peek(0).Length == 0) { - _log.Information("Loading the default log buffer in {Path}", _bufferPath); - var buffer = new LogBuffer(_bufferPath, _bufferSizeBytes); - if (buffer.Peek(0).Length == 0) - { - _log.Information("The default buffer is empty and will be removed until more data is received"); - buffer.Dispose(); - File.Delete(defaultDataFilePath); - var lockFilePath = Path.Combine(_bufferPath, LockFileName); - if (File.Exists(lockFilePath)) - File.Delete(lockFilePath); - } - else - { - _noApiKeyLogBuffer = new ActiveLogBuffer(buffer, _shipperFactory.Create(buffer, _outputConfig.GetApiKey(_dataProtector))); - } + _log.Information("The default buffer is empty and will be removed until more data is received"); + buffer.Dispose(); + File.Delete(defaultDataFilePath); + var lockFilePath = Path.Combine(_bufferPath, LockFileName); + if (File.Exists(lockFilePath)) + File.Delete(lockFilePath); } - - foreach (var subfolder in Directory.GetDirectories(_bufferPath)) + else { - var encodedApiKeyFilePath = Path.Combine(subfolder, ApiKeyFileName); - if (!File.Exists(encodedApiKeyFilePath)) - { - _log.Information("Folder {Path} does not appear to be a log buffer; skipping", subfolder); - continue; - } - - _log.Information("Loading an API-key specific buffer in {Path}", subfolder); - var apiKey = _dataProtector.Unprotect(File.ReadAllText(encodedApiKeyFilePath)); - - var buffer = new LogBuffer(subfolder, _bufferSizeBytes); - if (buffer.Peek(0).Length == 0) - { - _log.Information("API key-specific buffer in {Path} is empty and will be removed until more data is received", subfolder); - buffer.Dispose(); - Directory.Delete(subfolder, true); - } - else - { - var activeBuffer = new ActiveLogBuffer(buffer, _shipperFactory.Create(buffer, apiKey)); - _buffersByApiKey.Add(apiKey, activeBuffer); - } + _noApiKeyLogBuffer = new ActiveLogBuffer(buffer, _shipperFactory.Create(buffer, _connectionConfig.GetApiKey(_dataProtector))); } - - _loaded = true; } - } - public void Start() - { - lock (_sync) + foreach (var subfolder in Directory.GetDirectories(_bufferPath)) { - if (!_loaded) throw new InvalidOperationException("The log buffer map is not loaded."); - - foreach (var buffer in OpenBuffers) + var encodedApiKeyFilePath = Path.Combine(subfolder, ApiKeyFileName); + if (!File.Exists(encodedApiKeyFilePath)) { - buffer.Shipper.Start(); + _log.Information("Folder {Path} does not appear to be a log buffer; skipping", subfolder); + continue; } - } - } - public void Stop() - { - lock (_sync) - { - // Hard to ensure _loaded is set in all cases, better here to be forgiving and - // permit a clean shut-down. + _log.Information("Loading an API-key specific buffer in {Path}", subfolder); + var apiKey = _dataProtector.Unprotect(File.ReadAllText(encodedApiKeyFilePath)); - foreach (var buffer in OpenBuffers) + var buffer = new LogBuffer(subfolder, _bufferSizeBytes); + if (buffer.Peek(0).Length == 0) { - buffer.Shipper.Stop(); + _log.Information("API key-specific buffer in {Path} is empty and will be removed until more data is received", subfolder); + buffer.Dispose(); + Directory.Delete(subfolder, true); + } + else + { + var activeBuffer = new ActiveLogBuffer(buffer, _shipperFactory.Create(buffer, apiKey)); + _buffersByApiKey.Add(apiKey, activeBuffer); } } + + _loaded = true; } + } - public LogBuffer GetLogBuffer(string? apiKey) + public void Start() + { + lock (_sync) { - lock (_sync) - { - if (!_loaded) throw new RequestProcessingException("The forwarder service is starting up.", HttpStatusCode.ServiceUnavailable); - - if (apiKey == null) - { - if (_noApiKeyLogBuffer == null) - { - _log.Information("Creating a new default log buffer in {Path}", _bufferPath); - var buffer = new LogBuffer(_bufferPath, _bufferSizeBytes); - _noApiKeyLogBuffer = new ActiveLogBuffer(buffer, _shipperFactory.Create(buffer, _outputConfig.GetApiKey(_dataProtector))); - _noApiKeyLogBuffer.Shipper.Start(); - } - return _noApiKeyLogBuffer.Buffer; - } + if (!_loaded) throw new InvalidOperationException("The log buffer map is not loaded."); - if (_buffersByApiKey.TryGetValue(apiKey, out var existing)) - return existing.Buffer; - - var subfolder = Path.Combine(_bufferPath, Guid.NewGuid().ToString("n")); - _log.Information("Creating a new API key-specific log buffer in {Path}", subfolder); - Directory.CreateDirectory(subfolder); - File.WriteAllText(Path.Combine(subfolder, ".apikey"), _dataProtector.Protect(apiKey)); - var newBuffer = new LogBuffer(subfolder, _bufferSizeBytes); - var newActiveBuffer = new ActiveLogBuffer(newBuffer, _shipperFactory.Create(newBuffer, apiKey)); - _buffersByApiKey.Add(apiKey, newActiveBuffer); - newActiveBuffer.Shipper.Start(); - return newBuffer; + foreach (var buffer in OpenBuffers) + { + buffer.Shipper.Start(); } } + } - public void Dispose() + public void Stop() + { + lock (_sync) { - lock (_sync) + // Hard to ensure _loaded is set in all cases, better here to be forgiving and + // permit a clean shut-down. + + foreach (var buffer in OpenBuffers) { - foreach (var buffer in OpenBuffers) - { - buffer.Dispose(); - } + buffer.Shipper.Stop(); } } + } - public void Enumerate(Action action) + public LogBuffer GetLogBuffer(string? apiKey) + { + lock (_sync) { - if (action == null) throw new ArgumentNullException(nameof(action)); + if (!_loaded) throw new RequestProcessingException("The forwarder service is starting up.", HttpStatusCode.ServiceUnavailable); - lock (_sync) + if (apiKey == null) { - foreach (var buffer in OpenBuffers) + if (_noApiKeyLogBuffer == null) { - buffer.Buffer.Enumerate(action); + _log.Information("Creating a new default log buffer in {Path}", _bufferPath); + var buffer = new LogBuffer(_bufferPath, _bufferSizeBytes); + _noApiKeyLogBuffer = new ActiveLogBuffer(buffer, _shipperFactory.Create(buffer, _connectionConfig.GetApiKey(_dataProtector))); + _noApiKeyLogBuffer.Shipper.Start(); } + return _noApiKeyLogBuffer.Buffer; } + + if (_buffersByApiKey.TryGetValue(apiKey, out var existing)) + return existing.Buffer; + + var subfolder = Path.Combine(_bufferPath, Guid.NewGuid().ToString("n")); + _log.Information("Creating a new API key-specific log buffer in {Path}", subfolder); + Directory.CreateDirectory(subfolder); + File.WriteAllText(Path.Combine(subfolder, ".apikey"), _dataProtector.Protect(apiKey)); + var newBuffer = new LogBuffer(subfolder, _bufferSizeBytes); + var newActiveBuffer = new ActiveLogBuffer(newBuffer, _shipperFactory.Create(newBuffer, apiKey)); + _buffersByApiKey.Add(apiKey, newActiveBuffer); + newActiveBuffer.Shipper.Start(); + return newBuffer; } + } - public static void Truncate(string bufferPath) + public void Dispose() + { + lock (_sync) { - DeleteIfExists(Path.Combine(bufferPath, DataFileName)); - DeleteIfExists(Path.Combine(bufferPath, LockFileName)); - foreach (var subdirectory in Directory.GetDirectories(bufferPath)) + foreach (var buffer in OpenBuffers) { - if (File.Exists(Path.Combine(subdirectory, ApiKeyFileName))) - Directory.Delete(subdirectory, true); + buffer.Dispose(); } } + } - static void DeleteIfExists(string filePath) + public static void Truncate(string bufferPath) + { + DeleteIfExists(Path.Combine(bufferPath, DataFileName)); + DeleteIfExists(Path.Combine(bufferPath, LockFileName)); + foreach (var subdirectory in Directory.GetDirectories(bufferPath)) { - if (File.Exists(filePath)) - File.Delete(filePath); + if (File.Exists(Path.Combine(subdirectory, ApiKeyFileName))) + Directory.Delete(subdirectory, true); } + } - IEnumerable OpenBuffers + static void DeleteIfExists(string filePath) + { + if (File.Exists(filePath)) + File.Delete(filePath); + } + + IEnumerable OpenBuffers + { + get { - get - { - if (_noApiKeyLogBuffer != null) - yield return _noApiKeyLogBuffer; + if (_noApiKeyLogBuffer != null) + yield return _noApiKeyLogBuffer; - foreach (var buffer in _buffersByApiKey.Values) - yield return buffer; - } + foreach (var buffer in _buffersByApiKey.Values) + yield return buffer; } } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Multiplexing/HttpLogShipperFactory.cs b/src/SeqCli/Forwarder/Multiplexing/HttpLogShipperFactory.cs index 4de973ad..7c95215d 100644 --- a/src/SeqCli/Forwarder/Multiplexing/HttpLogShipperFactory.cs +++ b/src/SeqCli/Forwarder/Multiplexing/HttpLogShipperFactory.cs @@ -14,28 +14,27 @@ using System; using System.Net.Http; -using Seq.Forwarder.Config; -using Seq.Forwarder.Shipper; -using Seq.Forwarder.Storage; +using SeqCli.Config; +using SeqCli.Forwarder.Shipper; +using SeqCli.Forwarder.Storage; -namespace Seq.Forwarder.Multiplexing +namespace SeqCli.Forwarder.Multiplexing; + +class HttpLogShipperFactory : ILogShipperFactory { - class HttpLogShipperFactory : ILogShipperFactory - { - readonly HttpClient _outputHttpClient; - readonly ServerResponseProxy _serverResponseProxy; - readonly SeqForwarderOutputConfig _outputConfig; + readonly HttpClient _outputHttpClient; + readonly ServerResponseProxy _serverResponseProxy; + readonly ConnectionConfig _outputConfig; - public HttpLogShipperFactory(ServerResponseProxy serverResponseProxy, SeqForwarderOutputConfig outputConfig, HttpClient outputHttpClient) - { - _outputHttpClient = outputHttpClient; - _serverResponseProxy = serverResponseProxy ?? throw new ArgumentNullException(nameof(serverResponseProxy)); - _outputConfig = outputConfig ?? throw new ArgumentNullException(nameof(outputConfig)); - } + public HttpLogShipperFactory(ServerResponseProxy serverResponseProxy, ConnectionConfig outputConfig, HttpClient outputHttpClient) + { + _outputHttpClient = outputHttpClient; + _serverResponseProxy = serverResponseProxy ?? throw new ArgumentNullException(nameof(serverResponseProxy)); + _outputConfig = outputConfig ?? throw new ArgumentNullException(nameof(outputConfig)); + } - public LogShipper Create(LogBuffer logBuffer, string? apiKey) - { - return new HttpLogShipper(logBuffer, apiKey, _outputConfig, _serverResponseProxy, _outputHttpClient); - } + public LogShipper Create(LogBuffer logBuffer, string? apiKey) + { + return new HttpLogShipper(logBuffer, apiKey, _outputConfig, _serverResponseProxy, _outputHttpClient); } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Multiplexing/ILogShipperFactory.cs b/src/SeqCli/Forwarder/Multiplexing/ILogShipperFactory.cs index 554324de..773f455f 100644 --- a/src/SeqCli/Forwarder/Multiplexing/ILogShipperFactory.cs +++ b/src/SeqCli/Forwarder/Multiplexing/ILogShipperFactory.cs @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Seq.Forwarder.Shipper; -using Seq.Forwarder.Storage; +using SeqCli.Forwarder.Shipper; +using SeqCli.Forwarder.Storage; -namespace Seq.Forwarder.Multiplexing +namespace SeqCli.Forwarder.Multiplexing; + +public interface ILogShipperFactory { - public interface ILogShipperFactory - { - LogShipper Create(LogBuffer logBuffer, string? apiKey); - } -} + LogShipper Create(LogBuffer logBuffer, string? apiKey); +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Multiplexing/InertLogShipperFactory.cs b/src/SeqCli/Forwarder/Multiplexing/InertLogShipperFactory.cs index f0dd9e44..b6fff878 100644 --- a/src/SeqCli/Forwarder/Multiplexing/InertLogShipperFactory.cs +++ b/src/SeqCli/Forwarder/Multiplexing/InertLogShipperFactory.cs @@ -12,16 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Seq.Forwarder.Shipper; -using Seq.Forwarder.Storage; +using SeqCli.Forwarder.Shipper; +using SeqCli.Forwarder.Storage; -namespace Seq.Forwarder.Multiplexing +namespace SeqCli.Forwarder.Multiplexing; + +class InertLogShipperFactory : ILogShipperFactory { - class InertLogShipperFactory : ILogShipperFactory + public LogShipper Create(LogBuffer logBuffer, string? apiKey) { - public LogShipper Create(LogBuffer logBuffer, string? apiKey) - { - return new InertLogShipper(); - } + return new InertLogShipper(); } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Multiplexing/ServerResponseProxy.cs b/src/SeqCli/Forwarder/Multiplexing/ServerResponseProxy.cs index 86ccc768..b52dc988 100644 --- a/src/SeqCli/Forwarder/Multiplexing/ServerResponseProxy.cs +++ b/src/SeqCli/Forwarder/Multiplexing/ServerResponseProxy.cs @@ -14,39 +14,38 @@ using System.Collections.Generic; -namespace Seq.Forwarder.Multiplexing +namespace SeqCli.Forwarder.Multiplexing; + +public class ServerResponseProxy { - public class ServerResponseProxy - { - const string EmptyResponse = "{}"; + const string EmptyResponse = "{}"; - readonly object _syncRoot = new object(); - readonly Dictionary _lastResponseByApiKey = new Dictionary(); - string _lastNoApiKeyResponse = EmptyResponse; + readonly object _syncRoot = new(); + readonly Dictionary _lastResponseByApiKey = new(); + string _lastNoApiKeyResponse = EmptyResponse; - public void SuccessResponseReturned(string? apiKey, string response) + public void SuccessResponseReturned(string? apiKey, string response) + { + lock (_syncRoot) { - lock (_syncRoot) - { - if (apiKey == null) - _lastNoApiKeyResponse = response; - else - _lastResponseByApiKey[apiKey] = response; - } + if (apiKey == null) + _lastNoApiKeyResponse = response; + else + _lastResponseByApiKey[apiKey] = response; } + } - public string GetResponseText(string? apiKey) + public string GetResponseText(string? apiKey) + { + lock (_syncRoot) { - lock (_syncRoot) - { - if (apiKey == null) - return _lastNoApiKeyResponse; + if (apiKey == null) + return _lastNoApiKeyResponse; - if (_lastResponseByApiKey.TryGetValue(apiKey, out var response)) - return response; + if (_lastResponseByApiKey.TryGetValue(apiKey, out var response)) + return response; - return EmptyResponse; - } + return EmptyResponse; } } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Properties/AssemblyInfo.cs b/src/SeqCli/Forwarder/Properties/AssemblyInfo.cs deleted file mode 100644 index da681b10..00000000 --- a/src/SeqCli/Forwarder/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Seq.Forwarder.Tests")] diff --git a/src/SeqCli/Forwarder/Schema/EventSchema.cs b/src/SeqCli/Forwarder/Schema/EventSchema.cs index aa6c6f6c..c13f0325 100644 --- a/src/SeqCli/Forwarder/Schema/EventSchema.cs +++ b/src/SeqCli/Forwarder/Schema/EventSchema.cs @@ -4,184 +4,180 @@ using System.Globalization; using System.Linq; using Newtonsoft.Json.Linq; +using SeqCli.Forwarder.Util; using Serilog.Parsing; -using Seq.Forwarder.Util; -namespace Seq.Forwarder.Schema +namespace SeqCli.Forwarder.Schema; + +static class EventSchema { - static class EventSchema - { - static readonly MessageTemplateParser MessageTemplateParser = new MessageTemplateParser(); + static readonly MessageTemplateParser MessageTemplateParser = new(); - static readonly HashSet ClefReifiedProperties = new HashSet - { - "@t", "@m", "@mt", "@l", "@x", "@i", "@r" - }; + static readonly HashSet ClefReifiedProperties = ["@t", "@m", "@mt", "@l", "@x", "@i", "@r"]; - public static bool FromClefFormat(in int lineNumber, JObject compactFormat, [MaybeNullWhen(false)] out JObject rawFormat, [MaybeNullWhen(true)] out string error) + public static bool FromClefFormat(in int lineNumber, JObject compactFormat, [MaybeNullWhen(false)] out JObject rawFormat, [MaybeNullWhen(true)] out string error) + { + var result = new JObject(); + + var rawTimestamp = compactFormat["@t"]; + if (rawTimestamp == null) { - var result = new JObject(); + error = $"The event on line {lineNumber} does not carry an `@t` timestamp property."; + rawFormat = default; + return false; + } - var rawTimestamp = compactFormat["@t"]; - if (rawTimestamp == null) - { - error = $"The event on line {lineNumber} does not carry an `@t` timestamp property."; - rawFormat = default; - return false; - } + if (rawTimestamp.Type != JTokenType.String) + { + error = $"The event on line {lineNumber} has an invalid `@t` timestamp property; the value must be a JSON string."; + rawFormat = default; + return false; + } + + if (!DateTimeOffset.TryParse(rawTimestamp.Value(), out _)) + { + error = $"The timestamp value `{rawTimestamp}` on line {lineNumber} could not be parsed."; + rawFormat = default; + return false; + } + + result.Add("Timestamp", rawTimestamp); + + var properties = new JObject(); + foreach (var property in compactFormat.Properties()) + { + if (property.Name.StartsWith("@@")) + properties.Add(property.Name.Substring(1), property.Value); + else if (!ClefReifiedProperties.Contains(property.Name)) + properties.Add(property.Name, property.Value); + } - if (rawTimestamp.Type != JTokenType.String) + var x = compactFormat["@x"]; + if (x != null) + { + if (x.Type != JTokenType.String) { - error = $"The event on line {lineNumber} has an invalid `@t` timestamp property; the value must be a JSON string."; + error = $"The event on line {lineNumber} has a non-string `@x` exception property."; rawFormat = default; return false; } - if (!DateTimeOffset.TryParse(rawTimestamp.Value(), out _)) + result.Add("Exception", x); + } + + var l = compactFormat["@l"]; + if (l != null) + { + if (l.Type != JTokenType.String) { - error = $"The timestamp value `{rawTimestamp}` on line {lineNumber} could not be parsed."; + error = $"The event on line {lineNumber} has a non-string `@l` level property."; rawFormat = default; return false; } - result.Add("Timestamp", rawTimestamp); + result.Add("Level", l); + } - var properties = new JObject(); - foreach (var property in compactFormat.Properties()) + string? message = null; + var m = compactFormat["@m"]; + if (m != null) + { + if (m.Type != JTokenType.String) { - if (property.Name.StartsWith("@@")) - properties.Add(property.Name.Substring(1), property.Value); - else if (!ClefReifiedProperties.Contains(property.Name)) - properties.Add(property.Name, property.Value); + error = $"The event on line {lineNumber} has a non-string `@m` message property."; + rawFormat = default; + return false; } - var x = compactFormat["@x"]; - if (x != null) - { - if (x.Type != JTokenType.String) - { - error = $"The event on line {lineNumber} has a non-string `@x` exception property."; - rawFormat = default; - return false; - } - - result.Add("Exception", x); - } + message = m.Value(); + } - var l = compactFormat["@l"]; - if (l != null) + string? messageTemplate = null; + var mt = compactFormat["@mt"]; + if (mt != null) + { + if (mt.Type != JTokenType.String) { - if (l.Type != JTokenType.String) - { - error = $"The event on line {lineNumber} has a non-string `@l` level property."; - rawFormat = default; - return false; - } - - result.Add("Level", l); + error = $"The event on line {lineNumber} has a non-string `@mt` message template property."; + rawFormat = default; + return false; } - string? message = null; - var m = compactFormat["@m"]; - if (m != null) - { - if (m.Type != JTokenType.String) - { - error = $"The event on line {lineNumber} has a non-string `@m` message property."; - rawFormat = default; - return false; - } + messageTemplate = mt.Value(); + } - message = m.Value(); - } + if (message != null) + { + result.Add("RenderedMessage", message); + } + else if (messageTemplate != null && compactFormat["@r"] is JArray renderingsArray) + { + var template = MessageTemplateParser.Parse(messageTemplate); + var withFormat = template.Tokens.OfType().Where(pt => pt.Format != null); - string? messageTemplate = null; - var mt = compactFormat["@mt"]; - if (mt != null) + // ReSharper disable once PossibleMultipleEnumeration + if (withFormat.Count() == renderingsArray.Count) { - if (mt.Type != JTokenType.String) - { - error = $"The event on line {lineNumber} has a non-string `@mt` message template property."; - rawFormat = default; - return false; - } + // ReSharper disable once PossibleMultipleEnumeration + var renderingsByProperty = withFormat + .Zip(renderingsArray, (p, j) => new { p.PropertyName, Format = p.Format!, Rendering = j.Value() }) + .GroupBy(p => p.PropertyName) + .ToDictionary(g => g.Key, g => g.ToDictionaryDistinct(p => p.Format, p => p.Rendering)); - messageTemplate = mt.Value(); - } + var renderings = new JObject(); + result.Add("Renderings", renderings); - if (message != null) - { - result.Add("RenderedMessage", message); - } - else if (messageTemplate != null && compactFormat["@r"] is JArray renderingsArray) - { - var template = MessageTemplateParser.Parse(messageTemplate); - var withFormat = template.Tokens.OfType().Where(pt => pt.Format != null); - - // ReSharper disable once PossibleMultipleEnumeration - if (withFormat.Count() == renderingsArray.Count) + foreach (var (property, propertyRenderings) in renderingsByProperty) { - // ReSharper disable once PossibleMultipleEnumeration - var renderingsByProperty = withFormat - .Zip(renderingsArray, (p, j) => new { p.PropertyName, Format = p.Format!, Rendering = j.Value() }) - .GroupBy(p => p.PropertyName) - .ToDictionary(g => g.Key, g => g.ToDictionaryDistinct(p => p.Format, p => p.Rendering)); - - var renderings = new JObject(); - result.Add("Renderings", renderings); + var byFormat = new JArray(); + renderings.Add(property, byFormat); - foreach (var (property, propertyRenderings) in renderingsByProperty) + foreach (var (format, rendering) in propertyRenderings) { - var byFormat = new JArray(); - renderings.Add(property, byFormat); - - foreach (var (format, rendering) in propertyRenderings) - { - var element = new JObject {{"Format", format}, {"Rendering", rendering}}; - byFormat.Add(element); - } + var element = new JObject {{"Format", format}, {"Rendering", rendering}}; + byFormat.Add(element); } } } + } - messageTemplate ??= message ?? "No template provided"; - result.Add("MessageTemplate", messageTemplate); + messageTemplate ??= message ?? "No template provided"; + result.Add("MessageTemplate", messageTemplate); - var eventTypeToken = compactFormat["@i"]; - if (eventTypeToken != null) + var eventTypeToken = compactFormat["@i"]; + if (eventTypeToken != null) + { + if (eventTypeToken.Type == JTokenType.Integer) { - if (eventTypeToken.Type == JTokenType.Integer) - { - result.Add("EventType", uint.Parse(eventTypeToken.Value()!)); - } - else if (eventTypeToken.Type == JTokenType.String) - { - if (uint.TryParse(eventTypeToken.Value(), NumberStyles.HexNumber, + result.Add("EventType", uint.Parse(eventTypeToken.Value()!)); + } + else if (eventTypeToken.Type == JTokenType.String) + { + if (uint.TryParse(eventTypeToken.Value(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var eventType)) - { - result.Add("EventType", eventType); - } - else - { - // Seq would calculate a hash value from the string, here. Forwarder will ignore that - // case and preserve the value in an `@i` property for now. - result.Add("@i", eventTypeToken); - } + { + result.Add("EventType", eventType); } else { - error = $"The `@i` event type value on line {lineNumber} is not in a string or numeric format."; - rawFormat = default; - return false; + // Seq would calculate a hash value from the string, here. Forwarder will ignore that + // case and preserve the value in an `@i` property for now. + result.Add("@i", eventTypeToken); } } + else + { + error = $"The `@i` event type value on line {lineNumber} is not in a string or numeric format."; + rawFormat = default; + return false; + } + } - if (properties.Count != 0) - result.Add("Properties", properties); + if (properties.Count != 0) + result.Add("Properties", properties); - rawFormat = result; - error = null; - return true; - } + rawFormat = result; + error = null; + return true; } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/SeqForwarderModule.cs b/src/SeqCli/Forwarder/SeqForwarderModule.cs deleted file mode 100644 index d8323510..00000000 --- a/src/SeqCli/Forwarder/SeqForwarderModule.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright Datalust Pty Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Net.Http; -using System.Threading; -using Autofac; -using Seq.Forwarder.Config; -using Seq.Forwarder.Cryptography; -using Seq.Forwarder.Multiplexing; -using Seq.Forwarder.Web.Host; - -namespace Seq.Forwarder -{ - class SeqForwarderModule : Module - { - readonly string _bufferPath; - readonly SeqForwarderConfig _config; - - public SeqForwarderModule(string bufferPath, SeqForwarderConfig config) - { - _bufferPath = bufferPath ?? throw new ArgumentNullException(nameof(bufferPath)); - _config = config ?? throw new ArgumentNullException(nameof(config)); - } - - protected override void Load(ContainerBuilder builder) - { - builder.RegisterType().SingleInstance(); - builder.RegisterType() - .WithParameter("bufferPath", _bufferPath) - .SingleInstance(); - - builder.RegisterType().As(); - builder.RegisterType().SingleInstance(); - - builder.Register(c => - { - var outputConfig = c.Resolve(); - var baseUri = outputConfig.ServerUrl; - if (string.IsNullOrWhiteSpace(baseUri)) - throw new ArgumentException("The destination Seq server URL must be configured in SeqForwarder.json."); - - if (!baseUri.EndsWith("/")) - baseUri += "/"; - - // additional configuration options that require the use of SocketsHttpHandler should be added to - // this expression, using an "or" operator. - - var hasSocketHandlerOption = - (outputConfig.PooledConnectionLifetimeMilliseconds.HasValue); - - if (hasSocketHandlerOption) - { - var httpMessageHandler = new SocketsHttpHandler() - { - PooledConnectionLifetime = (outputConfig.PooledConnectionLifetimeMilliseconds.HasValue) ? TimeSpan.FromMilliseconds(outputConfig.PooledConnectionLifetimeMilliseconds.Value) : Timeout.InfiniteTimeSpan, - }; - - return new HttpClient(httpMessageHandler) { BaseAddress = new Uri(baseUri) }; - } - - return new HttpClient() { BaseAddress = new Uri(baseUri) }; - - }).SingleInstance(); - - builder.RegisterInstance(StringDataProtector.CreatePlatformDefault()); - - builder.RegisterInstance(_config); - builder.RegisterInstance(_config.Api); - builder.RegisterInstance(_config.Diagnostics); - builder.RegisterInstance(_config.Output); - builder.RegisterInstance(_config.Storage); - } - } -} diff --git a/src/SeqCli/Forwarder/Shipper/ExponentialBackoffConnectionSchedule.cs b/src/SeqCli/Forwarder/Shipper/ExponentialBackoffConnectionSchedule.cs index 84c32f6c..9439f4ba 100644 --- a/src/SeqCli/Forwarder/Shipper/ExponentialBackoffConnectionSchedule.cs +++ b/src/SeqCli/Forwarder/Shipper/ExponentialBackoffConnectionSchedule.cs @@ -14,61 +14,60 @@ using System; -namespace Seq.Forwarder.Shipper +namespace SeqCli.Forwarder.Shipper; + +class ExponentialBackoffConnectionSchedule { - class ExponentialBackoffConnectionSchedule - { - static readonly TimeSpan MinimumBackoffPeriod = TimeSpan.FromSeconds(5); - static readonly TimeSpan MaximumBackoffInterval = TimeSpan.FromMinutes(10); + static readonly TimeSpan MinimumBackoffPeriod = TimeSpan.FromSeconds(5); + static readonly TimeSpan MaximumBackoffInterval = TimeSpan.FromMinutes(10); - readonly TimeSpan _period; + readonly TimeSpan _period; - int _failuresSinceSuccessfulConnection; + int _failuresSinceSuccessfulConnection; - public ExponentialBackoffConnectionSchedule(TimeSpan period) - { - if (period < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(period), "The connection retry period must be a positive timespan"); + public ExponentialBackoffConnectionSchedule(TimeSpan period) + { + if (period < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(period), "The connection retry period must be a positive timespan."); - _period = period; - } + _period = period; + } - public void MarkSuccess() - { - _failuresSinceSuccessfulConnection = 0; - } + public void MarkSuccess() + { + _failuresSinceSuccessfulConnection = 0; + } - public void MarkFailure() - { - ++_failuresSinceSuccessfulConnection; - } + public void MarkFailure() + { + ++_failuresSinceSuccessfulConnection; + } - public bool LastConnectionFailed => _failuresSinceSuccessfulConnection != 0; + public bool LastConnectionFailed => _failuresSinceSuccessfulConnection != 0; - public TimeSpan NextInterval + public TimeSpan NextInterval + { + get { - get - { - // Available, and first failure, just try the batch interval - if (_failuresSinceSuccessfulConnection <= 1) return _period; + // Available, and first failure, just try the batch interval + if (_failuresSinceSuccessfulConnection <= 1) return _period; - // Second failure, start ramping up the interval - first 2x, then 4x, ... - var backoffFactor = Math.Pow(2, (_failuresSinceSuccessfulConnection - 1)); + // Second failure, start ramping up the interval - first 2x, then 4x, ... + var backoffFactor = Math.Pow(2, (_failuresSinceSuccessfulConnection - 1)); - // If the period is ridiculously short, give it a boost so we get some - // visible backoff. - var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks); + // If the period is ridiculously short, give it a boost so we get some + // visible backoff. + var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks); - // The "ideal" interval - var backedOff = (long)(backoffPeriod * backoffFactor); + // The "ideal" interval + var backedOff = (long)(backoffPeriod * backoffFactor); - // Capped to the maximum interval - var cappedBackoff = Math.Min(MaximumBackoffInterval.Ticks, backedOff); + // Capped to the maximum interval + var cappedBackoff = Math.Min(MaximumBackoffInterval.Ticks, backedOff); - // Unless that's shorter than the base interval, in which case we'll just apply the period - var actual = Math.Max(_period.Ticks, cappedBackoff); + // Unless that's shorter than the base interval, in which case we'll just apply the period + var actual = Math.Max(_period.Ticks, cappedBackoff); - return TimeSpan.FromTicks(actual); - } + return TimeSpan.FromTicks(actual); } } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Shipper/HttpLogShipper.cs b/src/SeqCli/Forwarder/Shipper/HttpLogShipper.cs index 5ebe7eb1..f39159b1 100644 --- a/src/SeqCli/Forwarder/Shipper/HttpLogShipper.cs +++ b/src/SeqCli/Forwarder/Shipper/HttpLogShipper.cs @@ -19,233 +19,232 @@ using System.Net.Http.Headers; using System.Text; using System.Threading; -using Seq.Forwarder.Config; -using Seq.Forwarder.Storage; -using Serilog; using System.Threading.Tasks; -using Seq.Forwarder.Multiplexing; -using Seq.Forwarder.Util; +using SeqCli.Config; +using SeqCli.Forwarder.Multiplexing; +using SeqCli.Forwarder.Storage; +using SeqCli.Forwarder.Util; +using Serilog; + +namespace SeqCli.Forwarder.Shipper; -namespace Seq.Forwarder.Shipper +sealed class HttpLogShipper : LogShipper { - sealed class HttpLogShipper : LogShipper - { - const string BulkUploadResource = "api/events/raw"; + const string BulkUploadResource = "api/events/raw"; - readonly string? _apiKey; - readonly LogBuffer _logBuffer; - readonly SeqForwarderOutputConfig _outputConfig; - readonly HttpClient _httpClient; - readonly ExponentialBackoffConnectionSchedule _connectionSchedule; - readonly ServerResponseProxy _serverResponseProxy; - DateTime _nextRequiredLevelCheck; + readonly string? _apiKey; + readonly LogBuffer _logBuffer; + readonly ConnectionConfig _outputConfig; + readonly HttpClient _httpClient; + readonly ExponentialBackoffConnectionSchedule _connectionSchedule; + readonly ServerResponseProxy _serverResponseProxy; + DateTime _nextRequiredLevelCheck; - readonly object _stateLock = new object(); - readonly Timer _timer; - bool _started; + readonly object _stateLock = new(); + readonly Timer _timer; + bool _started; - volatile bool _unloading; + volatile bool _unloading; - static readonly TimeSpan QuietWaitPeriod = TimeSpan.FromSeconds(2), MaximumConnectionInterval = TimeSpan.FromMinutes(2); + static readonly TimeSpan QuietWaitPeriod = TimeSpan.FromSeconds(2), MaximumConnectionInterval = TimeSpan.FromMinutes(2); - public HttpLogShipper(LogBuffer logBuffer, string? apiKey, SeqForwarderOutputConfig outputConfig, ServerResponseProxy serverResponseProxy, HttpClient outputHttpClient) - { - _apiKey = apiKey; - _httpClient = outputHttpClient ?? throw new ArgumentNullException(nameof(outputHttpClient)); - _logBuffer = logBuffer ?? throw new ArgumentNullException(nameof(logBuffer)); - _outputConfig = outputConfig ?? throw new ArgumentNullException(nameof(outputConfig)); - _serverResponseProxy = serverResponseProxy ?? throw new ArgumentNullException(nameof(serverResponseProxy)); - _connectionSchedule = new ExponentialBackoffConnectionSchedule(QuietWaitPeriod); - _timer = new Timer(s => OnTick()); - } + public HttpLogShipper(LogBuffer logBuffer, string? apiKey, ConnectionConfig outputConfig, ServerResponseProxy serverResponseProxy, HttpClient outputHttpClient) + { + _apiKey = apiKey; + _httpClient = outputHttpClient ?? throw new ArgumentNullException(nameof(outputHttpClient)); + _logBuffer = logBuffer ?? throw new ArgumentNullException(nameof(logBuffer)); + _outputConfig = outputConfig ?? throw new ArgumentNullException(nameof(outputConfig)); + _serverResponseProxy = serverResponseProxy ?? throw new ArgumentNullException(nameof(serverResponseProxy)); + _connectionSchedule = new ExponentialBackoffConnectionSchedule(QuietWaitPeriod); + _timer = new Timer(_ => OnTick()); + } - public override void Start() + public override void Start() + { + lock (_stateLock) { - lock (_stateLock) - { - if (_started) - throw new InvalidOperationException("The shipper has already started."); + if (_started) + throw new InvalidOperationException("The shipper has already started."); - if (_unloading) - throw new InvalidOperationException("The shipper is unloading."); + if (_unloading) + throw new InvalidOperationException("The shipper is unloading."); - Log.Information("Log shipper started, events will be dispatched to {ServerUrl}", _outputConfig.ServerUrl); + Log.Information("Log shipper started, events will be dispatched to {ServerUrl}", _outputConfig.ServerUrl); - _nextRequiredLevelCheck = DateTime.UtcNow.Add(MaximumConnectionInterval); - _started = true; - SetTimer(); - } + _nextRequiredLevelCheck = DateTime.UtcNow.Add(MaximumConnectionInterval); + _started = true; + SetTimer(); } + } - public override void Stop() + public override void Stop() + { + lock (_stateLock) { - lock (_stateLock) - { - if (_unloading) - return; + if (_unloading) + return; - _unloading = true; + _unloading = true; - if (!_started) - return; - } - - var wh = new ManualResetEvent(false); - if (_timer.Dispose(wh)) - wh.WaitOne(); + if (!_started) + return; } + + var wh = new ManualResetEvent(false); + if (_timer.Dispose(wh)) + wh.WaitOne(); + } - public override void Dispose() - { - Stop(); - } + public override void Dispose() + { + Stop(); + } - void SetTimer() - { - _timer.Change(_connectionSchedule.NextInterval, Timeout.InfiniteTimeSpan); - } + void SetTimer() + { + _timer.Change(_connectionSchedule.NextInterval, Timeout.InfiniteTimeSpan); + } - void OnTick() - { - OnTickAsync().Wait(); - } + void OnTick() + { + OnTickAsync().Wait(); + } - async Task OnTickAsync() + async Task OnTickAsync() + { + try { - try + var sendingSingles = 0; + do { - var sendingSingles = 0; - do + var available = _logBuffer.Peek((int)_outputConfig.PayloadLimitBytes); + if (available.Length == 0) { - var available = _logBuffer.Peek((int)_outputConfig.RawPayloadLimitBytes); - if (available.Length == 0) + if (DateTime.UtcNow < _nextRequiredLevelCheck || _connectionSchedule.LastConnectionFailed) { - if (DateTime.UtcNow < _nextRequiredLevelCheck || _connectionSchedule.LastConnectionFailed) - { - // For whatever reason, there's nothing waiting to send. This means we should try connecting again at the - // regular interval, so mark the attempt as successful. - _connectionSchedule.MarkSuccess(); - break; - } + // For whatever reason, there's nothing waiting to send. This means we should try connecting again at the + // regular interval, so mark the attempt as successful. + _connectionSchedule.MarkSuccess(); + break; } + } - MakePayload(available, sendingSingles > 0, out Stream payload, out ulong lastIncluded); + MakePayload(available, sendingSingles > 0, out Stream payload, out ulong lastIncluded); - var content = new StreamContent(new UnclosableStreamWrapper(payload)); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json") - { - CharSet = Encoding.UTF8.WebName - }; + var content = new StreamContent(new UnclosableStreamWrapper(payload)); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json") + { + CharSet = Encoding.UTF8.WebName + }; - if (_apiKey != null) - { - content.Headers.Add(SeqApi.ApiKeyHeaderName, _apiKey); - } + if (_apiKey != null) + { + content.Headers.Add(SeqApi.ApiKeyHeaderName, _apiKey); + } - var result = await _httpClient.PostAsync(BulkUploadResource, content); - if (result.IsSuccessStatusCode) - { - _connectionSchedule.MarkSuccess(); - _logBuffer.Dequeue(lastIncluded); - if (sendingSingles > 0) - sendingSingles--; + var result = await _httpClient.PostAsync(BulkUploadResource, content); + if (result.IsSuccessStatusCode) + { + _connectionSchedule.MarkSuccess(); + _logBuffer.Dequeue(lastIncluded); + if (sendingSingles > 0) + sendingSingles--; - _serverResponseProxy.SuccessResponseReturned(_apiKey, await result.Content.ReadAsStringAsync()); - _nextRequiredLevelCheck = DateTime.UtcNow.Add(MaximumConnectionInterval); - } - else if (result.StatusCode == HttpStatusCode.BadRequest || - result.StatusCode == HttpStatusCode.RequestEntityTooLarge) - { - // The connection attempt was successful - the payload we sent was the problem. - _connectionSchedule.MarkSuccess(); + _serverResponseProxy.SuccessResponseReturned(_apiKey, await result.Content.ReadAsStringAsync()); + _nextRequiredLevelCheck = DateTime.UtcNow.Add(MaximumConnectionInterval); + } + else if (result.StatusCode == HttpStatusCode.BadRequest || + result.StatusCode == HttpStatusCode.RequestEntityTooLarge) + { + // The connection attempt was successful - the payload we sent was the problem. + _connectionSchedule.MarkSuccess(); - if (sendingSingles != 0) - { - payload.Position = 0; - var payloadText = await new StreamReader(payload, Encoding.UTF8).ReadToEndAsync(); - Log.Error("HTTP shipping failed with {StatusCode}: {Result}; payload was {InvalidPayload}", result.StatusCode, await result.Content.ReadAsStringAsync(), payloadText); - _logBuffer.Dequeue(lastIncluded); - sendingSingles = 0; - } - else - { - // Unscientific (should "binary search" in batches) but sending the next - // hundred events singly should flush out the problematic one. - sendingSingles = 100; - } + if (sendingSingles != 0) + { + payload.Position = 0; + var payloadText = await new StreamReader(payload, Encoding.UTF8).ReadToEndAsync(); + Log.Error("HTTP shipping failed with {StatusCode}: {Result}; payload was {InvalidPayload}", result.StatusCode, await result.Content.ReadAsStringAsync(), payloadText); + _logBuffer.Dequeue(lastIncluded); + sendingSingles = 0; } else { - _connectionSchedule.MarkFailure(); - Log.Error("Received failed HTTP shipping result {StatusCode}: {Result}", result.StatusCode, await result.Content.ReadAsStringAsync()); - break; + // Unscientific (should "binary search" in batches) but sending the next + // hundred events singly should flush out the problematic one. + sendingSingles = 100; } } - while (true); - } - catch (HttpRequestException hex) - { - Log.Warning(hex, "HTTP request failed when sending a batch from the log shipper"); - _connectionSchedule.MarkFailure(); - } - catch (Exception ex) - { - Log.Error(ex, "Exception while sending a batch from the log shipper"); - _connectionSchedule.MarkFailure(); - } - finally - { - lock (_stateLock) + else { - if (!_unloading) - SetTimer(); + _connectionSchedule.MarkFailure(); + Log.Error("Received failed HTTP shipping result {StatusCode}: {Result}", result.StatusCode, await result.Content.ReadAsStringAsync()); + break; } } + while (true); } - - void MakePayload(LogBufferEntry[] entries, bool oneOnly, out Stream utf8Payload, out ulong lastIncluded) + catch (HttpRequestException hex) { - if (entries == null) throw new ArgumentNullException(nameof(entries)); - lastIncluded = 0; - - var raw = new MemoryStream(); - var content = new StreamWriter(raw, Encoding.UTF8); - content.Write("{\"Events\":["); - content.Flush(); - var contentRemainingBytes = (int) _outputConfig.RawPayloadLimitBytes - 13; // Includes closing delims - - var delimStart = ""; - foreach (var logBufferEntry in entries) + Log.Warning(hex, "HTTP request failed when sending a batch from the log shipper"); + _connectionSchedule.MarkFailure(); + } + catch (Exception ex) + { + Log.Error(ex, "Exception while sending a batch from the log shipper"); + _connectionSchedule.MarkFailure(); + } + finally + { + lock (_stateLock) { - if ((ulong)logBufferEntry.Value.Length > _outputConfig.EventBodyLimitBytes) - { - Log.Information("Oversized event will be skipped, {Payload}", Encoding.UTF8.GetString(logBufferEntry.Value)); - lastIncluded = logBufferEntry.Key; - continue; - } - - // lastIncluded indicates we've added at least one event - if (lastIncluded != 0 && contentRemainingBytes - (delimStart.Length + logBufferEntry.Value.Length) < 0) - break; + if (!_unloading) + SetTimer(); + } + } + } - content.Write(delimStart); - content.Flush(); - contentRemainingBytes -= delimStart.Length; + void MakePayload(LogBufferEntry[] entries, bool oneOnly, out Stream utf8Payload, out ulong lastIncluded) + { + if (entries == null) throw new ArgumentNullException(nameof(entries)); + lastIncluded = 0; - raw.Write(logBufferEntry.Value, 0, logBufferEntry.Value.Length); - contentRemainingBytes -= logBufferEntry.Value.Length; + var raw = new MemoryStream(); + var content = new StreamWriter(raw, Encoding.UTF8); + content.Write("{\"Events\":["); + content.Flush(); + var contentRemainingBytes = (int) _outputConfig.PayloadLimitBytes - 13; // Includes closing delimiters + var delimStart = ""; + foreach (var logBufferEntry in entries) + { + if ((ulong)logBufferEntry.Value.Length > _outputConfig.EventBodyLimitBytes) + { + Log.Information("Oversize event will be skipped, {Payload}", Encoding.UTF8.GetString(logBufferEntry.Value)); lastIncluded = logBufferEntry.Key; - - delimStart = ","; - if (oneOnly) - break; + continue; } - content.Write("]}"); + // lastIncluded indicates we've added at least one event + if (lastIncluded != 0 && contentRemainingBytes - (delimStart.Length + logBufferEntry.Value.Length) < 0) + break; + + content.Write(delimStart); content.Flush(); - raw.Position = 0; - utf8Payload = raw; + contentRemainingBytes -= delimStart.Length; + + raw.Write(logBufferEntry.Value, 0, logBufferEntry.Value.Length); + contentRemainingBytes -= logBufferEntry.Value.Length; + + lastIncluded = logBufferEntry.Key; + + delimStart = ","; + if (oneOnly) + break; } + + content.Write("]}"); + content.Flush(); + raw.Position = 0; + utf8Payload = raw; } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Shipper/InertLogShipper.cs b/src/SeqCli/Forwarder/Shipper/InertLogShipper.cs index 164a2939..1ae106e3 100644 --- a/src/SeqCli/Forwarder/Shipper/InertLogShipper.cs +++ b/src/SeqCli/Forwarder/Shipper/InertLogShipper.cs @@ -12,20 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Seq.Forwarder.Shipper +namespace SeqCli.Forwarder.Shipper; + +class InertLogShipper : LogShipper { - class InertLogShipper : LogShipper - { - public override void Start() - { - } + public override void Start() + { + } - public override void Stop() - { - } + public override void Stop() + { + } - public override void Dispose() - { - } + public override void Dispose() + { } } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Shipper/LogShipper.cs b/src/SeqCli/Forwarder/Shipper/LogShipper.cs index ac8f5157..83e8beb3 100644 --- a/src/SeqCli/Forwarder/Shipper/LogShipper.cs +++ b/src/SeqCli/Forwarder/Shipper/LogShipper.cs @@ -14,12 +14,11 @@ using System; -namespace Seq.Forwarder.Shipper +namespace SeqCli.Forwarder.Shipper; + +public abstract class LogShipper : IDisposable { - public abstract class LogShipper : IDisposable - { - public abstract void Start(); - public abstract void Stop(); - public abstract void Dispose(); - } -} + public abstract void Start(); + public abstract void Stop(); + public abstract void Dispose(); +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Shipper/SeqApi.cs b/src/SeqCli/Forwarder/Shipper/SeqApi.cs index 330dc3b8..5e7c45e2 100644 --- a/src/SeqCli/Forwarder/Shipper/SeqApi.cs +++ b/src/SeqCli/Forwarder/Shipper/SeqApi.cs @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Seq.Forwarder.Shipper +namespace SeqCli.Forwarder.Shipper; + +static class SeqApi { - static class SeqApi - { - public const string ApiKeyHeaderName = "X-Seq-ApiKey"; - } + public const string ApiKeyHeaderName = "X-Seq-ApiKey"; } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Storage/LogBuffer.cs b/src/SeqCli/Forwarder/Storage/LogBuffer.cs index bac905ea..f58fe8db 100644 --- a/src/SeqCli/Forwarder/Storage/LogBuffer.cs +++ b/src/SeqCli/Forwarder/Storage/LogBuffer.cs @@ -16,265 +16,264 @@ using System.Collections.Generic; using Serilog; -namespace Seq.Forwarder.Storage +namespace SeqCli.Forwarder.Storage; + +public class LogBuffer : IDisposable { - public class LogBuffer : IDisposable + readonly ulong _bufferSizeBytes; + // readonly LightningEnvironment _env; + readonly object _sync = new(); + bool _isDisposed; + ulong _nextId = 0, _entries = 0, _writtenSinceRotateCheck; + + public LogBuffer(string bufferPath, ulong bufferSizeBytes) { - readonly ulong _bufferSizeBytes; - // readonly LightningEnvironment _env; - readonly object _sync = new object(); - bool _isDisposed; - ulong _nextId = 0, _entries = 0, _writtenSinceRotateCheck; + _bufferSizeBytes = bufferSizeBytes; + if (bufferPath == null) throw new ArgumentNullException(nameof(bufferPath)); + + // _env = new LightningEnvironment(bufferPath) + // { + // // Sparse; we'd hope fragmentation never gets this bad... + // MapSize = (long) bufferSizeBytes*10 + // }; + // + // _env.Open(); + // + // using (var tx = _env.BeginTransaction()) + // using (var db = tx.OpenDatabase()) + // { + // using (var cur = tx.CreateCursor(db)) + // { + // if (!cur.MoveToLast()) + // { + // _nextId = 1; + // } + // else + // { + // var current = cur.GetCurrent(); + // _nextId = ByteKeyToULongKey(current.Key) + 1; + // _entries = (ulong) tx.GetEntriesCount(db); + // } + // } + // } + + Log.Information("Log buffer open on {BufferPath}; {Entries} entries, next key will be {NextId}", bufferPath, _entries, _nextId); + } - public LogBuffer(string bufferPath, ulong bufferSizeBytes) + public void Dispose() + { + lock (_sync) { - _bufferSizeBytes = bufferSizeBytes; - if (bufferPath == null) throw new ArgumentNullException(nameof(bufferPath)); + if (!_isDisposed) + { + _isDisposed = true; + // _env.Dispose(); + } + } + } - // _env = new LightningEnvironment(bufferPath) - // { - // // Sparse; we'd hope fragmentation never gets this bad... - // MapSize = (long) bufferSizeBytes*10 - // }; - // - // _env.Open(); + public void Enqueue(byte[][] values) + { + if (values == null) throw new ArgumentNullException(nameof(values)); + + lock (_sync) + { + RequireNotDisposed(); + + // var totalPayloadWritten = 0UL; // // using (var tx = _env.BeginTransaction()) // using (var db = tx.OpenDatabase()) // { - // using (var cur = tx.CreateCursor(db)) + // foreach (var v in values) // { - // if (!cur.MoveToLast()) - // { - // _nextId = 1; - // } - // else - // { - // var current = cur.GetCurrent(); - // _nextId = ByteKeyToULongKey(current.Key) + 1; - // _entries = (ulong) tx.GetEntriesCount(db); - // } + // if (v == null) throw new ArgumentException("Value array may not contain null."); + // + // tx.Put(db, ULongKeyToByteKey(_nextId++), v); + // totalPayloadWritten += (ulong) v.Length; // } + // + // tx.Commit(); + // _entries += (ulong) values.Length; + // _writtenSinceRotateCheck += totalPayloadWritten; // } - - Log.Information("Log buffer open on {BufferPath}; {Entries} entries, next key will be {NextId}", bufferPath, _entries, _nextId); - } - public void Dispose() - { - lock (_sync) - { - if (!_isDisposed) - { - _isDisposed = true; - // _env.Dispose(); - } - } + RotateIfRequired(); } + } - public void Enqueue(byte[][] values) - { - if (values == null) throw new ArgumentNullException(nameof(values)); - - lock (_sync) - { - RequireNotDisposed(); - - // var totalPayloadWritten = 0UL; - // - // using (var tx = _env.BeginTransaction()) - // using (var db = tx.OpenDatabase()) - // { - // foreach (var v in values) - // { - // if (v == null) throw new ArgumentException("Value array may not contain null."); - // - // tx.Put(db, ULongKeyToByteKey(_nextId++), v); - // totalPayloadWritten += (ulong) v.Length; - // } - // - // tx.Commit(); - // _entries += (ulong) values.Length; - // _writtenSinceRotateCheck += totalPayloadWritten; - // } + void RotateIfRequired() + { + if (_writtenSinceRotateCheck < _bufferSizeBytes/10) + return; - RotateIfRequired(); - } - } + _writtenSinceRotateCheck = 0; + // + // using (var tx = _env.BeginTransaction()) + // using (var db = tx.OpenDatabase()) + // { + // int err; + // if (0 != (err = Lmdb.mdb_env_info(_env.Handle(), out var estat))) + // throw new Exception(Lmdb.mdb_strerror(err)); + // + // MDBStat stat; + // if (0 != (err = Lmdb.mdb_stat(tx.Handle(), db.Handle(), out stat))) + // throw new Exception(Lmdb.mdb_strerror(err)); + // + // // http://www.openldap.org/lists/openldap-technical/201303/msg00145.html + // // 1) MDB_stat gives you the page size. + // // 2) MDB_envinfo tells the mapsize and the last_pgno.If you divide mapsize + // // by pagesize you'll get max pgno. The MAP_FULL error is returned when last_pgno reaches max pgno. + // + // var targetPages = _bufferSizeBytes/stat.ms_psize; + // if ((ulong) estat.me_last_pgno < targetPages && (double) (ulong) estat.me_last_pgno/targetPages < 0.75) + // return; + // + // var count = tx.GetEntriesCount(db); + // if (count == 0) + // { + // Log.Warning("Attempting to rotate buffer but no events are present"); + // return; + // } + // + // var toPurge = Math.Max(count / 4, 1); + // Log.Warning("Buffer is full; dropping {ToPurge} events to make room for new ones", + // toPurge); + // + // using (var cur = tx.CreateCursor(db)) + // { + // cur.MoveToFirst(); + // + // for (var i = 0; i < toPurge; ++i) + // { + // cur.Delete(); + // cur.MoveNext(); + // } + // } + // + // tx.Commit(); + // } + } - void RotateIfRequired() + public LogBufferEntry[] Peek(int maxValueBytesHint) + { + lock (_sync) { - if (_writtenSinceRotateCheck < _bufferSizeBytes/10) - return; + RequireNotDisposed(); - _writtenSinceRotateCheck = 0; + var entries = new List(); // - // using (var tx = _env.BeginTransaction()) + // using (var tx = _env.BeginTransaction(TransactionBeginFlags.ReadOnly)) // using (var db = tx.OpenDatabase()) // { - // int err; - // if (0 != (err = Lmdb.mdb_env_info(_env.Handle(), out var estat))) - // throw new Exception(Lmdb.mdb_strerror(err)); + // using (var cur = tx.CreateCursor(db)) + // { + // if (cur.MoveToFirst()) + // { + // var entriesBytes = 0; // - // MDBStat stat; - // if (0 != (err = Lmdb.mdb_stat(tx.Handle(), db.Handle(), out stat))) - // throw new Exception(Lmdb.mdb_strerror(err)); + // do + // { + // var current = cur.GetCurrent(); + // var entry = new LogBufferEntry + // { + // Key = ByteKeyToULongKey(current.Key), + // Value = current.Value + // }; // - // // http://www.openldap.org/lists/openldap-technical/201303/msg00145.html - // // 1) MDB_stat gives you the page size. - // // 2) MDB_envinfo tells the mapsize and the last_pgno.If you divide mapsize - // // by pagesize you'll get max pgno. The MAP_FULL error is returned when last_pgno reaches max pgno. + // entriesBytes += entry.Value.Length; + // if (entries.Count != 0 && entriesBytes > maxValueBytesHint) + // break; // - // var targetPages = _bufferSizeBytes/stat.ms_psize; - // if ((ulong) estat.me_last_pgno < targetPages && (double) (ulong) estat.me_last_pgno/targetPages < 0.75) - // return; + // entries.Add(entry); // - // var count = tx.GetEntriesCount(db); - // if (count == 0) - // { - // Log.Warning("Attempting to rotate buffer but no events are present"); - // return; + // } while (cur.MoveNext()); + // } // } + // } + + return entries.ToArray(); + } + } + + public void Dequeue(ulong toKey) + { + lock (_sync) + { + RequireNotDisposed(); + + // ulong deleted = 0; // - // var toPurge = Math.Max(count / 4, 1); - // Log.Warning("Buffer is full; dropping {ToPurge} events to make room for new ones", - // toPurge); - // + // using (var tx = _env.BeginTransaction()) + // using (var db = tx.OpenDatabase()) + // { // using (var cur = tx.CreateCursor(db)) // { - // cur.MoveToFirst(); - // - // for (var i = 0; i < toPurge; ++i) + // if (cur.MoveToFirst()) // { - // cur.Delete(); - // cur.MoveNext(); + // do + // { + // var current = cur.GetCurrent(); + // if (ByteKeyToULongKey(current.Key) > toKey) + // break; + // + // cur.Delete(); + // deleted++; + // } while (cur.MoveNext()); // } // } // // tx.Commit(); + // _entries -= deleted; // } } + } - public LogBufferEntry[] Peek(int maxValueBytesHint) - { - lock (_sync) - { - RequireNotDisposed(); - - var entries = new List(); - // - // using (var tx = _env.BeginTransaction(TransactionBeginFlags.ReadOnly)) - // using (var db = tx.OpenDatabase()) - // { - // using (var cur = tx.CreateCursor(db)) - // { - // if (cur.MoveToFirst()) - // { - // var entriesBytes = 0; - // - // do - // { - // var current = cur.GetCurrent(); - // var entry = new LogBufferEntry - // { - // Key = ByteKeyToULongKey(current.Key), - // Value = current.Value - // }; - // - // entriesBytes += entry.Value.Length; - // if (entries.Count != 0 && entriesBytes > maxValueBytesHint) - // break; - // - // entries.Add(entry); - // - // } while (cur.MoveNext()); - // } - // } - // } - - return entries.ToArray(); - } - } - - public void Dequeue(ulong toKey) - { - lock (_sync) - { - RequireNotDisposed(); - - // ulong deleted = 0; - // - // using (var tx = _env.BeginTransaction()) - // using (var db = tx.OpenDatabase()) - // { - // using (var cur = tx.CreateCursor(db)) - // { - // if (cur.MoveToFirst()) - // { - // do - // { - // var current = cur.GetCurrent(); - // if (ByteKeyToULongKey(current.Key) > toKey) - // break; - // - // cur.Delete(); - // deleted++; - // } while (cur.MoveNext()); - // } - // } - // - // tx.Commit(); - // _entries -= deleted; - // } - } - } + void RequireNotDisposed() + { + if (_isDisposed) + throw new ObjectDisposedException(typeof(LogBuffer).FullName); + } - void RequireNotDisposed() - { - if (_isDisposed) - throw new ObjectDisposedException(typeof(LogBuffer).FullName); - } + static ulong ByteKeyToULongKey(byte[] key) + { + var copy = new byte[key.Length]; + for (var i = 0; i < key.Length; ++i) + copy[copy.Length - (i + 1)] = key[i]; - static ulong ByteKeyToULongKey(byte[] key) - { - var copy = new byte[key.Length]; - for (var i = 0; i < key.Length; ++i) - copy[copy.Length - (i + 1)] = key[i]; + return BitConverter.ToUInt64(copy, 0); + } - return BitConverter.ToUInt64(copy, 0); - } + static byte[] ULongKeyToByteKey(ulong key) + { + var k = BitConverter.GetBytes(key); + Array.Reverse(k); + return k; + } - static byte[] ULongKeyToByteKey(ulong key) - { - var k = BitConverter.GetBytes(key); - Array.Reverse(k); - return k; - } + public void Enumerate(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); - public void Enumerate(Action action) + lock (_sync) { - if (action == null) throw new ArgumentNullException(nameof(action)); - - lock (_sync) - { - RequireNotDisposed(); + RequireNotDisposed(); - // using (var tx = _env.BeginTransaction(TransactionBeginFlags.ReadOnly)) - // using (var db = tx.OpenDatabase()) - // { - // using (var cur = tx.CreateCursor(db)) - // { - // if (cur.MoveToFirst()) - // { - // do - // { - // var current = cur.GetCurrent(); - // action(ByteKeyToULongKey(current.Key), current.Value); - // } while (cur.MoveNext()); - // } - // } - // } - } + // using (var tx = _env.BeginTransaction(TransactionBeginFlags.ReadOnly)) + // using (var db = tx.OpenDatabase()) + // { + // using (var cur = tx.CreateCursor(db)) + // { + // if (cur.MoveToFirst()) + // { + // do + // { + // var current = cur.GetCurrent(); + // action(ByteKeyToULongKey(current.Key), current.Value); + // } while (cur.MoveNext()); + // } + // } + // } } } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Storage/LogBufferEntry.cs b/src/SeqCli/Forwarder/Storage/LogBufferEntry.cs index 464a7175..649be980 100644 --- a/src/SeqCli/Forwarder/Storage/LogBufferEntry.cs +++ b/src/SeqCli/Forwarder/Storage/LogBufferEntry.cs @@ -14,11 +14,10 @@ // ReSharper disable InconsistentNaming -namespace Seq.Forwarder.Storage +namespace SeqCli.Forwarder.Storage; + +public struct LogBufferEntry { - public struct LogBufferEntry - { - public ulong Key; - public byte[] Value; - } + public ulong Key; + public byte[] Value; } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Util/CaptiveProcess.cs b/src/SeqCli/Forwarder/Util/CaptiveProcess.cs index dc12482e..0c2e3e44 100644 --- a/src/SeqCli/Forwarder/Util/CaptiveProcess.cs +++ b/src/SeqCli/Forwarder/Util/CaptiveProcess.cs @@ -16,67 +16,66 @@ using System.Diagnostics; using System.Threading; -namespace Seq.Forwarder.Util +namespace SeqCli.Forwarder.Util; + +public static class CaptiveProcess { - public static class CaptiveProcess + public static int Run( + string fullExePath, + string? args = null, + Action? writeStdout = null, + Action? writeStderr = null, + string? workingDirectory = null) { - public static int Run( - string fullExePath, - string? args = null, - Action? writeStdout = null, - Action? writeStderr = null, - string? workingDirectory = null) - { - if (fullExePath == null) throw new ArgumentNullException(nameof(fullExePath)); + if (fullExePath == null) throw new ArgumentNullException(nameof(fullExePath)); - args ??= ""; - writeStdout ??= delegate { }; - writeStderr ??= delegate { }; + args ??= ""; + writeStdout ??= delegate { }; + writeStderr ??= delegate { }; - var startInfo = new ProcessStartInfo - { - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true, - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - ErrorDialog = false, - FileName = fullExePath, - Arguments = args - }; + var startInfo = new ProcessStartInfo + { + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + ErrorDialog = false, + FileName = fullExePath, + Arguments = args + }; - if (!string.IsNullOrEmpty(workingDirectory)) - startInfo.WorkingDirectory = workingDirectory; + if (!string.IsNullOrEmpty(workingDirectory)) + startInfo.WorkingDirectory = workingDirectory; - using var process = Process.Start(startInfo)!; - using var outputComplete = new ManualResetEvent(false); - using var errorComplete = new ManualResetEvent(false); - // ReSharper disable AccessToDisposedClosure + using var process = Process.Start(startInfo)!; + using var outputComplete = new ManualResetEvent(false); + using var errorComplete = new ManualResetEvent(false); + // ReSharper disable AccessToDisposedClosure - process.OutputDataReceived += (_, e) => - { - if (e.Data == null) - outputComplete.Set(); - else - writeStdout(e.Data); - }; - process.BeginOutputReadLine(); + process.OutputDataReceived += (_, e) => + { + if (e.Data == null) + outputComplete.Set(); + else + writeStdout(e.Data); + }; + process.BeginOutputReadLine(); - process.ErrorDataReceived += (_, e) => - { - if (e.Data == null) - errorComplete.Set(); - else - writeStderr(e.Data); - }; - process.BeginErrorReadLine(); + process.ErrorDataReceived += (_, e) => + { + if (e.Data == null) + errorComplete.Set(); + else + writeStderr(e.Data); + }; + process.BeginErrorReadLine(); - process.WaitForExit(); + process.WaitForExit(); - outputComplete.WaitOne(); - errorComplete.WaitOne(); + outputComplete.WaitOne(); + errorComplete.WaitOne(); - return process.ExitCode; - } + return process.ExitCode; } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Util/EnumerableExtensions.cs b/src/SeqCli/Forwarder/Util/EnumerableExtensions.cs index 612bc684..6f78a435 100644 --- a/src/SeqCli/Forwarder/Util/EnumerableExtensions.cs +++ b/src/SeqCli/Forwarder/Util/EnumerableExtensions.cs @@ -1,20 +1,19 @@ using System; using System.Collections.Generic; -namespace Seq.Forwarder.Util +namespace SeqCli.Forwarder.Util; + +static class EnumerableExtensions { - static class EnumerableExtensions - { - public static Dictionary ToDictionaryDistinct( - this IEnumerable enumerable, Func keySelector, Func valueSelector) + public static Dictionary ToDictionaryDistinct( + this IEnumerable enumerable, Func keySelector, Func valueSelector) where TKey: notnull + { + var result = new Dictionary(); + foreach (var e in enumerable) { - var result = new Dictionary(); - foreach (var e in enumerable) - { - result[keySelector(e)] = valueSelector(e); - } - return result; + result[keySelector(e)] = valueSelector(e); } + return result; } } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Util/ExecutionEnvironment.cs b/src/SeqCli/Forwarder/Util/ExecutionEnvironment.cs index 9e94295a..e0449984 100644 --- a/src/SeqCli/Forwarder/Util/ExecutionEnvironment.cs +++ b/src/SeqCli/Forwarder/Util/ExecutionEnvironment.cs @@ -1,20 +1,19 @@ -namespace Seq.Forwarder.Util +namespace SeqCli.Forwarder.Util; + +static class ExecutionEnvironment { - static class ExecutionEnvironment - { - public static bool SupportsStandardIO => !IsRunningAsWindowsService; + public static bool SupportsStandardIO => !IsRunningAsWindowsService; - static bool IsRunningAsWindowsService + static bool IsRunningAsWindowsService + { + get { - get - { #if WINDOWS var parent = WindowsProcess.GetParentProcess(); return parent?.ProcessName == "services"; #else - return false; + return false; #endif - } } } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Util/UnclosableStreamWrapper.cs b/src/SeqCli/Forwarder/Util/UnclosableStreamWrapper.cs index ce86ea12..2134c49b 100644 --- a/src/SeqCli/Forwarder/Util/UnclosableStreamWrapper.cs +++ b/src/SeqCli/Forwarder/Util/UnclosableStreamWrapper.cs @@ -15,46 +15,45 @@ using System; using System.IO; -namespace Seq.Forwarder.Util +namespace SeqCli.Forwarder.Util; + +class UnclosableStreamWrapper : Stream { - class UnclosableStreamWrapper : Stream + readonly Stream _stream; + + public UnclosableStreamWrapper(Stream stream) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override void Flush() + { + _stream.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _stream.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) { - readonly Stream _stream; - - public UnclosableStreamWrapper(Stream stream) - { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - } - - public override void Flush() - { - _stream.Flush(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - return _stream.Seek(offset, origin); - } - - public override void SetLength(long value) - { - _stream.SetLength(value); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _stream.Read(buffer, offset, count); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _stream.Write(buffer, offset, count); - } - - public override bool CanRead => _stream.CanRead; - public override bool CanSeek => _stream.CanSeek; - public override bool CanWrite => _stream.CanWrite; - public override long Length => _stream.Length; - public override long Position { get { return _stream.Position; } set { _stream.Position = value; } } + return _stream.Read(buffer, offset, count); } + + public override void Write(byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + } + + public override bool CanRead => _stream.CanRead; + public override bool CanSeek => _stream.CanSeek; + public override bool CanWrite => _stream.CanWrite; + public override long Length => _stream.Length; + public override long Position { get { return _stream.Position; } set { _stream.Position = value; } } } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Web/Api/ApiRootController.cs b/src/SeqCli/Forwarder/Web/Api/ApiRootController.cs index e688faac..9747a8d2 100644 --- a/src/SeqCli/Forwarder/Web/Api/ApiRootController.cs +++ b/src/SeqCli/Forwarder/Web/Api/ApiRootController.cs @@ -15,43 +15,42 @@ using System.IO; using System.Text; using Microsoft.AspNetCore.Mvc; -using Seq.Forwarder.Config; -using Seq.Forwarder.Diagnostics; +using SeqCli.Config.Forwarder; +using SeqCli.Forwarder.Diagnostics; using Serilog.Formatting.Display; -namespace Seq.Forwarder.Web.Api +namespace SeqCli.Forwarder.Web.Api; + +public class ApiRootController : Controller { - public class ApiRootController : Controller - { - static readonly Encoding Encoding = new UTF8Encoding(false); - readonly MessageTemplateTextFormatter _ingestionLogFormatter; + static readonly Encoding Encoding = new UTF8Encoding(false); + readonly MessageTemplateTextFormatter _ingestionLogFormatter; - public ApiRootController(SeqForwarderDiagnosticConfig diagnosticConfig) - { - var template = "[{Timestamp:o} {Level:u3}] {Message}{NewLine}"; - if (diagnosticConfig.IngestionLogShowDetail) - template += "Client IP address: {ClientHostIP}{NewLine}First {StartToLog} characters of payload: {DocumentStart:l}{NewLine}{Exception}{NewLine}"; + public ApiRootController(ForwarderDiagnosticConfig diagnosticConfig) + { + var template = "[{Timestamp:o} {Level:u3}] {Message}{NewLine}"; + if (diagnosticConfig.IngestionLogShowDetail) + template += "Client IP address: {ClientHostIP}{NewLine}First {StartToLog} characters of payload: {DocumentStart:l}{NewLine}{Exception}{NewLine}"; - _ingestionLogFormatter = new MessageTemplateTextFormatter(template); - } + _ingestionLogFormatter = new MessageTemplateTextFormatter(template); + } - [HttpGet, Route("")] - public IActionResult Index() + [HttpGet, Route("")] + public IActionResult Index() + { + var events = IngestionLog.Read(); + using var log = new StringWriter(); + foreach (var logEvent in events) { - var events = IngestionLog.Read(); - using var log = new StringWriter(); - foreach (var logEvent in events) - { - _ingestionLogFormatter.Format(logEvent, log); - } - - return Content(log.ToString(), "text/plain", Encoding); + _ingestionLogFormatter.Format(logEvent, log); } - [HttpGet, Route("api")] - public IActionResult Resources() - { - return Content("{\"Links\":{\"Events\":\"/api/events/describe\"}}", "application/json", Encoding); - } + return Content(log.ToString(), "text/plain", Encoding); } -} + + [HttpGet, Route("api")] + public IActionResult Resources() + { + return Content("{\"Links\":{\"Events\":\"/api/events/describe\"}}", "application/json", Encoding); + } +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Web/Api/IngestionController.cs b/src/SeqCli/Forwarder/Web/Api/IngestionController.cs index 76fbbc11..047a8898 100644 --- a/src/SeqCli/Forwarder/Web/Api/IngestionController.cs +++ b/src/SeqCli/Forwarder/Web/Api/IngestionController.cs @@ -24,223 +24,222 @@ using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Seq.Forwarder.Config; -using Seq.Forwarder.Diagnostics; -using Seq.Forwarder.Multiplexing; -using Seq.Forwarder.Schema; -using Seq.Forwarder.Shipper; +using SeqCli.Config; +using SeqCli.Forwarder.Diagnostics; +using SeqCli.Forwarder.Multiplexing; +using SeqCli.Forwarder.Schema; +using SeqCli.Forwarder.Shipper; -namespace Seq.Forwarder.Web.Api +namespace SeqCli.Forwarder.Web.Api; + +public class IngestionController : Controller { - public class IngestionController : Controller + static readonly Encoding Encoding = new UTF8Encoding(false); + const string ClefMediaType = "application/vnd.serilog.clef"; + + readonly ActiveLogBufferMap _logBufferMap; + readonly ConnectionConfig _outputConfig; + readonly ServerResponseProxy _serverResponseProxy; + + readonly JsonSerializer _rawSerializer = JsonSerializer.Create( + new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }); + + public IngestionController(ActiveLogBufferMap logBufferMap, ConnectionConfig outputConfig, ServerResponseProxy serverResponseProxy) { - static readonly Encoding Encoding = new UTF8Encoding(false); - const string ClefMediaType = "application/vnd.serilog.clef"; + _logBufferMap = logBufferMap; + _outputConfig = outputConfig; + _serverResponseProxy = serverResponseProxy; + } - readonly ActiveLogBufferMap _logBufferMap; - readonly SeqForwarderOutputConfig _outputConfig; - readonly ServerResponseProxy _serverResponseProxy; + IPAddress ClientHostIP => Request.HttpContext.Connection.RemoteIpAddress!; - readonly JsonSerializer _rawSerializer = JsonSerializer.Create( - new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }); + [HttpGet, Route("api/events/describe")] + public IActionResult Resources() + { + return Content("{\"Links\":{\"Raw\":\"/api/events/raw{?clef}\"}}", "application/json", Encoding); + } - public IngestionController(ActiveLogBufferMap logBufferMap, SeqForwarderOutputConfig outputConfig, ServerResponseProxy serverResponseProxy) - { - _logBufferMap = logBufferMap; - _outputConfig = outputConfig; - _serverResponseProxy = serverResponseProxy; - } + [HttpPost, Route("api/events/raw")] + public async Task Ingest() + { + var clef = DefaultedBoolQuery("clef"); - IPAddress ClientHostIP => Request.HttpContext.Connection.RemoteIpAddress!; + if (clef) + return await IngestCompactFormat(); - [HttpGet, Route("api/events/describe")] - public IActionResult Resources() + var contentType = (string?) Request.Headers[HeaderNames.ContentType]; + if (contentType != null && contentType.StartsWith(ClefMediaType)) + return await IngestCompactFormat(); + + return IngestRawFormat(); + } + + IActionResult IngestRawFormat() + { + // The compact format ingestion path works with async IO. + HttpContext.Features.Get()!.AllowSynchronousIO = true; + + JObject posted; + try { - return Content("{\"Links\":{\"Raw\":\"/api/events/raw{?clef}\"}}", "application/json", Encoding); + posted = _rawSerializer.Deserialize(new JsonTextReader(new StreamReader(Request.Body))) ?? + throw new RequestProcessingException("Request body payload is JSON `null`."); } - - [HttpPost, Route("api/events/raw")] - public async Task Ingest() + catch (Exception ex) { - var clef = DefaultedBoolQuery("clef"); - - if (clef) - return await IngestCompactFormat(); - - var contentType = (string?) Request.Headers[HeaderNames.ContentType]; - if (contentType != null && contentType.StartsWith(ClefMediaType)) - return await IngestCompactFormat(); + IngestionLog.ForClient(ClientHostIP).Debug(ex,"Rejecting payload due to invalid JSON, request body could not be parsed"); + throw new RequestProcessingException("Invalid raw event JSON, body could not be parsed."); + } - return IngestRawFormat(); + if (!(posted.TryGetValue("events", StringComparison.Ordinal, out var eventsToken) || + posted.TryGetValue("Events", StringComparison.Ordinal, out eventsToken))) + { + IngestionLog.ForClient(ClientHostIP).Debug("Rejecting payload due to invalid JSON structure"); + throw new RequestProcessingException("Invalid raw event JSON, body must contain an 'Events' array."); } - IActionResult IngestRawFormat() + if (!(eventsToken is JArray events)) { - // The compact format ingestion path works with async IO. - HttpContext.Features.Get()!.AllowSynchronousIO = true; - - JObject posted; - try - { - posted = _rawSerializer.Deserialize(new JsonTextReader(new StreamReader(Request.Body))) ?? - throw new RequestProcessingException("Request body payload is JSON `null`."); - } - catch (Exception ex) - { - IngestionLog.ForClient(ClientHostIP).Debug(ex,"Rejecting payload due to invalid JSON, request body could not be parsed"); - throw new RequestProcessingException("Invalid raw event JSON, body could not be parsed."); - } + IngestionLog.ForClient(ClientHostIP).Debug("Rejecting payload due to invalid Events property structure"); + throw new RequestProcessingException("Invalid raw event JSON, the 'Events' property must be an array."); + } - if (!(posted.TryGetValue("events", StringComparison.Ordinal, out var eventsToken) || - posted.TryGetValue("Events", StringComparison.Ordinal, out eventsToken))) - { - IngestionLog.ForClient(ClientHostIP).Debug("Rejecting payload due to invalid JSON structure"); - throw new RequestProcessingException("Invalid raw event JSON, body must contain an 'Events' array."); - } + var encoded = EncodeRawEvents(events); + return Enqueue(encoded); + } - if (!(eventsToken is JArray events)) - { - IngestionLog.ForClient(ClientHostIP).Debug("Rejecting payload due to invalid Events property structure"); - throw new RequestProcessingException("Invalid raw event JSON, the 'Events' property must be an array."); - } + async Task IngestCompactFormat() + { + var rawFormat = new List(); + var reader = new StreamReader(Request.Body); - var encoded = EncodeRawEvents(events); - return Enqueue(encoded); - } + var line = await reader.ReadLineAsync(); + var lineNumber = 1; - async Task IngestCompactFormat() + while (line != null) { - var rawFormat = new List(); - var reader = new StreamReader(Request.Body); - - var line = await reader.ReadLineAsync(); - var lineNumber = 1; - - while (line != null) + if (!string.IsNullOrWhiteSpace(line)) { - if (!string.IsNullOrWhiteSpace(line)) + JObject item; + try { - JObject item; - try - { - item = _rawSerializer.Deserialize(new JsonTextReader(new StringReader(line))) ?? - throw new RequestProcessingException("Request body payload is JSON `null`."); - } - catch (Exception ex) - { - IngestionLog.ForPayload(ClientHostIP, line).Debug(ex, "Rejecting CLEF payload due to invalid JSON, item could not be parsed"); - throw new RequestProcessingException($"Invalid raw event JSON, item on line {lineNumber} could not be parsed."); - } - - if (!EventSchema.FromClefFormat(lineNumber, item, out var evt, out var err)) - { - IngestionLog.ForPayload(ClientHostIP, line).Debug("Rejecting CLEF payload due to invalid event JSON structure: {NormalizationError}", err); - throw new RequestProcessingException(err); - } + item = _rawSerializer.Deserialize(new JsonTextReader(new StringReader(line))) ?? + throw new RequestProcessingException("Request body payload is JSON `null`."); + } + catch (Exception ex) + { + IngestionLog.ForPayload(ClientHostIP, line).Debug(ex, "Rejecting CLEF payload due to invalid JSON, item could not be parsed"); + throw new RequestProcessingException($"Invalid raw event JSON, item on line {lineNumber} could not be parsed."); + } - rawFormat.Add(evt); + if (!EventSchema.FromClefFormat(lineNumber, item, out var evt, out var err)) + { + IngestionLog.ForPayload(ClientHostIP, line).Debug("Rejecting CLEF payload due to invalid event JSON structure: {NormalizationError}", err); + throw new RequestProcessingException(err); } - line = await reader.ReadLineAsync(); - ++lineNumber; + rawFormat.Add(evt); } - var encoded = EncodeRawEvents(rawFormat); - return Enqueue(encoded); + line = await reader.ReadLineAsync(); + ++lineNumber; } - byte[][] EncodeRawEvents(ICollection events) + var encoded = EncodeRawEvents(rawFormat); + return Enqueue(encoded); + } + + byte[][] EncodeRawEvents(ICollection events) + { + var encoded = new byte[events.Count][]; + var i = 0; + foreach (var e in events) { - var encoded = new byte[events.Count][]; - var i = 0; - foreach (var e in events) + var s = e.ToString(Formatting.None); + var payload = Encoding.UTF8.GetBytes(s); + + if (payload.Length > (int) _outputConfig.EventBodyLimitBytes) { - var s = e.ToString(Formatting.None); - var payload = Encoding.UTF8.GetBytes(s); + IngestionLog.ForPayload(ClientHostIP, s).Debug("An oversized event was dropped"); - if (payload.Length > (int) _outputConfig.EventBodyLimitBytes) + var jo = e as JObject; + // ReSharper disable SuspiciousTypeConversion.Global + var timestamp = (string?) (dynamic?) jo?.GetValue("Timestamp") ?? DateTime.UtcNow.ToString("o"); + var level = (string?) (dynamic?) jo?.GetValue("Level") ?? "Warning"; + + if (jo != null) { - IngestionLog.ForPayload(ClientHostIP, s).Debug("An oversized event was dropped"); + jo.Remove("Timestamp"); + jo.Remove("Level"); + } - var jo = e as JObject; - // ReSharper disable SuspiciousTypeConversion.Global - var timestamp = (string?) (dynamic?) jo?.GetValue("Timestamp") ?? DateTime.UtcNow.ToString("o"); - var level = (string?) (dynamic?) jo?.GetValue("Level") ?? "Warning"; + var startToLog = (int) Math.Min(_outputConfig.EventBodyLimitBytes / 2, 1024); + var compactPrefix = e.ToString(Formatting.None).Substring(0, startToLog); - if (jo != null) + encoded[i] = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new + { + Timestamp = timestamp, + MessageTemplate = "Seq Forwarder received and dropped an oversized event", + Level = level, + Properties = new { - jo.Remove("Timestamp"); - jo.Remove("Level"); + Partial = compactPrefix, + Environment.MachineName, + _outputConfig.EventBodyLimitBytes, + PayloadBytes = payload.Length } - - var startToLog = (int) Math.Min(_outputConfig.EventBodyLimitBytes / 2, 1024); - var compactPrefix = e.ToString(Formatting.None).Substring(0, startToLog); - - encoded[i] = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new - { - Timestamp = timestamp, - MessageTemplate = "Seq Forwarder received and dropped an oversized event", - Level = level, - Properties = new - { - Partial = compactPrefix, - Environment.MachineName, - _outputConfig.EventBodyLimitBytes, - PayloadBytes = payload.Length - } - })); - } - else - { - encoded[i] = payload; - } - - i++; + })); + } + else + { + encoded[i] = payload; } - return encoded; + i++; } + + return encoded; + } - IActionResult Enqueue(byte[][] encodedEvents) - { - var apiKey = GetRequestApiKeyToken(); - _logBufferMap.GetLogBuffer(apiKey).Enqueue(encodedEvents); + IActionResult Enqueue(byte[][] encodedEvents) + { + var apiKey = GetRequestApiKeyToken(); + _logBufferMap.GetLogBuffer(apiKey).Enqueue(encodedEvents); - var response = Content(_serverResponseProxy.GetResponseText(apiKey), "application/json", Encoding); - response.StatusCode = (int)HttpStatusCode.Created; - return response; - } + var response = Content(_serverResponseProxy.GetResponseText(apiKey), "application/json", Encoding); + response.StatusCode = (int)HttpStatusCode.Created; + return response; + } - string? GetRequestApiKeyToken() - { - var apiKeyToken = Request.Headers[SeqApi.ApiKeyHeaderName].FirstOrDefault(); + string? GetRequestApiKeyToken() + { + var apiKeyToken = Request.Headers[SeqApi.ApiKeyHeaderName].FirstOrDefault(); - if (string.IsNullOrWhiteSpace(apiKeyToken)) - apiKeyToken = Request.Query["apiKey"]; + if (string.IsNullOrWhiteSpace(apiKeyToken)) + apiKeyToken = Request.Query["apiKey"]; - var normalized = apiKeyToken?.Trim(); - if (string.IsNullOrEmpty(normalized)) - return null; + var normalized = apiKeyToken?.Trim(); + if (string.IsNullOrEmpty(normalized)) + return null; - return normalized; - } + return normalized; + } - bool DefaultedBoolQuery(string queryParameterName) - { - var parameter = Request.Query[queryParameterName]; - if (parameter.Count != 1) - return false; + bool DefaultedBoolQuery(string queryParameterName) + { + var parameter = Request.Query[queryParameterName]; + if (parameter.Count != 1) + return false; - var value = (string?) parameter; + var value = (string?) parameter; - if (value == "" && ( + if (value == "" && ( Request.QueryString.Value!.Contains($"&{queryParameterName}=") || Request.QueryString.Value.Contains($"?{queryParameterName}="))) - { - return false; - } - - return "true".Equals(value, StringComparison.OrdinalIgnoreCase) || value == "" || value == queryParameterName; + { + return false; } + + return "true".Equals(value, StringComparison.OrdinalIgnoreCase) || value == "" || value == queryParameterName; } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Web/Host/ServerService.cs b/src/SeqCli/Forwarder/Web/Host/ServerService.cs index 98d0d5dc..9118eb24 100644 --- a/src/SeqCli/Forwarder/Web/Host/ServerService.cs +++ b/src/SeqCli/Forwarder/Web/Host/ServerService.cs @@ -14,54 +14,53 @@ using System; using Microsoft.Extensions.Hosting; -using Seq.Forwarder.Diagnostics; -using Seq.Forwarder.Multiplexing; +using SeqCli.Forwarder.Diagnostics; +using SeqCli.Forwarder.Multiplexing; using Serilog; -namespace Seq.Forwarder.Web.Host +namespace SeqCli.Forwarder.Web.Host; + +class ServerService { - class ServerService - { - readonly ActiveLogBufferMap _logBufferMap; - readonly IHost _host; - readonly string _listenUri; + readonly ActiveLogBufferMap _logBufferMap; + readonly IHost _host; + readonly string _listenUri; - public ServerService(ActiveLogBufferMap logBufferMap, IHost host, string listenUri) - { - _logBufferMap = logBufferMap; - _host = host; - _listenUri = listenUri; - } + public ServerService(ActiveLogBufferMap logBufferMap, IHost host, string listenUri) + { + _logBufferMap = logBufferMap; + _host = host; + _listenUri = listenUri; + } - public void Start() + public void Start() + { + try { - try - { - Log.Debug("Starting HTTP server..."); + Log.Debug("Starting HTTP server..."); - _host.Start(); + _host.Start(); - Log.Information("Seq Forwarder listening on {ListenUri}", _listenUri); - IngestionLog.Log.Debug("Seq Forwarder is accepting events"); + Log.Information("Seq Forwarder listening on {ListenUri}", _listenUri); + IngestionLog.Log.Debug("Seq Forwarder is accepting events"); - _logBufferMap.Load(); - _logBufferMap.Start(); - } - catch (Exception ex) - { - Log.Fatal(ex, "Error running the server application"); - throw; - } + _logBufferMap.Load(); + _logBufferMap.Start(); } - - public void Stop() + catch (Exception ex) { - Log.Debug("Seq Forwarder stopping"); + Log.Fatal(ex, "Error running the server application"); + throw; + } + } - _host.StopAsync().Wait(); - _logBufferMap.Stop(); + public void Stop() + { + Log.Debug("Seq Forwarder stopping"); - Log.Information("Seq Forwarder stopped cleanly"); - } + _host.StopAsync().Wait(); + _logBufferMap.Stop(); + + Log.Information("Seq Forwarder stopped cleanly"); } -} +} \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Web/Host/Startup.cs b/src/SeqCli/Forwarder/Web/Host/Startup.cs index 57379a33..804e2065 100644 --- a/src/SeqCli/Forwarder/Web/Host/Startup.cs +++ b/src/SeqCli/Forwarder/Web/Host/Startup.cs @@ -2,39 +2,38 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -namespace Seq.Forwarder.Web.Host +namespace SeqCli.Forwarder.Web.Host; + +class Startup { - class Startup + public void ConfigureServices(IServiceCollection serviceCollection) { - public void ConfigureServices(IServiceCollection serviceCollection) - { - serviceCollection.AddMvc(); - } + serviceCollection.AddMvc(); + } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app) + { + app.Use(async (context, next) => { - app.Use(async (context, next) => + try { - try - { - await next(); - } - catch (RequestProcessingException rex) - { - if (context.Response.HasStarted) - throw; - - context.Response.StatusCode = (int)rex.StatusCode; - context.Response.ContentType = "text/plain; charset=UTF-8"; - await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(rex.Message)); - await context.Response.CompleteAsync(); - } - }); - app.UseRouting(); - app.UseEndpoints(endpoints => + await next(); + } + catch (RequestProcessingException rex) { - endpoints.MapControllers(); - }); - } + if (context.Response.HasStarted) + throw; + + context.Response.StatusCode = (int)rex.StatusCode; + context.Response.ContentType = "text/plain; charset=UTF-8"; + await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(rex.Message)); + await context.Response.CompleteAsync(); + } + }); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } \ No newline at end of file diff --git a/src/SeqCli/Forwarder/Web/RequestProcessingException.cs b/src/SeqCli/Forwarder/Web/RequestProcessingException.cs index d5968643..bfef07af 100644 --- a/src/SeqCli/Forwarder/Web/RequestProcessingException.cs +++ b/src/SeqCli/Forwarder/Web/RequestProcessingException.cs @@ -15,16 +15,15 @@ using System; using System.Net; -namespace Seq.Forwarder.Web +namespace SeqCli.Forwarder.Web; + +class RequestProcessingException : Exception { - class RequestProcessingException : Exception + public RequestProcessingException(string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest) + : base(message) { - public RequestProcessingException(string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest) - : base(message) - { - StatusCode = statusCode; - } - - public HttpStatusCode StatusCode { get; } + StatusCode = statusCode; } -} + + public HttpStatusCode StatusCode { get; } +} \ No newline at end of file diff --git a/src/SeqCli/PlainText/Framing/FrameReader.cs b/src/SeqCli/PlainText/Framing/FrameReader.cs index c39bdb0e..d0f769e0 100644 --- a/src/SeqCli/PlainText/Framing/FrameReader.cs +++ b/src/SeqCli/PlainText/Framing/FrameReader.cs @@ -58,7 +58,7 @@ public async Task TryReadAsync() } else if (_unawaitedNextLine != null) { - var index = Task.WaitAny(new Task[] {_unawaitedNextLine}, _trailingLineArrivalDeadline); + var index = Task.WaitAny([_unawaitedNextLine], _trailingLineArrivalDeadline); if (index == -1) return new Frame(); @@ -81,7 +81,7 @@ public async Task TryReadAsync() while (true) { readLine = readLine ?? Task.Run(_source.ReadLineAsync); - var index = Task.WaitAny(new Task[] {readLine}, _trailingLineArrivalDeadline); + var index = Task.WaitAny([readLine], _trailingLineArrivalDeadline); if (index == -1) { if (hasValue) diff --git a/src/SeqCli/PlainText/ReifiedProperties.cs b/src/SeqCli/PlainText/ReifiedProperties.cs index 98bc98f0..9b63c64d 100644 --- a/src/SeqCli/PlainText/ReifiedProperties.cs +++ b/src/SeqCli/PlainText/ReifiedProperties.cs @@ -13,10 +13,7 @@ public const string SpanId = "@sp", TraceId = "@tr"; - static readonly HashSet All = new() - { - Message, Timestamp, Level, Exception, StartTimestamp, SpanId, TraceId - }; + static readonly HashSet All = [Message, Timestamp, Level, Exception, StartTimestamp, SpanId, TraceId]; public static bool IsReifiedProperty(string name) { diff --git a/src/SeqCli/Syntax/QueryBuilder.cs b/src/SeqCli/Syntax/QueryBuilder.cs index 30b6aeaf..b2338ec4 100644 --- a/src/SeqCli/Syntax/QueryBuilder.cs +++ b/src/SeqCli/Syntax/QueryBuilder.cs @@ -21,10 +21,10 @@ namespace SeqCli.Syntax; class QueryBuilder { - readonly List<(string, string)> _columns = new List<(string, string)>(); - readonly List _where = new List(); - readonly List _groupBy = new List(); - readonly List _having = new List(); + readonly List<(string, string)> _columns = new(); + readonly List _where = new(); + readonly List _groupBy = new(); + readonly List _having = new(); public void Select(string value, string label) { diff --git a/test/SeqCli.EndToEnd/Args.cs b/test/SeqCli.EndToEnd/Args.cs index 258e17f8..f3b0bd97 100644 --- a/test/SeqCli.EndToEnd/Args.cs +++ b/test/SeqCli.EndToEnd/Args.cs @@ -18,7 +18,7 @@ public Regex[] TestCases() => _args .ToArray(); // Simple replacement so `Events.*` becomes `Events\..*` - static Regex ToArgRegex(string arg) => new Regex(arg.Replace(".", "\\.").Replace("*", ".*")); + static Regex ToArgRegex(string arg) => new(arg.Replace(".", "\\.").Replace("*", ".*")); public bool Multiuser() => _args.Any(a => a == "--license-certificate-stdin"); diff --git a/test/SeqCli.EndToEnd/RetentionPolicy/RetentionPolicyBasicsTestCase.cs b/test/SeqCli.EndToEnd/RetentionPolicy/RetentionPolicyBasicsTestCase.cs index fdb554d7..2a64a0e8 100644 --- a/test/SeqCli.EndToEnd/RetentionPolicy/RetentionPolicyBasicsTestCase.cs +++ b/test/SeqCli.EndToEnd/RetentionPolicy/RetentionPolicyBasicsTestCase.cs @@ -1,12 +1,11 @@ using System; using System.Threading.Tasks; -using System.Linq; using Seq.Api; using SeqCli.EndToEnd.Support; using Serilog; using Xunit; -namespace SeqCli.EndToEnd.RetentionPolicies; +namespace SeqCli.EndToEnd.RetentionPolicy; // ReSharper disable once UnusedType.Global public class RetentionPolicyBasicsTestCase : ICliTestCase diff --git a/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs b/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs index a08bd598..1f7bb834 100644 --- a/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs +++ b/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs @@ -12,11 +12,11 @@ public sealed class CaptiveProcess : ITestProcess, IDisposable readonly string _stopCommandFullExePath; readonly string _stopCommandArgs; readonly Process _process; - readonly ManualResetEvent _outputComplete = new ManualResetEvent(false); - readonly ManualResetEvent _errorComplete = new ManualResetEvent(false); + readonly ManualResetEvent _outputComplete = new(false); + readonly ManualResetEvent _errorComplete = new(false); - readonly object _sync = new object(); - readonly StringWriter _output = new StringWriter(); + readonly object _sync = new(); + readonly StringWriter _output = new(); public CaptiveProcess( string fullExePath, diff --git a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs index 9d9ba88a..78bea1e3 100644 --- a/test/SeqCli.Tests/Cli/CommandLineHostTests.cs +++ b/test/SeqCli.Tests/Cli/CommandLineHostTests.cs @@ -26,7 +26,7 @@ public async Task CheckCommandLineHostPicksCorrectCommand() new CommandMetadata {Name = "test2"}) }; var commandLineHost = new CommandLineHost(availableCommands); - await commandLineHost.Run(new []{ "test"},new LoggingLevelSwitch()); + await commandLineHost.Run(["test"],new LoggingLevelSwitch()); Assert.Equal("test", executed.First()); } @@ -46,7 +46,7 @@ public async Task WhenMoreThanOneSubcommandAndTheUserRunsWithSubcommandEnsurePic new CommandMetadata {Name = "test", SubCommand = "subcommand2"}) }; var commandLineHost = new CommandLineHost(availableCommands); - await commandLineHost.Run(new[] { "test", "subcommand2" }, new LoggingLevelSwitch()); + await commandLineHost.Run(["test", "subcommand2"], new LoggingLevelSwitch()); Assert.Equal("test-subcommand2", commandsRan.First()); } @@ -66,7 +66,7 @@ public async Task VerboseOptionSetsLoggingLevelToInformation() var commandLineHost = new CommandLineHost(availableCommands); - await commandLineHost.Run(new[] { "test", "--verbose" }, levelSwitch); + await commandLineHost.Run(["test", "--verbose"], levelSwitch); Assert.Equal(LogEventLevel.Information, levelSwitch.MinimumLevel); } diff --git a/test/SeqCli.Tests/Forwarder/Multiplexing/ActiveLogBufferMapTests.cs b/test/SeqCli.Tests/Forwarder/Multiplexing/ActiveLogBufferMapTests.cs index 46188948..11db09d1 100644 --- a/test/SeqCli.Tests/Forwarder/Multiplexing/ActiveLogBufferMapTests.cs +++ b/test/SeqCli.Tests/Forwarder/Multiplexing/ActiveLogBufferMapTests.cs @@ -1,83 +1,81 @@ using System.IO; using System.Linq; -using Seq.Forwarder.Config; -using Seq.Forwarder.Cryptography; -using Seq.Forwarder.Multiplexing; -using Seq.Forwarder.Tests.Support; +using SeqCli.Config; +using SeqCli.Forwarder.Cryptography; +using SeqCli.Forwarder.Multiplexing; using SeqCli.Tests.Support; using Xunit; -namespace Seq.Forwarder.Tests.Multiplexing -{ - public class ActiveLogBufferMapTests - { - [Fact] - public void AnEmptyMapCreatesNoFiles() - { - using var tmp = new TempFolder("Buffer"); - using var map = CreateActiveLogBufferMap(tmp); - Assert.Empty(Directory.GetFileSystemEntries(tmp.Path)); - } +namespace SeqCli.Tests.Forwarder.Multiplexing; - [Fact] - public void TheDefaultBufferWritesDataInTheBufferRoot() - { - using var tmp = new TempFolder("Buffer"); - using var map = CreateActiveLogBufferMap(tmp); - var entry = map.GetLogBuffer(null); - Assert.NotNull(entry); - Assert.True(File.Exists(Path.Combine(tmp.Path, "data.mdb"))); - Assert.Empty(Directory.GetDirectories(tmp.Path)); - Assert.Same(entry, map.GetLogBuffer(null)); - } +public class ActiveLogBufferMapTests +{ + [Fact] + public void AnEmptyMapCreatesNoFiles() + { + using var tmp = new TempFolder("Buffer"); + using var map = CreateActiveLogBufferMap(tmp); + Assert.Empty(Directory.GetFileSystemEntries(tmp.Path)); + } - [Fact] - public void ApiKeySpecificBuffersWriteDataToSubfolders() - { - using var tmp = new TempFolder("Buffer"); - using var map = CreateActiveLogBufferMap(tmp); - string key1 = Some.ApiKey(), key2 = Some.ApiKey(); - var entry1 = map.GetLogBuffer(key1); - var entry2 = map.GetLogBuffer(key2); + [Fact] + public void TheDefaultBufferWritesDataInTheBufferRoot() + { + using var tmp = new TempFolder("Buffer"); + using var map = CreateActiveLogBufferMap(tmp); + var entry = map.GetLogBuffer(null); + Assert.NotNull(entry); + Assert.True(File.Exists(Path.Combine(tmp.Path, "data.mdb"))); + Assert.Empty(Directory.GetDirectories(tmp.Path)); + Assert.Same(entry, map.GetLogBuffer(null)); + } - Assert.NotNull(entry1); - Assert.NotNull(entry2); - Assert.Same(entry1, map.GetLogBuffer(key1)); - Assert.NotSame(entry1, entry2); - var subdirs = Directory.GetDirectories(tmp.Path); - Assert.Equal(2, subdirs.Length); - Assert.True(File.Exists(Path.Combine(subdirs[0], "data.mdb"))); - Assert.True(File.Exists(Path.Combine(subdirs[0], ".apikey"))); - } + [Fact] + public void ApiKeySpecificBuffersWriteDataToSubfolders() + { + using var tmp = new TempFolder("Buffer"); + using var map = CreateActiveLogBufferMap(tmp); + string key1 = Some.ApiKey(), key2 = Some.ApiKey(); + var entry1 = map.GetLogBuffer(key1); + var entry2 = map.GetLogBuffer(key2); - [Fact] - public void EntriesSurviveReloads() - { - var apiKey = Some.ApiKey(); - var value = Some.Bytes(100); + Assert.NotNull(entry1); + Assert.NotNull(entry2); + Assert.Same(entry1, map.GetLogBuffer(key1)); + Assert.NotSame(entry1, entry2); + var subdirs = Directory.GetDirectories(tmp.Path); + Assert.Equal(2, subdirs.Length); + Assert.True(File.Exists(Path.Combine(subdirs[0], "data.mdb"))); + Assert.True(File.Exists(Path.Combine(subdirs[0], ".apikey"))); + } - using var tmp = new TempFolder("Buffer"); - using (var map = CreateActiveLogBufferMap(tmp)) - { - map.GetLogBuffer(null).Enqueue(new[] {value}); - map.GetLogBuffer(apiKey).Enqueue(new[] {value}); - } + [Fact] + public void EntriesSurviveReloads() + { + var apiKey = Some.ApiKey(); + var value = Some.Bytes(100); - using (var map = CreateActiveLogBufferMap(tmp)) - { - var first = map.GetLogBuffer(null).Peek(0).Single(); - var second = map.GetLogBuffer(apiKey).Peek(0).Single(); - Assert.Equal(value, first.Value); - Assert.Equal(value, second.Value); - } + using var tmp = new TempFolder("Buffer"); + using (var map = CreateActiveLogBufferMap(tmp)) + { + map.GetLogBuffer(null).Enqueue([value]); + map.GetLogBuffer(apiKey).Enqueue([value]); } - static ActiveLogBufferMap CreateActiveLogBufferMap(TempFolder tmp) + using (var map = CreateActiveLogBufferMap(tmp)) { - var config = new SeqForwarderConfig(); - var map = new ActiveLogBufferMap(tmp.Path, config.Storage, config.Output, new InertLogShipperFactory(), StringDataProtector.CreatePlatformDefault()); - map.Load(); - return map; + var first = map.GetLogBuffer(null).Peek(0).Single(); + var second = map.GetLogBuffer(apiKey).Peek(0).Single(); + Assert.Equal(value, first.Value); + Assert.Equal(value, second.Value); } } -} + + static ActiveLogBufferMap CreateActiveLogBufferMap(TempFolder tmp) + { + var config = new SeqCliConfig(); + var map = new ActiveLogBufferMap(tmp.Path, config.Forwarder.Storage, config.Connection, new InertLogShipperFactory(), StringDataProtector.CreatePlatformDefault()); + map.Load(); + return map; + } +} \ No newline at end of file diff --git a/test/SeqCli.Tests/Forwarder/Schema/EventSchemaTests.cs b/test/SeqCli.Tests/Forwarder/Schema/EventSchemaTests.cs index 2032215e..8d1abcba 100644 --- a/test/SeqCli.Tests/Forwarder/Schema/EventSchemaTests.cs +++ b/test/SeqCli.Tests/Forwarder/Schema/EventSchemaTests.cs @@ -1,73 +1,72 @@ using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Seq.Forwarder.Schema; +using SeqCli.Forwarder.Schema; using Xunit; -namespace Seq.Forwarder.Tests.Schema +namespace SeqCli.Tests.Forwarder.Schema; + +public class EventSchemaTests { - public class EventSchemaTests - { - static readonly JsonSerializer RawSerializer = JsonSerializer.Create( - new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }); + static readonly JsonSerializer RawSerializer = JsonSerializer.Create( + new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }); - [Fact] - public void ClefNormalizationAcceptsDuplicateRenderings() - { - var payload = "{\"@t\": \"2015-05-09T12:09:08.12345Z\"," + - " \"@mt\": \"{A:000} and {A:000}\"," + - " \"@r\": [\"424\",\"424\"]}"; + [Fact] + public void ClefNormalizationAcceptsDuplicateRenderings() + { + var payload = "{\"@t\": \"2015-05-09T12:09:08.12345Z\"," + + " \"@mt\": \"{A:000} and {A:000}\"," + + " \"@r\": [\"424\",\"424\"]}"; - AssertCanNormalizeClef(payload); - } + AssertCanNormalizeClef(payload); + } - [Fact] - public void ClefNormalizationPropagatesRenderings() - { - const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12,\"@r\":[\"012\"]}"; - var evt = AssertCanNormalizeClef(payload); - Assert.Single(evt.Renderings); - } + [Fact] + public void ClefNormalizationPropagatesRenderings() + { + const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12,\"@r\":[\"012\"]}"; + var evt = AssertCanNormalizeClef(payload); + Assert.Single(evt.Renderings); + } - [Fact] - public void ClefNormalizationIgnoresMissingRenderings() - { - const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12}"; - AssertCanNormalizeClef(payload); - } + [Fact] + public void ClefNormalizationIgnoresMissingRenderings() + { + const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12}"; + AssertCanNormalizeClef(payload); + } - [Fact] - public void ClefNormalizationFixesTooFewRenderings1() - { - const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12,\"@r\":[]}"; - var evt = AssertCanNormalizeClef(payload); - Assert.Null(evt.Renderings); - } + [Fact] + public void ClefNormalizationFixesTooFewRenderings1() + { + const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12,\"@r\":[]}"; + var evt = AssertCanNormalizeClef(payload); + Assert.Null(evt.Renderings); + } - [Fact] - public void ClefNormalizationFixesTooFewRenderings2() - { - const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000} {Q:x}!\",\"P\":12,\"@r\":[\"012\"]}"; - var evt = AssertCanNormalizeClef(payload); - Assert.Null(evt.Renderings); - } + [Fact] + public void ClefNormalizationFixesTooFewRenderings2() + { + const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000} {Q:x}!\",\"P\":12,\"@r\":[\"012\"]}"; + var evt = AssertCanNormalizeClef(payload); + Assert.Null(evt.Renderings); + } - [Fact] - public void ClefNormalizationIgnoresTooManyRenderings() - { - const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12,\"@r\":[\"012\",\"013\"]}"; - var evt = AssertCanNormalizeClef(payload); - Assert.Null(evt.Renderings); - } + [Fact] + public void ClefNormalizationIgnoresTooManyRenderings() + { + const string payload = "{\"@t\":\"2018-12-02T09:05:47.256725+03:00\",\"@mt\":\"Hello {P:000}!\",\"P\":12,\"@r\":[\"012\",\"013\"]}"; + var evt = AssertCanNormalizeClef(payload); + Assert.Null(evt.Renderings); + } - static dynamic AssertCanNormalizeClef(string payload) - { - var jo = RawSerializer.Deserialize(new JsonTextReader(new StringReader(payload)))!; + static dynamic AssertCanNormalizeClef(string payload) + { + var jo = RawSerializer.Deserialize(new JsonTextReader(new StringReader(payload)))!; - var valid = EventSchema.FromClefFormat(1, jo, out var rawFormat, out var error); - Assert.True(valid, error); - Assert.NotNull(rawFormat); - return rawFormat!; - } + var valid = EventSchema.FromClefFormat(1, jo, out var rawFormat, out var error); + Assert.True(valid, error); + Assert.NotNull(rawFormat); + return rawFormat!; } } \ No newline at end of file diff --git a/test/SeqCli.Tests/Forwarder/Shipper/ServerResponseProxyTests.cs b/test/SeqCli.Tests/Forwarder/Shipper/ServerResponseProxyTests.cs index 1ac2db7c..107a6973 100644 --- a/test/SeqCli.Tests/Forwarder/Shipper/ServerResponseProxyTests.cs +++ b/test/SeqCli.Tests/Forwarder/Shipper/ServerResponseProxyTests.cs @@ -1,49 +1,46 @@ -using Seq.Forwarder.Multiplexing; -using Seq.Forwarder.Shipper; -using Seq.Forwarder.Tests.Support; +using SeqCli.Forwarder.Multiplexing; using SeqCli.Tests.Support; using Xunit; -namespace Seq.Forwarder.Tests.Shipper +namespace SeqCli.Tests.Forwarder.Shipper; + +public class ServerResponseProxyTests { - public class ServerResponseProxyTests + [Fact] + public void WhenNoResponseRecordedEmptyIsReturned() { - [Fact] - public void WhenNoResponseRecordedEmptyIsReturned() - { - var proxy = new ServerResponseProxy(); - var response = proxy.GetResponseText(Some.ApiKey()); - Assert.Equal("{}", response); - } + var proxy = new ServerResponseProxy(); + var response = proxy.GetResponseText(Some.ApiKey()); + Assert.Equal("{}", response); + } - [Fact] - public void WhenApiKeysDontMatchEmptyResponseReturned() - { - var proxy = new ServerResponseProxy(); - proxy.SuccessResponseReturned(Some.ApiKey(), "this is never used"); - var response = proxy.GetResponseText(Some.ApiKey()); - Assert.Equal("{}", response); - } + [Fact] + public void WhenApiKeysDontMatchEmptyResponseReturned() + { + var proxy = new ServerResponseProxy(); + proxy.SuccessResponseReturned(Some.ApiKey(), "this is never used"); + var response = proxy.GetResponseText(Some.ApiKey()); + Assert.Equal("{}", response); + } - [Fact] - public void WhenApiKeysMatchTheResponseIsReturned() - { - var proxy = new ServerResponseProxy(); - var apiKey = Some.ApiKey(); - var responseText = "some response"; - proxy.SuccessResponseReturned(apiKey, responseText); - var response = proxy.GetResponseText(apiKey); - Assert.Equal(responseText, response); - } + [Fact] + public void WhenApiKeysMatchTheResponseIsReturned() + { + var proxy = new ServerResponseProxy(); + var apiKey = Some.ApiKey(); + var responseText = "some response"; + proxy.SuccessResponseReturned(apiKey, responseText); + var response = proxy.GetResponseText(apiKey); + Assert.Equal(responseText, response); + } - [Fact] - public void NullApiKeysAreConsideredMatching() - { - var proxy = new ServerResponseProxy(); - var responseText = "some response"; - proxy.SuccessResponseReturned(null, responseText); - var response = proxy.GetResponseText(null); - Assert.Equal(responseText, response); - } + [Fact] + public void NullApiKeysAreConsideredMatching() + { + var proxy = new ServerResponseProxy(); + var responseText = "some response"; + proxy.SuccessResponseReturned(null, responseText); + var response = proxy.GetResponseText(null); + Assert.Equal(responseText, response); } -} +} \ No newline at end of file diff --git a/test/SeqCli.Tests/Forwarder/Storage/LogBufferTests.cs b/test/SeqCli.Tests/Forwarder/Storage/LogBufferTests.cs index e6468d00..cee028fc 100644 --- a/test/SeqCli.Tests/Forwarder/Storage/LogBufferTests.cs +++ b/test/SeqCli.Tests/Forwarder/Storage/LogBufferTests.cs @@ -1,151 +1,149 @@ using System.Collections.Generic; -using Seq.Forwarder.Storage; -using Seq.Forwarder.Tests.Support; +using SeqCli.Forwarder.Storage; using SeqCli.Tests.Support; using Xunit; -namespace Seq.Forwarder.Tests.Storage +namespace SeqCli.Tests.Forwarder.Storage; + +public class LogBufferTests { - public class LogBufferTests + const ulong DefaultBufferSize = 10 * 1024 * 1024; + + [Fact] + public void ANewLogBufferIsEmpty() { - const ulong DefaultBufferSize = 10 * 1024 * 1024; + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + var contents = buffer.Peek((int)DefaultBufferSize); + Assert.Empty(contents); + } - [Fact] - public void ANewLogBufferIsEmpty() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - var contents = buffer.Peek((int)DefaultBufferSize); - Assert.Empty(contents); - } - - [Fact] - public void PeekingDoesNotChangeState() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - buffer.Enqueue(new[] { Some.Bytes(140) }); + [Fact] + public void PeekingDoesNotChangeState() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + buffer.Enqueue([Some.Bytes(140)]); - var contents = buffer.Peek((int)DefaultBufferSize); - Assert.Single(contents); + var contents = buffer.Peek((int)DefaultBufferSize); + Assert.Single(contents); - var remainder = buffer.Peek((int)DefaultBufferSize); - Assert.Single(remainder); - } + var remainder = buffer.Peek((int)DefaultBufferSize); + Assert.Single(remainder); + } - [Fact] - public void EnqueuedEntriesAreDequeuedFifo() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); - buffer.Enqueue(new[] { a1, a2 }); - buffer.Enqueue(new[] { a3 }); - - var contents = buffer.Peek((int)DefaultBufferSize); - - Assert.Equal(3, contents.Length); - Assert.Equal(a1, contents[0].Value); - Assert.Equal(a2, contents[1].Value); - Assert.Equal(a3, contents[2].Value); - } - - [Fact] - public void EntriesOverLimitArePurgedFifo() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), 4096); - byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); - buffer.Enqueue(new[] { a1, a2, a3 }); + [Fact] + public void EnqueuedEntriesAreDequeuedFifo() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); + buffer.Enqueue([a1, a2]); + buffer.Enqueue([a3]); + + var contents = buffer.Peek((int)DefaultBufferSize); + + Assert.Equal(3, contents.Length); + Assert.Equal(a1, contents[0].Value); + Assert.Equal(a2, contents[1].Value); + Assert.Equal(a3, contents[2].Value); + } - var contents = buffer.Peek((int)DefaultBufferSize); + [Fact] + public void EntriesOverLimitArePurgedFifo() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), 4096); + byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); + buffer.Enqueue([a1, a2, a3]); - Assert.Equal(2, contents.Length); - Assert.Equal(a2, contents[0].Value); - Assert.Equal(a3, contents[1].Value); - } + var contents = buffer.Peek((int)DefaultBufferSize); - [Fact] - public void SizeHintLimitsDequeuedEventCount() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); - buffer.Enqueue(new[] { a1, a2, a3 }); + Assert.Equal(2, contents.Length); + Assert.Equal(a2, contents[0].Value); + Assert.Equal(a3, contents[1].Value); + } - var contents = buffer.Peek(300); + [Fact] + public void SizeHintLimitsDequeuedEventCount() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); + buffer.Enqueue([a1, a2, a3]); - Assert.Equal(2, contents.Length); - Assert.Equal(a1, contents[0].Value); - Assert.Equal(a2, contents[1].Value); - } + var contents = buffer.Peek(300); - [Fact] - public void AtLeastOneEventIsAlwaysDequeued() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); - buffer.Enqueue(new[] { a1, a2, a3 }); + Assert.Equal(2, contents.Length); + Assert.Equal(a1, contents[0].Value); + Assert.Equal(a2, contents[1].Value); + } - var contents = buffer.Peek(30); + [Fact] + public void AtLeastOneEventIsAlwaysDequeued() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); + buffer.Enqueue([a1, a2, a3]); - Assert.Single(contents); - Assert.Equal(a1, contents[0].Value); - } + var contents = buffer.Peek(30); - [Fact] - public void GivingTheLastSeenEventKeyRemovesPrecedingEvents() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); - buffer.Enqueue(new[] { a1, a2, a3 }); + Assert.Single(contents); + Assert.Equal(a1, contents[0].Value); + } - var contents = buffer.Peek(420); - Assert.Equal(3, contents.Length); + [Fact] + public void GivingTheLastSeenEventKeyRemovesPrecedingEvents() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); + buffer.Enqueue([a1, a2, a3]); - buffer.Dequeue(contents[2].Key); + var contents = buffer.Peek(420); + Assert.Equal(3, contents.Length); - var remaining = buffer.Peek(420); - Assert.Empty(remaining); - } + buffer.Dequeue(contents[2].Key); - [Fact] - public void GivingTheLastSeeEventKeyLeavesSuccessiveEvents() - { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); - buffer.Enqueue(new[] { a1, a2, a3 }); + var remaining = buffer.Peek(420); + Assert.Empty(remaining); + } + + [Fact] + public void GivingTheLastSeeEventKeyLeavesSuccessiveEvents() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); + buffer.Enqueue([a1, a2, a3]); - var contents = buffer.Peek(30); - Assert.Single(contents); + var contents = buffer.Peek(30); + Assert.Single(contents); - buffer.Enqueue(new [] { Some.Bytes(140) }); + buffer.Enqueue([Some.Bytes(140)]); - buffer.Dequeue(contents[0].Key); + buffer.Dequeue(contents[0].Key); - var remaining = buffer.Peek(420); - Assert.Equal(3, remaining.Length); - } + var remaining = buffer.Peek(420); + Assert.Equal(3, remaining.Length); + } - [Fact] - public void EnumerationIsInOrder() + [Fact] + public void EnumerationIsInOrder() + { + using var temp = TempFolder.ForCaller(); + using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); + byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); + buffer.Enqueue([a1, a2, a3]); + + var contents = new List(); + buffer.Enumerate((k, v) => { - using var temp = TempFolder.ForCaller(); - using var buffer = new LogBuffer(temp.AllocateFilename("mdb"), DefaultBufferSize); - byte[] a1 = Some.Bytes(140), a2 = Some.Bytes(140), a3 = Some.Bytes(140); - buffer.Enqueue(new[] { a1, a2, a3 }); - - var contents = new List(); - buffer.Enumerate((k, v) => - { - contents.Add(v); - }); - - Assert.Equal(3, contents.Count); - Assert.Equal(new[] { a1, a2, a3 }, contents); - } + contents.Add(v); + }); + + Assert.Equal(3, contents.Count); + Assert.Equal(new[] { a1, a2, a3 }, contents); } -} +} \ No newline at end of file diff --git a/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs b/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs index 8deac0f2..cfcb203e 100644 --- a/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs +++ b/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs @@ -35,7 +35,7 @@ public void TheTrailingIndentPatternDoesNotMatchLinesStartingWithWhitespace() Assert.Equal(frame, remainder); } - static NameValueExtractor ClassMethodPattern { get; } = new NameValueExtractor(new[] + static NameValueExtractor ClassMethodPattern { get; } = new(new[] { new SimplePatternElement(Matchers.Identifier, "class"), new SimplePatternElement(Matchers.LiteralText(".")), diff --git a/test/SeqCli.Tests/Signals/SignalExpressionParserTests.cs b/test/SeqCli.Tests/Signals/SignalExpressionParserTests.cs index 92eba39d..6b9e2fad 100644 --- a/test/SeqCli.Tests/Signals/SignalExpressionParserTests.cs +++ b/test/SeqCli.Tests/Signals/SignalExpressionParserTests.cs @@ -17,15 +17,15 @@ public void ParseSuccessfully((string, string) inputs) } public static IEnumerable _sources = new []{ - new object[] { ("signal-1 ", "signal-1") }, - - new object[] { ("(signal-1)", "signal-1") }, - - new object[] { ("signal-1 ,signal-2", "signal-1,signal-2") }, - - new object[] { (" signal-1,signal-2~ signal-3", "(signal-1,signal-2)~signal-3") }, - - new object[] { ("signal-1,signal-2,(signal-3~signal-4)", "(signal-1,signal-2),(signal-3~signal-4)") }, + [("signal-1 ", "signal-1")], + + [("(signal-1)", "signal-1")], + + [("signal-1 ,signal-2", "signal-1,signal-2")], + + [(" signal-1,signal-2~ signal-3", "(signal-1,signal-2)~signal-3")], + + [("signal-1,signal-2,(signal-3~signal-4)", "(signal-1,signal-2),(signal-3~signal-4)")], new object[] { ("signal-1~( (signal-2~signal-3) ,signal-4)", "signal-1~((signal-2~signal-3),signal-4)") } }; diff --git a/test/SeqCli.Tests/Support/TempFolder.cs b/test/SeqCli.Tests/Support/TempFolder.cs index f7d358ef..968fd857 100644 --- a/test/SeqCli.Tests/Support/TempFolder.cs +++ b/test/SeqCli.Tests/Support/TempFolder.cs @@ -1,51 +1,50 @@ -using System; +#nullable enable + +using System; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; -#nullable enable +namespace SeqCli.Tests.Support; -namespace Seq.Forwarder.Tests.Support +class TempFolder : IDisposable { - class TempFolder : IDisposable - { - static readonly Guid Session = Guid.NewGuid(); + static readonly Guid Session = Guid.NewGuid(); - public TempFolder(string name) - { - Path = System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Seq.Forwarder.Tests", - Session.ToString("n"), - name); + public TempFolder(string name) + { + Path = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Seq.Forwarder.Tests", + Session.ToString("n"), + name); - Directory.CreateDirectory(Path); - } + Directory.CreateDirectory(Path); + } - public string Path { get; } + public string Path { get; } - public void Dispose() + public void Dispose() + { + try { - try - { - if (Directory.Exists(Path)) - Directory.Delete(Path, true); - } - catch (Exception ex) - { - Debug.WriteLine(ex); - } + if (Directory.Exists(Path)) + Directory.Delete(Path, true); } - - public static TempFolder ForCaller([CallerMemberName] string? caller = null) + catch (Exception ex) { - if (caller == null) throw new ArgumentNullException(nameof(caller)); - return new TempFolder(caller); + Debug.WriteLine(ex); } + } - public string AllocateFilename(string? ext = null) - { - return System.IO.Path.Combine(Path, Guid.NewGuid().ToString("n") + "." + (ext ?? "tmp")); - } + public static TempFolder ForCaller([CallerMemberName] string? caller = null) + { + if (caller == null) throw new ArgumentNullException(nameof(caller)); + return new TempFolder(caller); + } + + public string AllocateFilename(string? ext = null) + { + return System.IO.Path.Combine(Path, Guid.NewGuid().ToString("n") + "." + (ext ?? "tmp")); } -} +} \ No newline at end of file diff --git a/test/SeqCli.Tests/Templates/TemplateWriterTests.cs b/test/SeqCli.Tests/Templates/TemplateWriterTests.cs index 49629e07..84707b47 100644 --- a/test/SeqCli.Tests/Templates/TemplateWriterTests.cs +++ b/test/SeqCli.Tests/Templates/TemplateWriterTests.cs @@ -29,8 +29,8 @@ public async Task WritesTemplates() Id = "test-stuff", Name = "Test Stuff", ReferencedId = "test-ref", - Numbers = new List { 1, 2, 3 }, - Strings = new List { "test" }, + Numbers = [1, 2, 3], + Strings = ["test"], Dictionary = new Dictionary{ ["First"] = "a" } };