Implement new request trace identifier format
The format: The trace identifier begins with connection ID and ends with a number that increments with each request per connection. Example: Connection ID = xyz Request 1 = "xyz:00000001" Request 2 = "xyz:00000002" ... Request 15 = "xyz:0000000F" Request 16 = "xyz:00000010"
This commit is contained in:
parent
3375cf0e28
commit
4dc7946cd8
|
|
@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private string _requestId;
|
||||
private int _remainingRequestHeadersBytesAllowed;
|
||||
private int _requestHeadersParsed;
|
||||
private uint _requestCount;
|
||||
|
||||
protected readonly long _keepAliveTicks;
|
||||
private readonly long _requestHeadersTimeoutTicks;
|
||||
|
|
@ -128,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
// don't generate an ID until it is requested
|
||||
if (_requestId == null)
|
||||
{
|
||||
_requestId = CorrelationIdGenerator.GetNextId();
|
||||
_requestId = StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', _requestCount);
|
||||
}
|
||||
return _requestId;
|
||||
}
|
||||
|
|
@ -388,6 +389,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
_requestHeadersParsed = 0;
|
||||
|
||||
_responseBytesWritten = 0;
|
||||
_requestCount++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
string key = new string('\0', keyLength);
|
||||
fixed (char* keyBuffer = key)
|
||||
{
|
||||
if (!AsciiUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
|
||||
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
|
||||
{
|
||||
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidCharactersInHeaderName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
{
|
||||
// This version if AsciiUtilities returns null if there are any null (0 byte) characters
|
||||
// in the string
|
||||
if (!AsciiUtilities.TryGetAsciiString(buffer, output, span.Length))
|
||||
if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||
{
|
||||
internal class AsciiUtilities
|
||||
internal class StringUtilities
|
||||
{
|
||||
public static unsafe bool TryGetAsciiString(byte* input, char* output, int count)
|
||||
{
|
||||
|
|
@ -75,5 +75,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private static readonly string _encode16Chars = "0123456789ABCDEF";
|
||||
|
||||
/// <summary>
|
||||
/// A faster version of String.Concat(<paramref name="str"/>, <paramref name="separator"/>, <paramref name="number"/>.ToString("X8"))
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="separator"></param>
|
||||
/// <param name="number"></param>
|
||||
/// <returns></returns>
|
||||
public static unsafe string ConcatAsHexSuffix(string str, char separator, uint number)
|
||||
{
|
||||
var length = 1 + 8;
|
||||
if (str != null)
|
||||
{
|
||||
length += str.Length;
|
||||
}
|
||||
|
||||
// stackalloc to allocate array on stack rather than heap
|
||||
char* charBuffer = stackalloc char[length];
|
||||
|
||||
var i = 0;
|
||||
if (str != null)
|
||||
{
|
||||
for (i = 0; i < str.Length; i++)
|
||||
{
|
||||
charBuffer[i] = str[i];
|
||||
}
|
||||
}
|
||||
|
||||
charBuffer[i] = separator;
|
||||
|
||||
charBuffer[i + 1] = _encode16Chars[(int)(number >> 28) & 0xF];
|
||||
charBuffer[i + 2] = _encode16Chars[(int)(number >> 24) & 0xF];
|
||||
charBuffer[i + 3] = _encode16Chars[(int)(number >> 20) & 0xF];
|
||||
charBuffer[i + 4] = _encode16Chars[(int)(number >> 16) & 0xF];
|
||||
charBuffer[i + 5] = _encode16Chars[(int)(number >> 12) & 0xF];
|
||||
charBuffer[i + 6] = _encode16Chars[(int)(number >> 8) & 0xF];
|
||||
charBuffer[i + 7] = _encode16Chars[(int)(number >> 4) & 0xF];
|
||||
charBuffer[i + 8] = _encode16Chars[(int)number & 0xF];
|
||||
|
||||
// string ctor overload that takes char*
|
||||
return new string(charBuffer, 0, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +141,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.NotEqual(nextId, secondId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceIdentifierCountsRequestsPerFrame()
|
||||
{
|
||||
var connectionId = _frameContext.ConnectionId;
|
||||
var feature = ((IFeatureCollection)_frame).Get<IHttpRequestIdentifierFeature>();
|
||||
// Reset() is called once in the test ctor
|
||||
var count = 1;
|
||||
void Reset()
|
||||
{
|
||||
_frame.Reset();
|
||||
count++;
|
||||
}
|
||||
|
||||
var nextId = feature.TraceIdentifier;
|
||||
Assert.Equal($"{connectionId}:00000001", nextId);
|
||||
|
||||
Reset();
|
||||
var secondId = feature.TraceIdentifier;
|
||||
Assert.Equal($"{connectionId}:00000002", secondId);
|
||||
|
||||
var big = 1_000_000;
|
||||
while (big-- > 0) Reset();
|
||||
Assert.Equal($"{connectionId}:{count:X8}", feature.TraceIdentifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceIdentifierGeneratesWhenNull()
|
||||
{
|
||||
|
|
@ -149,7 +174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.NotNull(id);
|
||||
Assert.Equal(id, _frame.TraceIdentifier);
|
||||
|
||||
_frame.TraceIdentifier = null;
|
||||
_frame.Reset();
|
||||
Assert.NotEqual(id, _frame.TraceIdentifier);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
public class StringUtilitiesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(uint.MinValue)]
|
||||
[InlineData(0xF)]
|
||||
[InlineData(0xA)]
|
||||
[InlineData(0xFF)]
|
||||
[InlineData(0xFFC59)]
|
||||
[InlineData(uint.MaxValue)]
|
||||
public void ConvertsToHex(uint value)
|
||||
{
|
||||
var str = CorrelationIdGenerator.GetNextId();
|
||||
Assert.Equal($"{str}:{value:X8}", StringUtilities.ConcatAsHexSuffix(str, ':', value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNull()
|
||||
{
|
||||
uint value = 0x23BC0234;
|
||||
Assert.Equal(":23BC0234", StringUtilities.ConcatAsHexSuffix(null, ':', value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -611,13 +611,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
[Fact]
|
||||
public async Task TraceIdentifierIsUnique()
|
||||
{
|
||||
const int IdentifierLength = 13;
|
||||
const int identifierLength = 22;
|
||||
const int iterations = 10;
|
||||
|
||||
using (var server = new TestServer(async context =>
|
||||
{
|
||||
Assert.Equal(IdentifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier));
|
||||
context.Response.ContentLength = IdentifierLength;
|
||||
Assert.Equal(identifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier));
|
||||
context.Response.ContentLength = identifierLength;
|
||||
await context.Response.WriteAsync(context.TraceIdentifier);
|
||||
}))
|
||||
{
|
||||
|
|
@ -635,7 +635,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
// requests on same connection
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
var buffer = new char[IdentifierLength];
|
||||
var buffer = new char[identifierLength];
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
await connection.Send("GET / HTTP/1.1",
|
||||
|
|
@ -645,12 +645,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
|
||||
await connection.Receive($"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
$"Content-Length: {IdentifierLength}",
|
||||
$"Content-Length: {identifierLength}",
|
||||
"",
|
||||
"").TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
|
||||
var read = await connection.Reader.ReadAsync(buffer, 0, IdentifierLength);
|
||||
Assert.Equal(IdentifierLength, read);
|
||||
var read = await connection.Reader.ReadAsync(buffer, 0, identifierLength);
|
||||
Assert.Equal(identifierLength, read);
|
||||
var id = new string(buffer, 0, read);
|
||||
Assert.DoesNotContain(id, usedIds.ToArray());
|
||||
usedIds.Add(id);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||
{
|
||||
[Config(typeof(CoreConfig))]
|
||||
public class StringUtilitiesBenchmark
|
||||
{
|
||||
private const int Iterations = 500_000;
|
||||
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
|
||||
public void UintToString()
|
||||
{
|
||||
var connectionId = CorrelationIdGenerator.GetNextId();
|
||||
for (uint i = 0; i < Iterations; i++)
|
||||
{
|
||||
var id = connectionId + ':' + i.ToString("X8");
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public void ConcatAsHexSuffix()
|
||||
{
|
||||
var connectionId = CorrelationIdGenerator.GetNextId();
|
||||
for (uint i = 0; i < Iterations; i++)
|
||||
{
|
||||
var id = StringUtilities.ConcatAsHexSuffix(connectionId, ':', i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue