Skip to content

Commit ed7cee7

Browse files
authored
Merge pull request #14 from nblumhardt/f-multiprocess
Multi-process shared files
2 parents 6805678 + c0abb4a commit ed7cee7

File tree

8 files changed

+329
-39
lines changed

8 files changed

+329
-39
lines changed

example/Sample/Program.cs

+7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
using System;
22
using System.IO;
33
using Serilog;
4+
using Serilog.Debugging;
45

56
namespace Sample
67
{
78
public class Program
89
{
910
public static void Main(string[] args)
1011
{
12+
SelfLog.Enable(Console.Out);
13+
1114
Log.Logger = new LoggerConfiguration()
1215
.WriteTo.File("log.txt")
1316
.CreateLogger();
@@ -24,6 +27,10 @@ public static void Main(string[] args)
2427
sw.Stop();
2528

2629
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");
30+
Console.WriteLine($"Size: {new FileInfo("log.txt").Length}");
31+
32+
Console.WriteLine("Press any key to delete the temporary log file...");
33+
Console.ReadKey(true);
2734

2835
File.Delete("log.txt");
2936
}

example/Sample/Properties/AssemblyInfo.cs

-19
This file was deleted.

example/Sample/project.json

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
{
2-
"version": "1.0.0-*",
32
"buildOptions": {
43
"emitEntryPoint": true
54
},
65

76
"dependencies": {
8-
"Serilog.Sinks.File": { "target": "project" },
9-
"Microsoft.NETCore.App": {
10-
"type": "platform",
11-
"version": "1.0.0"
12-
}
7+
"Serilog.Sinks.File": { "target": "project" }
138
},
149

1510
"frameworks": {
1611
"netcoreapp1.0": {
17-
"imports": "dnxcore50"
18-
}
19-
}
12+
"imports": "dnxcore50",
13+
"dependencies": {
14+
"Microsoft.NETCore.App": {
15+
"type": "platform",
16+
"version": "1.0.0"
17+
}
18+
}
19+
},
20+
"net4.5": {}
21+
},
22+
"runtimes": { "win10-x64": {} }
2023
}

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

+35-9
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public static class FileLoggerConfigurationExtensions
4747
/// will be written in full even if it exceeds the limit.</param>
4848
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
4949
/// is false.</param>
50+
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
5051
/// <returns>Configuration object allowing method chaining.</returns>
5152
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
5253
public static LoggerConfiguration File(
@@ -57,14 +58,15 @@ public static LoggerConfiguration File(
5758
IFormatProvider formatProvider = null,
5859
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
5960
LoggingLevelSwitch levelSwitch = null,
60-
bool buffered = false)
61+
bool buffered = false,
62+
bool shared = false)
6163
{
6264
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
6365
if (path == null) throw new ArgumentNullException(nameof(path));
6466
if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate));
6567

6668
var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
67-
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered);
69+
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared);
6870
}
6971

7072
/// <summary>
@@ -73,7 +75,7 @@ public static LoggerConfiguration File(
7375
/// <param name="sinkConfiguration">Logger sink configuration.</param>
7476
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
7577
/// text for the file. If control of regular text formatting is required, use the other
76-
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool)"/>
78+
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool)"/>
7779
/// and specify the outputTemplate parameter instead.
7880
/// </param>
7981
/// <param name="path">Path to the file.</param>
@@ -86,6 +88,7 @@ public static LoggerConfiguration File(
8688
/// will be written in full even if it exceeds the limit.</param>
8789
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
8890
/// is false.</param>
91+
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
8992
/// <returns>Configuration object allowing method chaining.</returns>
9093
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
9194
public static LoggerConfiguration File(
@@ -95,9 +98,10 @@ public static LoggerConfiguration File(
9598
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
9699
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
97100
LoggingLevelSwitch levelSwitch = null,
98-
bool buffered = false)
101+
bool buffered = false,
102+
bool shared = false)
99103
{
100-
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered);
104+
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared);
101105
}
102106

