Skip to content

Commit 2c96836

Browse files
authored
Merge pull request #27 from nblumhardt/sharing-mutex
Multiprocess sharing on .NET Core
2 parents 77eed1b + b023427 commit 2c96836

File tree

8 files changed

+172
-27
lines changed

8 files changed

+172
-27
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,4 @@ _Pvt_Extensions
234234

235235
# FAKE - F# Make
236236
.fake/
237+
example/Sample/log.txt

example/Sample/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ public static void Main(string[] args)
1111
{
1212
SelfLog.Enable(Console.Out);
1313

14+
var sw = System.Diagnostics.Stopwatch.StartNew();
15+
1416
Log.Logger = new LoggerConfiguration()
1517
.WriteTo.File("log.txt")
1618
.CreateLogger();
1719

18-
var sw = System.Diagnostics.Stopwatch.StartNew();
19-
2020
for (var i = 0; i < 1000000; ++i)
2121
{
2222
Log.Information("Hello, file logger!");

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -180,32 +180,20 @@ static LoggerConfiguration ConfigureFile(
180180
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
181181
if (path == null) throw new ArgumentNullException(nameof(path));
182182
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
183-
184-
if (shared)
185-
{
186-
#if ATOMIC_APPEND
187-
if (buffered)
188-
throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered));
189-
#else
190-
throw new NotSupportedException("File sharing is not supported on this platform.");
191-
#endif
192-
}
183+
if (shared && buffered)
184+
throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered));
193185

