-
Notifications
You must be signed in to change notification settings - Fork 765
/
SelfDiagnosticsConfigRefresher.cs
276 lines (248 loc) · 12.6 KB
/
SelfDiagnosticsConfigRefresher.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.IO.MemoryMappedFiles;
namespace OpenTelemetry.Internal;
/// <summary>
/// SelfDiagnosticsConfigRefresher class checks a location for a configuration file
/// and open a MemoryMappedFile of a configured size at the configured file path.
/// The class provides a stream object with proper write position if the configuration
/// file is present and valid. Otherwise, the stream object would be unavailable,
/// nothing will be logged to any file.
/// </summary>
internal class SelfDiagnosticsConfigRefresher : IDisposable
{
public static readonly byte[] MessageOnNewFile = "If you are seeing this message, it means that the OpenTelemetry SDK has successfully created the log file used to write self-diagnostic logs. This file will be appended with logs as they appear. If you do not see any logs following this line, it means no logs of the configured LogLevel is occurring. You may change the LogLevel to show lower log levels, so that logs of lower severities will be shown.\n"u8.ToArray();
private const int ConfigurationUpdatePeriodMilliSeconds = 10000;
private readonly CancellationTokenSource cancellationTokenSource;
private readonly Task worker;
private readonly SelfDiagnosticsConfigParser configParser;
/// <summary>
/// memoryMappedFileCache is a handle kept in thread-local storage as a cache to indicate whether the cached
/// viewStream is created from the current m_memoryMappedFile.
/// </summary>
private readonly ThreadLocal<MemoryMappedFile> memoryMappedFileCache = new(true);
private readonly ThreadLocal<MemoryMappedViewStream> viewStream = new(true);
private bool disposedValue;
// Once the configuration file is valid, an eventListener object will be created.
private SelfDiagnosticsEventListener? eventListener;
private volatile FileStream? underlyingFileStreamForMemoryMappedFile;
private volatile MemoryMappedFile? memoryMappedFile;
private string? logDirectory; // Log directory for log files
private int logFileSize; // Log file size in bytes
private long logFilePosition; // The logger will write into the byte at this position
private EventLevel logEventLevel = (EventLevel)(-1);
public SelfDiagnosticsConfigRefresher()
{
this.configParser = new SelfDiagnosticsConfigParser();
this.UpdateMemoryMappedFileFromConfiguration();
this.cancellationTokenSource = new CancellationTokenSource();
this.worker = Task.Run(() => this.Worker(this.cancellationTokenSource.Token), this.cancellationTokenSource.Token);
}
/// <inheritdoc/>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Try to get the log stream which is seeked to the position where the next line of log should be written.
/// </summary>
/// <param name="byteCount">The number of bytes that need to be written.</param>
/// <param name="stream">When this method returns, contains the Stream object where `byteCount` of bytes can be written.</param>
/// <param name="availableByteCount">The number of bytes that is remaining until the end of the stream.</param>
/// <returns>Whether the logger should log in the stream.</returns>
public virtual bool TryGetLogStream(
int byteCount,
[NotNullWhen(true)]
out Stream? stream,
out int availableByteCount)
{
if (this.memoryMappedFile == null)
{
stream = null;
availableByteCount = 0;
return false;
}
try
{
var cachedViewStream = this.viewStream.Value;
// Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile.
// Once worker thread updates the MemoryMappedFile, all the cached ViewStream objects become
// obsolete.
// Each thread creates a new MemoryMappedViewStream the next time it tries to retrieve it.
// Whether the MemoryMappedViewStream is obsolete is determined by comparing the current
// MemoryMappedFile object with the MemoryMappedFile object cached at the creation time of the
// MemoryMappedViewStream.
if (cachedViewStream == null || this.memoryMappedFileCache.Value != this.memoryMappedFile)
{
// Race condition: The code might reach here right after the worker thread sets memoryMappedFile
// to null in CloseLogFile().
// In this case, let the NullReferenceException be caught and fail silently.
// By design, all events captured will be dropped during a configuration file refresh if
// the file changed, regardless whether the file is deleted or updated.
cachedViewStream = this.memoryMappedFile.CreateViewStream();
this.viewStream.Value = cachedViewStream;
this.memoryMappedFileCache.Value = this.memoryMappedFile;
}
long beginPosition, endPosition;
do
{
beginPosition = this.logFilePosition;
endPosition = beginPosition + byteCount;
if (endPosition >= this.logFileSize)
{
endPosition %= this.logFileSize;
}
}
while (beginPosition != Interlocked.CompareExchange(ref this.logFilePosition, endPosition, beginPosition));
availableByteCount = (int)(this.logFileSize - beginPosition);
cachedViewStream.Seek(beginPosition, SeekOrigin.Begin);
stream = cachedViewStream;
return true;
}
catch (Exception)
{
stream = null;
availableByteCount = 0;
return false;
}
}
private async Task Worker(CancellationToken cancellationToken)
{
await Task.Delay(ConfigurationUpdatePeriodMilliSeconds, cancellationToken).ConfigureAwait(false);
while (!cancellationToken.IsCancellationRequested)
{
this.UpdateMemoryMappedFileFromConfiguration();
await Task.Delay(ConfigurationUpdatePeriodMilliSeconds, cancellationToken).ConfigureAwait(false);
}
}
private void UpdateMemoryMappedFileFromConfiguration()
{
if (this.configParser.TryGetConfiguration(out string? newLogDirectory, out int fileSizeInKB, out EventLevel newEventLevel))
{
int newFileSize = fileSizeInKB * 1024;
if (!newLogDirectory.Equals(this.logDirectory) || this.logFileSize != newFileSize)
{
this.CloseLogFile();
this.OpenLogFile(newLogDirectory, newFileSize);
}
if (!newEventLevel.Equals(this.logEventLevel))
{
if (this.eventListener != null)
{
this.eventListener.Dispose();
}
this.eventListener = new SelfDiagnosticsEventListener(newEventLevel, this);
this.logEventLevel = newEventLevel;
}
}
else
{
this.CloseLogFile();
}
}
private void CloseLogFile()
{
MemoryMappedFile? mmf = Interlocked.CompareExchange(ref this.memoryMappedFile, null, this.memoryMappedFile);
if (mmf != null)
{
// Each thread has its own MemoryMappedViewStream created from the only one MemoryMappedFile.
// Once worker thread closes the MemoryMappedFile, all the ViewStream objects should be disposed
// properly.
foreach (MemoryMappedViewStream stream in this.viewStream.Values)
{
if (stream != null)
{
stream.Dispose();
}
}
mmf.Dispose();
}
FileStream? fs = Interlocked.CompareExchange(
ref this.underlyingFileStreamForMemoryMappedFile,
null,
this.underlyingFileStreamForMemoryMappedFile);
fs?.Dispose();
}
private void OpenLogFile(string newLogDirectory, int newFileSize)
{
try
{
Directory.CreateDirectory(newLogDirectory);
var fileName = Path.GetFileName(Process.GetCurrentProcess().MainModule?.FileName ?? "OpenTelemetrySdk") + "."
+ Process.GetCurrentProcess().Id + ".log";
var filePath = Path.Combine(newLogDirectory, fileName);
// Because the API [MemoryMappedFile.CreateFromFile][1](the string version) behaves differently on
// .NET Framework and .NET Core, here I am using the [FileStream version][2] of it.
// Taking the last four parameter values from [.NET Framework]
// (https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,148)
// and [.NET Core]
// (https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L152)
// The parameter for FileAccess is different in type but the same in rules, both are Read and Write.
// The parameter for FileShare is different in values and in behavior.
// .NET Framework doesn't allow sharing but .NET Core allows reading by other programs.
// The last two parameters are the same values for both frameworks.
// [1]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_String_System_IO_FileMode_System_String_System_Int64_
// [2]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_IO_FileStream_System_String_System_Int64_System_IO_MemoryMappedFiles_MemoryMappedFileAccess_System_IO_HandleInheritability_System_Boolean_
this.underlyingFileStreamForMemoryMappedFile =
new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 0x1000, FileOptions.None);
// The parameter values for MemoryMappedFileSecurity, HandleInheritability and leaveOpen are the same
// values for .NET Framework and .NET Core:
// https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,172
// https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L168-L179
this.memoryMappedFile = MemoryMappedFile.CreateFromFile(
this.underlyingFileStreamForMemoryMappedFile,
null,
newFileSize,
MemoryMappedFileAccess.ReadWrite,
HandleInheritability.None,
false);
this.logDirectory = newLogDirectory;
this.logFileSize = newFileSize;
this.logFilePosition = MessageOnNewFile.Length;
using var stream = this.memoryMappedFile.CreateViewStream();
stream.Write(MessageOnNewFile, 0, MessageOnNewFile.Length);
}
catch (Exception ex)
{
OpenTelemetrySdkEventSource.Log.SelfDiagnosticsFileCreateException(newLogDirectory, ex);
}
}
private void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
this.cancellationTokenSource.Cancel(false);
try
{
this.worker.Wait();
}
catch (AggregateException)
{
}
finally
{
this.cancellationTokenSource.Dispose();
}
// Dispose EventListener before files, because EventListener writes to files.
if (this.eventListener != null)
{
this.eventListener.Dispose();
}
// Ensure worker thread properly finishes.
// Or it might have created another MemoryMappedFile in that thread
// after the CloseLogFile() below is called.
this.CloseLogFile();
// Dispose ThreadLocal variables after the file handles are disposed.
this.viewStream.Dispose();
this.memoryMappedFileCache.Dispose();
}
this.disposedValue = true;
}
}
}