Add WireMockAspNetCoreLogger to log Kestrel warnings/errors (#1432)

* Add WireMockAspNetCoreLogger

* fix tests

* x

* .
This commit is contained in:
Stef Heyenrath
2026-03-14 10:30:56 +01:00
committed by GitHub
parent d08ce944b6
commit ca788cb9b0
9 changed files with 159 additions and 37 deletions

View File

@@ -8,6 +8,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using SharpYaml.Model;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Models; using WireMock.Models;
@@ -288,7 +289,24 @@ namespace WireMock.Net.ConsoleApplication
var todos = new Dictionary<int, Todo>(); var todos = new Dictionary<int, Todo>();
var server = WireMockServer.Start(); var server = WireMockServer.Start(new WireMockServerSettings
{
Logger = new WireMockConsoleLogger(),
Port = 9091
});
server
.WhenRequest(r => r
.WithPath("/Content-Length")
.UsingAnyMethod()
)
.ThenRespondWith(r => r
.WithStatusCode(HttpStatusCode.OK)
.WithHeader("Content-Length", "42")
);
System.Console.ReadLine();
//server //server
// .Given(Request.Create() // .Given(Request.Create()

View File

@@ -0,0 +1,38 @@
// Copyright © WireMock.Net
using Microsoft.Extensions.Logging;
namespace WireMock.Logging;
internal sealed class WireMockAspNetCoreLogger(IWireMockLogger logger, string categoryName) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Warning;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
var message = formatter(state, exception);
if (exception != null)
{
message = $"{message} | Exception: {exception}";
}
switch (logLevel)
{
case LogLevel.Warning:
logger.Warn("[{0}] {1}", categoryName, message);
break;
default:
logger.Error("[{0}] {1}", categoryName, message);
break;
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright © WireMock.Net
using Microsoft.Extensions.Logging;
namespace WireMock.Logging;
internal sealed class WireMockAspNetCoreLoggerProvider : ILoggerProvider
{
private readonly IWireMockLogger _logger;
public WireMockAspNetCoreLoggerProvider(IWireMockLogger logger)
{
_logger = logger;
}
public ILogger CreateLogger(string categoryName) => new WireMockAspNetCoreLogger(_logger, categoryName);
public void Dispose() { }
}

View File

@@ -1,9 +1,9 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Stef.Validation; using Stef.Validation;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Owin.Mappers; using WireMock.Owin.Mappers;
@@ -57,6 +57,12 @@ internal partial class AspNetCoreSelfHost
_host = builder _host = builder
.UseSetting("suppressStatusMessages", "True") // https://andrewlock.net/suppressing-the-startup-and-shutdown-messages-in-asp-net-core/ .UseSetting("suppressStatusMessages", "True") // https://andrewlock.net/suppressing-the-startup-and-shutdown-messages-in-asp-net-core/
.ConfigureAppConfigurationUsingEnvironmentVariables() .ConfigureAppConfigurationUsingEnvironmentVariables()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddProvider(new WireMockAspNetCoreLoggerProvider(_logger));
logging.SetMinimumLevel(LogLevel.Warning);
})
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddSingleton(_wireMockMiddlewareOptions); services.AddSingleton(_wireMockMiddlewareOptions);
@@ -169,10 +175,10 @@ internal partial class AspNetCoreSelfHost
return _host.RunAsync(token); return _host.RunAsync(token);
} }
catch (Exception e) catch (Exception ex)
{ {
RunningException = e; RunningException = ex;
_logger.Error(e.ToString()); _logger.Error("Error while RunAsync", ex);
IsStarted = false; IsStarted = false;

View File

@@ -1,9 +1,7 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Net; using System.Net;
using System.Reflection;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -88,19 +86,15 @@ namespace WireMock.Owin.Mappers
break; break;
} }
var statusCodeType = responseMessage.StatusCode?.GetType(); if (responseMessage.StatusCode is HttpStatusCode or int)
if (statusCodeType != null)
{ {
if (statusCodeType == typeof(int) || statusCodeType == typeof(int?) || statusCodeType.GetTypeInfo().IsEnum) response.StatusCode = MapStatusCode((int) responseMessage.StatusCode);
{ }
response.StatusCode = MapStatusCode((int)responseMessage.StatusCode!); else if (responseMessage.StatusCode is string statusCodeAsString)
} {
else if (statusCodeType == typeof(string)) // Note: this case will also match on null
{ _ = int.TryParse(statusCodeAsString, out var statusCodeTypeAsInt);
// Note: this case will also match on null response.StatusCode = MapStatusCode(statusCodeTypeAsInt);
int.TryParse(responseMessage.StatusCode as string, out var statusCodeTypeAsInt);
response.StatusCode = MapStatusCode(statusCodeTypeAsInt);
}
} }
SetResponseHeaders(responseMessage, bytes != null, response); SetResponseHeaders(responseMessage, bytes != null, response);

