Skip to content

Commit 4fbf3a3

Browse files
authored
Merge pull request #105 from serilog/dev
1.1.0 Release
2 parents 18b6f45 + 4190817 commit 4fbf3a3

File tree

10 files changed

+168
-27
lines changed

10 files changed

+168
-27
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Serilog.Sinks.OpenTelemetry [![Build status](https://ci.appveyor.com/api/projects/status/sqmrvw34pcuatwl5/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-opentelemetry/branch/dev) [![NuGet Version](http://img.shields.io/nuget/vpre/Serilog.Sinks.OpenTelemetry.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.OpenTelemetry/)
1+
# Serilog.Sinks.OpenTelemetry [![Build status](https://ci.appveyor.com/api/projects/status/sqmrvw34pcuatwl5/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-opentelemetry/branch/dev) [![NuGet Version](https://img.shields.io/nuget/vpre/Serilog.Sinks.OpenTelemetry.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.OpenTelemetry/)
22

33
This Serilog sink transforms Serilog events into OpenTelemetry
44
`LogRecord`s and sends them to an OTLP (gRPC or HTTP) endpoint.

src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<Description>This Serilog sink transforms Serilog events into OpenTelemetry
44
logs and sends them to an OTLP (gRPC or HTTP) endpoint.</Description>
5-
<VersionPrefix>1.0.2</VersionPrefix>
5+
<VersionPrefix>1.1.0</VersionPrefix>
66
<Authors>Serilog Contributors</Authors>
77
<TargetFrameworks>net6.0;netstandard2.1;netstandard2.0;net462</TargetFrameworks>
88
<PackageTags>serilog;sink;opentelemetry</PackageTags>

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,25 @@ public enum IncludedData
5656
/// means <c>service.name</c> if not supplied, along with the <c>telemetry.sdk.*</c> group of attributes.
5757
/// </summary>
5858
SpecRequiredResourceAttributes = 16,
59+
60+
/// <summary>
61+
/// Include the log event's message template in the OTLP <c>body</c> instead of the rendered messsage. For
62+
/// example, the string <c>Hello {Name}!</c>.
63+
/// </summary>
64+
/// <remarks>
65+
/// Note: It is often desirable to remove <see cref="IncludedData.MessageTemplateTextAttribute"/> when using
66+
/// <see cref="IncludedData.TemplateBody"/> but otherwise use defaults.
67+
/// <code>
68+
/// .WriteTo.OpenTelemetry(options =>
69+
/// {
70+
/// options.IncludedData = (options.IncludedData | IncludedData.TemplateBody) &amp; ~IncludedData.MessageTemplateTextAttribute;
71+
/// })
72+
/// </code>
73+
/// </remarks>
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
5980
}

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

Lines changed: 44 additions & 5 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

@@ -28,22 +32,33 @@ public static LogRecord ToLogRecord(LogEvent logEvent, IFormatProvider? formatPr
2832

2933
ProcessProperties(logRecord, logEvent);
3034
ProcessTimestamp(logRecord, logEvent);
31-
ProcessMessage(logRecord, logEvent, formatProvider);
35+
ProcessMessage(logRecord, logEvent, includedFields, formatProvider);
3236
ProcessLevel(logRecord, logEvent);
3337
ProcessException(logRecord, logEvent);
3438
ProcessIncludedFields(logRecord, logEvent, includedFields, activityContextCollector);
3539

3640
return logRecord;
3741
}
3842

39-
public static void ProcessMessage(LogRecord logRecord, LogEvent logEvent, IFormatProvider? formatProvider)
43+
public static void ProcessMessage(LogRecord logRecord, LogEvent logEvent, IncludedData includedFields, IFormatProvider? formatProvider)
4044
{
41-
var renderedMessage = CleanMessageTemplateFormatter.Format(logEvent.MessageTemplate, logEvent.Properties, formatProvider);
42-
if (renderedMessage.Trim() != "")
45+
if (!includedFields.HasFlag(IncludedData.TemplateBody))
46+
{
47+
var renderedMessage = CleanMessageTemplateFormatter.Format(logEvent.MessageTemplate, logEvent.Properties, formatProvider);
48+
49+
if (renderedMessage.Trim() != "")
50+
{
51+
logRecord.Body = new AnyValue
52+
{
53+
StringValue = renderedMessage
54+
};
55+
}
56+
}
57+
else if (includedFields.HasFlag(IncludedData.TemplateBody) && logEvent.MessageTemplate.Text.Trim() != "")
4358
{
4459
logRecord.Body = new AnyValue
4560
{
46-
StringValue = renderedMessage
61+
StringValue = logEvent.MessageTemplate.Text
4762
};
4863
}
4964
}
@@ -126,5 +141,29 @@ static void ProcessIncludedFields(LogRecord logRecord, LogEvent logEvent, Includ
126141
StringValue = PrimitiveConversions.Md5Hash(logEvent.MessageTemplate.Text)
127142
}));
128143
}
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+
}
129168
}
130169
}

