Skip to content

Commit 7e64b80

Browse files
committed
Add tags and baggage as log scope (#46571)
1 parent bee1e0b commit 7e64b80

4 files changed

Lines changed: 331 additions & 6 deletions

File tree

src/libraries/Microsoft.Extensions.Logging/src/LoggerFactory.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<Log
6666
_factoryOptions = options == null || options.Value == null ? new LoggerFactoryOptions() : options.Value;
6767

6868
const ActivityTrackingOptions ActivityTrackingOptionsMask = ~(ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId | ActivityTrackingOptions.ParentId |
69-
ActivityTrackingOptions.TraceFlags | ActivityTrackingOptions.TraceState);
69+
ActivityTrackingOptions.TraceFlags | ActivityTrackingOptions.TraceState | ActivityTrackingOptions.Tags
70+
| ActivityTrackingOptions.Baggage);
71+
7072

7173
if ((_factoryOptions.ActivityTrackingOptions & ActivityTrackingOptionsMask) != 0)
7274
{

src/libraries/Microsoft.Extensions.Logging/src/LoggerFactoryScopeProvider.cs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Text;
6-
using System.Globalization;
76
using System.Threading;
87
using System.Collections;
98
using System.Diagnostics;
@@ -48,12 +47,47 @@ void Report(Scope current)
4847
}
4948

5049
callback(activityLogScope, state);
50+
51+
// Tags and baggage are opt-in and thus we assume that most of the time it will not be used.
52+
if ((_activityTrackingOption & ActivityTrackingOptions.Tags) != 0
53+
&& activity.TagObjects.GetEnumerator().MoveNext())
54+
{
55+
// As TagObjects is a IEnumerable<KeyValuePair<string, object?>> this can be used directly as a scope.
56+
// We do this to safe the allocation of a wrapper object.
57+
callback(activity.TagObjects, state);
58+
}
59+
60+
if ((_activityTrackingOption & ActivityTrackingOptions.Baggage) != 0)
61+
{
62+
// Only access activity.Baggage as every call leads to an allocation
63+
IEnumerable<KeyValuePair<string, string?>> baggage = activity.Baggage;
64+
if (baggage.GetEnumerator().MoveNext())
65+
{
66+
// For the baggage a wrapper object is necessary because we need to be able to overwrite ToString().
67+
// In contrast to the TagsObject, Baggage doesn't have one underlining type where we can do this overwrite.
68+
ActivityBaggageLogScopeWrapper scope = GetOrCreateActivityBaggageLogScopeWrapper(activity, baggage);
69+
callback(scope, state);
70+
}
71+
}
5172
}
5273
}
5374

5475
Report(_currentScope.Value);
5576
}
5677

78+
private static ActivityBaggageLogScopeWrapper GetOrCreateActivityBaggageLogScopeWrapper(Activity activity, IEnumerable<KeyValuePair<string, string?>> items)
79+
{
80+
const string additionalItemsBaggagePropertyKey = "__ActivityBaggageItemsLogScope__";
81+
var activityBaggageLogScopeWrapper = activity.GetCustomProperty(additionalItemsBaggagePropertyKey) as ActivityBaggageLogScopeWrapper;
82+
if (activityBaggageLogScopeWrapper == null)
83+
{
84+
activityBaggageLogScopeWrapper = new ActivityBaggageLogScopeWrapper(items);
85+
activity.SetCustomProperty(additionalItemsBaggagePropertyKey, activityBaggageLogScopeWrapper);
86+
}
87+
88+
return activityBaggageLogScopeWrapper;
89+
}
90+
5791
public IDisposable Push(object state)
5892
{
5993
Scope parent = _currentScope.Value;
@@ -185,7 +219,59 @@ IEnumerator IEnumerable.GetEnumerator()
185219
return GetEnumerator();
186220
}
187221
}
222+
223+
private class ActivityBaggageLogScopeWrapper : IEnumerable<KeyValuePair<string, string?>>
224+
{
225+
private readonly IEnumerable<KeyValuePair<string, string?>> _items;
226+
227+
private StringBuilder? _stringBuilder;
228+
229+
public ActivityBaggageLogScopeWrapper (IEnumerable<KeyValuePair<string, string?>> items)
230+
{
231+
_items = items;
232+
}
233+
234+
public IEnumerator<KeyValuePair<string, string?>> GetEnumerator()
235+
{
236+
return _items.GetEnumerator();
237+
}
238+
239+
IEnumerator IEnumerable.GetEnumerator()
240+
{
241+
return _items.GetEnumerator();
242+
}
243+
244+
public override string ToString()
245+
{
246+
lock (this)
247+
{
248+
IEnumerator<KeyValuePair<string, string?>> enumerator = _items.GetEnumerator();
249+
if (!enumerator.MoveNext())
250+
{
251+
return string.Empty;
252+
}
253+
254+
_stringBuilder ??= new StringBuilder();
255+
_stringBuilder.Append(enumerator.Current.Key);
256+
_stringBuilder.Append(':');
257+
_stringBuilder.Append(enumerator.Current.Value);
258+
259+
while (enumerator.MoveNext())
260+
{
261+
_stringBuilder.Append(", ");
262+
_stringBuilder.Append(enumerator.Current.Key);
263+
_stringBuilder.Append(':');
264+
_stringBuilder.Append(enumerator.Current.Value);
265+
}
266+
267+
string result = _stringBuilder.ToString();
268+
_stringBuilder.Clear();
269+
return result;
270+
}
271+
}
272+
}
188273
}
274+
189275
internal static class ActivityExtensions
190276
{
191277
public static string GetSpanId(this Activity activity)

src/libraries/Microsoft.Extensions.Logging/tests/Common/LoggerFactoryTest.cs

Lines changed: 208 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Text;
6-
using System.Globalization;
76
using System.Diagnostics;
87
using System.Collections.Generic;
98
using Microsoft.Extensions.DependencyInjection;
@@ -82,6 +81,7 @@ private static ILoggerProvider CreateProvider()
8281
return disposableProvider.Object;
8382
}
8483