194186
ILogEventSink sink;
195187
try
196188
{
197-
#if ATOMIC_APPEND
198189
if (shared)
199190
{
200191
sink = new SharedFileSink(path, formatter, fileSizeLimitBytes);
201192
}
202193
else
203194
{
204-
#endif
205195
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
206-
#if ATOMIC_APPEND
207196
}
208-
#endif
209197
}
210198
catch (Exception ex)
211199
{

src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs renamed to src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposa
5252
/// <returns>Configuration object allowing method chaining.</returns>
5353
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
5454
/// <exception cref="IOException"></exception>
55-
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes,
56-
Encoding encoding = null)
55+
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null)
5756
{
5857
if (path == null) throw new ArgumentNullException(nameof(path));
5958
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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 OS_MUTEX
16+
17+
using System;
18+
using System.IO;
19+
using System.Text;
20+
using Serilog.Core;
21+
using Serilog.Events;
22+
using Serilog.Formatting;
23+
using System.Threading;
24+
using Serilog.Debugging;
25+
26+
namespace Serilog.Sinks.File
27+
{
28+
/// <summary>
29+
/// Write log events to a disk file.
30+
/// </summary>
31+
public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable
32+
{
33+
readonly TextWriter _output;
34+
readonly FileStream _underlyingStream;
35+
readonly ITextFormatter _textFormatter;
36+
readonly long? _fileSizeLimitBytes;
37+
readonly object _syncRoot = new object();
38+
39+
const string MutexNameSuffix = ".serilog";
40+
const int MutexWaitTimeout = 10000;
41+
readonly Mutex _mutex;
42+
43+
/// <summary>Construct a <see cref="FileSink"/>.</summary>
44+
/// <param name="path">Path to the file.</param>
45+
/// <param name="textFormatter">Formatter used to convert log events to text.</param>
46+
/// <param name="fileSizeLimitBytes">The approximate maximum size, in bytes, to which a log file will be allowed to grow.
47+
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
48+
/// will be written in full even if it exceeds the limit.</param>
49+
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
50+
/// <returns>Configuration object allowing method chaining.</returns>
51+
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
52+
/// <exception cref="IOException"></exception>
53+
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null)
54+
{
55+
if (path == null) throw new ArgumentNullException(nameof(path));
56+
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
57+
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0)
58+
throw new ArgumentException("Negative value provided; file size limit must be non-negative");
59+
60+
_textFormatter = textFormatter;
61+
_fileSizeLimitBytes = fileSizeLimitBytes;
62+
63+
var directory = Path.GetDirectoryName(path);
64+
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
65+
{
66+
Directory.CreateDirectory(directory);
67+
}
68+
69+
var mutexName = Path.GetFullPath(path).Replace(Path.DirectorySeparatorChar, ':') + MutexNameSuffix;
70+
_mutex = new Mutex(false, mutexName);
71+
_underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
72+
_output = new StreamWriter(_underlyingStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
73+
}
74+
75+
/// <summary>
76+
/// Emit the provided log event to the sink.
77+
/// </summary>
78+
/// <param name="logEvent">The log event to write.</param>
79+
public void Emit(LogEvent logEvent)
80+
{
81+
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
82+
83+
lock (_syncRoot)
84+
{
85+
if (!TryAcquireMutex())
86+
return;
87+
88+
try
89+
{
90+
_underlyingStream.Seek(0, SeekOrigin.End);
91+
if (_fileSizeLimitBytes != null)
92+
{
93+
if (_underlyingStream.Length >= _fileSizeLimitBytes.Value)
94+
return;
95+
}
96+
97+
_textFormatter.Format(logEvent, _output);
98+
_output.Flush();
99+
_underlyingStream.Flush();
100+
}
101+
finally
102+
{
103+
ReleaseMutex();
104+
}
105+
}
106+
}
107+
108+
/// <inheritdoc />
109+
public void Dispose()
110+
{
111+
lock (_syncRoot)
112+
{
113+
_output.Dispose();
114+
_mutex.Dispose();
115+
}
116+
}
117+
118+
/// <inheritdoc />
119+
public void FlushToDisk()
120+
{
121+
lock (_syncRoot)
122+
{
123+
if (!TryAcquireMutex())
124+
return;
125+
126+
try
127+
{
128+
_underlyingStream.Flush(true);
129+
}
130+
finally
131+
{
132+
ReleaseMutex();
133+
}
134+
}
135+
}
136+
137+
bool TryAcquireMutex()
138+
{
139+
try
140+
{
141+
if (!_mutex.WaitOne(MutexWaitTimeout))
142+
{
143+
SelfLog.WriteLine("Shared file mutex could not be acquired within {0} ms", MutexWaitTimeout);
144+
return false;
145+
}
146+
}
147+
catch (AbandonedMutexException)
148+
{
149+
SelfLog.WriteLine("Inherited shared file mutex after abandonment by another process");
150+
}
151+
152+
return true;
153+
}
154+
155+
void ReleaseMutex()
156+
{
157+
_mutex.ReleaseMutex();
158+
}
159+
}
160+
}
161+
162+
#endif

src/Serilog.Sinks.File/project.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
"buildOptions": { "define": [ "ATOMIC_APPEND" ] }
2121
},
2222
"netstandard1.3": {
23+
"buildOptions": { "define": [ "OS_MUTEX" ] },
2324
"dependencies": {
2425
"System.IO": "4.1.0",
2526
"System.IO.FileSystem": "4.0.1",
2627
"System.IO.FileSystem.Primitives": "4.0.1",
2728
"System.Text.Encoding.Extensions": "4.0.11",
28-
"System.Threading.Timer": "4.0.1"
29+
"System.Threading.Timer": "4.0.1",
30+
"System.Threading": "4.0.11"
2931
}
3032
}
3133
}

test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
#if ATOMIC_APPEND
2-
3-
using System.IO;
1+
using System.IO;
42
using Xunit;
53
using Serilog.Formatting.Json;
64
using Serilog.Sinks.File.Tests.Support;
@@ -102,5 +100,3 @@ public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted()
102100
}
103101
}
104102
}
105-
106-
#endif

test/Serilog.Sinks.File.Tests/project.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919
]
2020
},
2121
"net4.5.2": {
22-
"buildOptions": {
23-
"define": ["ATOMIC_APPEND"]
24-
}
2522
}
2623
}
2724
}

0 commit comments

Comments
 (0)