Skip to content

Commit 5e1527d

Browse files
authored
Merge pull request #260 from AndreReise/improve-event-id-cache
Size-based `EventId` `LogEventProperty` cache
2 parents 7fc50b0 + 2bc0ea5 commit 5e1527d

File tree

5 files changed

+225
-43
lines changed

5 files changed

+225
-43
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Serilog.Extensions.Logging;
16+
17+
using System.Collections.Concurrent;
18+
using Microsoft.Extensions.Logging;
19+
using Serilog.Events;
20+
21+
class EventIdPropertyCache
22+
{
23+
readonly int _maxCachedProperties;
24+
readonly ConcurrentDictionary<EventKey, LogEventProperty> _propertyCache = new();
25+
26+
int _count;
27+
28+
public EventIdPropertyCache(int maxCachedProperties = 1024)
29+
{
30+
_maxCachedProperties = maxCachedProperties;
31+
}
32+
33+
public LogEventProperty GetOrCreateProperty(in EventId eventId)
34+
{
35+
var eventKey = new EventKey(eventId);
36+
37+
LogEventProperty? property;
38+
39+
if (_count >= _maxCachedProperties)
40+
{
41+
if (!_propertyCache.TryGetValue(eventKey, out property))
42+
{
43+
property = CreateProperty(in eventKey);
44+
}
45+
}
46+
else
47+
{
48+
if (!_propertyCache.TryGetValue(eventKey, out property))
49+
{
50+
// GetOrAdd is moved to a separate method to prevent closure allocation
51+
property = GetOrAddCore(in eventKey);
52+
}
53+
}
54+
55+
return property;
56+
}
57+
58+
static LogEventProperty CreateProperty(in EventKey eventKey)
59+
{
60+
var properties = new List<LogEventProperty>(2);
61+
62+
if (eventKey.Id != 0)
63+
{
64+
properties.Add(new LogEventProperty("Id", new ScalarValue(eventKey.Id)));
65+
}
66+
67+
if (eventKey.Name != null)
68+
{
69+
properties.Add(new LogEventProperty("Name", new ScalarValue(eventKey.Name)));
70+
}
71+
72+
return new LogEventProperty("EventId", new StructureValue(properties));
73+
}
74+
75+
LogEventProperty GetOrAddCore(in EventKey eventKey) =>
76+
_propertyCache.GetOrAdd(
77+
eventKey,
78+
key =>
79+
{
80+
Interlocked.Increment(ref _count);
81+
82+
return CreateProperty(in key);
83+
});
84+
85+
readonly record struct EventKey
86+
{
87+
public EventKey(EventId eventId)
88+
{
89+
Id = eventId.Id;
90+
Name = eventId.Name;
91+
}
92+
93+
public int Id { get; }
94+
95+
public string? Name { get; }
96+
}
97+
}

