如何捕获自定义启动的WebAPI Core服务的Kestrel标准输出?
捕获ASP.NET Core WebAPI服务的输出(不影响全局标准输出)
问题
我搭建了一个WebAPI Core服务,启动时标准输出会打印以下内容:
info: Microsoft.Hosting.Lifetime[14] Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[14] Now listening on: https://localhost:5001 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.
我希望将这些输出(以及其他输出)捕获为字符串。我采用非标准方式启动服务,自定义了HostTestService类:
public static class HostTestService { private static WebApplication? currentApp; public static async Task<List<string>?> StartService() { if (currentApp != null) { await StopService(); } var builder = WebApplication.CreateBuilder(); var app = builder.Build(); app.Start(); var server = app.Services.GetService<IServer>(); var addressFeature = server?.Features.Get<IServerAddressesFeature>(); var addresses = addressFeature?.Addresses.ToList(); // Set the Current app so that calls to shut it down will now succeed. currentApp = app; return addresses; } public static async Task StopService() { if (currentApp != null) { await currentApp.StopAsync(TimeSpan.FromSeconds(10)); await currentApp.DisposeAsync(); currentApp = null; } } }
我在NUnit测试中调用HostTestService.StartService()启动服务,但需要捕获该服务的输出,且不希望重定向所有标准输出(避免影响其他依赖标准输出的功能)。
解决方案
方法1:添加自定义日志提供程序(推荐)
ASP.NET Core的日志系统支持自定义提供程序,我们可以创建一个专门收集日志的提供程序,直接捕获服务的所有日志输出,完全不影响全局标准输出。
- 实现自定义日志提供程序和日志类:
public class CapturedLog { public LogLevel Level { get; set; } public string Message { get; set; } = string.Empty; } public class CustomLoggerProvider : ILoggerProvider { public List<CapturedLog> CapturedLogs { get; } = new(); public ILogger CreateLogger(string categoryName) { return new CustomLogger(this, categoryName); } public void Dispose() { } private class CustomLogger : ILogger { private readonly CustomLoggerProvider _provider; private readonly string _categoryName; public CustomLogger(CustomLoggerProvider provider, string categoryName) { _provider = provider; _categoryName = categoryName; } public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { var message = formatter(state, exception); _provider.CapturedLogs.Add(new CapturedLog { Level = logLevel, Message = $"[{_categoryName}] {message}" }); } } }
- 修改
HostTestService,添加日志提供程序并返回捕获的日志:
public static class HostTestService { private static WebApplication? currentApp; private static CustomLoggerProvider? _loggerProvider; public static async Task<(List<string>? Addresses, List<CapturedLog> CapturedLogs)> StartService() { if (currentApp != null) { await StopService(); } _loggerProvider = new CustomLoggerProvider(); var builder = WebApplication.CreateBuilder(); // 添加自定义日志提供程序 builder.Logging.ClearProviders(); // 可选:移除默认控制台提供,避免重复输出 builder.Logging.AddProvider(_loggerProvider); var app = builder.Build(); app.Start(); var server = app.Services.GetService<IServer>(); var addressFeature = server?.Features.Get<IServerAddressesFeature>(); var addresses = addressFeature?.Addresses.ToList(); currentApp = app; return (addresses, _loggerProvider.CapturedLogs); } public static async Task StopService() { if (currentApp != null) { await currentApp.StopAsync(TimeSpan.FromSeconds(10)); await currentApp.DisposeAsync(); currentApp = null; _loggerProvider = null; } } }
- 在NUnit测试中获取日志:
[Test] public async Task TestServiceOutput() { var result = await HostTestService.StartService(); var capturedLogs = result.CapturedLogs; // 遍历capturedLogs即可获取所有输出内容 foreach (var log in capturedLogs) { Console.WriteLine($"Captured: {log.Message}"); // 这里用全局输出是测试用,实际可以直接处理字符串 } await HostTestService.StopService(); }
方法2:临时重定向控制台输出流
如果需要保留默认日志格式但仅捕获该服务的输出,可以临时替换Console.Out为内存流,启动服务后再恢复原流。这种方式要注意线程安全,避免影响其他输出。
修改HostTestService如下:
public static class HostTestService { private static WebApplication? currentApp; private static StringWriter? _outputWriter; private static TextWriter? _originalOut; public static async Task<(List<string>? Addresses, string CapturedOutput)> StartService() { if (currentApp != null) { await StopService(); } // 保存原始输出流,创建内存流捕获输出 _originalOut = Console.Out; _outputWriter = new StringWriter(); Console.SetOut(_outputWriter); var builder = WebApplication.CreateBuilder(); var app = builder.Build(); app.Start(); // 恢复原始输出流,避免影响后续操作 Console.SetOut(_originalOut); var server = app.Services.GetService<IServer>(); var addressFeature = server?.Features.Get<IServerAddressesFeature>(); var addresses = addressFeature?.Addresses.ToList(); currentApp = app; var capturedOutput = _outputWriter.ToString(); return (addresses, capturedOutput); } public static async Task StopService() { if (currentApp != null) { await currentApp.StopAsync(TimeSpan.FromSeconds(10)); await currentApp.DisposeAsync(); currentApp = null; _outputWriter?.Dispose(); _outputWriter = null; _originalOut = null; } } }
说明
- 方法1是ASP.NET Core推荐的方式,更灵活,能精准控制捕获的日志级别、分类,完全隔离全局输出。
- 方法2适合需要保留原始控制台输出格式的场景,但要注意启动服务过程中如果有其他线程写入控制台,可能会被意外捕获,所以建议在测试中确保只有当前服务启动时使用该方式。
内容的提问来源于stack exchange,提问作者Vaccano
相关产品推荐
相关产品推荐