84+
8585
[Fact]
8686
public void Dispose_ThrowException_SwallowsException()
8787
{
@@ -181,8 +181,10 @@ public void TestActivityIds(ActivityTrackingOptions options)
181181

182182
var logger = loggerFactory.CreateLogger("Logger");
183183

184-
Activity a = new Activity("ScopeActivity");
185-
a.Start();
184+
Activity activity = new Activity("ScopeActivity");
185+
activity.AddBaggage("baggageTestKey1", "baggageTestValue");
186+
activity.AddTag("tagTestKey", "tagTestValue");
187+
activity.Start();
186188
string activity1String = GetActivityLogString(options);
187189
string activity2String;
188190

@@ -199,10 +201,11 @@ public void TestActivityIds(ActivityTrackingOptions options)
199201
}
200202
b.Stop();
201203
}
202-
a.Stop();
204+
activity.Stop();
203205

204206
Assert.Equal(activity1String, loggerProvider.LogText[1]);
205207
Assert.Equal(activity2String, loggerProvider.LogText[4]);
208+
Assert.Equal(7, loggerProvider.LogText.Count); // Ensure that Baggage and Tags aren't added.
206209
}
207210

208211
[Fact]
@@ -213,6 +216,195 @@ public void TestInvalidActivityTrackingOptions()
213216
);
214217
}
215218