View File

@@ -3,9 +3,7 @@
// This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License. // This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License.
// For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root.
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using AnyOfTypes; using AnyOfTypes;
using JetBrains.Annotations; using JetBrains.Annotations;
using JsonConverter.Newtonsoft.Json; using JsonConverter.Newtonsoft.Json;

View File

@@ -27,7 +27,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
message.Headers.GetValues("x").Should().Equal(new[] { "value-1" }); message.Headers.GetValues("x").Should().Equal(["value-1"]);
} }
[Fact] [Fact]
@@ -101,7 +101,7 @@ public class HttpRequestMessageHelperTests
// Assert // Assert
(await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}"); (await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}");
message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/json" }); message.Content.Headers.GetValues("Content-Type").Should().Equal(["application/json"]);
} }
[Fact] [Fact]
@@ -121,7 +121,7 @@ public class HttpRequestMessageHelperTests
// Assert // Assert
(await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}"); (await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}");
message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/json; charset=utf-8" }); message.Content.Headers.GetValues("Content-Type").Should().Equal(["application/json; charset=utf-8"]);
} }
[Fact] [Fact]
@@ -142,7 +142,7 @@ public class HttpRequestMessageHelperTests
// Assert // Assert
(await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}"); (await message.Content!.ReadAsStringAsync(_ct)).Should().Be("{\"x\":42}");
message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "multipart/form-data" }); message.Content.Headers.GetValues("Content-Type").Should().Equal(["multipart/form-data"]);
} }
@@ -162,7 +162,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
message.Content!.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/xml" }); message.Content!.Headers.GetValues("Content-Type").Should().Equal(["application/xml"]);
} }
[Fact] [Fact]
@@ -181,7 +181,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
message.Content!.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/xml; charset=UTF-8" }); message.Content!.Headers.GetValues("Content-Type").Should().Equal(["application/xml; charset=UTF-8"]);
} }
[Fact] [Fact]
@@ -200,7 +200,7 @@ public class HttpRequestMessageHelperTests
var message = HttpRequestMessageHelper.Create(request, "http://url"); var message = HttpRequestMessageHelper.Create(request, "http://url");
// Assert // Assert
message.Content!.Headers.GetValues("Content-Type").Should().Equal(new[] { "application/xml; charset=Ascii" }); message.Content!.Headers.GetValues("Content-Type").Should().Equal(["application/xml; charset=Ascii"]);
} }
[Fact] [Fact]
@@ -242,7 +242,7 @@ public class HttpRequestMessageHelperTests
// Assert // Assert
(await message.Content!.ReadAsStringAsync(_ct)).Should().Be(body); (await message.Content!.ReadAsStringAsync(_ct)).Should().Be(body);
message.Content.Headers.GetValues("Content-Type").Should().Equal(new[] { "multipart/form-data" }); message.Content.Headers.GetValues("Content-Type").Should().Equal(["multipart/form-data"]);
} }
[Theory] [Theory]
@@ -269,7 +269,4 @@ public class HttpRequestMessageHelperTests
// Assert // Assert
message.Content?.Headers.ContentLength.Should().Be(resultShouldBe ? value : null); message.Content?.Headers.ContentLength.Should().Be(resultShouldBe ? value : null);
} }
} }