103107
/// <summary>
@@ -136,7 +140,7 @@ public static LoggerConfiguration File(
136140
/// <param name="sinkConfiguration">Logger sink configuration.</param>
137141
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
138142
/// text for the file. If control of regular text formatting is required, use the other
139-
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool)"/>
143+
/// overload of <see cref="File(LoggerAuditSinkConfiguration, string, LogEventLevel, string, IFormatProvider, LoggingLevelSwitch)"/>
140144
/// and specify the outputTemplate parameter instead.
141145
/// </param>
142146
/// <param name="path">Path to the file.</param>
@@ -164,17 +168,39 @@ static LoggerConfiguration ConfigureFile(
164168
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
165169
LoggingLevelSwitch levelSwitch = null,
166170
bool buffered = false,
167-
bool propagateExceptions = false)
171+
bool propagateExceptions = false,
172+
bool shared = false)
168173
{
169174
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
170175
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
171176
if (path == null) throw new ArgumentNullException(nameof(path));
172177
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
173178

174-
FileSink sink;
179+
if (shared)
180+
{
181+
#if !ATOMIC_APPEND
182+
throw new NotSupportedException("File sharing is not supported on this platform.");
183+
#endif
184+
185+
if (buffered)
186+
throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered));
187+
}
188+
189+
ILogEventSink sink;
175190
try
176191
{
177-
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
192+
#if ATOMIC_APPEND
193+
if (shared)
194+
{
195+
sink = new SharedFileSink(path, formatter, fileSizeLimitBytes);
196+
}
197+
else
198+
{
199+
#endif
200+
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
201+
#if ATOMIC_APPEND
202+
}
203+
#endif
178204
}
179205
catch (Exception ex)
180206
{

src/Serilog.Sinks.File/Sinks/File/FileSink.cs

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
using System;
1616
using System.IO;
17+
#if ATOMIC_APPEND
18+
using System.Security.AccessControl;
19+
#endif
1720
using System.Text;
1821
using Serilog.Core;
1922
using Serilog.Events;
@@ -61,7 +64,13 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy
6164
Directory.CreateDirectory(directory);
6265
}
6366