219+
[Fact]
220+
public void TestActivityTrackingOptions_ShouldAddBaggageItemsAsNewScope_WhenBaggageOptionIsSet()
221+
{
222+
var loggerProvider = new ExternalScopeLoggerProvider();
223+
224+
var loggerFactory = LoggerFactory.Create(builder =>
225+
{
226+
builder
227+
.Configure(o => o.ActivityTrackingOptions = ActivityTrackingOptions.Baggage)
228+
.AddProvider(loggerProvider);
229+
});
230+
231+
var logger = loggerFactory.CreateLogger("Logger");
232+
233+
Activity activity = new Activity("ScopeActivity");
234+
activity.AddBaggage("testKey1", null);
235+
activity.AddBaggage("testKey2", string.Empty);
236+
activity.AddBaggage("testKey3", "testValue");
237+
activity.Start();
238+
239+
logger.LogInformation("Message1");
240+
241+
activity.Stop();
242+
243+
foreach (string s in loggerProvider.LogText)
244+
{
245+
System.Console.WriteLine(s);
246+
}
247+
248+
Assert.Equal("Message1", loggerProvider.LogText[0]);
249+
Assert.Equal("testKey3:testValue, testKey2:, testKey1:", loggerProvider.LogText[2]);
250+
}
251+
252+
[Fact]
253+
public void TestActivityTrackingOptions_ShouldAddTagsAsNewScope_WhenTagsOptionIsSet()
254+
{
255+
var loggerProvider = new ExternalScopeLoggerProvider();
256+
257+
var loggerFactory = LoggerFactory.Create(builder =>
258+
{
259+
builder
260+
.Configure(o => o.ActivityTrackingOptions = ActivityTrackingOptions.TraceId | ActivityTrackingOptions.Tags)
261+
.AddProvider(loggerProvider);
262+
});
263+
264+
var logger = loggerFactory.CreateLogger("Logger");
265+
266+
Activity activity = new Activity("ScopeActivity");
267+
activity.AddTag("testKey1", null);
268+
activity.AddTag("testKey2", string.Empty);
269+
activity.AddTag("testKey3", "testValue");
270+
activity.AddTag("testKey4", new Dummy());
271+
activity.Start();
272+
273+
logger.LogInformation("Message1");
274+
275+
activity.Stop();
276+
277+
Assert.Equal("Message1", loggerProvider.LogText[0]);
278+
Assert.Equal("testKey1:, testKey2:, testKey3:testValue, testKey4:DummyToString", loggerProvider.LogText[2]);
279+
}
280+
281+
[Fact]
282+
public void TestActivityTrackingOptions_ShouldAddTagsAndBaggageAsOneScopeAndTraceIdAsOtherScope_WhenTagsBaggageAndTraceIdOptionAreSet()
283+
{
284+
var loggerProvider = new ExternalScopeLoggerProvider();
285+
286+
var loggerFactory = LoggerFactory.Create(builder =>
287+
{
288+
builder
289+
.Configure(o => o.ActivityTrackingOptions = ActivityTrackingOptions.TraceId | ActivityTrackingOptions.Baggage | ActivityTrackingOptions.Tags)
290+
.AddProvider(loggerProvider);
291+
});
292+
293+
var logger = loggerFactory.CreateLogger("Logger");
294+
295+
Activity activity = new Activity("ScopeActivity");
296+
activity.AddTag("testTagKey1", "testTagValue");
297+
activity.AddBaggage("testBaggageKey1", "testBaggageValue");
298+
activity.Start();
299+
logger.LogInformation("Message1");
300+
string traceIdActivityLogString = GetActivityLogString(ActivityTrackingOptions.TraceId);
301+
activity.Stop();
302+
303+
Assert.Equal("Message1", loggerProvider.LogText[0]);
304+
Assert.Equal(traceIdActivityLogString, loggerProvider.LogText[1]);
305+
Assert.Equal("testTagKey1:testTagValue", loggerProvider.LogText[2]);
306+
Assert.Equal("testBaggageKey1:testBaggageValue", loggerProvider.LogText[3]);
307+
}
308+
309+
[Fact]
310+
public void TestActivityTrackingOptions_ShouldAddNewTagAndBaggageItemsAtRuntime_WhenTagsAndBaggageOptionAreSetAndWithNestedScopes()
311+
{
312+
var loggerProvider = new ExternalScopeLoggerProvider();
313+
314+
var loggerFactory = LoggerFactory.Create(builder =>
315+
{
316+
builder
317+
.Configure(o => o.ActivityTrackingOptions = ActivityTrackingOptions.Baggage | ActivityTrackingOptions.Tags)
318+
.AddProvider(loggerProvider);
319+
});
320+
321+
var logger = loggerFactory.CreateLogger("Logger");
322+
323+
Activity activity = new Activity("ScopeActivity");
324+
activity.Start();
325+
326+
// Add baggage and tag items before the first log entry.
327+
activity.AddTag("MyTagKey1", "1");
328+
activity.AddBaggage("MyBaggageKey1", "1");
329+
330+
// Log a message, this should create any cached objects.
331+
logger.LogInformation("Message1");
332+
333+
// Start the first scope, add some more items and log.
334+
using (logger.BeginScope("Scope1"))
335+
{
336+
activity.AddTag("MyTagKey2", "2");
337+
activity.AddBaggage("MyBaggageKey2", "2");
338+
logger.LogInformation("Message2");
339+
340+
// Add two additional scopes and also replace some tag and baggage items.
341+
using (logger.BeginScope("Scope2"))
342+
{
343+
activity.AddTag("MyTagKey3", "3");
344+
activity.AddBaggage("MyBaggageKey3", "3");
345+
346+
using (logger.BeginScope("Scope3"))
347+
{
348+
activity.SetTag("MyTagKey3", "4");
349+
activity.SetBaggage("MyBaggageKey3", "4");
350+
logger.LogInformation("Message3");
351+
}
352+
}
353+
354+
// Along with this message we expect all baggage and tags items
355+
// as well as the Scope1 but not the Scope2 and Scope3.
356+
logger.LogInformation("Message4");
357+
358+
activity.Stop();
359+
}
360+
361+
Assert.Equal("Message1", loggerProvider.LogText[0]);
362+
Assert.Equal("MyTagKey1:1", loggerProvider.LogText[2]);
363+
Assert.Equal("MyBaggageKey1:1", loggerProvider.LogText[3]);
364+
365+
Assert.Equal("Message2", loggerProvider.LogText[4]);
366+
Assert.Equal("MyTagKey1:1, MyTagKey2:2", loggerProvider.LogText[6]);
367+
Assert.Equal("MyBaggageKey2:2, MyBaggageKey1:1", loggerProvider.LogText[7]);
368+
Assert.Equal("Scope1", loggerProvider.LogText[8]);
369+
370+
Assert.Equal("Message3", loggerProvider.LogText[9]);
371+
Assert.Equal("MyTagKey1:1, MyTagKey2:2, MyTagKey3:4", loggerProvider.LogText[11]);
372+
Assert.Equal("MyBaggageKey3:4, MyBaggageKey2:2, MyBaggageKey1:1", loggerProvider.LogText[12]);
373+
Assert.Equal("Scope1", loggerProvider.LogText[13]);
374+
Assert.Equal("Scope2", loggerProvider.LogText[14]);
375+
Assert.Equal("Scope3", loggerProvider.LogText[15]);
376+
377+
Assert.Equal("Message4", loggerProvider.LogText[16]);
378+
Assert.Equal("MyTagKey1:1, MyTagKey2:2, MyTagKey3:4", loggerProvider.LogText[18]);
379+
Assert.Equal("MyBaggageKey3:4, MyBaggageKey2:2, MyBaggageKey1:1", loggerProvider.LogText[19]);
380+
Assert.Equal("Scope1", loggerProvider.LogText[20]);
381+
}
382+
383+
[Fact]
384+
public void TestActivityTrackingOptions_ShouldNotAddAdditionalScope_WhenTagsBaggageOptionAreSetButTagsAndBaggageAreEmpty()
385+
{
386+
var loggerProvider = new ExternalScopeLoggerProvider();
387+
388+
var loggerFactory = LoggerFactory.Create(builder =>
389+
{
390+
builder
391+
.Configure(o => o.ActivityTrackingOptions = ActivityTrackingOptions.TraceId | ActivityTrackingOptions.Baggage | ActivityTrackingOptions.Tags)
392+
.AddProvider(loggerProvider);
393+
});
394+
395+
var logger = loggerFactory.CreateLogger("Logger");
396+
397+
Activity activity = new Activity("ScopeActivity");
398+
activity.Start();
399+
logger.LogInformation("Message1");
400+
string traceIdActivityLogString = GetActivityLogString(ActivityTrackingOptions.TraceId);
401+
activity.Stop();
402+
403+
Assert.Equal("Message1", loggerProvider.LogText[0]);
404+
Assert.Equal(traceIdActivityLogString, loggerProvider.LogText[1]);
405+
Assert.Equal(2, loggerProvider.LogText.Count); // Ensure that the additional scopes for tags and baggage aren't added.
406+
}
407+
216408
[Fact]
217409
public void CallsSetScopeProvider_OnSupportedProviders()
218410
{
@@ -367,6 +559,10 @@ public ILogger CreateLogger(string categoryName)
367559
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
368560
{
369561
LogText.Add(formatter(state, exception));
562+
563+
// Notice that other ILoggers maybe not call "ToString()" on the scope but enumerate it and this isn't covered by this implementation.
564+
// E.g. the SimpleConsoleFormatter calls "ToString()" like it's done here but the "JsonConsoleFormatter" enumerates a scope
565+
// if the Scope is of type IEnumerable<KeyValuePair<string, object>>.
370566
ScopeProvider.ForEachScope((scope, builder) => builder.Add(scope.ToString()), LogText);
371567
}
372568

@@ -381,6 +577,14 @@ public IDisposable BeginScope<TState>(TState state)
381577
return null;
382578
}
383579
}
580+
581+
private class Dummy
582+
{
583+
public override string ToString()
584+
{
585+
return "DummyToString";
586+
}
587+
}
384588
}
385589

386590
internal static class ActivityExtensions

0 commit comments

Comments
 (0)