Skip to content

Commit 4190817

Browse files
authored
Merge pull request #106 from nblumhardt/messagetemplate-renderings
Add `IncludedData.MessageTemplateRenderingsAttribute`
2 parents 826727e + ebc3e34 commit 4190817

File tree

5 files changed

+92
-7
lines changed

5 files changed

+92
-7
lines changed

src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,10 @@ public enum IncludedData
7171
/// })
7272
/// </code>
7373
/// </remarks>
74-
TemplateBody = 32
74+
TemplateBody = 32,
75+
76+
/// <summary>
77+
/// Include pre-rendered values for any message template placeholders that use custom format specifiers, in <c>message_template.renderings</c>.
78+
/// </summary>
79+
MessageTemplateRenderingsAttribute = 64
7580
}

src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/LogRecordBuilder.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
// ReSharper disable PossibleMultipleEnumeration
16+
17+
using System.Globalization;
1518
using OpenTelemetry.Proto.Common.V1;
1619
using OpenTelemetry.Proto.Logs.V1;
1720
using Serilog.Events;
21+
using Serilog.Parsing;
1822
using Serilog.Sinks.OpenTelemetry.Formatting;
1923
using Serilog.Sinks.OpenTelemetry.ProtocolHelpers;
2024

@@ -137,5 +141,29 @@ static void ProcessIncludedFields(LogRecord logRecord, LogEvent logEvent, Includ
137141
StringValue = PrimitiveConversions.Md5Hash(logEvent.MessageTemplate.Text)
138142
}));
139143
}
144+
145+
if ((includedFields & IncludedData.MessageTemplateRenderingsAttribute) != IncludedData.None)
146+
{
147+
var tokensWithFormat = logEvent.MessageTemplate.Tokens
148+
.OfType<PropertyToken>()
149+
.Where(pt => pt.Format != null);
150+
151+
// Better not to allocate an array in the 99.9% of cases where this is false
152+
if (tokensWithFormat.Any())
153+
{
154+
var renderings = new ArrayValue();
155+
156+
foreach (var propertyToken in tokensWithFormat)
157+
{
158+
var space = new StringWriter();
159+
propertyToken.Render(logEvent.Properties, space, CultureInfo.InvariantCulture);
160+
renderings.Values.Add(new AnyValue { StringValue = space.ToString() });
161+
}
162+
163+
logRecord.Attributes.Add(PrimitiveConversions.NewAttribute(
164+
SemanticConventions.AttributeMessageTemplateRenderings,
165+
new AnyValue { ArrayValue = renderings }));
166+
}
167+
}
140168
}
141169
}

src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/SemanticConventions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ static class SemanticConventions
3131
/// </summary>
3232
public const string AttributeMessageTemplateMD5Hash = "message_template.hash.md5";
3333

34+
/// <summary>
35+
/// If any placeholders in the message template use custom format specifiers, an array containing a pre-rendered string for each such token.
36+
/// </summary>
37+
public const string AttributeMessageTemplateRenderings = "message_template.renderings";
38+
3439
/// <summary>
3540
/// OpenTelemetry standard service name resource attribute.
3641
/// </summary>

test/Serilog.Sinks.OpenTelemetry.Tests/LogRecordBuilderTests.cs

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,9 @@ public void IncludeTraceIdWhenActivityIsNull()
161161
[Fact]
162162
public void IncludeTraceIdAndSpanId()
163163
{
164-
using var listener = new ActivityListener
165-
{
166-
ShouldListenTo = _ => true,
167-
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
168-
};
164+
using var listener = new ActivityListener();
165+
listener.ShouldListenTo = _ => true;
166+
listener.Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData;
169167

170168
ActivitySource.AddActivityListener(listener);
171169

@@ -187,11 +185,59 @@ public void IncludeTraceIdAndSpanId()
187185
[Fact]
188186
public void TemplateBodyIncludesMessageTemplateInBody()
189187
{
190-
var messageTemplate = "Hello, {Name}";
188+
const string messageTemplate = "Hello, {Name}";
191189
var properties = new List<LogEventProperty> { new("Name", new ScalarValue("World")) };
192190

193191
var logRecord = LogRecordBuilder.ToLogRecord(Some.SerilogEvent(messageTemplate, properties), null, IncludedData.TemplateBody, new());
194192
Assert.NotNull(logRecord.Body);
195193
Assert.Equal(messageTemplate, logRecord.Body.StringValue);
196194
}
195+
196+
[Fact]
197+
public void NoRenderingsIncludedWhenNoneInTemplate()
198+
{
199+
var logEvent = Some.SerilogEvent(messageTemplate: "Hello, {Name}", properties: new [] { new LogEventProperty("Name", new ScalarValue("World"))});
200+
201+
var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateRenderingsAttribute, new());
202+
203+
Assert.DoesNotContain(SemanticConventions.AttributeMessageTemplateRenderings, logRecord.Attributes.Select(a => a.Key));
204+
}
205+
206+
[Fact]
207+
public void RenderingsIncludedWhenPresentInTemplate()
208+
{
209+
var logEvent = Some.SerilogEvent(messageTemplate: "{First:0} {Second} {Third:0.00}", properties: new []
210+
{
211+
new LogEventProperty("First", new ScalarValue(123.456)),
212+
new LogEventProperty("Second", new ScalarValue(234.567)),
213+
new LogEventProperty("Third", new ScalarValue(345.678))
214+
});
215+
216+
var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateRenderingsAttribute, new());
217+
218+
var expectedAttribute = new KeyValue { Key = SemanticConventions.AttributeMessageTemplateRenderings, Value = new()
219+
{
220+
ArrayValue = new ArrayValue {
221+
Values =
222+
{
223+
// Only values for tokens with format strings are included.
224+
new AnyValue{ StringValue = "123"},
225+
new AnyValue{ StringValue = "345.68"},
226+
}
227+
}
228+
}};
229+
Assert.Contains(expectedAttribute, logRecord.Attributes);
230+
}
231+
232+
[Fact]
233+
public void RenderingsNotIncludedWhenIncludedDataDoesNotSpecifyThem()
234+
{
235+
var logEvent = Some.SerilogEvent(messageTemplate: "{First:0}", properties: new []
236+
{
237+
new LogEventProperty("First", new ScalarValue(123.456))
238+
});
239+
240+
var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, OpenTelemetrySinkOptions.DefaultIncludedData, new());
241+
Assert.DoesNotContain(SemanticConventions.AttributeMessageTemplateRenderings, logRecord.Attributes.Select(a => a.Key));
242+
}
197243
}

test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ namespace Serilog.Sinks.OpenTelemetry
2525
SpanIdField = 8,
2626
SpecRequiredResourceAttributes = 16,
2727
TemplateBody = 32,
28+
MessageTemplateRenderingsAttribute = 64,
2829
}
2930
public class OpenTelemetrySinkOptions
3031
{

0 commit comments

Comments
 (0)