< Summary

Information
Class: AmbientServices.ConsoleBuffer
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/AlternateImplementations/AmbientConsoleLogger.cs
Tag: 332_35464845198
Line coverage
91%
Covered lines: 43
Uncovered lines: 4
Coverable lines: 47
Total lines: 261
Line coverage: 91.4%
Branch coverage
92%
Covered branches: 13
Total branches: 14
Branch coverage: 92.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
FlusherThread()100%11100%
BufferLine(...)100%11100%
Buffer(...)100%11100%
Release()100%1160%
Release()83.33%66100%
Flush()100%11100%
ConsoleBufferBackgroundFlusher()100%8884.62%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/AlternateImplementations/AmbientConsoleLogger.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Diagnostics;
 4using System.Diagnostics.CodeAnalysis;
 5
 6#if NET5_0_OR_GREATER
 7using System.Runtime.Versioning;
 8#endif
 9using System.Text;
 10using System.Threading;
 11using System.Threading.Tasks;
 12
 13namespace AmbientServices;
 14
 15/// <summary>
 16/// A very basic ambient logger that just sends log data to a high-performance asynchronous wrapper on the system consol
 17/// This logger is higher performance than the default one that writes to files, but its output persists only if somethi
 18/// so using the file logger by default is better for diagnosing issues that occur before the user is able to switch log
 19/// Switch to this logger for better performance, but less persistent log data.
 20/// Turn the logger off for maximum performance.
 21/// </summary>
 22/// <remarks>
 23/// <pitch>Asynchronous logging to standard output — the natural choice for containers and CLI tools where the console s
 24/// <pledge><see cref="IAmbientLogger"/></pledge>
 25/// <pledge><see cref="IAmbientStructuredLogger"/></pledge>
 26/// <plan>
 27/// A stateless singleton (<see cref="Instance"/>, private constructor) that forwards every line to the process-wide <se
 28/// Trade-off profile: same buffering machinery and speed as <see cref="AmbientTraceLogger"/>, but delivered to stdout i
 29/// </plan>
 30/// <priority>
 31/// <see cref="IAmbientLogger"/>
 32/// 1. Logging-path speed over durability: persistence is whatever is capturing stdout, and with nothing capturing it th
 33/// </priority>
 34/// </remarks>
 35public class AmbientConsoleLogger : IAmbientLogger, IAmbientStructuredLogger
 36{
 37    /// <summary>
 38    /// Gets the default instance of the ambient console logger.
 39    /// </summary>
 40    public static AmbientConsoleLogger Instance { get; } = new();
 41
 42    /// <summary>
 43    /// Constructs an ambient console logger, an implementation of <see cref="IAmbientLogger"/> that outputs log data to
 44    /// </summary>
 45    private AmbientConsoleLogger()
 46    {
 47    }
 48    /// <summary>
 49    /// Buffers the specified structured data to be asynchronously logged.
 50    /// </summary>
 51    /// <param name="structuredData">The structured data object.</param>
 52#if NET5_0_OR_GREATER
 53    [UnsupportedOSPlatform("browser")]
 54#endif
 55    public void Log(object structuredData)
 56    {
 57#if NET5_0_OR_GREATER
 58        ArgumentNullException.ThrowIfNull(structuredData);
 59#else
 60        if (structuredData is null) throw new ArgumentNullException(nameof(structuredData));
 61#endif
 62        string message = AmbientLogger.ConvertStructuredDataIntoSimpleMessage(structuredData);
 63        Log(message);
 64    }
 65    /// <summary>
 66    /// Adds the specified message to the log.
 67    /// </summary>
 68    /// <param name="message">An optional message to log.</param>
 69#if NET5_0_OR_GREATER
 70    [UnsupportedOSPlatform("browser")]
 71#endif
 72    public void Log(string message)
 73    {
 74        ConsoleBuffer.BufferLine(message);
 75    }
 76    /// <summary>
 77    /// Asynchronously flushes log entries to the system console output.
 78    /// </summary>
 79#if NET5_0_OR_GREATER
 80    [UnsupportedOSPlatform("browser")]
 81#endif
 82    public async ValueTask Flush(CancellationToken cancel = default)
 83    {
 84        await ConsoleBuffer.Flush(cancel);
 85    }
 86}
 87/// <summary>
 88/// A class to buffer debug console messages and display them asynchronously.
 89/// </summary>
 90/// <remarks>
 91/// <pitch>The process-wide asynchronous buffer between logging callers and <see cref="Console"/>: buffering a line is a
 92/// <pledge>
 93/// Buffering never blocks on console I/O and may be called concurrently from any thread; buffered lines are written to 
 94/// When the in-memory buffer is at capacity, additional lines spill to the ambient <see cref="IAmbientLogOverflowWriter
 95/// </pledge>
 96/// <plan>
 97/// A static <see cref="ConcurrentQueue{T}"/> drained by one dedicated below-normal-priority background thread that batc
 98/// Structurally a sibling of <see cref="TraceBuffer"/> with <see cref="Console"/> substituted for <see cref="System.Dia
 99/// </plan>
 100/// </remarks>
 101#if NET5_0_OR_GREATER
 102[UnsupportedOSPlatform("browser")]
 103#endif
 104public static class ConsoleBuffer
 105{
 2106    private static readonly string _FlushString = Guid.NewGuid().ToString();
 2107    private static readonly ConcurrentQueue<string> _Queue = new();
 2108    private static readonly SemaphoreSlim _Semaphore = new(0, short.MaxValue);
 2109    private static readonly Thread _FlusherThread = FlusherThread();
 2110    private static readonly SemaphoreSlim _FlusherSemaphore = new(0, short.MaxValue);
 111
 112    private static Thread FlusherThread()
 113    {
 114        // fire up a background thread to flush the console data
 2115        Thread thread = new(new ThreadStart(ConsoleBufferBackgroundFlusher)) {
 2116            IsBackground = true,
 2117            Name = "ConsoleBuffer.FlusherThread",
 2118            Priority = ThreadPriority.BelowNormal,
 2119        };
 2120        thread.Start();
 2121        return thread;
 122    }
 123    /// <summary>
 124    /// Buffers the specified line to the concurrent buffer.
 125    /// </summary>
 126    /// <param name="s">The string to buffer.</param>
 127    public static void BufferLine(string s)
 128    {
 2129        Buffer(s + Environment.NewLine);
 2130    }
 131    private static void Buffer(string s)
 132    {
 133        // enqueue the string given to us (or spill to the standard local overflow log when the buffer is full)
 2134        AmbientLogBufferLimits.EnqueueOrOverflow(_Queue, s);
 135        // release the semaphore so the data gets processed
 2136        Release(false).Wait();
 2137    }
 138    [DebuggerStepThrough]
 139    private static bool Release()
 140    {
 141        try
 142        {
 2143            _Semaphore.Release();
 2144            return true;
 145        }
 0146        catch (SemaphoreFullException)
 147        {
 148            // failure!
 0149            return false;
 150        }
 2151    }
 152    private static async Task Release(bool flush, CancellationToken cancel = default)
 153    {
 154        try
 155        {
 156            // if the release fails, flush the queue
 2157            if (!Release()) flush = true;
 2158            cancel.ThrowIfCancellationRequested();
 159            // are we flushing?
 2160            if (flush)
 161            {
 162                // boost the priority of the flusher thread for a bit
 2163                _FlusherThread.Priority = ThreadPriority.AboveNormal;
 2164                cancel.ThrowIfCancellationRequested();
 165                // wait for the flush to happen
 2166                await _FlusherSemaphore.WaitAsync(cancel);
 167            }
 2168        }
 169        finally
 170        {
 171            // restore the thread priority
 2172            if (flush) _FlusherThread.Priority = ThreadPriority.BelowNormal;
 173        }
 2174    }
 175    /// <summary>
 176    /// Asynchronously flushes any queued console lines.
 177    /// </summary>
 178    /// <param name="cancel">A <see cref="CancellationToken"/> that the caller can use to interrupt the operation before
 179    public static async ValueTask Flush(CancellationToken cancel = default)
 180    {
 181        // queue a flush command
 2182        _Queue.Enqueue(_FlushString);
 183        // release the semaphore so the data gets processed
 2184        await Release(true, cancel);
 2185    }
 186    /// <summary>
 187    /// Peeks at all unflushed messages synchronously (for diagnostic purposes only).
 188    /// </summary>
 189    [ExcludeFromCoverage]
 190    [ExcludeFromCodeCoverage, Obsolete("This property should not be used directly--it's only for debugging!")]
 191    public static string PeekUnflushed
 192    {
 193        get
 194        {
 195            StringBuilder ret = new();
 196            foreach (string s in _Queue)
 197            {
 198                // add this to the result
 199                ret.Append(s);
 200            }
 201            // return the data
 202            return ret.ToString();
 203        }
 204    }
 205    private static void ConsoleBufferBackgroundFlusher()
 206    {
 207        // loop forever!
 208        while (true)
 209        {
 210            try
 211            {
 2212                StringBuilder consoleData = new();
 213                // get up to 10 lines of console data
 2214                for (int line = 0; line < 10; ++line)
 215                {
 216                    // get the oldest item on the queue
 217                    string? s;
 218                    // is there a string to console?
 2219                    if (_Queue.TryDequeue(out s))
 220                    {
 2221                        if (s == _FlushString)
 222                        {
 223                            // release the flusher that told us to flush
 2224                            _FlusherSemaphore.Release();
 225                        }
 226                        else
 227                        {
 228                            // add this to the console data
 2229                            consoleData.Append(s);
 230                        }
 231                        // is there more data? (don't wait if there isn't)
 2232                        if (_Semaphore.Wait(0))
 233                        {
 234                            // try to get some more data (up to ten lines)
 235                            continue;
 236                        }
 237                        // else no data left in queue--no point in waiting before we flush to the output
 238                    }
 239                    // else nothing left in the queue
 240                    else break;
 241                }
 242                // is there a string to console?
 2243                if (consoleData.Length > 0)
 244                {
 245                    // console out this string
 2246                    Console.Write(consoleData.ToString());
 247                }
 248                else
 249                {
 250                    // wait for more work (ie. stop using CPU until there is more work to do)
 2251                    _Semaphore.Wait(TimeSpan.FromMinutes(5));   // we shouldn't ever hang here, but just in case, exit *
 252                }
 2253            }
 254            catch (Exception ex)
 255            {
 256                // console out this string
 0257                Console.Write(ex.ToString());
 0258            }
 259        }
 260    }
 261}