|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Enrichment;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Extensions.Http.Diagnostics.Test.Logging.Internal;
using Microsoft.Extensions.Http.Logging.Internal;
using Microsoft.Extensions.Http.Logging.Test.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Telemetry.Internal;
using Microsoft.Extensions.Time.Testing;
using Microsoft.Shared.Collections;
using Moq;
using Xunit;
namespace Microsoft.Extensions.Http.Logging.Test;
public class HttpClientLoggerTest
{
private const string TestRequestHeader = "Request-Header";
private const string TestResponseHeader = "Response-Header";
private const string TestExpectedRequestHeaderKey = $"{HttpClientLoggingTagNames.RequestHeaderPrefix}request-header";
private const string TestExpectedResponseHeaderKey = $"{HttpClientLoggingTagNames.ResponseHeaderPrefix}response-header";
private const string TextPlain = "text/plain";
private const string Redacted = "REDACTED";
private readonly Fixture _fixture;
public HttpClientLoggerTest()
{
_fixture = new();
}
[Fact]
public async Task SendAsync_NullRequest_ThrowsException()
{
var responseCode = _fixture.Create<HttpStatusCode>();
using var httpResponseMessage = new HttpResponseMessage { StatusCode = responseCode };
var options = new LoggingOptions
{
BodyReadTimeout = TimeSpan.FromMinutes(5)
};
using var handler = new TestLoggingHandler(
new HttpClientLogger(
NullLogger<HttpClientLogger>.Instance,
Mock.Of<IHttpRequestReader>(),
Empty.Enumerable<IHttpClientLogEnricher>(),
options),
new TestingHandlerStub((_, _) => Task.FromResult(httpResponseMessage)));
using var client = new HttpClient(handler);
var act = async () =>
await client.SendAsync(null!, It.IsAny<CancellationToken>()).ConfigureAwait(false);
await Assert.ThrowsAsync<ArgumentNullException>(act);
}
[Fact]
public async Task SendAsync_HttpRequestException_ThrowsException()
{
var input = _fixture.Create<string>();
var exception = new HttpRequestException();
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new("http://default-uri.com"),
Content = new StringContent(input, Encoding.UTF8, TextPlain)
};
var options = new LoggingOptions();
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object);
using var handler = new TestLoggingHandler(
new HttpClientLogger(
NullLogger<HttpClientLogger>.Instance,
new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of<IHttpRouteParser>(), headersReader, RequestMetadataContext),
Enumerable.Empty<IHttpClientLogEnricher>(),
options),
new TestingHandlerStub((_, _) => throw exception));
using var client = new HttpClient(handler);
var act = async () =>
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>()).ConfigureAwait(false);
var actualException = await Assert.ThrowsAsync<HttpRequestException>(act);
Assert.Same(exception, actualException);
}
[Fact]
public async Task Logger_WhenReadRequestAsyncThrows_DoesntThrow()
{
using var cancellationTokenSource = new CancellationTokenSource();
var input = _fixture.Create<string>();
var operationCanceledException = new OperationCanceledException();
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new("http://default-uri.com"),
Content = new StringContent(input, Encoding.UTF8, TextPlain)
};
var requestReaderMock = new Mock<IHttpRequestReader>(MockBehavior.Strict);
requestReaderMock.Setup(e =>
e.ReadRequestAsync(It.IsAny<LogRecord>(),
It.IsAny<HttpRequestMessage>(),
It.IsAny<List<KeyValuePair<string, string>>>(),
It.IsAny<CancellationToken>()))
.Throws(operationCanceledException);
var mockedRequestReader = new MockedRequestReader(requestReaderMock);
var options = new LoggingOptions();
using var handler = new TestLoggingHandler(
new HttpClientLogger(
NullLogger<HttpClientLogger>.Instance,
mockedRequestReader,
Enumerable.Empty<IHttpClientLogEnricher>(),
options),
new TestingHandlerStub((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))));
using var httpClient = new HttpClient(handler);
var act = async () =>
await httpClient.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>()).ConfigureAwait(false);
var exception = await Record.ExceptionAsync(act);
Assert.Null(exception);
}
[Fact]
public async Task HttpLoggingHandler_AllOptions_LogsOutgoingRequest()
{
var requestContent = _fixture.Create<string>();
var responseContent = _fixture.Create<string>();
var testRequestHeaderValue = _fixture.Create<string>();
var testResponseHeaderValue = _fixture.Create<string>();
var testEnricher = new TestEnricher();
var testSharedRequestHeaderKey = $"{HttpClientLoggingTagNames.RequestHeaderPrefix}header3";
var testSharedResponseHeaderKey = $"{HttpClientLoggingTagNames.ResponseHeaderPrefix}header3";
var expectedLogRecord = new LogRecord
{
Host = "default-uri.com",
Method = HttpMethod.Post,
Path = "foo/bar",
StatusCode = 200,
ResponseHeaders = [new(TestExpectedResponseHeaderKey, Redacted), new(testSharedResponseHeaderKey, Redacted)],
RequestHeaders = [new(TestExpectedRequestHeaderKey, Redacted), new(testSharedRequestHeaderKey, Redacted)],
RequestBody = requestContent,
ResponseBody = responseContent,
EnrichmentTags = testEnricher.EnrichmentBag
};
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"),
Content = new StringContent(requestContent, Encoding.UTF8, TextPlain)
};
httpRequestMessage.Headers.Add(TestRequestHeader, testRequestHeaderValue);
httpRequestMessage.Headers.Add("Header3", testRequestHeaderValue);
using var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseContent, Encoding.UTF8, TextPlain),
};
httpResponseMessage.Headers.Add(TestResponseHeader, testResponseHeaderValue);
httpResponseMessage.Headers.Add("Header3", testRequestHeaderValue);
var options = new LoggingOptions
{
ResponseHeadersDataClasses = new Dictionary<string, DataClassification> { { TestResponseHeader, FakeTaxonomy.PrivateData }, { "Header3", FakeTaxonomy.PrivateData } },
RequestHeadersDataClasses = new Dictionary<string, DataClassification> { { TestRequestHeader, FakeTaxonomy.PrivateData }, { "Header3", FakeTaxonomy.PrivateData } },
ResponseBodyContentTypes = new HashSet<string> { TextPlain },
RequestBodyContentTypes = new HashSet<string> { TextPlain },
BodySizeLimit = 32000,
BodyReadTimeout = TimeSpan.FromMinutes(5),
RequestPathLoggingMode = OutgoingPathLoggingMode.Structured,
LogRequestStart = false,
LogBody = true
};
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object);
var fakeLogger = new FakeLogger<HttpClientLogger>();
var logger = new HttpClientLogger(
fakeLogger,
new HttpRequestReader(
options,
GetHttpRouteFormatter(),
Mock.Of<IHttpRouteParser>(),
headersReader, RequestMetadataContext),
new List<IHttpClientLogEnricher> { testEnricher },
options);
using var handler = new TestLoggingHandler(
logger,
new TestingHandlerStub((_, _) => Task.FromResult(httpResponseMessage)));
using var client = new HttpClient(handler);
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecords = fakeLogger.Collector.GetSnapshot();
var logRecord = Assert.Single(logRecords);
var logRecordState = logRecord.GetStructuredState();
logRecordState.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host);
logRecordState.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString());
logRecordState.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted);
logRecordState.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration);
logRecordState.Contains(HttpClientLoggingTagNames.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture));
logRecordState.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody);
logRecordState.Contains(HttpClientLoggingTagNames.ResponseBody, expectedLogRecord.ResponseBody);
logRecordState.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders[0].Value);
logRecordState.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders[0].Value);
logRecordState.Contains(testSharedResponseHeaderKey, expectedLogRecord.ResponseHeaders[1].Value);
logRecordState.Contains(testSharedRequestHeaderKey, expectedLogRecord.RequestHeaders[1].Value);
logRecordState.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key));
}
[Fact]
public async Task HttpLoggingHandler_AllOptionsWithLogRequestStart_LogsOutgoingRequestWithTwoRecords()
{
var requestContent = _fixture.Create<string>();
var responseContent = _fixture.Create<string>();
var requestHeaderValue = _fixture.Create<string>();
var responseHeaderValue = _fixture.Create<string>();
var testEnricher = new TestEnricher();
var expectedLogRecord = new LogRecord
{
Host = "default-uri.com",
Method = HttpMethod.Post,
Path = "foo/bar",
StatusCode = 200,
ResponseHeaders = [new(TestResponseHeader, Redacted)],
RequestHeaders = [new(TestRequestHeader, Redacted)],
RequestBody = requestContent,
ResponseBody = responseContent,
EnrichmentTags = testEnricher.EnrichmentBag
};
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"),
Content = new StringContent(requestContent, Encoding.UTF8, TextPlain)
};
httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue);
using var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseContent, Encoding.UTF8, TextPlain),
};
httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue);
var options = new LoggingOptions
{
ResponseHeadersDataClasses = new Dictionary<string, DataClassification> { { TestResponseHeader, FakeTaxonomy.PrivateData } },
RequestHeadersDataClasses = new Dictionary<string, DataClassification> { { TestRequestHeader, FakeTaxonomy.PrivateData } },
ResponseBodyContentTypes = new HashSet<string> { TextPlain },
RequestBodyContentTypes = new HashSet<string> { TextPlain },
BodySizeLimit = 32000,
BodyReadTimeout = TimeSpan.FromMinutes(5),
RequestPathLoggingMode = OutgoingPathLoggingMode.Structured,
LogRequestStart = true,
LogBody = true
};
var fakeLogger = new FakeLogger<HttpClientLogger>(
new FakeLogCollector(
Options.Options.Create(
new FakeLogCollectorOptions())));
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor
.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object);
var logger = new HttpClientLogger(
fakeLogger,
new HttpRequestReader(
options,
GetHttpRouteFormatter(),
Mock.Of<IHttpRouteParser>(),
headersReader, RequestMetadataContext),
new List<IHttpClientLogEnricher> { testEnricher },
options);
using var handler = new TestLoggingHandler(
logger,
new TestingHandlerStub((_, _) => Task.FromResult(httpResponseMessage)));
using var client = new HttpClient(handler);
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecords = fakeLogger.Collector.GetSnapshot();
Assert.Equal(2, logRecords.Count);
var logRecordRequest = logRecords[0].GetStructuredState();
logRecordRequest.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host);
logRecordRequest.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString());
logRecordRequest.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted);
logRecordRequest.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody);
logRecordRequest.NotContains(HttpClientLoggingTagNames.StatusCode);
logRecordRequest.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value);
logRecordRequest.NotContains(testEnricher.KvpRequest.Key);
var logRecordFull = logRecords[1].GetStructuredState();
logRecordFull.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host);
logRecordFull.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString());
logRecordFull.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted);
logRecordFull.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration);
logRecordFull.Contains(HttpClientLoggingTagNames.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture));
logRecordFull.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody);
logRecordFull.Contains(HttpClientLoggingTagNames.ResponseBody, expectedLogRecord.ResponseBody);
logRecordFull.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value);
logRecordFull.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value);
logRecordFull.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key));
logRecordFull.Contains(testEnricher.KvpResponse.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpResponse.Key));
}
[Fact]
public async Task HttpLoggingHandler_AllOptionsSendAsyncFailed_LogsRequestInformation()
{
var requestContent = _fixture.Create<string>();
var responseContent = _fixture.Create<string>();
var requestHeaderValue = _fixture.Create<string>();
var responseHeaderValue = _fixture.Create<string>();
var testEnricher = new TestEnricher();
var expectedLogRecord = new LogRecord
{
Host = "default-uri.com",
Method = HttpMethod.Post,
Path = "foo/bar",
StatusCode = 200,
ResponseHeaders = [new(TestResponseHeader, Redacted)],
RequestHeaders = [new(TestRequestHeader, Redacted)],
RequestBody = requestContent,
ResponseBody = responseContent,
EnrichmentTags = testEnricher.EnrichmentBag
};
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"),
Content = new StringContent(requestContent, Encoding.UTF8, TextPlain)
};
httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue);
using var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseContent, Encoding.UTF8, TextPlain),
};
httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue);
var options = new LoggingOptions
{
ResponseHeadersDataClasses = new Dictionary<string, DataClassification> { { TestResponseHeader, FakeTaxonomy.PrivateData } },
RequestHeadersDataClasses = new Dictionary<string, DataClassification> { { TestRequestHeader, FakeTaxonomy.PrivateData } },
ResponseBodyContentTypes = new HashSet<string> { TextPlain },
RequestBodyContentTypes = new HashSet<string> { TextPlain },
BodySizeLimit = 32000,
BodyReadTimeout = TimeSpan.FromMinutes(5),
RequestPathLoggingMode = OutgoingPathLoggingMode.Structured,
LogRequestStart = false,
LogBody = true
};
var fakeLogger = new FakeLogger<HttpClientLogger>(new FakeLogCollector(Options.Options.Create(new FakeLogCollectorOptions())));
var exception = new TaskCanceledException();
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object);
using var handler = new TestLoggingHandler(
new HttpClientLogger(
fakeLogger,
new HttpRequestReader(
options,
GetHttpRouteFormatter(),
Mock.Of<IHttpRouteParser>(),
headersReader, RequestMetadataContext),
new List<IHttpClientLogEnricher> { testEnricher },
options),
new TestingHandlerStub((_, _) => throw exception));
using var client = new HttpClient(handler);
var act = () => client.SendAsync(httpRequestMessage, CancellationToken.None);
await Assert.ThrowsAsync<TaskCanceledException>(act);
var logRecords = fakeLogger.Collector.GetSnapshot();
var logRecord = Assert.Single(logRecords);
Assert.Equal($"{httpRequestMessage.Method} {httpRequestMessage.RequestUri.Host}/{TelemetryConstants.Redacted}", logRecord.Message);
Assert.Same(exception, logRecord.Exception);
var logRecordState = logRecord.GetStructuredState();
logRecordState.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host);
logRecordState.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString());
logRecordState.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted);
logRecordState.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody);
logRecordState.NotContains(HttpClientLoggingTagNames.ResponseBody);
logRecordState.NotContains(HttpClientLoggingTagNames.StatusCode);
logRecordState.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value);
logRecordState.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key));
logRecordState.NotContains(testEnricher.KvpResponse.Key);
logRecordState.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration);
Assert.DoesNotContain(logRecordState, kvp => kvp.Key.StartsWith(HttpClientLoggingTagNames.ResponseHeaderPrefix));
}
[Fact(Skip = "Flaky test, see https://github.com/dotnet/extensions/issues/4530")]
public async Task HttpLoggingHandler_ReadResponseThrows_LogsException()
{
var requestContent = _fixture.Create<string>();
var responseContent = _fixture.Create<string>();
var requestHeaderValue = _fixture.Create<string>();
var responseHeaderValue = _fixture.Create<string>();
var testEnricher = new TestEnricher();
var expectedLogRecord = new LogRecord
{
Host = "default-uri.com",
Method = HttpMethod.Post,
Path = "foo/bar",
StatusCode = 200,
ResponseHeaders = [new(TestResponseHeader, Redacted)],
RequestHeaders = [new(TestRequestHeader, Redacted)],
RequestBody = requestContent,
ResponseBody = responseContent,
EnrichmentTags = testEnricher.EnrichmentBag
};
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"),
Content = new StringContent(requestContent, Encoding.UTF8, TextPlain)
};
httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue);
using var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseContent, Encoding.UTF8, TextPlain),
};
httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue);
var options = new LoggingOptions
{
ResponseHeadersDataClasses = new Dictionary<string, DataClassification> { { TestResponseHeader, FakeTaxonomy.PrivateData } },
RequestHeadersDataClasses = new Dictionary<string, DataClassification> { { TestRequestHeader, FakeTaxonomy.PrivateData } },
ResponseBodyContentTypes = new HashSet<string> { TextPlain },
RequestBodyContentTypes = new HashSet<string> { TextPlain },
BodySizeLimit = 32000,
BodyReadTimeout = TimeSpan.FromMinutes(5),
RequestPathLoggingMode = OutgoingPathLoggingMode.Structured,
LogRequestStart = false,
LogBody = true
};
var fakeLogger = new FakeLogger<HttpClientLogger>(
new FakeLogCollector(
Options.Options.Create(new FakeLogCollectorOptions())));
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor
.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object);
var exception = new InvalidOperationException("test");
var actualRequestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of<IHttpRouteParser>(), headersReader, RequestMetadataContext);
var mockedRequestReader = new Mock<IHttpRequestReader>();
mockedRequestReader
.Setup(m =>
m.ReadRequestAsync(// so this method is not mocked
It.IsAny<LogRecord>(),
It.IsAny<HttpRequestMessage>(),
It.IsAny<List<KeyValuePair<string, string>>>(),
It.IsAny<CancellationToken>()))
.Returns((LogRecord a, HttpRequestMessage b, List<KeyValuePair<string, string>> c, CancellationToken d) =>
actualRequestReader.ReadRequestAsync(a, b, c, d));
mockedRequestReader
.Setup(m =>
m.ReadResponseAsync(// but this method is setup to throw an exception
It.IsAny<LogRecord>(),
It.IsAny<HttpResponseMessage>(),
It.IsAny<List<KeyValuePair<string, string>>>(),
It.IsAny<CancellationToken>()))
.ThrowsAsync(exception);
using var handler = new TestLoggingHandler(
new HttpClientLogger(
fakeLogger,
mockedRequestReader.Object,
new List<IHttpClientLogEnricher> { testEnricher },
options),
new TestingHandlerStub((_, _) => Task.FromResult(httpResponseMessage)));
using var client = new HttpClient(handler);
var act = async () => await client
.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>())
.ConfigureAwait(false);
await Assert.ThrowsAsync<InvalidOperationException>(act);
var logRecords = fakeLogger.Collector.GetSnapshot();
var logRecord = Assert.Single(logRecords);
var logRecordState = logRecord.GetStructuredState();
logRecordState.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host);
logRecordState.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString());
logRecordState.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted);
logRecordState.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody);
logRecordState.NotContains(HttpClientLoggingTagNames.ResponseBody);
logRecordState.NotContains(HttpClientLoggingTagNames.StatusCode);
logRecordState.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value);
logRecordState.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key));
logRecordState.Contains(testEnricher.KvpResponse.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpResponse.Key));
logRecordState.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration);
Assert.DoesNotContain(logRecordState, kvp => kvp.Key.StartsWith(HttpClientLoggingTagNames.ResponseHeaderPrefix));
}
[Fact]
public async Task HttpLoggingHandler_AllOptionsTransferEncodingIsNotChunked_LogsOutgoingRequest()
{
var requestContent = _fixture.Create<string>();
var responseContent = _fixture.Create<string>();
var requestHeaderValue = _fixture.Create<string>();
var responseHeaderValue = _fixture.Create<string>();
var testEnricher = new TestEnricher();
var expectedLogRecord = new LogRecord
{
Host = "default-uri.com",
Method = HttpMethod.Post,
Path = "foo/bar",
StatusCode = 200,
ResponseHeaders = [new(TestResponseHeader, Redacted)],
RequestHeaders = [new(TestRequestHeader, Redacted)],
RequestBody = requestContent,
ResponseBody = responseContent,
EnrichmentTags = testEnricher.EnrichmentBag,
};
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"),
Content = new StringContent(requestContent, Encoding.UTF8, TextPlain)
};
httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue);
using var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseContent, Encoding.UTF8, TextPlain),
};
httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue);
httpResponseMessage.Headers.TransferEncoding.Add(new("compress"));
var options = new LoggingOptions
{
ResponseHeadersDataClasses = new Dictionary<string, DataClassification> { { TestResponseHeader, FakeTaxonomy.PrivateData } },
RequestHeadersDataClasses = new Dictionary<string, DataClassification> { { TestRequestHeader, FakeTaxonomy.PrivateData } },
ResponseBodyContentTypes = new HashSet<string> { TextPlain },
RequestBodyContentTypes = new HashSet<string> { TextPlain },
BodySizeLimit = 32000,
BodyReadTimeout = TimeSpan.FromMinutes(5),
RequestPathLoggingMode = OutgoingPathLoggingMode.Structured,
LogRequestStart = false,
LogBody = true
};
var fakeLogger = new FakeLogger<HttpClientLogger>(new FakeLogCollector(Options.Options.Create(new FakeLogCollectorOptions())));
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object);
using var handler = new TestLoggingHandler(
new HttpClientLogger(
fakeLogger,
new HttpRequestReader(
options,
GetHttpRouteFormatter(),
Mock.Of<IHttpRouteParser>(),
headersReader, RequestMetadataContext),
new List<IHttpClientLogEnricher> { testEnricher },
options),
new TestingHandlerStub((_, _) => Task.FromResult(httpResponseMessage)));
using var client = new HttpClient(handler);
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecords = fakeLogger.Collector.GetSnapshot();
var logRecord = Assert.Single(logRecords);
var logRecordState = logRecord.GetStructuredState();
logRecordState.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host);
logRecordState.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString());
logRecordState.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted);
logRecordState.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration);
logRecordState.Contains(HttpClientLoggingTagNames.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture));
logRecordState.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody);
logRecordState.Contains(HttpClientLoggingTagNames.ResponseBody, expectedLogRecord.ResponseBody);
logRecordState.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value);
logRecordState.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value);
logRecordState.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key));
}
[Fact]
public async Task HttpLoggingHandler_WithEnrichers_CallsEnrichMethodExactlyOnce()
{
var enricher1 = new Mock<IHttpClientLogEnricher>();
var enricher2 = new Mock<IHttpClientLogEnricher>();
var options = new LoggingOptions();
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var fakeLogger = new FakeLogger<HttpClientLogger>();
using var handler = new TestLoggingHandler(
new HttpClientLogger(
fakeLogger,
new HttpRequestReader(
options,
GetHttpRouteFormatter(),
Mock.Of<IHttpRouteParser>(),
new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object),
RequestMetadataContext),
new List<IHttpClientLogEnricher> { enricher1.Object, enricher2.Object },
options),
new TestingHandlerStub((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))));
using var client = new HttpClient(handler);
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://default-uri.com/foo/bar"),
Content = new StringContent(_fixture.Create<string>(), Encoding.UTF8, TextPlain)
};
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecords = fakeLogger.Collector.GetSnapshot();
_ = Assert.Single(logRecords);
enricher1.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
enricher2.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
}
[Fact]
public async Task HttpLoggingHandler_WithEnrichersAndLogRequestStart_CallsEnrichMethodExactlyOnce()
{
var enricher1 = new Mock<IHttpClientLogEnricher>();
var enricher2 = new Mock<IHttpClientLogEnricher>();
var options = new LoggingOptions
{
LogRequestStart = true
};
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var fakeLogger = new FakeLogger<HttpClientLogger>();
using var handler = new TestLoggingHandler(
new HttpClientLogger(
fakeLogger,
new HttpRequestReader(
options,
GetHttpRouteFormatter(),
Mock.Of<IHttpRouteParser>(),
new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object),
RequestMetadataContext),
new List<IHttpClientLogEnricher> { enricher1.Object, enricher2.Object },
options),
new TestingHandlerStub((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))));
using var client = new HttpClient(handler);
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://default-uri.com/foo/bar"),
Content = new StringContent(_fixture.Create<string>(), Encoding.UTF8, TextPlain)
};
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecords = fakeLogger.Collector.GetSnapshot();
Assert.Equal(2, logRecords.Count);
enricher1.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
enricher2.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
}
[Fact]
public async Task HttpLoggingHandler_WithEnrichers_OneEnricherThrows_LogsEnrichmentErrorAndRequest()
{
var exception = new ArgumentNullException();
var enricher1 = new Mock<IHttpClientLogEnricher>();
enricher1
.Setup(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()))
.Throws(exception);
var enricher2 = new Mock<IHttpClientLogEnricher>();
var fakeLogger = new FakeLogger<HttpClientLogger>();
var options = new LoggingOptions();
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), Mock.Of<IHttpHeadersRedactor>());
var requestReader = new HttpRequestReader(options, GetHttpRouteFormatter(), Mock.Of<IHttpRouteParser>(), headersReader, RequestMetadataContext);
var enrichers = new List<IHttpClientLogEnricher> { enricher1.Object, enricher2.Object };
using var handler = new TestLoggingHandler(
new HttpClientLogger(fakeLogger, requestReader, enrichers, options),
new TestingHandlerStub(
(_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))));
using var client = new HttpClient(handler);
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://default-uri.com/foo/bar"),
Content = new StringContent(_fixture.Create<string>(), Encoding.UTF8, TextPlain)
};
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecords = fakeLogger.Collector.GetSnapshot();
Assert.Equal(2, logRecords.Count);
Assert.Equal(nameof(Log.EnrichmentError), logRecords[0].Id.Name);
Assert.Equal(exception, logRecords[0].Exception);
Assert.Equal(nameof(Log.OutgoingRequest), logRecords[1].Id.Name);
enricher1.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
enricher2.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
}
[Fact]
public async Task HttpLoggingHandler_WithEnrichers_SendAsyncAndOneEnricher_LogsEnrichmentErrorAndRequestError()
{
var enrichmentException = new ArgumentNullException();
var enricher1 = new Mock<IHttpClientLogEnricher>();
enricher1
.Setup(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()))
.Throws(enrichmentException)
.Verifiable();
var enricher2 = new Mock<IHttpClientLogEnricher>();
var fakeLogger = new FakeLogger<HttpClientLogger>();
var sendAsyncException = new TaskCanceledException();
using var handler = new TestLoggingHandler(
new HttpClientLogger(
fakeLogger,
Mock.Of<IHttpRequestReader>(),
new List<IHttpClientLogEnricher> { enricher1.Object, enricher2.Object },
new LoggingOptions()),
new TestingHandlerStub((_, _) => throw sendAsyncException));
using var client = new HttpClient(handler);
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://default-uri.com/foo/bar"),
Content = new StringContent(_fixture.Create<string>(), Encoding.UTF8, TextPlain)
};
var act = () => client.SendAsync(httpRequestMessage, CancellationToken.None);
await Assert.ThrowsAsync<TaskCanceledException>(act);
var logRecords = fakeLogger.Collector.GetSnapshot();
Assert.Equal(2, logRecords.Count);
Assert.Equal(nameof(Log.EnrichmentError), logRecords[0].Id.Name);
Assert.Equal(enrichmentException, logRecords[0].Exception);
Assert.Equal(nameof(Log.OutgoingRequestError), logRecords[1].Id.Name);
Assert.Equal(sendAsyncException, logRecords[1].Exception);
enricher1.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
enricher2.Verify(e => e.Enrich(It.IsAny<IEnrichmentTagCollector>(), It.IsAny<HttpRequestMessage>(), It.IsAny<HttpResponseMessage>(), It.IsAny<Exception>()), Times.Exactly(1));
}
[Fact]
public async Task HttpLoggingHandler_AllOptionsTransferEncodingChunked_LogsOutgoingRequest()
{
var requestInput = _fixture.Create<string>();
var responseInput = _fixture.Create<string>();
var requestHeaderValue = _fixture.Create<string>();
var responseHeaderValue = _fixture.Create<string>();
var fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var testEnricher = new TestEnricher();
var expectedLogRecord = new LogRecord
{
Host = "default-uri.com",
Method = HttpMethod.Post,
Path = "foo/bar",
Duration = 1000,
StatusCode = 200,
ResponseHeaders = [new(TestExpectedResponseHeaderKey, Redacted)],
RequestHeaders = [new(TestExpectedRequestHeaderKey, Redacted)],
RequestBody = requestInput,
ResponseBody = responseInput,
EnrichmentTags = testEnricher.EnrichmentBag
};
using var requestContent = new StreamContent(new NotSeekableStream(new(Encoding.UTF8.GetBytes(requestInput))));
requestContent.Headers.Add("Content-Type", TextPlain);
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://{expectedLogRecord.Host}/{expectedLogRecord.Path}"),
Content = requestContent,
};
httpRequestMessage.Headers.Add(TestRequestHeader, requestHeaderValue);
using var responseContent = new StreamContent(new NotSeekableStream(new(Encoding.UTF8.GetBytes(responseInput))));
responseContent.Headers.Add("Content-Type", TextPlain);
using var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = responseContent,
};
httpResponseMessage.Headers.Add(TestResponseHeader, responseHeaderValue);
httpResponseMessage.Headers.TransferEncoding.Add(new("chunked"));
var options = new LoggingOptions
{
ResponseHeadersDataClasses = new Dictionary<string, DataClassification> { { TestResponseHeader, FakeTaxonomy.PrivateData } },
RequestHeadersDataClasses = new Dictionary<string, DataClassification> { { TestRequestHeader, FakeTaxonomy.PrivateData } },
ResponseBodyContentTypes = new HashSet<string> { TextPlain },
RequestBodyContentTypes = new HashSet<string> { TextPlain },
BodySizeLimit = 32000,
BodyReadTimeout = TimeSpan.FromMinutes(5),
RequestPathLoggingMode = OutgoingPathLoggingMode.Structured,
LogRequestStart = false,
LogBody = true
};
var fakeLogger = new FakeLogger<HttpClientLogger>(new FakeLogCollector(Options.Options.Create(new FakeLogCollectorOptions())));
var mockHeadersRedactor = new Mock<IHttpHeadersRedactor>();
mockHeadersRedactor.Setup(r => r.Redact(It.IsAny<IEnumerable<string>>(), It.IsAny<DataClassification>()))
.Returns(Redacted);
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), mockHeadersRedactor.Object);
using var handler = new TestLoggingHandler(
new HttpClientLogger(
fakeLogger,
new HttpRequestReader(
options,
GetHttpRouteFormatter(),
Mock.Of<IHttpRouteParser>(),
headersReader, RequestMetadataContext),
new List<IHttpClientLogEnricher> { testEnricher },
options),
new TestingHandlerStub((_, _) =>
{
fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(1000));
return Task.FromResult(httpResponseMessage);
}));
using var client = new HttpClient(handler);
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecords = fakeLogger.Collector.GetSnapshot();
var logRecord = Assert.Single(logRecords).GetStructuredState();
logRecord.Contains(HttpClientLoggingTagNames.Host, expectedLogRecord.Host);
logRecord.Contains(HttpClientLoggingTagNames.Method, expectedLogRecord.Method.ToString());
logRecord.Contains(HttpClientLoggingTagNames.Path, TelemetryConstants.Redacted);
logRecord.Contains(HttpClientLoggingTagNames.Duration, EnsureLogRecordDuration);
logRecord.Contains(HttpClientLoggingTagNames.StatusCode, expectedLogRecord.StatusCode.Value.ToString(CultureInfo.InvariantCulture));
logRecord.Contains(HttpClientLoggingTagNames.RequestBody, expectedLogRecord.RequestBody);
logRecord.Contains(HttpClientLoggingTagNames.ResponseBody, expectedLogRecord.ResponseBody);
logRecord.Contains(TestExpectedRequestHeaderKey, expectedLogRecord.RequestHeaders.FirstOrDefault().Value);
logRecord.Contains(TestExpectedResponseHeaderKey, expectedLogRecord.ResponseHeaders.FirstOrDefault().Value);
logRecord.Contains(testEnricher.KvpRequest.Key, expectedLogRecord.GetEnrichmentProperty(testEnricher.KvpRequest.Key));
}
[Theory]
[InlineData(399, LogLevel.Information)]
[InlineData(400, LogLevel.Error)]
[InlineData(499, LogLevel.Error)]
[InlineData(500, LogLevel.Error)]
[InlineData(599, LogLevel.Error)]
[InlineData(600, LogLevel.Information)]
public async Task HttpLoggingHandler_OnDifferentHttpStatusCodes_LogsOutgoingRequestWithAppropriateLogLevel(
int httpStatusCode, LogLevel expectedLogLevel)
{
var fakeLogger = new FakeLogger<HttpClientLogger>();
var options = new LoggingOptions();
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), new Mock<IHttpHeadersRedactor>().Object);
var requestReader = new HttpRequestReader(
options, GetHttpRouteFormatter(), Mock.Of<IHttpRouteParser>(), headersReader, RequestMetadataContext);
using var handler = new TestLoggingHandler(
new HttpClientLogger(fakeLogger, requestReader, Array.Empty<IHttpClientLogEnricher>(), options),
new TestingHandlerStub((_, _) =>
Task.FromResult(new HttpResponseMessage((HttpStatusCode)httpStatusCode))));
using var client = new HttpClient(handler);
using var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new($"http://default-uri.com/foo/bar"),
Content = new StringContent("request_content", Encoding.UTF8, TextPlain)
};
await client.SendAsync(httpRequestMessage, It.IsAny<CancellationToken>());
var logRecord = fakeLogger.Collector.GetSnapshot().Single();
Assert.Equal(expectedLogLevel, logRecord.Level);
}
[Fact]
public async Task HttpClientLogger_LogsAnError_WhenResponseReaderThrows()
{
var exception = new InvalidOperationException("Test exception");
var requestReaderMock = new Mock<IHttpRequestReader>();
requestReaderMock
.Setup(r => r.ReadResponseAsync(It.IsAny<LogRecord>(), It.IsAny<HttpResponseMessage>(), It.IsAny<List<KeyValuePair<string, string>>?>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(exception);
var fakeLogger = new FakeLogger<HttpClientLogger>();
var logger = new HttpClientLogger(fakeLogger, requestReaderMock.Object, Array.Empty<IHttpClientLogEnricher>(), new());
using var httpRequestMessage = new HttpRequestMessage();
using var httpResponseMessage = new HttpResponseMessage();
await logger.LogRequestStopAsync(new LogRecord(), httpRequestMessage, httpResponseMessage, TimeSpan.Zero);
var logRecords = fakeLogger.Collector.GetSnapshot();
var logRecord = Assert.Single(logRecords);
Assert.Equal(LogLevel.Error, logRecord.Level);
Assert.Same(exception, logRecord.Exception);
}
[Fact]
public void SyncMethods_ShouldThrow()
{
var options = new LoggingOptions();
var headersReader = new HttpHeadersReader(options.ToOptionsMonitor(), Mock.Of<IHttpHeadersRedactor>());
var requestReader = new HttpRequestReader(
options, GetHttpRouteFormatter(), Mock.Of<IHttpRouteParser>(), headersReader, RequestMetadataContext);
var logger = new HttpClientLogger(new FakeLogger<HttpClientLogger>(), requestReader, Array.Empty<IHttpClientLogEnricher>(), options);
using var httpRequestMessage = new HttpRequestMessage();
using var httpResponseMessage = new HttpResponseMessage();
Assert.Throws<NotSupportedException>(() => logger.LogRequestStart(httpRequestMessage));
Assert.Throws<NotSupportedException>(() => logger.LogRequestStop(null, httpRequestMessage, httpResponseMessage, TimeSpan.Zero));
Assert.Throws<NotSupportedException>(() => logger.LogRequestFailed(null, httpRequestMessage, null, new InvalidOperationException(), TimeSpan.Zero));
}
private static IHttpRouteFormatter GetHttpRouteFormatter()
{
var builder = new ServiceCollection()
.AddFakeRedaction()
.AddHttpRouteProcessor()
.BuildServiceProvider();
return builder.GetService<IHttpRouteFormatter>()!;
}
private static void EnsureLogRecordDuration(string? actualValue)
{
Assert.NotNull(actualValue);
Assert.InRange(int.Parse(actualValue), 0, int.MaxValue);
}
private static IOutgoingRequestContext RequestMetadataContext
=> new Mock<IOutgoingRequestContext>().Object;
}
|