src/Serilog.Extensions.Logging/Extensions/Logging/SerilogLogger.cs

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,10 @@ internal static string GetKeyWithoutFirstSymbol(ConcurrentDictionary<string, str
2828

2929
readonly SerilogLoggerProvider _provider;
3030
readonly ILogger _logger;
31+
readonly EventIdPropertyCache _eventIdPropertyCache = new();
3132

3233
static readonly CachingMessageTemplateParser MessageTemplateParser = new();
3334

34-
// It's rare to see large event ids, as they are category-specific
35-
static readonly LogEventProperty[] LowEventIdValues = Enumerable.Range(0, 48)
36-
.Select(n => new LogEventProperty("Id", new ScalarValue(n)))
37-
.ToArray();
38-
3935
public SerilogLogger(
4036
SerilogLoggerProvider provider,
4137
ILogger? logger = null,
@@ -155,7 +151,7 @@ LogEvent PrepareWrite<TState>(LogEventLevel level, EventId eventId, TState state
155151
}
156152

157153
if (eventId.Id != 0 || eventId.Name != null)
158-
properties.Add(CreateEventIdProperty(eventId));
154+
properties.Add(_eventIdPropertyCache.GetOrCreateProperty(in eventId));
159155

160156
var (traceId, spanId) = Activity.Current is { } activity ?
161157
(activity.TraceId, activity.SpanId) :
@@ -172,25 +168,4 @@ LogEvent PrepareWrite<TState>(LogEventLevel level, EventId eventId, TState state
172168
stateObj = formatter(state, null);
173169
return stateObj ?? state;
174170
}
175-
176-
internal static LogEventProperty CreateEventIdProperty(EventId eventId)
177-
{
178-
var properties = new List<LogEventProperty>(2);
179-
180-
if (eventId.Id != 0)
181-
{
182-
if (eventId.Id >= 0 && eventId.Id < LowEventIdValues.Length)
183-
// Avoid some allocations
184-
properties.Add(LowEventIdValues[eventId.Id]);
185-
else
186-
properties.Add(new LogEventProperty("Id", new ScalarValue(eventId.Id)));
187-
}
188-
189-
if (eventId.Name != null)
190-
{
191-
properties.Add(new LogEventProperty("Name", new ScalarValue(eventId.Name)));
192-
}
193-
194-
return new LogEventProperty("EventId", new StructureValue(properties));
195-
}
196171
}

test/Serilog.Extensions.Logging.Benchmarks/LogEventBenchmark.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ private class Person
3434

3535
readonly IMelLogger _melLogger;
3636
readonly Person _bob, _alice;
37+
readonly EventId _eventId = new EventId(1, "Test");
3738

3839
public LogEventBenchmark()
3940
{
@@ -61,5 +62,16 @@ public void LogInformationScoped()
6162
using (var scope = _melLogger.BeginScope("Hi {@User} from {$Me}", _bob, _alice))
6263
_melLogger.LogInformation("Hi");
6364
}
65+
66+
[Benchmark]
67+
public void LogInformation_WithEventId()
68+
{
69+
this._melLogger.Log(
70+
LogLevel.Information,
71+
_eventId,
72+
"Hi {@User} from {$Me}",
73+
_bob,
74+
_alice);
75+
}
6476
}
6577
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) Serilog Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Microsoft.Extensions.Logging;
16+
using Serilog.Events;
17+
using Xunit;
18+
19+
namespace Serilog.Extensions.Logging.Tests;
20+
21+
public class EventIdPropertyCacheTests
22+
{
23+
[Fact]
24+
public void CreatesPropertyWithCorrectIdAndName()
25+
{
26+
// Arrange
27+
const int id = 101;
28+
const string name = "TestEvent";
29+
var eventId = new EventId(id, name);
30+
31+
var cache = new EventIdPropertyCache();
32+
33+
// Act
34+
var eventProperty = cache.GetOrCreateProperty(eventId);
35+
36+
// Assert
37+
var value = Assert.IsType<StructureValue>(eventProperty.Value);
38+
39+
Assert.Equal(2, value.Properties.Count);
40+
41+
var idValue = value.Properties.Single(property => property.Name == "Id").Value;
42+
var nameValue = value.Properties.Single(property => property.Name == "Name").Value;
43+
44+
var scalarId = Assert.IsType<ScalarValue>(idValue);
45+
var scalarName = Assert.IsType<ScalarValue>(nameValue);
46+
47+
Assert.Equal(id, scalarId.Value);
48+
Assert.Equal(name, scalarName.Value);
49+
}
50+
51+
[Fact]
52+
public void EventsWithDSameKeysHaveSameReferences()
53+
{
54+
// Arrange
55+
var cache = new EventIdPropertyCache();
56+
57+
// Act
58+
var property1 = cache.GetOrCreateProperty(new EventId(1, "Name1"));
59+
var property2 = cache.GetOrCreateProperty(new EventId(1, "Name1"));
60+
61+
// Assert
62+
Assert.Same(property1, property2);
63+
}
64+
65+
[Theory]
66+
[InlineData(1, "SomeName", 1, "AnotherName")]
67+
[InlineData(1, "SomeName", 2, "SomeName")]
68+
[InlineData(1, "SomeName", 2, "AnotherName")]
69+
public void EventsWithDifferentKeysHaveDifferentReferences(int firstId, string firstName, int secondId, string secondName)
70+
{
71+
// Arrange
72+
var cache = new EventIdPropertyCache();
73+
74+
// Act
75+
var property1 = cache.GetOrCreateProperty(new EventId(firstId, firstName));
76+
var property2 = cache.GetOrCreateProperty(new EventId(secondId, secondName));
77+
78+
// Assert
79+
Assert.NotSame(property1, property2);
80+
}
81+
82+
83+
[Fact]
84+
public void WhenLimitIsNotOverSameEventsHaveSameReferences()
85+
{
86+
// Arrange
87+
var eventId = new EventId(101, "test");
88+
var cache = new EventIdPropertyCache();
89+
90+
// Act
91+
var property1 = cache.GetOrCreateProperty(eventId);
92+
var property2 = cache.GetOrCreateProperty(eventId);
93+
94+
// Assert
95+
Assert.Same(property1, property2);
96+
}
97+
98+
[Fact]
99+
public void WhenLimitIsOverSameEventsHaveDifferentReferences()
100+
{
101+
// Arrange
102+
var cache = new EventIdPropertyCache(maxCachedProperties: 1);
103+
cache.GetOrCreateProperty(new EventId(1, "InitialEvent"));
104+
105+
var eventId = new EventId(101, "DifferentEvent");
106+
107+
// Act
108+
var property1 = cache.GetOrCreateProperty(eventId);
109+
var property2 = cache.GetOrCreateProperty(eventId);
110+
111+
// Assert
112+
Assert.NotSame(property1, property2);
113+
}
114+
}

test/Serilog.Extensions.Logging.Tests/SerilogLoggerTests.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -517,22 +517,6 @@ public void Dispose()
517517
}
518518
}
519519

520-
[Theory]
521-
[InlineData(1)]
522-
[InlineData(10)]
523-
[InlineData(48)]
524-
[InlineData(100)]
525-
public void LowAndHighNumberedEventIdsAreMapped(int id)
526-
{
527-
var orig = new EventId(id, "test");
528-
var mapped = SerilogLogger.CreateEventIdProperty(orig);
529-
var value = Assert.IsType<StructureValue>(mapped.Value);
530-
Assert.Equal(2, value.Properties.Count);
531-
var idValue = value.Properties.Single(p => p.Name == "Id").Value;
532-
var scalar = Assert.IsType<ScalarValue>(idValue);
533-
Assert.Equal(id, scalar.Value);
534-
}
535-
536520
[Fact]
537521
public void MismatchedMessageTemplateParameterCountIsHandled()
538522
{

0 commit comments

Comments
 (0)