Skip to content
This repository was archived by the owner on Jun 1, 2024. It is now read-only.

Commit 144369b

Browse files
authored
Added support for controlling the op_type when indexing (#356)
* Added support for controlling the op_type when indexing In order to write to data streams we'll need to set the op_type to create the default is index In order to don't serialize null values for the batch action and to not depend on custom serialization setting we're using LowLevelRequestResponseSerializer.Instance for serialization of the action. We won't throw exception when setting the TypeName to null since: - This is deprecated in v7 - Won't work in v8 * Consider op_type when parsing response from bulk action * Don't overwrite TypeName if it has been set to null In order to be able to use templates and at the same time remove the doc type we shouldn't override it if it's set to null. * Support for setting the op_type for durable sink Use the same logic for creating bulk action within the durable sink as in the standard sink. * Added change log
1 parent bf50349 commit 144369b

File tree

11 files changed

+252
-54
lines changed

11 files changed

+252
-54
lines changed

CHANGES.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
## Changelog
22

33
* Disable dot-escaping for field names, because ELK already supports dots in field names.
4+
* Support for explicitly setting `Options.TypeName` to `null` this will remove the
5+
deprecated `_type` from the bulk payload being sent to Elastic. Earlier an exception was
6+
thrown if the `Options.TypeName` was `null`. _If you're using `AutoRegisterTemplateVersion.ESv7`
7+
we'll not force the type to `_doc` if it's set to `null`. This is a small step towards support
8+
for writing logs to Elastic v8. #345
9+
* Support for setting the Elastic `op_type` e.g. `index` or `create` for bulk actions.
10+
This is a requirement for writing to [data streams](https://www.elastic.co/guide/en/elasticsearch/reference/7.9/data-streams.html)
11+
that's only supporting `create`. Data streams is a more slipped stream way to handle rolling
12+
indices, that previous required an ILM, template and a magic write alias. Now it's more integrated
13+
in Elasticsearch and Kibana. If you're running Elastic `7.9` you'll get rolling indices out of the box
14+
with this configuration:
15+
```
16+
TypeName = null,
17+
IndexFormat = "logs-my-stream",
18+
BatchAction = ElasticOpType.Create,
19+
```
20+
_Note: that current templates doesn't support data streams._ #355
421

522
8.2
623
* Allow the use of templateCustomSettings when reading from settings json (#315)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ This example shows the options that are currently available when using the appSe
6161
<add key="serilog:write-to:Elasticsearch.typeName" value="myCustomLogEventType"/>
6262
<add key="serilog:write-to:Elasticsearch.pipelineName" value="myCustomPipelineName"/>
6363
<add key="serilog:write-to:Elasticsearch.batchPostingLimit" value="50"/>
64+
<add key="serilog:write-to:Elasticsearch.batchAction" value="create"/>
6465
<add key="serilog:write-to:Elasticsearch.period" value="2"/>
6566
<add key="serilog:write-to:Elasticsearch.inlineFields" value="true"/>
6667
<add key="serilog:write-to:Elasticsearch.restrictedToMinimumLevel" value="Warning"/>
@@ -179,6 +180,7 @@ In your `appsettings.json` file, under the `Serilog` node, :
179180
"typeName": "myCustomLogEventType",
180181
"pipelineName": "myCustomPipelineName",
181182
"batchPostingLimit": 50,
183+
"batchAction": "create",
182184
"period": 2,
183185
"inlineFields": true,
184186
"restrictedToMinimumLevel": "Warning",

src/Serilog.Sinks.Elasticsearch/LoggerConfigurationElasticSearchExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ public static LoggerConfiguration Elasticsearch(
143143
/// <param name="failureSink">Sink to use when Elasticsearch is unable to accept the events. This is optionally and depends on the EmitEventFailure setting.</param>
144144
/// <param name="singleEventSizePostingLimit"><see cref="ElasticsearchSinkOptions.SingleEventSizePostingLimit"/>The maximum length of an event allowed to be posted to Elasticsearch.default null</param>
145145
/// <param name="templateCustomSettings">Add custom elasticsearch settings to the template</param>
146+
/// <param name="batchAction">Configures the OpType being used when inserting document in batch. Must be set to create for data streams.</param>
146147
/// <returns>LoggerConfiguration object</returns>
147148
/// <exception cref="ArgumentNullException"><paramref name="nodeUris"/> is <see langword="null" />.</exception>
148149
public static LoggerConfiguration Elasticsearch(
@@ -180,7 +181,8 @@ public static LoggerConfiguration Elasticsearch(
180181
ILogEventSink failureSink = null,
181182
long? singleEventSizePostingLimit = null,
182183
int? bufferFileCountLimit = null,
183-
Dictionary<string,string> templateCustomSettings = null)
184+
Dictionary<string,string> templateCustomSettings = null,
185+
ElasticOpType batchAction = ElasticOpType.Index)
184186
{
185187
if (string.IsNullOrEmpty(nodeUris))
186188
throw new ArgumentNullException(nameof(nodeUris), "No Elasticsearch node(s) specified.");
@@ -208,6 +210,7 @@ public static LoggerConfiguration Elasticsearch(
208210
}
209211

210212
options.BatchPostingLimit = batchPostingLimit;
213+
options.BatchAction = batchAction;
211214
options.SingleEventSizePostingLimit = singleEventSizePostingLimit;
212215
options.Period = TimeSpan.FromSeconds(period);
213216
options.InlineFields = inlineFields;

src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/DurableElasticsearchSink.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
using System;
1616
using System.Collections.Generic;
1717
using System.Text;
18-
using Elasticsearch.Net;
1918
using Serilog.Core;
2019
using Serilog.Events;
2120

@@ -55,15 +54,16 @@ public DurableElasticsearchSink(ElasticsearchSinkOptions options)
5554

5655

5756
var elasticSearchLogClient = new ElasticsearchLogClient(
58-
elasticLowLevelClient: _state.Client,
59-
cleanPayload: _state.Options.BufferCleanPayload);
57+
elasticLowLevelClient: _state.Client,
58+
cleanPayload: _state.Options.BufferCleanPayload,
59+
elasticOpType: _state.Options.BatchAction);
6060

6161
var payloadReader = new ElasticsearchPayloadReader(
6262
pipelineName: _state.Options.PipelineName,
6363
typeName:_state.Options.TypeName,
6464
serialize:_state.Serialize,
65-
getIndexForEvent: _state.GetBufferedIndexForEvent
66-
);
65+
getIndexForEvent: _state.GetBufferedIndexForEvent,
66+
elasticOpType: _state.Options.BatchAction);
6767

6868
_shipper = new ElasticsearchLogShipper(
6969
bufferBaseFilename: _state.Options.BufferBaseFilename,

src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/ElasticsearchLogClient.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Runtime.ExceptionServices;
5-
using System.Text;
6-
using System.Text.RegularExpressions;
74
using System.Threading.Tasks;
85
using Elasticsearch.Net;
96
using Serilog.Debugging;
@@ -17,17 +14,21 @@ public class ElasticsearchLogClient : ILogClient<List<string>>
1714
{
1815
private readonly IElasticLowLevelClient _elasticLowLevelClient;
1916
private readonly Func<string, long?, string, string> _cleanPayload;
17+
private readonly ElasticOpType _elasticOpType;
2018

2119
/// <summary>
2220
///
2321
/// </summary>
2422
/// <param name="elasticLowLevelClient"></param>
2523
/// <param name="cleanPayload"></param>
24+
/// <param name="elasticOpType"></param>
2625
public ElasticsearchLogClient(IElasticLowLevelClient elasticLowLevelClient,
27-
Func<string, long?, string, string> cleanPayload)
26+
Func<string, long?, string, string> cleanPayload,
27+
ElasticOpType elasticOpType)
2828
{
2929
_elasticLowLevelClient = elasticLowLevelClient;
3030
_cleanPayload = cleanPayload;
31+
_elasticOpType = elasticOpType;
3132
}
3233

3334
public async Task<SentPayloadResult> SendPayloadAsync(List<string> payload)
@@ -85,7 +86,7 @@ private InvalidResult GetInvalidPayloadAsync(DynamicResponse baseResult, List<st
8586
bool hasErrors = false;
8687
foreach (dynamic item in items)
8788
{
88-
var itemIndex = item?["index"];
89+
var itemIndex = item?[ElasticsearchSink.BulkAction(_elasticOpType)];
8990
long? status = itemIndex?["status"];
9091
i++;
9192
if (!status.HasValue || status < 300)

src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/ElasticsearchPayloadReader.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
using System.Globalization;
44
using System.IO;
55
using System.Linq;
6-
using System.Text;
7-
using System.Threading.Tasks;
6+
using Elasticsearch.Net;
87

98
namespace Serilog.Sinks.Elasticsearch.Durable
109
{
@@ -17,6 +16,7 @@ public class ElasticsearchPayloadReader: APayloadReader<List<string>>
1716
private readonly string _typeName;
1817
private readonly Func<object, string> _serialize;
1918
private readonly Func<string, DateTime,string> _getIndexForEvent;
19+
private readonly ElasticOpType _elasticOpType;
2020
private List<string> _payload;
2121
private int _count;
2222
private DateTime _date;
@@ -28,12 +28,15 @@ public class ElasticsearchPayloadReader: APayloadReader<List<string>>
2828
/// <param name="typeName"></param>
2929
/// <param name="serialize"></param>
3030
/// <param name="getIndexForEvent"></param>
31-
public ElasticsearchPayloadReader(string pipelineName,string typeName, Func<object,string> serialize,Func<string,DateTime,string> getIndexForEvent)
31+
/// <param name="elasticOpType"></param>
32+
public ElasticsearchPayloadReader(string pipelineName, string typeName, Func<object, string> serialize,
33+
Func<string, DateTime, string> getIndexForEvent, ElasticOpType elasticOpType)
3234
{
3335
_pipelineName = pipelineName;
3436
_typeName = typeName;
3537
_serialize = serialize;
3638
_getIndexForEvent = getIndexForEvent;
39+
_elasticOpType = elasticOpType;
3740
}
3841

3942
/// <summary>
@@ -80,18 +83,13 @@ protected override List<string> FinishPayLoad()
8083
protected override void AddToPayLoad(string nextLine)
8184
{
8285
var indexName = _getIndexForEvent(nextLine, _date);
83-
var action = default(object);
86+
var action = ElasticsearchSink.CreateElasticAction(
87+
opType: _elasticOpType,
88+
indexName: indexName, pipelineName: _pipelineName,
89+
id: _count + "_" + Guid.NewGuid(),
90+
mappingType: _typeName);
91+
var actionJson = LowLevelRequestResponseSerializer.Instance.SerializeToString(action);
8492

85-
if (string.IsNullOrWhiteSpace(_pipelineName))
86-
{
87-
action = new { index = new { _index = indexName, _type = _typeName, _id = _count + "_" + Guid.NewGuid() } };
88-
}
89-
else
90-
{
91-
action = new { index = new { _index = indexName, _type = _typeName, _id = _count + "_" + Guid.NewGuid(), pipeline = _pipelineName } };
92-
}
93-
94-
var actionJson = _serialize(action);
9593
_payload.Add(actionJson);
9694
_payload.Add(nextLine);
9795
_count++;

src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticSearchSink.cs

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
using System.Collections.Generic;
1717
using System.IO;
1818
using System.Linq;
19+
using System.Runtime.Serialization;
1920
using System.Threading.Tasks;
2021
using Elasticsearch.Net;
21-
using Elasticsearch.Net.Specification.SecurityApi;
2222
using Serilog.Debugging;
2323
using Serilog.Events;
2424
using Serilog.Sinks.PeriodicBatching;
@@ -82,7 +82,7 @@ protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
8282
if (events == null || !events.Any())
8383
return Task.FromResult<T>(default(T));
8484

85-
var payload = CreatePlayLoad<T>(events);
85+
var payload = CreatePlayLoad(events);
8686
return _state.Client.BulkAsync<T>(PostData.MultiJson(payload));
8787
}
8888

@@ -97,7 +97,7 @@ protected override async Task EmitBatchAsync(IEnumerable<LogEvent> events)
9797
if (events == null || !events.Any())
9898
return null;
9999

100-
var payload = CreatePlayLoad<T>(events);
100+
var payload = CreatePlayLoad(events);
101101
return _state.Client.Bulk<T>(PostData.MultiJson(payload));
102102
}
103103

@@ -165,8 +165,7 @@ private static bool HasProperty(dynamic settings, string name)
165165
return settings.GetType().GetProperty(name) != null;
166166
}
167167

168-
private IEnumerable<string> CreatePlayLoad<T>(IEnumerable<LogEvent> events)
169-
where T : class, IElasticsearchResponse, new()
168+
private IEnumerable<string> CreatePlayLoad(IEnumerable<LogEvent> events)
170169
{
171170
if (!_state.TemplateRegistrationSuccess && _state.Options.RegisterTemplateFailure == RegisterTemplateRecovery.FailSink)
172171
{
@@ -177,19 +176,15 @@ private IEnumerable<string> CreatePlayLoad<T>(IEnumerable<LogEvent> events)
177176
foreach (var e in events)
178177
{
179178
var indexName = _state.GetIndexForEvent(e, e.Timestamp.ToUniversalTime());
180-
var action = default(object);
181-
182179
var pipelineName = _state.Options.PipelineNameDecider?.Invoke(e) ?? _state.Options.PipelineName;
183-
if (string.IsNullOrWhiteSpace(pipelineName))
184-
{
185-
action = new { index = new { _index = indexName, _type = _state.Options.TypeName } };
186-
}
187-
else
188-
{
189-
action = new { index = new { _index = indexName, _type = _state.Options.TypeName, pipeline = pipelineName } };
190-
}
191-
var actionJson = _state.Serialize(action);
192-
payload.Add(actionJson);
180+
181+
var action = CreateElasticAction(
182+
opType: _state.Options.BatchAction,
183+
indexName: indexName,
184+
pipelineName: pipelineName,
185+
mappingType: _state.Options.TypeName);
186+
payload.Add(LowLevelRequestResponseSerializer.Instance.SerializeToString(action));
187+
193188
var sw = new StringWriter();
194189
_state.Formatter.Format(e, sw);
195190
payload.Add(sw.ToString());
@@ -204,10 +199,15 @@ private void HandleResponse(IEnumerable<LogEvent> events, DynamicResponse result
204199
if (result.Success && result.Body?["errors"] == true)
205200
{
206201
var indexer = 0;
202+
var opType = BulkAction(_state.Options.BatchAction);
207203
var items = result.Body["items"];
208204
foreach (var item in items)
209205
{
210-
if (item["index"] != null && HasProperty(item["index"], "error") && item["index"]["error"] != null)
206+
var action = item.ContainsKey(opType)
207+
? item[opType]
208+
: null;
209+
210+
if (action != null && action.ContainsKey("error"))
211211
{
212212
var e = events.ElementAt(indexer);
213213
if (_state.Options.EmitEventFailure.HasFlag(EmitEventFailureHandling.WriteToSelfLog))
@@ -216,8 +216,8 @@ private void HandleResponse(IEnumerable<LogEvent> events, DynamicResponse result
216216
SelfLog.WriteLine(
217217
"Failed to store event with template '{0}' into Elasticsearch. Elasticsearch reports for index {1} the following: {2}",
218218
e.MessageTemplate,
219-
item["index"]["_index"],
220-
_state.Serialize(item["index"]["error"]));
219+
action["_index"],
220+
_state.Serialize(action["error"]));
221221
}
222222

223223
if (_state.Options.EmitEventFailure.HasFlag(EmitEventFailureHandling.WriteToFailureSink) &&
@@ -251,7 +251,6 @@ private void HandleResponse(IEnumerable<LogEvent> events, DynamicResponse result
251251
_state.Options.FailureCallback);
252252
}
253253
}
254-
255254
}
256255
indexer++;
257256
}
@@ -261,5 +260,69 @@ private void HandleResponse(IEnumerable<LogEvent> events, DynamicResponse result
261260
HandleException(result.OriginalException, events);
262261
}
263262
}
263+
264+
internal static string BulkAction(ElasticOpType elasticOpType) =>
265+
elasticOpType == ElasticOpType.Create
266+
? "create"
267+
: "index";
268+
269+
internal static object CreateElasticAction(ElasticOpType opType, string indexName, string pipelineName = null, string id = null, string mappingType = null)
270+
{
271+
var actionPayload = new ElasticActionPayload(
272+
indexName: indexName,
273+
pipeline: string.IsNullOrWhiteSpace(pipelineName) ? null : pipelineName,
274+
id: id,
275+
mappingType: mappingType
276+
);
277+
278+
var action = opType == ElasticOpType.Create
279+
? (object) new ElasticCreateAction(actionPayload)
280+
: new ElasticIndexAction(actionPayload);
281+
return action;
282+
}
283+
284+
sealed class ElasticCreateAction
285+
{
286+
public ElasticCreateAction(ElasticActionPayload payload)
287+
{
288+
Payload = payload;
289+
}
290+
291+
[DataMember(Name = "create")]
292+
public ElasticActionPayload Payload { get; }
293+
}
294+
295+
sealed class ElasticIndexAction
296+
{
297+
public ElasticIndexAction(ElasticActionPayload payload)
298+
{
299+
Payload = payload;
300+
}
301+
302+
[DataMember(Name = "index")]
303+
public ElasticActionPayload Payload { get; }
304+
}
305+
306+
sealed class ElasticActionPayload {
307+
public ElasticActionPayload(string indexName, string pipeline = null, string id = null, string mappingType = null)
308+
{
309+
IndexName = indexName;
310+
Pipeline = pipeline;
311+
Id = id;
312+
MappingType = mappingType;
313+
}
314+
315+
[DataMember(Name = "_type")]
316+
public string MappingType { get; }
317+
318+
[DataMember(Name = "_index")]
319+
public string IndexName { get; }
320+
321+
[DataMember(Name = "pipeline")]
322+
public string Pipeline { get; }
323+
324+
[DataMember(Name = "_id")]
325+
public string Id { get; }
326+
}
264327
}
265328
}

0 commit comments

Comments
 (0)