Skip to content

Commit a6b73fa

Browse files
authored
Merge pull request #53 from avireddy02/dev
Added new CompactSplunkJsonFormatter from #52
2 parents 26c3cb2 + bee12ff commit a6b73fa

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2016 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 Serilog.Events;
16+
using Serilog.Formatting;
17+
using Serilog.Formatting.Json;
18+
using Serilog.Parsing;
19+
using System;
20+
using System.Globalization;
21+
using System.IO;
22+
using System.Linq;
23+
24+
namespace Serilog.Sinks.Splunk
25+
{
26+
/// <summary>
27+
/// Renders log events into a Compact JSON format for consumption by Splunk.
28+
/// </summary>
29+
public class CompactSplunkJsonFormatter : ITextFormatter
30+
{
31+
private static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter(typeTagName: "$type");
32+
private readonly string _suffix;
33+
private readonly bool _renderTemplate;
34+
35+
/// <summary>
36+
/// Construct a <see cref="CompactSplunkJsonFormatter"/>.
37+
/// </summary>
38+
/// <param name="source">The source of the event</param>
39+
/// <param name="sourceType">The source type of the event</param>
40+
/// <param name="host">The host of the event</param>
41+
/// <param name="index">The Splunk index to log to</param>
42+
/// <param name="renderTemplate">If true, the template used will be rendered and written to the output as a property named MessageTemplate</param>
43+
public CompactSplunkJsonFormatter(bool renderTemplate = false, string source = null, string sourceType = null, string host = null, string index = null)
44+
{
45+
_renderTemplate = renderTemplate;
46+
var suffixWriter = new StringWriter();
47+
suffixWriter.Write("}"); // Terminates "event"
48+
49+
if (!string.IsNullOrWhiteSpace(source))
50+
{
51+
suffixWriter.Write(",\"source\":");
52+
JsonValueFormatter.WriteQuotedJsonString(source, suffixWriter);
53+
}
54+
55+
if (!string.IsNullOrWhiteSpace(sourceType))
56+
{
57+
suffixWriter.Write(",\"sourcetype\":");
58+
JsonValueFormatter.WriteQuotedJsonString(sourceType, suffixWriter);
59+
}
60+
61+
if (!string.IsNullOrWhiteSpace(host))
62+
{
63+
suffixWriter.Write(",\"host\":");
64+
JsonValueFormatter.WriteQuotedJsonString(host, suffixWriter);
65+
}
66+
67+
if (!string.IsNullOrWhiteSpace(index))
68+
{
69+
suffixWriter.Write(",\"index\":");
70+
JsonValueFormatter.WriteQuotedJsonString(index, suffixWriter);
71+
}
72+
suffixWriter.Write('}'); // Terminates the payload
73+
_suffix = suffixWriter.ToString();
74+
}
75+
76+
/// <inheritdoc/>
77+
public void Format(LogEvent logEvent, TextWriter output)
78+
{
79+
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
80+
if (output == null) throw new ArgumentNullException(nameof(output));
81+
82+
output.Write("{\"time\":\"");
83+
output.Write(logEvent.Timestamp.ToEpoch().ToString(CultureInfo.InvariantCulture));
84+
output.Write("\",\"event\":{\"@l\":\"");
85+
output.Write(logEvent.Level);
86+
output.Write('"');
87+
88+
if (_renderTemplate)
89+
{
90+
output.Write(",\"@mt\":");
91+
JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output);
92+
93+
var tokensWithFormat = logEvent.MessageTemplate.Tokens
94+
.OfType<PropertyToken>()
95+
.Where(pt => pt.Format != null);
96+
97+
// Better not to allocate an array in the 99.9% of cases where this is false
98+
// ReSharper disable once PossibleMultipleEnumeration
99+
if (tokensWithFormat.Any())
100+
{
101+
output.Write(",\"@r\":[");
102+
var delim = "";
103+
foreach (var r in tokensWithFormat)
104+
{
105+
output.Write(delim);
106+
delim = ",";
107+
var space = new StringWriter();
108+
r.Render(logEvent.Properties, space);
109+
JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output);
110+
}
111+
output.Write(']');
112+
}
113+
}
114+
if (logEvent.Exception != null)
115+
{
116+
output.Write(",\"@x\":");
117+
JsonValueFormatter.WriteQuotedJsonString(logEvent.Exception.ToString(), output);
118+
}
119+
120+
foreach (var property in logEvent.Properties)
121+
{
122+
var name = property.Key;
123+
if (name.Length > 0 && name[0] == '@')
124+
{
125+
// Escape first '@' by doubling
126+
name = '@' + name;
127+
}
128+
129+
output.Write(',');
130+
JsonValueFormatter.WriteQuotedJsonString(name, output);
131+
output.Write(':');
132+
ValueFormatter.Format(property.Value, output);
133+
}
134+
output.WriteLine(_suffix);
135+
}
136+
}
137+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Newtonsoft.Json.Linq;
2+
using Serilog.Sinks.Splunk.Tests.Support;
3+
using System;
4+
using System.IO;
5+
using Xunit;
6+
7+
namespace Serilog.Sinks.Splunk.Tests
8+
{
9+
public class CompactSplunkJsonFormatterTests
10+
{
11+
private void AssertValidJson(Action<ILogger> act,
12+
string source = "",
13+
string sourceType = "",
14+
string host = "",
15+
string index = "")
16+
{
17+
StringWriter outputRendered = new StringWriter(), output = new StringWriter();
18+
var log = new LoggerConfiguration()
19+
.WriteTo.Sink(new TextWriterSink(output, new CompactSplunkJsonFormatter(false, source, sourceType, host, index)))
20+
.WriteTo.Sink(new TextWriterSink(outputRendered, new CompactSplunkJsonFormatter(true, source, sourceType, host, index)))
21+
.CreateLogger();
22+
23+
act(log);
24+
25+
// Unfortunately this will not detect all JSON formatting issues; better than nothing however.
26+
JObject.Parse(output.ToString());
27+
JObject.Parse(outputRendered.ToString());
28+
}
29+
30+
[Fact]
31+
public void AnEmptyEventIsValidJson()
32+
{
33+
AssertValidJson(log => log.Information("No properties"));
34+
}
35+
36+
[Fact]
37+
public void AMinimalEventIsValidJson()
38+
{
39+
AssertValidJson(log => log.Information("One {Property}", 42));
40+
}
41+
42+
[Fact]
43+
public void MultiplePropertiesAreDelimited()
44+
{
45+
AssertValidJson(log => log.Information("Property {First} and {Second}", "One", "Two"));
46+
}
47+
48+
[Fact]
49+
public void ExceptionsAreFormattedToValidJson()
50+
{
51+
AssertValidJson(log => log.Information(new DivideByZeroException(), "With exception"));
52+
}
53+
54+
[Fact]
55+
public void ExceptionAndPropertiesAreValidJson()
56+
{
57+
AssertValidJson(log => log.Information(new DivideByZeroException(), "With exception and {Property}", 42));
58+
}
59+
60+
[Fact]
61+
public void AMinimalEventWithSourceIsValidJson()
62+
{
63+
AssertValidJson(log => log.Information("One {Property}", 42), source: "A Test Source");
64+
}
65+
66+
[Fact]
67+
public void AMinimalEventWithSourceTypeIsValidJson()
68+
{
69+
AssertValidJson(log => log.Information("One {Property}", 42), sourceType: "A Test SourceType");
70+
}
71+
72+
[Fact]
73+
public void AMinimalEventWithHostIsValidJson()
74+
{
75+
AssertValidJson(log => log.Information("One {Property}", 42), host: "A Test Host");
76+
}
77+
78+
[Fact]
79+
public void AMinimalEventWithIndexIsValidJson()
80+
{
81+
AssertValidJson(log => log.Information("One {Property}", 42), host: "testindex");
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)