src/Elastic.OpenTelemetry.Core/Diagnostics/FileLogger.cs (108 lines of code) (raw):
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using Elastic.OpenTelemetry.Configuration;
using Microsoft.Extensions.Logging;
namespace Elastic.OpenTelemetry.Diagnostics;
internal sealed class FileLogger : IDisposable, IAsyncDisposable, ILogger
{
private readonly ConcurrentQueue<string> _logQueue = new();
private readonly SemaphoreSlim _logSemaphore = new(0);
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly StreamWriter _streamWriter;
private readonly LogLevel _configuredLogLevel;
private bool _disposed;
public bool FileLoggingEnabled { get; }
private readonly LoggerExternalScopeProvider _scopeProvider;
public FileLogger(CompositeElasticOpenTelemetryOptions options)
{
_scopeProvider = new LoggerExternalScopeProvider();
_configuredLogLevel = options.LogLevel;
_streamWriter = StreamWriter.Null;
WritingTask = Task.CompletedTask;
FileLoggingEnabled = options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.File);
if (!FileLoggingEnabled)
return;
try
{
var process = Process.GetCurrentProcess();
// When ordered by filename, we see logs from the same process grouped, then ordered by oldest to newest, then the PID for that instance
var logFileName = $"EDOT.{process.ProcessName}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{process.Id}.log";
var logDirectory = options.LogDirectory;
LogFilePath = Path.Combine(logDirectory, logFileName);
if (!Directory.Exists(logDirectory))
Directory.CreateDirectory(logDirectory);
// StreamWriter.Dispose disposes underlying stream too.
var stream = new FileStream(LogFilePath, FileMode.OpenOrCreate, FileAccess.Write);
_streamWriter = new StreamWriter(stream, Encoding.UTF8);
_streamWriter.WriteLine("DateTime (UTC) Thread SpanId Level Message");
_streamWriter.WriteLine();
WritingTask = Task.Run(async () =>
{
var cancellationToken = _cancellationTokenSource.Token;
while (!cancellationToken.IsCancellationRequested)
{
await _logSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
while (_logQueue.TryDequeue(out var logEntry))
{
await _streamWriter.WriteLineAsync(logEntry).ConfigureAwait(false);
}
}
// Flush remaining log entries before exiting
while (_logQueue.TryDequeue(out var logEntry))
{
await _streamWriter.WriteLineAsync(logEntry).ConfigureAwait(false);
}
});
_streamWriter.AutoFlush = true; // Ensure we don't lose logs by not flushing to the file.
if (options?.AdditionalLogger is not null)
options?.AdditionalLogger.LogInformation("File logging for EDOT .NET enabled. Logs are being written to '{LogFilePath}'.", LogFilePath);
else
Console.Out.WriteLine($"File logging for EDOT .NET enabled. Logs are being written to '{LogFilePath}'.");
return;
}
catch (Exception ex)
{
if (options?.AdditionalLogger is not null)
options?.AdditionalLogger.LogError(new EventId(530, "FileLoggingFailure"), ex, "Failed to set up file logging due to exception: {ExceptionMessage}.", ex.Message);
else
Console.Error.WriteLine($"Failed to set up file logging due to exception: {ex.Message}.");
}
// If we fall through the `try` block, consider file logging disabled.
FileLoggingEnabled = false;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
// We skip logging for any log level higher (numerically) than the configured log level
if (!IsEnabled(logLevel))
return;
var logLine = LogFormatter.Format(logLevel, eventId, state, exception, formatter);
if (exception is not null)
logLine = $"{logLine}{Environment.NewLine}{exception}";
_logQueue.Enqueue(logLine);
_logSemaphore.Release();
}
public bool IsEnabled(LogLevel logLevel) => FileLoggingEnabled && _configuredLogLevel <= logLevel;
public IDisposable BeginScope<TState>(TState state) where TState : notnull => _scopeProvider.Push(state);
public string? LogFilePath { get; }
public Task WritingTask { get; }
public void Dispose()
{
if (_disposed)
return;
_cancellationTokenSource.Cancel();
_logSemaphore.Release();
WritingTask?.Wait();
_streamWriter.Dispose();
_logSemaphore.Dispose();
_cancellationTokenSource.Dispose();
_disposed = true;
}
public async ValueTask DisposeAsync()
{
if (_disposed)
return;
_cancellationTokenSource.Cancel();
_logSemaphore.Release();
await WritingTask.ConfigureAwait(false);
_streamWriter.Dispose();
_logSemaphore.Dispose();
_cancellationTokenSource.Dispose();
_disposed = true;
}
}