src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OpenTelemetrySinkOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class OpenTelemetrySinkOptions
2626
internal const string DefaultEndpoint = "http://localhost:4317";
2727
internal const OtlpProtocol DefaultProtocol = OtlpProtocol.Grpc;
2828

29-
const IncludedData DefaultIncludedData = IncludedData.MessageTemplateTextAttribute |
29+
internal const IncludedData DefaultIncludedData = IncludedData.MessageTemplateTextAttribute |
3030
IncludedData.TraceIdField | IncludedData.SpanIdField |
3131
IncludedData.SpecRequiredResourceAttributes;
3232

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: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ public void TestProcessMessage()
2929
{
3030
var logRecord = new LogRecord();
3131

32-
LogRecordBuilder.ProcessMessage(logRecord, Some.SerilogEvent(messageTemplate: ""), null);
32+
LogRecordBuilder.ProcessMessage(logRecord, Some.SerilogEvent(messageTemplate: ""), OpenTelemetrySinkOptions.DefaultIncludedData, null);
3333
Assert.Null(logRecord.Body);
3434

35-
LogRecordBuilder.ProcessMessage(logRecord, Some.SerilogEvent(messageTemplate: "\t\f "), null);
35+
LogRecordBuilder.ProcessMessage(logRecord, Some.SerilogEvent(messageTemplate: "\t\f "), OpenTelemetrySinkOptions.DefaultIncludedData, null);
3636
Assert.Null(logRecord.Body);
3737

3838
const string message = "log message";
39-
LogRecordBuilder.ProcessMessage(logRecord, Some.SerilogEvent(messageTemplate: message), null);
39+
LogRecordBuilder.ProcessMessage(logRecord, Some.SerilogEvent(messageTemplate: message), OpenTelemetrySinkOptions.DefaultIncludedData, null);
4040
Assert.NotNull(logRecord.Body);
4141
Assert.Equal(message, logRecord.Body.StringValue);
4242
}
@@ -45,7 +45,7 @@ public void TestProcessMessage()
4545
public void TestProcessLevel()
4646
{
4747
var logRecord = new LogRecord();
48-
var logEvent = Some.SerilogEvent();
48+
var logEvent = Some.DefaultSerilogEvent();
4949

5050
LogRecordBuilder.ProcessLevel(logRecord, logEvent);
5151

@@ -57,7 +57,7 @@ public void TestProcessLevel()
5757
public void TestProcessProperties()
5858
{
5959
var logRecord = new LogRecord();
60-
var logEvent = Some.SerilogEvent();
60+
var logEvent = Some.DefaultSerilogEvent();
6161

6262
var prop = new LogEventProperty("property_name", new ScalarValue("ok"));
6363
var propertyKeyValue = PrimitiveConversions.NewStringAttribute("property_name", "ok");
@@ -76,7 +76,7 @@ public void TestTimestamp()
7676
var nowNano = PrimitiveConversions.ToUnixNano(now);
7777

7878
var logRecord = new LogRecord();
79-
var logEvent = Some.SerilogEvent(timestamp: now);
79+
var logEvent = Some.SerilogEvent(Some.TestMessageTemplate, timestamp: now);
8080

8181
LogRecordBuilder.ProcessTimestamp(logRecord, logEvent);
8282

@@ -96,7 +96,7 @@ public void TestException()
9696
catch (Exception ex)
9797
{
9898
var logRecord = new LogRecord();
99-
var logEvent = Some.SerilogEvent(ex: ex);
99+
var logEvent = Some.SerilogEvent(Some.TestMessageTemplate, ex: ex);
100100

101101
LogRecordBuilder.ProcessException(logRecord, logEvent);
102102

@@ -131,11 +131,14 @@ public void IncludeMessageTemplateMD5Hash()
131131
[Fact]
132132
public void IncludeMessageTemplateText()
133133
{
134-
var logEvent = Some.SerilogEvent(messageTemplate: Some.TestMessageTemplate);
134+
var messageTemplate = "Hello, {Name}";
135+
var properties = new List<LogEventProperty> { new("Name", new ScalarValue("World")) };
136+
137+
var logEvent = Some.SerilogEvent(messageTemplate, properties);
135138

136139
var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.MessageTemplateTextAttribute, new());
137140

138-
var expectedAttribute = new KeyValue { Key = SemanticConventions.AttributeMessageTemplateText, Value = new() { StringValue = Some.TestMessageTemplate }};
141+
var expectedAttribute = new KeyValue { Key = SemanticConventions.AttributeMessageTemplateText, Value = new() { StringValue = messageTemplate } };
139142
Assert.Contains(expectedAttribute, logRecord.Attributes);
140143
}
141144

@@ -146,7 +149,7 @@ public void IncludeTraceIdWhenActivityIsNull()
146149

147150
var collector = new ActivityContextCollector();
148151

149-
var logEvent = Some.SerilogEvent();
152+
var logEvent = Some.DefaultSerilogEvent();
150153
collector.CollectFor(logEvent);
151154

152155
var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.TraceIdField | IncludedData.SpanIdField, collector);
@@ -158,11 +161,9 @@ public void IncludeTraceIdWhenActivityIsNull()
158161
[Fact]
159162
public void IncludeTraceIdAndSpanId()
160163
{
161-
using var listener = new ActivityListener
162-
{
163-
ShouldListenTo = _ => true,
164-
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
165-
};
164+
using var listener = new ActivityListener();
165+
listener.ShouldListenTo = _ => true;
166+
listener.Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData;
166167

167168
ActivitySource.AddActivityListener(listener);
168169

@@ -172,12 +173,71 @@ public void IncludeTraceIdAndSpanId()
172173

173174
var collector = new ActivityContextCollector();
174175

175-
var logEvent = Some.SerilogEvent();
176+
var logEvent = Some.DefaultSerilogEvent();
176177
collector.CollectFor(logEvent);
177178

178179
var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.TraceIdField | IncludedData.SpanIdField, collector);
179180

180181
Assert.Equal(logRecord.TraceId, PrimitiveConversions.ToOpenTelemetryTraceId(Activity.Current.TraceId.ToHexString()));
181182
Assert.Equal(logRecord.SpanId, PrimitiveConversions.ToOpenTelemetrySpanId(Activity.Current.SpanId.ToHexString()));
182183
}
184+
185+
[Fact]
186+
public void TemplateBodyIncludesMessageTemplateInBody()
187+
{
188+
const string messageTemplate = "Hello, {Name}";
189+
var properties = new List<LogEventProperty> { new("Name", new ScalarValue("World")) };
190+
191+
var logRecord = LogRecordBuilder.ToLogRecord(Some.SerilogEvent(messageTemplate, properties), null, IncludedData.TemplateBody, new());
192+
Assert.NotNull(logRecord.Body);
193+
Assert.Equal(messageTemplate, logRecord.Body.StringValue);
194+
}
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+
}
183243
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class RequestTemplateFactoryTests
2424
// request template to another.
2525
public void TestNoDuplicateLogs()
2626
{
27-
var logEvent = Some.SerilogEvent();
27+
var logEvent = Some.DefaultSerilogEvent();
2828
var logRecord = LogRecordBuilder.ToLogRecord(logEvent, null, IncludedData.None, new());
2929

3030
var requestTemplate = RequestTemplateFactory.CreateRequestTemplate(new Dictionary<string, object>());

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ namespace Serilog.Sinks.OpenTelemetry
2424
TraceIdField = 4,
2525
SpanIdField = 8,
2626
SpecRequiredResourceAttributes = 16,
27+
TemplateBody = 32,
28+
MessageTemplateRenderingsAttribute = 64,
2729
}
2830
public class OpenTelemetrySinkOptions
2931
{

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,21 @@ static class Some
2323

2424
public const string TestMessageTemplate = "Message template {Variable}";
2525

26-
internal static LogEvent SerilogEvent(DateTimeOffset? timestamp = null, Exception? ex = null, string messageTemplate = TestMessageTemplate)
26+
internal static LogEvent DefaultSerilogEvent()
27+
{
28+
return SerilogEvent(
29+
TestMessageTemplate,
30+
new List<LogEventProperty> { new("Variable", new ScalarValue(42)) },
31+
DateTimeOffset.UtcNow,
32+
null);
33+
}
34+
35+
internal static LogEvent SerilogEvent(string messageTemplate, DateTimeOffset? timestamp = null, Exception? ex = null)
36+
{
37+
return SerilogEvent(messageTemplate, new List<LogEventProperty>(), timestamp, ex);
38+
}
39+
40+
internal static LogEvent SerilogEvent(string messageTemplate, IEnumerable<LogEventProperty> properties, DateTimeOffset? timestamp = null, Exception? ex = null)
2741
{
2842
var ts = timestamp ?? DateTimeOffset.UtcNow;
2943
var parser = new MessageTemplateParser();
@@ -33,7 +47,7 @@ internal static LogEvent SerilogEvent(DateTimeOffset? timestamp = null, Exceptio
3347
LogEventLevel.Warning,
3448
ex,
3549
template,
36-
new List<LogEventProperty>{ new("Variable", new ScalarValue(42)) });
50+
properties);
3751

3852
return logEvent;
3953
}

0 commit comments

Comments
 (0)