View File

@@ -706,7 +706,7 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc
public async Task WithWebSocketProxy_Should_Proxy_Multiple_TextMessages() public async Task WithWebSocketProxy_Should_Proxy_Multiple_TextMessages()
{ {
// Arrange - Start target echo server // Arrange - Start target echo server
using var exampleEchoServer = WireMockServer.Start(new WireMockServerSettings var exampleEchoServer = WireMockServer.Start(new WireMockServerSettings
{ {
Logger = new TestOutputHelperWireMockLogger(output), Logger = new TestOutputHelperWireMockLogger(output),
Urls = ["ws://localhost:0"] Urls = ["ws://localhost:0"]
@@ -722,7 +722,7 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc
); );
// Arrange - Start proxy server // Arrange - Start proxy server
using var sut = WireMockServer.Start(new WireMockServerSettings var sut = WireMockServer.Start(new WireMockServerSettings
{ {
Logger = new TestOutputHelperWireMockLogger(output), Logger = new TestOutputHelperWireMockLogger(output),
Urls = ["ws://localhost:0"] Urls = ["ws://localhost:0"]
@@ -755,6 +755,14 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc
} }
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct); await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test complete", _ct);
await Task.Delay(250, _ct);
sut.Stop();
sut.Dispose();
exampleEchoServer.Stop();
exampleEchoServer.Dispose();
} }
[Fact] [Fact]

View File

@@ -431,8 +431,13 @@ public partial class WireMockServerTests(ITestOutputHelper testOutputHelper)
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server server
.Given(Request.Create().WithPath(path).UsingHead()) .WhenRequest(r => r
.RespondWith(Response.Create().WithHeader(HttpKnownHeaderNames.ContentLength, length)); .WithPath(path)
.UsingHead()
)
.ThenRespondWith(r => r
.WithHeader(HttpKnownHeaderNames.ContentLength, length)
);
// Act // Act
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, path); var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, path);
@@ -442,6 +447,45 @@ public partial class WireMockServerTests(ITestOutputHelper testOutputHelper)
response.Content.Headers.GetValues(HttpKnownHeaderNames.ContentLength).Should().Contain(length); response.Content.Headers.GetValues(HttpKnownHeaderNames.ContentLength).Should().Contain(length);
} }
#if NET8_0_OR_GREATER
[Theory]
[InlineData("DELETE")]
[InlineData("GET")]
[InlineData("OPTIONS")]
[InlineData("PATCH")]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("TRACE")]
public async Task WireMockServer_Should_LogAndThrowExceptionWhenInvalidContentLength(string method)
{
// Assign
const string length = "42";
var path = $"/InvalidContentLength_{Guid.NewGuid()}";
using var server = WireMockServer.Start(new WireMockServerSettings
{
Logger = new TestOutputHelperWireMockLogger(testOutputHelper)
});
server
.WhenRequest(r => r
.WithPath(path)
.UsingAnyMethod()
)
.ThenRespondWith(r => r
.WithStatusCode(HttpStatusCode.OK)
.WithHeader(HttpKnownHeaderNames.ContentLength, length)
);
// Act
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Parse(method), path);
var response = await server.CreateClient().SendAsync(httpRequestMessage, _ct);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
testOutputHelper.Output.Should().Contain($"Response Content-Length mismatch: too few bytes written (0 of {length}).");
}
#endif
[Theory] [Theory]
[InlineData("TRACE")] [InlineData("TRACE")]
[InlineData("GET")] [InlineData("GET")]