67+
#if ATOMIC_APPEND
68+
// FileSystemRights.AppendData improves performance substantially (~30%) when available.
69+
Stream file = new FileStream(path, FileMode.Append, FileSystemRights.AppendData, FileShare.Read, 4096, FileOptions.None);
70+
#else
6471
Stream file = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
72+
#endif
73+
6574
if (_fileSizeLimitBytes != null)
6675
{
6776
file = _countingStreamWrapper = new WriteCountingStream(file);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright 2013-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+
#if ATOMIC_APPEND
16+
17+
using System;
18+
using System.IO;
19+
using System.Security.AccessControl;
20+
using System.Text;
21+
using Serilog.Core;
22+
using Serilog.Events;
23+
using Serilog.Formatting;
24+
25+
namespace Serilog.Sinks.File
26+
{
27+
/// <summary>
28+
/// Write log events to a disk file.
29+
/// </summary>
30+
public sealed class SharedFileSink : ILogEventSink, IDisposable
31+
{
32+
readonly MemoryStream _writeBuffer;
33+
readonly string _path;
34+
readonly TextWriter _output;
35+
readonly ITextFormatter _textFormatter;
36+
readonly long? _fileSizeLimitBytes;
37+
readonly object _syncRoot = new object();
38+
readonly FileInfo _fileInfo;
39+
40+
// The stream is reopened with a larger buffer if atomic writes beyond the current buffer size are needed.
41+
FileStream _fileOutput;
42+
int _fileStreamBufferLength = DefaultFileStreamBufferLength;
43+
44+
const int DefaultFileStreamBufferLength = 4096;
45+
46+
/// <summary>Construct a <see cref="FileSink"/>.</summary>
47+
/// <param name="path">Path to the file.</param>
48+
/// <param name="textFormatter">Formatter used to convert log events to text.</param>
49+
/// <param name="fileSizeLimitBytes">The approximate maximum size, in bytes, to which a log file will be allowed to grow.
50+
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
51+
/// will be written in full even if it exceeds the limit.</param>
52+
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
53+
/// <returns>Configuration object allowing method chaining.</returns>
54+
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
55+
/// <exception cref="IOException"></exception>
56+
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null)
57+
{
58+
if (path == null) throw new ArgumentNullException(nameof(path));
59+
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
60+
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
61+
62+
_path = path;
63+
_textFormatter = textFormatter;
64+
_fileSizeLimitBytes = fileSizeLimitBytes;
65+
66+
var directory = Path.GetDirectoryName(path);
67+
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
68+
{
69+
Directory.CreateDirectory(directory);
70+
}
71+
72+
// FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet
73+
// exposed by .NET Core.
74+
_fileOutput = new FileStream(
75+
path,
76+
FileMode.Append,
77+
FileSystemRights.AppendData,
78+
FileShare.Write,
79+
_fileStreamBufferLength,
80+
FileOptions.None);
81+
82+
if (_fileSizeLimitBytes != null)
83+
{
84+
_fileInfo = new FileInfo(path);
85+
}
86+
87+
_writeBuffer = new MemoryStream();
88+
_output = new StreamWriter(_writeBuffer, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
89+
}
90+
91+
/// <summary>
92+
/// Emit the provided log event to the sink.
93+
/// </summary>
94+
/// <param name="logEvent">The log event to write.</param>
95+
public void Emit(LogEvent logEvent)
96+
{
97+
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
98+
99+
if (_fileSizeLimitBytes != null)
100+
{
101+
if (_fileInfo.Length >= _fileSizeLimitBytes.Value)
102+
return;
103+
}
104+
105+
lock (_syncRoot)
106+
{
107+
try
108+
{
109+
_textFormatter.Format(logEvent, _output);
110+
_output.Flush();
111+
var bytes = _writeBuffer.GetBuffer();
112+
var length = (int)_writeBuffer.Length;
113+
if (length > _fileStreamBufferLength)
114+
{
115+
var oldOutput = _fileOutput;
116+
117+
_fileOutput = new FileStream(
118+
_path,
119+
FileMode.Append,
120+
FileSystemRights.AppendData,
121+
FileShare.Write,
122+
length,
123+
FileOptions.None);
124+
_fileStreamBufferLength = length;
125+
126+
oldOutput.Dispose();
127+
}
128+
129+
_fileOutput.Write(bytes, 0, length);
130+
_fileOutput.Flush();
131+
}
132+
catch
133+
{
134+
// Make sure there's no leftover cruft in there.
135+
_output.Flush();
136+
throw;
137+
}
138+
finally
139+
{
140+
_writeBuffer.Position = 0;
141+
_writeBuffer.SetLength(0);
142+
}
143+
}
144+
}
145+
146+
/// <summary>
147+
/// Performs application-defined tasks associated with freeing, releasing, or
148+
/// resetting unmanaged resources.
149+
/// </summary>
150+
public void Dispose() => _fileOutput.Dispose();
151+
}
152+
}
153+
154+
#endif

src/Serilog.Sinks.File/project.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "2.3.0-*",
2+
"version": "3.0.0-*",
33
"description": "Write Serilog events to a text file in plain or JSON format.",
44
"authors": [ "Serilog Contributors" ],
55
"packOptions": {
@@ -16,7 +16,9 @@
1616
"xmlDoc": true
1717
},
1818
"frameworks": {
19-
"net4.5": {},
19+
"net4.5": {
20+
"buildOptions": { "define": [ "ATOMIC_APPEND" ] }
21+
},
2022
"netstandard1.3": {
2123
"dependencies": {
2224
"System.IO": "4.1.0",

0 commit comments

Comments
 (0)