2021-04-29 15:10:34 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
2022-03-23 14:50:23 -04:00
using System ;
using System.Collections.Generic ;
using System.Collections.Immutable ;
using System.Diagnostics ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
2022-11-20 19:15:21 -05:00
using System.Runtime.CompilerServices ;
2022-03-23 14:50:23 -04:00
using System.Security.Claims ;
using System.Text ;
using System.Text.Json ;
using System.Threading ;
using System.Threading.Tasks ;
2021-04-29 15:10:34 -04:00
using EpicGames.Core ;
2022-11-18 12:34:37 -05:00
using EpicGames.Horde.Logs ;
using EpicGames.Horde.Storage ;
2022-06-07 15:53:33 -04:00
using Horde.Build.Agents.Sessions ;
using Horde.Build.Jobs ;
using Horde.Build.Logs.Data ;
2022-11-18 12:34:37 -05:00
using Horde.Build.Storage ;
2022-03-16 11:18:39 -04:00
using Horde.Build.Utilities ;
2022-03-23 14:50:23 -04:00
using HordeCommon ;
2021-04-29 15:10:34 -04:00
using Microsoft.Extensions.Caching.Memory ;
2022-03-23 14:50:23 -04:00
using Microsoft.Extensions.Hosting ;
2021-04-29 15:10:34 -04:00
using Microsoft.Extensions.Logging ;
2022-09-30 12:10:53 -04:00
using Microsoft.Extensions.Options ;
2021-04-29 15:10:34 -04:00
using MongoDB.Bson ;
2021-08-07 13:08:14 -04:00
using OpenTracing ;
2022-03-23 14:50:23 -04:00
using OpenTracing.Util ;
using Stream = System . IO . Stream ;
2021-04-29 15:10:34 -04:00
2022-06-07 15:53:33 -04:00
namespace Horde.Build.Logs
2021-04-29 15:10:34 -04:00
{
2021-10-19 16:12:44 -04:00
using JobId = ObjectId < IJob > ;
using LogId = ObjectId < ILogFile > ;
2021-12-03 11:35:15 -05:00
using SessionId = ObjectId < ISession > ;
2021-10-19 16:12:44 -04:00
2021-04-29 15:10:34 -04:00
/// <summary>
/// Metadata about a log file
/// </summary>
public class LogMetadata
{
/// <summary>
/// Length of the log file
/// </summary>
public long Length { get ; set ; }
/// <summary>
/// Number of lines in the log file
/// </summary>
public int MaxLineIndex { get ; set ; }
}
/// <summary>
/// Interface for the log file service
/// </summary>
public interface ILogFileService
{
/// <summary>
/// Creates a new log
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="jobId">Unique id of the job that owns this log file</param>
/// <param name="sessionId">Agent session allowed to update the log</param>
/// <param name="type">Type of events to be stored in the log</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2022-09-01 11:53:49 -04:00
/// <param name="logId">ID of the log file (optional)</param>
2021-04-29 15:10:34 -04:00
/// <returns>The new log file document</returns>
2022-10-26 07:27:13 -04:00
Task < ILogFile > CreateLogFileAsync ( JobId jobId , SessionId ? sessionId , LogType type , LogId ? logId = null , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Gets a logfile by ID
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFileId">Unique id of the log file</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>The logfile document</returns>
2022-10-26 07:27:13 -04:00
Task < ILogFile ? > GetLogFileAsync ( LogId logFileId , CancellationToken cancellationToken ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Gets a logfile by ID, returning a cached copy if available. This should only be used to retrieve constant properties set at creation, such as the session or job it's associated with.
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFileId">Unique id of the log file</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>The logfile document</returns>
2022-10-26 07:27:13 -04:00
Task < ILogFile ? > GetCachedLogFileAsync ( LogId logFileId , CancellationToken cancellationToken ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Returns a list of log files
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="index">Index of the first result to return</param>
/// <param name="count">Number of results to return</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>List of logfile documents</returns>
2022-10-26 07:27:13 -04:00
Task < List < ILogFile > > GetLogFilesAsync ( int? index = null , int? count = null , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
2022-11-18 13:35:05 -05:00
/// <summary>
/// Read a set of lines from the given log file
/// </summary>
/// <param name="logFile">Log file to read</param>
/// <param name="index">Index of the first line to read</param>
/// <param name="count">Maximum number of lines to return</param>
/// <param name="cancellationToken">Cancellation token for the operation</param>
/// <returns>List of lines</returns>
2022-11-19 14:15:32 -05:00
Task < List < Utf8String > > ReadLinesAsync ( ILogFile logFile , int index , int count , CancellationToken cancellationToken = default ) ;
2022-11-18 13:35:05 -05:00
2021-04-29 15:10:34 -04:00
/// <summary>
/// Writes out chunk data and assigns to a file
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file</param>
/// <param name="offset">Offset within the file of data</param>
/// <param name="lineIndex">Current line index of the data (need not be the starting of the line)</param>
/// <param name="data">the data to add</param>
/// <param name="flush">Whether the current chunk is complete and should be flushed</param>
/// <param name="maxChunkLength">The maximum chunk length. Defaults to 128kb.</param>
/// <param name="maxSubChunkLineCount">Maximum number of lines in each sub-chunk.</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns></returns>
2022-10-26 07:27:13 -04:00
Task < ILogFile ? > WriteLogDataAsync ( ILogFile logFile , long offset , int lineIndex , ReadOnlyMemory < byte > data , bool flush , int maxChunkLength = 256 * 1024 , int maxSubChunkLineCount = 128 , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Gets metadata about the log file
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file to query</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Metadata about the log file</returns>
2022-10-26 07:27:13 -04:00
Task < LogMetadata > GetMetadataAsync ( ILogFile logFile , CancellationToken cancellationToken ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Creates new log events
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="newEvents">List of events</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Async task</returns>
2022-10-26 07:27:13 -04:00
Task CreateEventsAsync ( List < NewLogEventData > newEvents , CancellationToken cancellationToken ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Find events for a particular log file
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file instance</param>
2022-04-15 15:14:04 -04:00
/// <param name="spanId">Issue span to return events for</param>
2022-03-23 14:50:23 -04:00
/// <param name="index">Index of the first event to retrieve</param>
/// <param name="count">Number of events to retrieve</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>List of log events</returns>
2022-10-26 07:27:13 -04:00
Task < List < ILogEvent > > FindEventsAsync ( ILogFile logFile , ObjectId ? spanId = null , int? index = null , int? count = null , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Adds events to a log span
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="events">The events to add</param>
/// <param name="spanId">The span id</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Async task</returns>
2022-10-26 07:27:13 -04:00
Task AddSpanToEventsAsync ( IEnumerable < ILogEvent > events , ObjectId spanId , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Find events for an issue
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="spanIds">The span ids</param>
/// <param name="logIds">Log ids to include</param>
/// <param name="index">Index within the events for results to return</param>
/// <param name="count">Number of results to return</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Async task</returns>
2022-10-26 07:27:13 -04:00
Task < List < ILogEvent > > FindEventsForSpansAsync ( IEnumerable < ObjectId > spanIds , LogId [ ] ? logIds , int index , int count , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Gets the data for an event
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file instance</param>
/// <param name="lineIndex">Index of the line in the file</param>
/// <param name="lineCount">Number of lines in the event</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>New event data instance</returns>
2022-10-26 07:27:13 -04:00
Task < ILogEventData > GetEventDataAsync ( ILogFile logFile , int lineIndex , int lineCount , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Gets lines from the given log
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Data for the requested range</returns>
2022-11-18 13:35:05 -05:00
Task < Stream > OpenRawStreamAsync ( ILogFile logFile , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
2022-05-24 10:37:55 -04:00
/// <summary>
/// Parses a stream of json text and outputs plain text
/// </summary>
/// <param name="logFile">The log file to query</param>
/// <param name="outputStream">Output stream to receive the text data</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2022-05-24 10:37:55 -04:00
/// <returns>Async text</returns>
2022-11-18 13:35:05 -05:00
Task CopyPlainTextStreamAsync ( ILogFile logFile , Stream outputStream , CancellationToken cancellationToken = default ) ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Search for the specified text in a log file
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file to search</param>
/// <param name="text">Text to search for</param>
/// <param name="firstLine">Line to start search from</param>
/// <param name="count">Number of results to return</param>
/// <param name="stats">Receives stats for the search</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>List of line numbers containing the given term</returns>
2022-11-20 19:15:21 -05:00
Task < List < int > > SearchLogDataAsync ( ILogFile logFile , string text , int firstLine , int count , SearchStats stats , CancellationToken cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Extension methods for dealing with log files
/// </summary>
public static class LogFileServiceExtensions
{
/// <summary>
/// Parses a stream of json text and outputs plain text
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFileService">The log file service</param>
/// <param name="logFile">The log file to query</param>
/// <param name="outputStream">Output stream to receive the text data</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Async text</returns>
2022-11-18 13:35:05 -05:00
public static async Task CopyRawStreamAsync ( this ILogFileService logFileService , ILogFile logFile , Stream outputStream , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-11-18 13:35:05 -05:00
await using Stream stream = await logFileService . OpenRawStreamAsync ( logFile , cancellationToken ) ;
2022-10-26 07:27:13 -04:00
await stream . CopyToAsync ( outputStream , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
}
/// <summary>
/// Wraps functionality for manipulating logs
/// </summary>
2021-12-14 15:43:25 -05:00
public sealed class LogFileService : IHostedService , ILogFileService , IDisposable
2021-04-29 15:10:34 -04:00
{
2022-09-30 12:10:53 -04:00
private const int MaxConcurrentChunkWrites = 10 ;
2022-03-23 14:50:23 -04:00
private readonly ILogger < LogFileService > _logger ;
private readonly ILogFileCollection _logFiles ;
private readonly ILogEventCollection _logEvents ;
private readonly ILogStorage _storage ;
private readonly ILogBuilder _builder ;
2022-11-18 12:34:37 -05:00
private readonly StorageService _storageService ;
private readonly IOptions < ServerSettings > _settings ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
// Lock object for the <see cref="_writeTasks"/> and <see cref="_writeChunks"/> members
private readonly object _writeLock = new object ( ) ;
private readonly List < Task > _writeTasks = new List < Task > ( ) ;
private readonly HashSet < ( LogId , long ) > _writeChunks = new HashSet < ( LogId , long ) > ( ) ;
private readonly IMemoryCache _logFileCache ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Streams log data to a caller
/// </summary>
class ResponseStream : Stream
{
/// <summary>
/// The log file service that created this stream
/// </summary>
2022-03-23 14:50:23 -04:00
readonly LogFileService _logFileService ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// The log file being read
/// </summary>
2022-03-23 14:50:23 -04:00
readonly ILogFile _logFile ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Starting offset within the file of the data to return
/// </summary>
2022-03-23 14:50:23 -04:00
readonly long _responseOffset ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Length of data to return
/// </summary>
2022-03-23 14:50:23 -04:00
readonly long _responseLength ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Current offset within the stream
/// </summary>
2022-03-23 14:50:23 -04:00
long _currentOffset ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// The current chunk index
/// </summary>
2022-03-23 14:50:23 -04:00
int _chunkIdx ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Buffer containing a message for missing data
/// </summary>
2022-03-23 14:50:23 -04:00
ReadOnlyMemory < byte > _sourceBuffer ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Offset within the source buffer
/// </summary>
2022-03-23 14:50:23 -04:00
int _sourcePos ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Length of the source buffer being copied from
/// </summary>
2022-03-23 14:50:23 -04:00
int _sourceEnd ;
2021-04-29 15:10:34 -04:00
/// <summary>
/// Constructor
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFileService">The log file service, for q</param>
/// <param name="logFile"></param>
/// <param name="offset"></param>
/// <param name="length"></param>
public ResponseStream ( LogFileService logFileService , ILogFile logFile , long offset , long length )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logFileService = logFileService ;
_logFile = logFile ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
_responseOffset = offset ;
_responseLength = length ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
_currentOffset = offset ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
_chunkIdx = logFile . Chunks . GetChunkForOffset ( offset ) ;
_sourceBuffer = null ! ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
public override bool CanRead = > true ;
/// <inheritdoc/>
public override bool CanSeek = > false ;
/// <inheritdoc/>
public override bool CanWrite = > false ;
/// <inheritdoc/>
2022-03-23 14:50:23 -04:00
public override long Length = > _responseLength ;
2021-04-29 15:10:34 -04:00
/// <inheritdoc/>
public override long Position
{
2022-03-23 14:50:23 -04:00
get = > _currentOffset - _responseOffset ;
2021-04-29 15:10:34 -04:00
set = > throw new NotImplementedException ( ) ;
}
/// <inheritdoc/>
public override void Flush ( )
{
}
/// <inheritdoc/>
2022-03-23 14:50:23 -04:00
public override int Read ( byte [ ] buffer , int offset , int count )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
return ReadAsync ( buffer , offset , count , CancellationToken . None ) . Result ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-03-23 14:50:23 -04:00
public override async Task < int > ReadAsync ( byte [ ] buffer , int offset , int length , CancellationToken cancellationToken )
2021-12-14 18:44:21 -05:00
{
2022-03-23 14:50:23 -04:00
return await ReadAsync ( buffer . AsMemory ( offset , length ) , cancellationToken ) ;
2021-12-14 18:44:21 -05:00
}
/// <inheritdoc/>
2022-03-23 14:50:23 -04:00
public override async ValueTask < int > ReadAsync ( Memory < byte > buffer , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
int readBytes = 0 ;
while ( readBytes < buffer . Length )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
if ( _sourcePos < _sourceEnd )
2021-04-29 15:10:34 -04:00
{
// Try to copy from the current buffer
2022-03-23 14:50:23 -04:00
int blockSize = Math . Min ( _sourceEnd - _sourcePos , buffer . Length - readBytes ) ;
_sourceBuffer . Slice ( _sourcePos , blockSize ) . Span . CopyTo ( buffer . Slice ( readBytes ) . Span ) ;
_currentOffset + = blockSize ;
readBytes + = blockSize ;
_sourcePos + = blockSize ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
else if ( _currentOffset < _responseOffset + _responseLength )
2021-04-29 15:10:34 -04:00
{
// Move to the right chunk
2022-03-23 14:50:23 -04:00
while ( _chunkIdx + 1 < _logFile . Chunks . Count & & _currentOffset > = _logFile . Chunks [ _chunkIdx + 1 ] . Offset )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_chunkIdx + + ;
2021-04-29 15:10:34 -04:00
}
// Get the chunk data
2022-03-23 14:50:23 -04:00
ILogChunk chunk = _logFile . Chunks [ _chunkIdx ] ;
LogChunkData chunkData = await _logFileService . ReadChunkAsync ( _logFile , _chunkIdx ) ;
2021-04-29 15:10:34 -04:00
// Figure out which sub-chunk to use
2022-03-23 14:50:23 -04:00
int subChunkIdx = chunkData . GetSubChunkForOffsetWithinChunk ( ( int ) ( _currentOffset - chunk . Offset ) ) ;
LogSubChunkData subChunkData = chunkData . SubChunks [ subChunkIdx ] ;
2021-04-29 15:10:34 -04:00
// Get the source data
2022-03-23 14:50:23 -04:00
long subChunkOffset = chunk . Offset + chunkData . SubChunkOffset [ subChunkIdx ] ;
_sourceBuffer = subChunkData . InflateText ( ) . Data ;
_sourcePos = ( int ) ( _currentOffset - subChunkOffset ) ;
_sourceEnd = ( int ) Math . Min ( _sourceBuffer . Length , ( _responseOffset + _responseLength ) - subChunkOffset ) ;
2021-04-29 15:10:34 -04:00
}
else
{
// End of the log
break ;
}
}
2022-03-23 14:50:23 -04:00
return readBytes ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
public override long Seek ( long offset , SeekOrigin origin ) = > throw new NotImplementedException ( ) ;
/// <inheritdoc/>
public override void SetLength ( long value ) = > throw new NotImplementedException ( ) ;
/// <inheritdoc/>
public override void Write ( byte [ ] buffer , int offset , int count ) = > throw new NotImplementedException ( ) ;
}
2022-11-18 12:34:37 -05:00
/// <summary>
/// Streams log data to a caller
/// </summary>
class NewLoggerResponseStream : Stream
{
readonly TreeReader _reader ;
readonly LogNode _rootNode ;
/// <summary>
/// Starting offset within the file of the data to return
/// </summary>
readonly long _responseOffset ;
/// <summary>
/// Length of data to return
/// </summary>
readonly long _responseLength ;
/// <summary>
/// Current offset within the stream
/// </summary>
long _currentOffset ;
/// <summary>
/// The current chunk index
/// </summary>
int _chunkIdx ;
/// <summary>
/// Buffer containing a message for missing data
/// </summary>
ReadOnlyMemory < byte > _sourceBuffer ;
/// <summary>
/// Offset within the source buffer
/// </summary>
int _sourcePos ;
/// <summary>
/// Length of the source buffer being copied from
/// </summary>
int _sourceEnd ;
/// <summary>
/// Constructor
/// </summary>
public NewLoggerResponseStream ( TreeReader reader , LogNode rootNode , long offset , long length )
{
_reader = reader ;
_rootNode = rootNode ;
_responseOffset = offset ;
_responseLength = length ;
_currentOffset = offset ;
_chunkIdx = rootNode . TextChunkRefs . GetChunkForOffset ( offset ) ;
_sourceBuffer = null ! ;
}
/// <inheritdoc/>
public override bool CanRead = > true ;
/// <inheritdoc/>
public override bool CanSeek = > false ;
/// <inheritdoc/>
public override bool CanWrite = > false ;
/// <inheritdoc/>
public override long Length = > _responseLength ;
/// <inheritdoc/>
public override long Position
{
get = > _currentOffset - _responseOffset ;
set = > throw new NotImplementedException ( ) ;
}
/// <inheritdoc/>
public override void Flush ( )
{
}
/// <inheritdoc/>
public override int Read ( byte [ ] buffer , int offset , int count )
{
return ReadAsync ( buffer , offset , count , CancellationToken . None ) . Result ;
}
/// <inheritdoc/>
public override async Task < int > ReadAsync ( byte [ ] buffer , int offset , int length , CancellationToken cancellationToken )
{
return await ReadAsync ( buffer . AsMemory ( offset , length ) , cancellationToken ) ;
}
/// <inheritdoc/>
public override async ValueTask < int > ReadAsync ( Memory < byte > buffer , CancellationToken cancellationToken )
{
int readBytes = 0 ;
while ( readBytes < buffer . Length )
{
if ( _sourcePos < _sourceEnd )
{
// Try to copy from the current buffer
int blockSize = Math . Min ( _sourceEnd - _sourcePos , buffer . Length - readBytes ) ;
_sourceBuffer . Slice ( _sourcePos , blockSize ) . Span . CopyTo ( buffer . Slice ( readBytes ) . Span ) ;
_currentOffset + = blockSize ;
readBytes + = blockSize ;
_sourcePos + = blockSize ;
}
else if ( _currentOffset < _responseOffset + _responseLength )
{
// Move to the right chunk
while ( _chunkIdx + 1 < _rootNode . TextChunkRefs . Count & & _currentOffset > = _rootNode . TextChunkRefs [ _chunkIdx + 1 ] . Offset )
{
_chunkIdx + + ;
}
// Get the chunk data
LogChunkRef chunk = _rootNode . TextChunkRefs [ _chunkIdx ] ;
LogChunkNode chunkNode = await chunk . ExpandCopyAsync ( _reader , cancellationToken ) ;
// Get the source data
_sourceBuffer = chunkNode . Data ;
_sourcePos = ( int ) ( _currentOffset - chunk . Offset ) ;
_sourceEnd = ( int ) Math . Min ( _sourceBuffer . Length , ( _responseOffset + _responseLength ) - chunk . Offset ) ;
}
else
{
// End of the log
break ;
}
}
return readBytes ;
}
/// <inheritdoc/>
public override long Seek ( long offset , SeekOrigin origin ) = > throw new NotImplementedException ( ) ;
/// <inheritdoc/>
public override void SetLength ( long value ) = > throw new NotImplementedException ( ) ;
/// <inheritdoc/>
public override void Write ( byte [ ] buffer , int offset , int count ) = > throw new NotImplementedException ( ) ;
}
2022-11-19 14:15:32 -05:00
readonly LogTailService _logTailService ;
2022-03-23 14:50:23 -04:00
readonly ITicker _ticker ;
2021-12-14 15:43:25 -05:00
2021-04-29 15:10:34 -04:00
/// <summary>
/// Constructor
/// </summary>
2022-11-19 14:15:32 -05:00
public LogFileService ( ILogFileCollection logFiles , ILogEventCollection logEvents , ILogBuilder builder , ILogStorage storage , IClock clock , LogTailService logTailService , StorageService storageService , IOptions < ServerSettings > settings , ILogger < LogFileService > logger )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logFiles = logFiles ;
_logEvents = logEvents ;
_logFileCache = new MemoryCache ( new MemoryCacheOptions ( ) ) ;
_builder = builder ;
_storage = storage ;
2022-10-12 08:41:13 -04:00
_ticker = clock . AddSharedTicker < LogFileService > ( TimeSpan . FromSeconds ( 30.0 ) , TickAsync , logger ) ;
2022-11-19 14:15:32 -05:00
_logTailService = logTailService ;
2022-11-18 12:34:37 -05:00
_storageService = storageService ;
2022-09-30 12:10:53 -04:00
_settings = settings ;
2022-03-23 14:50:23 -04:00
_logger = logger ;
2021-12-14 15:43:25 -05:00
}
/// <inheritdoc/>
2022-03-23 14:50:23 -04:00
public Task StartAsync ( CancellationToken cancellationToken ) = > _ticker . StartAsync ( ) ;
2021-12-14 15:43:25 -05:00
/// <inheritdoc/>
2022-03-23 14:50:23 -04:00
public async Task StopAsync ( CancellationToken cancellationToken )
2021-12-14 15:43:25 -05:00
{
2022-03-23 14:50:23 -04:00
_logger . LogInformation ( "Stopping log file service" ) ;
if ( _builder . FlushOnShutdown )
2021-12-14 15:43:25 -05:00
{
await FlushAsync ( ) ;
}
2022-03-23 14:50:23 -04:00
await _ticker . StopAsync ( ) ;
_logger . LogInformation ( "Log service stopped" ) ;
2021-12-14 15:43:25 -05:00
}
/// <inheritdoc/>
public void Dispose ( )
{
2022-03-23 14:50:23 -04:00
_logFileCache . Dispose ( ) ;
_storage . Dispose ( ) ;
_ticker . Dispose ( ) ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public Task < ILogFile > CreateLogFileAsync ( JobId jobId , SessionId ? sessionId , LogType type , LogId ? logId , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
return _logFiles . CreateLogFileAsync ( jobId , sessionId , type , logId , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public async Task < ILogFile ? > GetLogFileAsync ( LogId logFileId , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
ILogFile ? logFile = await _logFiles . GetLogFileAsync ( logFileId , cancellationToken ) ;
2022-03-23 14:50:23 -04:00
if ( logFile ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
AddCachedLogFile ( logFile ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
return logFile ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Adds a log file to the cache
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file to cache</param>
void AddCachedLogFile ( ILogFile logFile )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
MemoryCacheEntryOptions options = new MemoryCacheEntryOptions ( ) . SetSlidingExpiration ( TimeSpan . FromSeconds ( 30 ) ) ;
_logFileCache . Set ( logFile . Id , logFile , options ) ;
2021-04-29 15:10:34 -04:00
}
2022-10-26 07:27:13 -04:00
/// <inheritdoc />
public async Task < ILogFile ? > GetCachedLogFileAsync ( LogId logFileId , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
object? logFile ;
if ( ! _logFileCache . TryGetValue ( logFileId , out logFile ) )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
logFile = await GetLogFileAsync ( logFileId , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
return ( ILogFile ? ) logFile ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public Task < List < ILogFile > > GetLogFilesAsync ( int? index = null , int? count = null , CancellationToken cancellationToken = default )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
return _logFiles . GetLogFilesAsync ( index , count , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
2022-11-18 13:35:05 -05:00
/// <inheritdoc/>
2022-11-19 14:15:32 -05:00
public async Task < List < Utf8String > > ReadLinesAsync ( ILogFile logFile , int index , int count , CancellationToken cancellationToken )
2022-11-18 13:35:05 -05:00
{
List < Utf8String > lines = new List < Utf8String > ( ) ;
2022-11-19 14:15:32 -05:00
if ( _settings . Value . FeatureFlags . EnableNewLogger )
2022-11-18 13:35:05 -05:00
{
2022-11-19 14:15:32 -05:00
TreeReader reader = await GetTreeReaderAsync ( cancellationToken ) ;
int maxIndex = index + count ;
LogNode ? root = await reader . TryReadNodeAsync < LogNode > ( logFile . RefName , cancellationToken : cancellationToken ) ;
if ( root ! = null )
2022-11-18 13:35:05 -05:00
{
2022-11-19 14:15:32 -05:00
int chunkIdx = root . TextChunkRefs . GetChunkForLine ( index ) ;
for ( ; index < maxIndex & & chunkIdx < root . TextChunkRefs . Count ; chunkIdx + + )
{
LogChunkRef chunk = root . TextChunkRefs [ chunkIdx ] ;
LogChunkNode chunkData = await chunk . ExpandAsync ( reader , cancellationToken ) ;
for ( ; index < maxIndex & & index < chunk . LineIndex ; index + + )
{
lines . Add ( $"Internal error; missing data for line {index}\n" ) ;
}
for ( ; index < maxIndex & & index < chunk . LineIndex + chunk . LineCount ; index + + )
{
lines . Add ( chunkData . GetLine ( index - chunk . LineIndex ) ) ;
}
}
}
if ( root = = null | | ! root . Complete )
{
await _logTailService . EnableTailingAsync ( logFile . Id , root ? . LineCount ? ? 0 ) ;
if ( index < maxIndex )
{
await _logTailService . ReadAsync ( logFile . Id , index , maxIndex - index , lines ) ;
}
}
}
else
{
( _ , long minOffset ) = await GetLineOffsetAsync ( logFile , index , cancellationToken ) ;
( _ , long maxOffset ) = await GetLineOffsetAsync ( logFile , index + Math . Min ( count , Int32 . MaxValue - index ) , cancellationToken ) ;
byte [ ] result ;
using ( System . IO . Stream stream = await OpenRawStreamAsync ( logFile , minOffset , maxOffset - minOffset , cancellationToken ) )
{
result = new byte [ stream . Length ] ;
await stream . ReadFixedSizeDataAsync ( result , 0 , result . Length ) ;
}
int offset = 0 ;
for ( int idx = 0 ; idx < result . Length ; idx + + )
{
if ( result [ idx ] = = ( byte ) '\n' )
{
lines . Add ( new Utf8String ( result . AsMemory ( offset , idx - offset ) ) ) ;
offset = idx + 1 ;
}
2022-11-18 13:35:05 -05:00
}
}
return lines ;
}
2021-04-29 15:10:34 -04:00
class WriteState
{
2022-03-23 14:50:23 -04:00
public long _offset ;
public int _lineIndex ;
public ReadOnlyMemory < byte > _memory ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
public WriteState ( long offset , int lineIndex , ReadOnlyMemory < byte > memory )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_offset = offset ;
_lineIndex = lineIndex ;
_memory = memory ;
2021-04-29 15:10:34 -04:00
}
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public async Task < ILogFile ? > WriteLogDataAsync ( ILogFile logFile , long offset , int lineIndex , ReadOnlyMemory < byte > data , bool flush , int maxChunkLength , int maxSubChunkLineCount , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "WriteLogDataAsync" ) . StartActive ( ) ;
scope . Span . SetTag ( "LogId" , logFile . Id . ToString ( ) ) ;
scope . Span . SetTag ( "Offset" , offset . ToString ( CultureInfo . InvariantCulture ) ) ;
scope . Span . SetTag ( "Length" , data . Length . ToString ( CultureInfo . InvariantCulture ) ) ;
scope . Span . SetTag ( "LineIndex" , lineIndex . ToString ( CultureInfo . InvariantCulture ) ) ;
2021-04-29 15:10:34 -04:00
// Make sure the data ends in a newline
2022-03-23 14:50:23 -04:00
if ( data . Length > 0 & & data . Span [ data . Length - 1 ] ! = '\n' )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
throw new ArgumentException ( "Log data must consist of a whole number of lines" , nameof ( data ) ) ;
2021-04-29 15:10:34 -04:00
}
// Make sure the line count is a power of two
2022-03-23 14:50:23 -04:00
if ( ( maxSubChunkLineCount & ( maxSubChunkLineCount - 1 ) ) ! = 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
throw new ArgumentException ( "Maximum line count per sub-chunk must be a power of two" , nameof ( maxSubChunkLineCount ) ) ;
2021-04-29 15:10:34 -04:00
}
// List of the flushed chunks
2022-03-23 14:50:23 -04:00
List < long > completeOffsets = new List < long > ( ) ;
2021-04-29 15:10:34 -04:00
// Add the data to new chunks
2022-03-23 14:50:23 -04:00
WriteState state = new WriteState ( offset , lineIndex , data ) ;
while ( state . _memory . Length > 0 )
2021-04-29 15:10:34 -04:00
{
// Find an existing chunk to append to
2022-03-23 14:50:23 -04:00
int chunkIdx = logFile . Chunks . GetChunkForOffset ( state . _offset ) ;
if ( chunkIdx > = 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
ILogChunk chunk = logFile . Chunks [ chunkIdx ] ;
2022-10-26 07:27:13 -04:00
if ( await WriteLogChunkDataAsync ( logFile , chunk , state , completeOffsets , maxChunkLength , maxSubChunkLineCount , cancellationToken ) )
2021-04-29 15:10:34 -04:00
{
continue ;
}
}
// Create a new chunk. Ensure that there's a chunk at the start of the file, even if the current write is beyond it.
2022-03-23 14:50:23 -04:00
ILogFile ? newLogFile ;
if ( logFile . Chunks . Count = = 0 )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
newLogFile = await _logFiles . TryAddChunkAsync ( logFile , 0 , 0 , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
else
{
2022-10-26 07:27:13 -04:00
newLogFile = await _logFiles . TryAddChunkAsync ( logFile , state . _offset , state . _lineIndex , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
// Try to add a new chunk at the new location
2022-03-23 14:50:23 -04:00
if ( newLogFile = = null )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
newLogFile = await _logFiles . GetLogFileAsync ( logFile . Id , cancellationToken ) ;
2022-03-23 14:50:23 -04:00
if ( newLogFile = = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( "Unable to update log file {LogId}" , logFile . Id ) ;
2021-04-29 15:10:34 -04:00
return null ;
}
2022-03-23 14:50:23 -04:00
logFile = newLogFile ;
2021-04-29 15:10:34 -04:00
}
else
{
2021-05-11 13:12:19 -04:00
// Logger.LogDebug("Added new chunk at offset {Offset} to log {LogId}", State.Offset, LogFile.Id);
2022-03-23 14:50:23 -04:00
logFile = newLogFile ;
2021-04-29 15:10:34 -04:00
}
}
// Flush any pending chunks on this log file
2022-03-23 14:50:23 -04:00
if ( flush )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
foreach ( ILogChunk chunk in logFile . Chunks )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
if ( chunk . Length = = 0 & & ! completeOffsets . Contains ( chunk . Offset ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
await _builder . CompleteChunkAsync ( logFile . Id , chunk . Offset ) ;
completeOffsets . Add ( chunk . Offset ) ;
2021-04-29 15:10:34 -04:00
}
}
}
// Write all the chunks
2022-03-23 14:50:23 -04:00
if ( completeOffsets . Count > 0 | | flush )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
ILogFile ? newLogFile = await WriteCompleteChunksForLogAsync ( logFile , completeOffsets , flush , cancellationToken ) ;
2022-03-23 14:50:23 -04:00
if ( newLogFile = = null )
2021-04-29 15:10:34 -04:00
{
return null ;
}
2022-03-23 14:50:23 -04:00
logFile = newLogFile ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
return logFile ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Append data to an existing chunk.
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file to append to</param>
/// <param name="chunk">Chunk within the log file to update</param>
/// <param name="state">Data remaining to be written</param>
/// <param name="completeOffsets">List of complete chunks</param>
/// <param name="maxChunkLength">Maximum length of each chunk</param>
/// <param name="maxSubChunkLineCount">Maximum number of lines in each subchunk</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>True if data was appended to </returns>
2022-10-26 07:27:13 -04:00
private async Task < bool > WriteLogChunkDataAsync ( ILogFile logFile , ILogChunk chunk , WriteState state , List < long > completeOffsets , int maxChunkLength , int maxSubChunkLineCount , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
// Don't allow data to be appended if the chunk is complete
2022-03-23 14:50:23 -04:00
if ( chunk . Length > 0 )
2021-04-29 15:10:34 -04:00
{
return false ;
}
// Otherwise keep appending subchunks
2022-10-18 11:08:00 -04:00
bool result = false ;
2021-04-29 15:10:34 -04:00
for ( ; ; )
{
// Flush the current sub-chunk if we're on a boundary
2022-03-23 14:50:23 -04:00
if ( state . _lineIndex > 0 & & ( state . _lineIndex & ( maxSubChunkLineCount - 1 ) ) = = 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogDebug ( "Completing log {LogId} chunk offset {Offset} sub-chunk at line {LineIndex}" , logFile . Id , chunk . Offset , state . _lineIndex ) ;
await _builder . CompleteSubChunkAsync ( logFile . Id , chunk . Offset ) ;
2021-04-29 15:10:34 -04:00
}
// Figure out the max length to write to the current chunk
2022-03-23 14:50:23 -04:00
int maxLength = Math . Min ( ( int ) ( ( chunk . Offset + maxChunkLength ) - state . _offset ) , state . _memory . Length ) ;
2021-04-29 15:10:34 -04:00
// Figure out the maximum line index for the current sub chunk
2022-03-23 14:50:23 -04:00
int minLineIndex = state . _lineIndex ;
int maxLineIndex = ( minLineIndex & ~ ( maxSubChunkLineCount - 1 ) ) + maxSubChunkLineCount ;
2021-04-29 15:10:34 -04:00
// Append this data
2022-03-23 14:50:23 -04:00
( int length , int lineCount ) = GetWriteLength ( state . _memory . Span , maxLength , maxLineIndex - minLineIndex , state . _offset = = chunk . Offset ) ;
if ( length > 0 )
2021-04-29 15:10:34 -04:00
{
// Append this data
2022-03-23 14:50:23 -04:00
ReadOnlyMemory < byte > appendData = state . _memory . Slice ( 0 , length ) ;
if ( ! await _builder . AppendAsync ( logFile . Id , chunk . Offset , state . _offset , state . _lineIndex , lineCount , appendData , logFile . Type ) )
2021-04-29 15:10:34 -04:00
{
break ;
}
// Update the state
//Logger.LogDebug("Append to log {LogId} chunk offset {Offset} (LineIndex={LineIndex}, LineCount={LineCount}, Offset={WriteOffset}, Length={WriteLength})", LogFile.Id, Chunk.Offset, State.LineIndex, LineCount, State.Offset, Length);
2022-03-23 14:50:23 -04:00
state . _offset + = length ;
state . _lineIndex + = lineCount ;
state . _memory = state . _memory . Slice ( length ) ;
2022-10-18 11:08:00 -04:00
result = true ;
2021-04-29 15:10:34 -04:00
// If this is the end of the data, bail out
2022-03-23 14:50:23 -04:00
if ( state . _memory . Length = = 0 )
2021-04-29 15:10:34 -04:00
{
break ;
}
}
// Flush the sub-chunk if it's full
2022-03-23 14:50:23 -04:00
if ( state . _lineIndex < maxLineIndex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogDebug ( "Completing chunk for log {LogId} at offset {Offset}" , logFile . Id , chunk . Offset ) ;
await _builder . CompleteChunkAsync ( logFile . Id , chunk . Offset ) ;
completeOffsets . Add ( chunk . Offset ) ;
2021-04-29 15:10:34 -04:00
break ;
}
}
2022-10-18 11:08:00 -04:00
return result ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Get the amount of data to write from the given span
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="span">Data to write</param>
/// <param name="maxLength">Maximum length of the data to write</param>
/// <param name="maxLineCount">Maximum number of lines to write</param>
2022-10-18 11:08:00 -04:00
/// <param name="isEmptyChunk">Whether the current chunk is empty</param>
2021-04-29 15:10:34 -04:00
/// <returns>A tuple consisting of the amount of data to write and number of lines in it</returns>
2022-10-18 11:08:00 -04:00
private static ( int , int ) GetWriteLength ( ReadOnlySpan < byte > span , int maxLength , int maxLineCount , bool isEmptyChunk )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
int length = 0 ;
int lineCount = 0 ;
2022-10-18 11:08:00 -04:00
for ( int idx = 0 ; idx < maxLength | | isEmptyChunk ; idx + + )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
if ( span [ idx ] = = '\n' )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
length = idx + 1 ;
lineCount + + ;
2022-10-18 11:08:00 -04:00
isEmptyChunk = false ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
if ( lineCount > = maxLineCount )
2021-04-29 15:10:34 -04:00
{
break ;
}
}
}
2022-03-23 14:50:23 -04:00
return ( length , lineCount ) ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public async Task < LogMetadata > GetMetadataAsync ( ILogFile logFile , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
LogMetadata metadata = new LogMetadata ( ) ;
2022-11-19 14:30:54 -05:00
if ( _settings . Value . FeatureFlags . EnableNewLogger )
2021-04-29 15:10:34 -04:00
{
2022-11-19 14:30:54 -05:00
metadata . MaxLineIndex = logFile . LineCount ;
2022-11-20 19:15:21 -05:00
int nextIdx = await _logTailService . GetTailNextAsync ( logFile . Id ) ;
2022-11-19 14:30:54 -05:00
if ( nextIdx ! = - 1 )
2021-04-29 15:10:34 -04:00
{
2022-11-19 14:30:54 -05:00
metadata . MaxLineIndex = nextIdx ;
2021-04-29 15:10:34 -04:00
}
2022-11-19 14:30:54 -05:00
}
else
{
if ( logFile . Chunks . Count > 0 )
2021-04-29 15:10:34 -04:00
{
2022-11-19 14:30:54 -05:00
ILogChunk chunk = logFile . Chunks [ logFile . Chunks . Count - 1 ] ;
if ( logFile . MaxLineIndex = = null | | chunk . Length = = 0 )
{
LogChunkData chunkData = await ReadChunkAsync ( logFile , logFile . Chunks . Count - 1 ) ;
metadata . Length = chunk . Offset + chunkData . Length ;
metadata . MaxLineIndex = chunk . LineIndex + chunkData . LineCount ;
}
else
{
metadata . Length = chunk . Offset + chunk . Length ;
metadata . MaxLineIndex = logFile . MaxLineIndex . Value ;
}
2021-04-29 15:10:34 -04:00
}
}
2022-03-23 14:50:23 -04:00
return metadata ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public Task CreateEventsAsync ( List < NewLogEventData > newEvents , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
return _logEvents . AddManyAsync ( newEvents ) ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public Task < List < ILogEvent > > FindEventsAsync ( ILogFile logFile , ObjectId ? spanId = null , int? index = null , int? count = null , CancellationToken cancellationToken = default )
2021-04-29 15:10:34 -04:00
{
2022-04-15 15:14:04 -04:00
return _logEvents . FindAsync ( logFile . Id , spanId , index , count ) ;
2021-04-29 15:10:34 -04:00
}
class LogEventLine : ILogEventLine
{
2022-03-23 14:50:23 -04:00
readonly LogLevel _level ;
2021-04-29 15:10:34 -04:00
public EventId ? EventId { get ; }
public string Message { get ; }
public JsonElement Data { get ; }
2022-03-23 14:50:23 -04:00
LogLevel ILogEventLine . Level = > _level ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
public LogEventLine ( ReadOnlySpan < byte > data )
: this ( JsonSerializer . Deserialize < JsonElement > ( data ) )
2021-04-29 15:10:34 -04:00
{
}
2022-03-23 14:50:23 -04:00
public LogEventLine ( JsonElement data )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
Data = data ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
JsonElement levelElement ;
if ( ! data . TryGetProperty ( "level" , out levelElement ) | | ! Enum . TryParse ( levelElement . GetString ( ) , out _level ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_level = LogLevel . Information ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
JsonElement idElement ;
if ( data . TryGetProperty ( "id" , out idElement ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
int idValue ;
if ( idElement . TryGetInt32 ( out idValue ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
EventId = idValue ;
2021-04-29 15:10:34 -04:00
}
}
2022-03-23 14:50:23 -04:00
JsonElement messageElement ;
if ( data . TryGetProperty ( "renderedMessage" , out messageElement ) | | data . TryGetProperty ( "message" , out messageElement ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
Message = messageElement . GetString ( ) ? ? "(Invalid)" ;
2021-04-29 15:10:34 -04:00
}
else
{
Message = "(Missing message or renderedMessage field)" ;
}
}
}
class LogEventData : ILogEventData
{
public IReadOnlyList < ILogEventLine > Lines { get ; }
EventId ? ILogEventData . EventId = > ( Lines . Count > 0 ) ? Lines [ 0 ] . EventId : null ;
EventSeverity ILogEventData . Severity = > ( Lines . Count = = 0 ) ? EventSeverity . Information : ( Lines [ 0 ] . Level = = LogLevel . Warning ) ? EventSeverity . Warning : EventSeverity . Error ;
string ILogEventData . Message = > String . Join ( "\n" , Lines . Select ( x = > x . Message ) ) ;
2022-03-23 14:50:23 -04:00
public LogEventData ( IReadOnlyList < ILogEventLine > lines )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
Lines = lines ;
2021-04-29 15:10:34 -04:00
}
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public Task AddSpanToEventsAsync ( IEnumerable < ILogEvent > events , ObjectId spanId , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
return _logEvents . AddSpanToEventsAsync ( events , spanId ) ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public Task < List < ILogEvent > > FindEventsForSpansAsync ( IEnumerable < ObjectId > spanIds , LogId [ ] ? logIds , int index , int count , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
return _logEvents . FindEventsForSpansAsync ( spanIds , logIds , index , count ) ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public async Task < ILogEventData > GetEventDataAsync ( ILogFile logFile , int lineIndex , int lineCount , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "GetEventDataAsync" ) . StartActive ( ) ;
scope . Span . SetTag ( "LogId" , logFile . Id . ToString ( ) ) ;
scope . Span . SetTag ( "LineIndex" , lineIndex . ToString ( CultureInfo . InvariantCulture ) ) ;
scope . Span . SetTag ( "LineCount" , lineCount . ToString ( CultureInfo . InvariantCulture ) ) ;
2021-04-29 15:10:34 -04:00
2022-11-18 13:35:05 -05:00
List < Utf8String > lines = await ReadLinesAsync ( logFile , lineIndex , lineCount , cancellationToken ) ;
List < LogEventLine > eventLines = new List < LogEventLine > ( lines . Count ) ;
2021-04-29 15:10:34 -04:00
2022-11-18 13:35:05 -05:00
foreach ( Utf8String line in lines )
2021-04-29 15:10:34 -04:00
{
try
{
2022-11-18 13:35:05 -05:00
eventLines . Add ( new LogEventLine ( line . Span ) ) ;
2021-04-29 15:10:34 -04:00
}
2022-11-18 13:35:05 -05:00
catch ( JsonException ex )
2021-04-29 15:10:34 -04:00
{
2022-11-18 13:35:05 -05:00
_logger . LogWarning ( ex , "Unable to parse line from log file: {Line}" , line ) ;
2021-04-29 15:10:34 -04:00
}
}
2022-11-18 13:35:05 -05:00
return new LogEventData ( eventLines ) ;
}
/// <inheritdoc/>
public Task < Stream > OpenRawStreamAsync ( ILogFile logFile , CancellationToken cancellationToken )
{
return OpenRawStreamAsync ( logFile , 0 , Int64 . MaxValue , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public async Task < Stream > OpenRawStreamAsync ( ILogFile logFile , long offset , long length , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-11-18 12:34:37 -05:00
if ( _settings . Value . FeatureFlags . EnableNewLogger )
2021-04-29 15:10:34 -04:00
{
2022-11-18 12:34:37 -05:00
TreeReader reader = await GetTreeReaderAsync ( cancellationToken ) ;
LogNode ? root = await reader . TryReadNodeAsync < LogNode > ( logFile . RefName , cancellationToken : cancellationToken ) ;
if ( root = = null | | root . TextChunkRefs . Count = = 0 )
{
return new MemoryStream ( Array . Empty < byte > ( ) , false ) ;
}
else
{
int lastChunkIdx = root . TextChunkRefs . Count - 1 ;
// Clamp the length of the request
LogChunkRef lastChunk = root . TextChunkRefs [ lastChunkIdx ] ;
if ( length > lastChunk . Offset )
{
long lastChunkLength = lastChunk . Length ;
if ( lastChunkLength < = 0 )
{
LogChunkNode lastChunkNode = await lastChunk . ExpandAsync ( reader , cancellationToken ) ;
lastChunkLength = lastChunkNode . Length ;
}
length = Math . Min ( length , ( lastChunk . Offset + lastChunkLength ) - offset ) ;
}
// Create the new stream
return new NewLoggerResponseStream ( reader , root , offset , length ) ;
}
2021-04-29 15:10:34 -04:00
}
else
{
2022-11-18 12:34:37 -05:00
if ( logFile . Chunks . Count = = 0 )
2021-04-29 15:10:34 -04:00
{
2022-11-18 12:34:37 -05:00
return new MemoryStream ( Array . Empty < byte > ( ) , false ) ;
2021-04-29 15:10:34 -04:00
}
2022-11-18 12:34:37 -05:00
else
{
int lastChunkIdx = logFile . Chunks . Count - 1 ;
2021-04-29 15:10:34 -04:00
2022-11-18 12:34:37 -05:00
// Clamp the length of the request
ILogChunk lastChunk = logFile . Chunks [ lastChunkIdx ] ;
if ( length > lastChunk . Offset )
{
long lastChunkLength = lastChunk . Length ;
if ( lastChunkLength < = 0 )
{
LogChunkData lastChunkData = await ReadChunkAsync ( logFile , lastChunkIdx ) ;
lastChunkLength = lastChunkData . Length ;
}
length = Math . Min ( length , ( lastChunk . Offset + lastChunkLength ) - offset ) ;
}
// Create the new stream
return new ResponseStream ( this , logFile , offset , length ) ;
}
2021-04-29 15:10:34 -04:00
}
}
2022-05-24 10:37:55 -04:00
/// <summary>
/// Helper method for catching exceptions in <see cref="LogText.ConvertToPlainText(ReadOnlySpan{Byte}, Byte[], Int32)"/>
/// </summary>
2022-09-03 09:28:43 -04:00
public static int GuardedConvertToPlainText ( ReadOnlySpan < byte > input , byte [ ] output , int outputOffset , ILogger logger )
2022-05-24 10:37:55 -04:00
{
try
{
return LogText . ConvertToPlainText ( input , output , outputOffset ) ;
}
2022-09-03 09:28:43 -04:00
catch ( Exception ex )
2022-05-24 10:37:55 -04:00
{
2022-09-03 09:28:43 -04:00
logger . LogWarning ( ex , "Unable to convert log line to plain text: {Line}" , Encoding . UTF8 . GetString ( input ) ) ;
2022-05-24 10:37:55 -04:00
output [ outputOffset ] = ( byte ) '\n' ;
return outputOffset + 1 ;
}
}
/// <inheritdoc/>
2022-11-18 13:35:05 -05:00
public async Task CopyPlainTextStreamAsync ( ILogFile logFile , Stream outputStream , CancellationToken cancellationToken )
2022-05-24 10:37:55 -04:00
{
2022-11-18 13:35:05 -05:00
long offset = 0 ;
long length = Int64 . MaxValue ;
2022-10-26 07:27:13 -04:00
using ( Stream stream = await OpenRawStreamAsync ( logFile , 0 , Int64 . MaxValue , cancellationToken ) )
2022-05-24 10:37:55 -04:00
{
byte [ ] readBuffer = new byte [ 4096 ] ;
int readBufferLength = 0 ;
byte [ ] writeBuffer = new byte [ 4096 ] ;
int writeBufferLength = 0 ;
while ( length > 0 )
{
// Add more data to the buffer
2022-10-26 07:27:13 -04:00
int readBytes = await stream . ReadAsync ( readBuffer . AsMemory ( readBufferLength , readBuffer . Length - readBufferLength ) , cancellationToken ) ;
2022-05-24 10:37:55 -04:00
readBufferLength + = readBytes ;
// Copy as many lines as possible to the output
int convertedBytes = 0 ;
for ( int endIdx = 1 ; endIdx < readBufferLength ; endIdx + + )
{
if ( readBuffer [ endIdx ] = = '\n' )
{
writeBufferLength = GuardedConvertToPlainText ( readBuffer . AsSpan ( convertedBytes , endIdx - convertedBytes ) , writeBuffer , writeBufferLength , _logger ) ;
convertedBytes = endIdx + 1 ;
}
}
// If there's anything in the write buffer, write it out
if ( writeBufferLength > 0 )
{
if ( offset < writeBufferLength )
{
int writeLength = ( int ) Math . Min ( ( long ) writeBufferLength - offset , length ) ;
2022-10-26 07:27:13 -04:00
await outputStream . WriteAsync ( writeBuffer . AsMemory ( ( int ) offset , writeLength ) , cancellationToken ) ;
2022-05-24 10:37:55 -04:00
length - = writeLength ;
}
offset = Math . Max ( offset - writeBufferLength , 0 ) ;
writeBufferLength = 0 ;
}
// If we were able to read something, shuffle down the rest of the buffer. Otherwise expand the read buffer.
if ( convertedBytes > 0 )
{
Buffer . BlockCopy ( readBuffer , convertedBytes , readBuffer , 0 , readBufferLength - convertedBytes ) ;
readBufferLength - = convertedBytes ;
}
else if ( readBufferLength > 0 )
{
Array . Resize ( ref readBuffer , readBuffer . Length + 128 ) ;
writeBuffer = new byte [ readBuffer . Length ] ;
}
// Exit if we didn't read anything in this iteration
if ( readBytes = = 0 )
{
break ;
}
}
}
}
2022-11-18 12:34:37 -05:00
async Task < TreeReader > GetTreeReaderAsync ( CancellationToken cancellationToken )
{
IStorageClient store = await _storageService . GetClientAsync ( Namespace . Logs , cancellationToken ) ;
return new TreeReader ( store , _logFileCache , _logger ) ;
}
2021-04-29 15:10:34 -04:00
/// <inheritdoc/>
2022-10-26 07:27:13 -04:00
public async Task < ( int , long ) > GetLineOffsetAsync ( ILogFile logFile , int lineIdx , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-11-18 12:34:37 -05:00
if ( _settings . Value . FeatureFlags . EnableNewLogger )
2021-04-29 15:10:34 -04:00
{
2022-11-18 12:34:37 -05:00
TreeReader reader = await GetTreeReaderAsync ( cancellationToken ) ;
2021-04-29 15:10:34 -04:00
2022-11-18 12:34:37 -05:00
LogNode ? root = await reader . TryReadNodeAsync < LogNode > ( logFile . RefName , cancellationToken : cancellationToken ) ;
if ( root = = null )
{
return ( 0 , 0 ) ;
}
int chunkIdx = root . TextChunkRefs . GetChunkForLine ( lineIdx ) ;
LogChunkRef chunk = root . TextChunkRefs [ chunkIdx ] ;
LogChunkNode chunkData = await chunk . ExpandAsync ( reader , cancellationToken ) ;
if ( lineIdx < chunk . LineIndex )
{
lineIdx = chunk . LineIndex ;
}
int maxLineIndex = chunk . LineIndex + chunkData . LineCount ;
if ( lineIdx > = maxLineIndex )
{
lineIdx = maxLineIndex ;
}
long offset = chunk . Offset + chunkData . LineOffsets [ lineIdx - chunk . LineIndex ] ;
return ( lineIdx , offset ) ;
}
else
2021-04-29 15:10:34 -04:00
{
2022-11-18 12:34:37 -05:00
int chunkIdx = logFile . Chunks . GetChunkForLine ( lineIdx ) ;
2021-04-29 15:10:34 -04:00
2022-11-18 12:34:37 -05:00
ILogChunk chunk = logFile . Chunks [ chunkIdx ] ;
LogChunkData chunkData = await ReadChunkAsync ( logFile , chunkIdx ) ;
if ( lineIdx < chunk . LineIndex )
{
lineIdx = chunk . LineIndex ;
}
int maxLineIndex = chunk . LineIndex + chunkData . LineCount ;
if ( lineIdx > = maxLineIndex )
{
lineIdx = maxLineIndex ;
}
long offset = chunk . Offset + chunkData . GetLineOffsetWithinChunk ( lineIdx - chunk . LineIndex ) ;
return ( lineIdx , offset ) ;
}
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Executes a background task
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="stoppingToken">Cancellation token</param>
async ValueTask TickAsync ( CancellationToken stoppingToken )
2021-04-29 15:10:34 -04:00
{
2022-09-27 10:31:33 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "LogFileService.TickAsync" ) . StartActive ( ) ;
2022-03-23 14:50:23 -04:00
lock ( _writeLock )
2021-04-29 15:10:34 -04:00
{
2021-06-03 10:55:49 -04:00
try
{
2022-03-23 14:50:23 -04:00
_writeTasks . RemoveCompleteTasks ( ) ;
2021-06-03 10:55:49 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-06-03 10:55:49 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( ex , "Exception while waiting for write tasks to complete" ) ;
2021-06-03 10:55:49 -04:00
}
2021-04-29 15:10:34 -04:00
}
2022-09-30 12:10:53 -04:00
await IncrementalFlushAsync ( stoppingToken ) ;
2021-04-29 15:10:34 -04:00
}
2021-12-14 15:43:25 -05:00
2021-04-29 15:10:34 -04:00
/// <summary>
/// Flushes complete chunks to the storage provider
/// </summary>
/// <returns>Async task</returns>
2022-09-30 12:10:53 -04:00
private async Task IncrementalFlushAsync ( CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-09-27 10:31:33 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "LogFileService.IncrementalFlush" ) . StartActive ( ) ;
2021-04-29 15:10:34 -04:00
// Get all the chunks older than 20 minutes
2022-03-23 14:50:23 -04:00
List < ( LogId , long ) > flushChunks = await _builder . TouchChunksAsync ( TimeSpan . FromMinutes ( 10.0 ) ) ;
2021-04-29 15:10:34 -04:00
2022-09-27 10:31:33 -04:00
scope . Span . SetTag ( "numChunks" , flushChunks . Count ) ;
2021-04-29 15:10:34 -04:00
// Mark them all as complete
2022-03-23 14:50:23 -04:00
foreach ( ( LogId logId , long offset ) in flushChunks )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
await _builder . CompleteChunkAsync ( logId , offset ) ;
2021-04-29 15:10:34 -04:00
}
2022-11-18 12:34:37 -05:00
if ( _settings . Value . FeatureFlags . LimitConcurrentLogChunkWriting )
2022-09-30 12:10:53 -04:00
{
// Flush all the chunks and await completion instead of running them async
await WriteCompleteChunksV2Async ( flushChunks , true , cancellationToken ) ;
}
else
{
// Add tasks for flushing all the chunks
WriteCompleteChunks ( flushChunks , true ) ;
}
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Flushes the write cache
/// </summary>
/// <returns>Async task</returns>
public async Task FlushAsync ( )
{
2022-09-27 10:31:33 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "LogFileService.FlushAsync" ) . StartActive ( ) ;
2022-03-23 14:50:23 -04:00
_logger . LogInformation ( "Forcing flush of pending log chunks..." ) ;
2021-04-29 15:10:34 -04:00
// Mark everything in the cache as complete
2022-03-23 14:50:23 -04:00
List < ( LogId , long ) > writeChunks = await _builder . TouchChunksAsync ( TimeSpan . Zero ) ;
WriteCompleteChunks ( writeChunks , true ) ;
2021-04-29 15:10:34 -04:00
// Wait for everything to flush
await FlushPendingWritesAsync ( ) ;
}
/// <summary>
/// Flush any writes in progress
/// </summary>
/// <returns>Async task</returns>
public async Task FlushPendingWritesAsync ( )
{
for ( ; ; )
{
// Capture the current contents of the WriteTasks list
2022-03-23 14:50:23 -04:00
List < Task > tasks ;
lock ( _writeLock )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_writeTasks . RemoveCompleteTasks ( ) ;
tasks = new List < Task > ( _writeTasks ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
if ( tasks . Count = = 0 )
2021-04-29 15:10:34 -04:00
{
break ;
}
// Also add a delay so we'll periodically refresh the list
2022-03-23 14:50:23 -04:00
tasks . Add ( Task . Delay ( TimeSpan . FromSeconds ( 5.0 ) ) ) ;
await Task . WhenAny ( tasks ) ;
2021-04-29 15:10:34 -04:00
}
}
/// <summary>
/// Adds tasks for writing a list of complete chunks
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="chunksToWrite">List of chunks to write</param>
2022-10-18 11:08:00 -04:00
/// <param name="createIndex">Create an index for the log</param>
private void WriteCompleteChunks ( List < ( LogId , long ) > chunksToWrite , bool createIndex )
2021-04-29 15:10:34 -04:00
{
2022-09-27 10:31:33 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "LogFileService.WriteCompleteChunks" ) . StartActive ( ) ;
2022-09-28 07:45:13 -04:00
int numTasksCreated = 0 ;
2022-09-27 10:31:33 -04:00
2022-03-23 14:50:23 -04:00
foreach ( IGrouping < LogId , long > group in chunksToWrite . GroupBy ( x = > x . Item1 , x = > x . Item2 ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
LogId logId = group . Key ;
2021-04-29 15:10:34 -04:00
// Find offsets of new chunks to write
2022-03-23 14:50:23 -04:00
List < long > offsets = new List < long > ( ) ;
lock ( _writeLock )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
foreach ( long offset in group . OrderBy ( x = > x ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
if ( _writeChunks . Add ( ( logId , offset ) ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
offsets . Add ( offset ) ;
2021-04-29 15:10:34 -04:00
}
}
}
// Create the write task
2022-03-23 14:50:23 -04:00
if ( offsets . Count > 0 )
2021-04-29 15:10:34 -04:00
{
2022-10-18 11:08:00 -04:00
Task task = Task . Run ( ( ) = > WriteCompleteChunksForLogAsync ( logId , offsets , createIndex ) ) ;
2022-09-28 07:45:13 -04:00
numTasksCreated + + ;
2022-03-23 14:50:23 -04:00
lock ( _writeLock )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_writeTasks . Add ( task ) ;
2021-04-29 15:10:34 -04:00
}
}
}
2022-09-28 07:45:13 -04:00
scope . Span . SetTag ( "numWriteTasksCreated" , numTasksCreated ) ;
_logger . LogInformation ( "{NumWriteTasksCreated} write tasks created" , numTasksCreated ) ;
2021-04-29 15:10:34 -04:00
}
2022-09-30 12:10:53 -04:00
/// <summary>
/// Writes list of complete chunks
/// </summary>
/// <param name="chunksToWrite">List of chunks to write</param>
2022-10-18 11:08:00 -04:00
/// <param name="createIndex">Create an index for the log</param>
2022-09-30 12:10:53 -04:00
/// <param name="cancellationToken">Cancellation token</param>
2022-10-18 11:08:00 -04:00
private async Task WriteCompleteChunksV2Async ( List < ( LogId , long ) > chunksToWrite , bool createIndex , CancellationToken cancellationToken )
2022-09-30 12:10:53 -04:00
{
using IScope scope = GlobalTracer . Instance . BuildSpan ( "LogFileService.WriteCompleteChunksV2Async" ) . StartActive ( ) ;
HashSet < ( LogId , long ) > writeChunks = new ( ) ;
List < ( LogId , List < long > ) > offsetsToWrite = new ( ) ;
foreach ( IGrouping < LogId , long > group in chunksToWrite . GroupBy ( x = > x . Item1 , x = > x . Item2 ) )
{
LogId logId = group . Key ;
// Find offsets of new chunks to write
List < long > offsets = new ( ) ;
foreach ( long offset in group . OrderBy ( x = > x ) )
{
if ( writeChunks . Add ( ( logId , offset ) ) )
{
offsets . Add ( offset ) ;
}
}
// Create the write task
if ( offsets . Count > 0 )
{
offsetsToWrite . Add ( ( logId , offsets ) ) ;
}
}
scope . Span . SetTag ( "numOffsetsToWrite" , offsetsToWrite . Count ) ;
ParallelOptions opts = new ( ) { MaxDegreeOfParallelism = MaxConcurrentChunkWrites , CancellationToken = cancellationToken } ;
await Parallel . ForEachAsync ( offsetsToWrite , opts , async ( x , innerCt ) = >
{
( LogId logId , List < long > offsets ) = x ;
2022-10-18 11:08:00 -04:00
await WriteCompleteChunksForLogAsync ( logId , offsets , createIndex , innerCt ) ;
2022-09-30 12:10:53 -04:00
} ) ;
}
2021-04-29 15:10:34 -04:00
/// <summary>
/// Writes a set of chunks to the database
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logId">Log file to update</param>
/// <param name="offsets">Chunks to write</param>
2022-10-18 11:08:00 -04:00
/// <param name="createIndex">Whether to create the index for this log</param>
2022-09-30 12:10:53 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Async task</returns>
2022-10-18 11:08:00 -04:00
private async Task < ILogFile ? > WriteCompleteChunksForLogAsync ( LogId logId , List < long > offsets , bool createIndex , CancellationToken cancellationToken = default )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
ILogFile ? logFile = await _logFiles . GetLogFileAsync ( logId , cancellationToken ) ;
2022-03-23 14:50:23 -04:00
if ( logFile ! = null )
2021-04-29 15:10:34 -04:00
{
2022-10-18 11:08:00 -04:00
logFile = await WriteCompleteChunksForLogAsync ( logFile , offsets , createIndex , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
return logFile ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Writes a set of chunks to the database
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFileInterface">Log file to update</param>
/// <param name="offsets">Chunks to write</param>
2022-10-18 11:08:00 -04:00
/// <param name="createIndex">Whether to create the index for this log</param>
2022-09-30 12:10:53 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Async task</returns>
2022-10-18 11:08:00 -04:00
private async Task < ILogFile ? > WriteCompleteChunksForLogAsync ( ILogFile logFileInterface , List < long > offsets , bool createIndex , CancellationToken cancellationToken = default )
2021-04-29 15:10:34 -04:00
{
2022-06-13 05:12:28 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "WriteCompleteChunksForLogAsync" ) . StartActive ( ) ;
scope . Span . SetTag ( "LogId" , logFileInterface . Id . ToString ( ) ) ;
scope . Span . SetTag ( "NumOffsets" , offsets . Count ) ;
2022-10-18 11:08:00 -04:00
scope . Span . SetTag ( "CreateIndex" , createIndex ) ;
2022-06-13 05:12:28 -04:00
2021-04-29 15:10:34 -04:00
// Write the data to the storage provider
2022-03-23 14:50:23 -04:00
List < Task < LogChunkData ? > > chunkWriteTasks = new List < Task < LogChunkData ? > > ( ) ;
foreach ( long offset in offsets )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
int chunkIdx = logFileInterface . Chunks . BinarySearch ( x = > x . Offset , offset ) ;
if ( chunkIdx > = 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogDebug ( "Queuing write of log {LogId} chunk {ChunkIdx} offset {Offset}" , logFileInterface . Id , chunkIdx , offset ) ;
int lineIndex = logFileInterface . Chunks [ chunkIdx ] . LineIndex ;
chunkWriteTasks . Add ( Task . Run ( ( ) = > WriteChunkAsync ( logFileInterface . Id , offset , lineIndex ) ) ) ;
2021-04-29 15:10:34 -04:00
}
}
2022-09-30 12:10:53 -04:00
scope . Span . SetTag ( "NumWriteTasks" , chunkWriteTasks . Count ) ;
2021-04-29 15:10:34 -04:00
// Wait for the tasks to complete, periodically updating the log file object
2022-03-23 14:50:23 -04:00
ILogFile ? logFile = logFileInterface ;
while ( chunkWriteTasks . Count > 0 )
2021-04-29 15:10:34 -04:00
{
// Wait for all tasks to be complete OR (any task has completed AND 30 seconds has elapsed)
2022-03-23 14:50:23 -04:00
Task allCompleteTask = Task . WhenAll ( chunkWriteTasks ) ;
Task anyCompleteTask = Task . WhenAny ( chunkWriteTasks ) ;
2022-10-03 17:01:13 -04:00
await Task . WhenAny ( allCompleteTask , Task . WhenAll ( anyCompleteTask , Task . Delay ( TimeSpan . FromSeconds ( 30.0 ) , cancellationToken ) ) ) ;
2021-04-29 15:10:34 -04:00
// Update the log file with the written chunks
2022-03-23 14:50:23 -04:00
List < LogChunkData ? > writtenChunks = chunkWriteTasks . RemoveCompleteTasks ( ) ;
while ( logFile ! = null )
2021-04-29 15:10:34 -04:00
{
// Update the length of any complete chunks
2022-03-23 14:50:23 -04:00
List < CompleteLogChunkUpdate > updates = new List < CompleteLogChunkUpdate > ( ) ;
foreach ( LogChunkData ? chunkData in writtenChunks )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
if ( chunkData ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
int chunkIdx = logFile . Chunks . GetChunkForOffset ( chunkData . Offset ) ;
if ( chunkIdx > = 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
ILogChunk chunk = logFile . Chunks [ chunkIdx ] ;
if ( chunk . Offset = = chunkData . Offset )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
CompleteLogChunkUpdate update = new CompleteLogChunkUpdate ( chunkIdx , chunkData . Length , chunkData . LineCount ) ;
updates . Add ( update ) ;
2021-04-29 15:10:34 -04:00
}
}
}
}
// Try to apply the updates
2022-10-26 07:27:13 -04:00
ILogFile ? newLogFile = await _logFiles . TryCompleteChunksAsync ( logFile , updates , cancellationToken ) ;
2022-03-23 14:50:23 -04:00
if ( newLogFile ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
logFile = newLogFile ;
2021-04-29 15:10:34 -04:00
break ;
}
// Update the log file
2022-10-26 07:27:13 -04:00
logFile = await GetLogFileAsync ( logFile . Id , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
}
// Create the index if necessary
2022-10-18 11:08:00 -04:00
if ( createIndex & & logFile ! = null )
2021-04-29 15:10:34 -04:00
{
try
{
2022-10-26 07:27:13 -04:00
logFile = await CreateIndexAsync ( logFile , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( ex , "Failed to create index for log {LogId}" , logFileInterface . Id ) ;
2021-04-29 15:10:34 -04:00
}
}
2022-03-23 14:50:23 -04:00
return logFile ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Creates an index for the given log file
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file object</param>
2022-10-26 07:27:13 -04:00
/// <param name="cancellationToken">Cancellation token for the call</param>
2021-04-29 15:10:34 -04:00
/// <returns>Updated log file</returns>
2022-10-26 07:27:13 -04:00
private async Task < ILogFile ? > CreateIndexAsync ( ILogFile logFile , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
if ( logFile . Chunks . Count = = 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
return logFile ;
2021-04-29 15:10:34 -04:00
}
// Get the new length of the log, and early out if it won't be any longer
2022-03-23 14:50:23 -04:00
ILogChunk lastChunk = logFile . Chunks [ logFile . Chunks . Count - 1 ] ;
if ( lastChunk . Offset + lastChunk . Length < = ( logFile . IndexLength ? ? 0 ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
return logFile ;
2021-04-29 15:10:34 -04:00
}
// Save stats for the index creation
2022-03-23 14:50:23 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "CreateIndexAsync" ) . StartActive ( ) ;
scope . Span . SetTag ( "LogId" , logFile . Id . ToString ( ) ) ;
scope . Span . SetTag ( "Length" , ( lastChunk . Offset + lastChunk . Length ) . ToString ( CultureInfo . InvariantCulture ) ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
long newLength = 0 ;
int newLineCount = 0 ;
2021-04-29 15:10:34 -04:00
// Read the existing index if there is one
2022-03-23 14:50:23 -04:00
List < LogIndexData > indexes = new List < LogIndexData > ( ) ;
if ( logFile . IndexLength ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
LogIndexData ? existingIndex = await ReadIndexAsync ( logFile , logFile . IndexLength . Value ) ;
if ( existingIndex ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
indexes . Add ( existingIndex ) ;
newLineCount = existingIndex . LineCount ;
2021-04-29 15:10:34 -04:00
}
}
// Add all the new chunks
2022-03-23 14:50:23 -04:00
int chunkIdx = logFile . Chunks . GetChunkForLine ( newLineCount ) ;
if ( chunkIdx < 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
int firstLine = ( logFile . Chunks . Count > 0 ) ? logFile . Chunks [ 0 ] . LineIndex : - 1 ;
throw new Exception ( $"Invalid chunk index {chunkIdx}. Index.LineCount={newLineCount}, Chunks={logFile.Chunks.Count}, First line={firstLine}" ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
for ( ; chunkIdx < logFile . Chunks . Count ; chunkIdx + + )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
ILogChunk chunk = logFile . Chunks [ chunkIdx ] ;
LogChunkData chunkData = await ReadChunkAsync ( logFile , chunkIdx ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
int subChunkIdx = chunkData . GetSubChunkForLine ( Math . Max ( newLineCount - chunk . LineIndex , 0 ) ) ;
if ( subChunkIdx < 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
throw new Exception ( $"Invalid subchunk index {subChunkIdx}. Chunk {chunkIdx}/{logFile.Chunks.Count}. Index.LineCount={newLineCount}, Chunk.LineIndex={chunk.LineIndex}, First subchunk {chunkData.SubChunkLineIndex[0]}" ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
for ( ; subChunkIdx < chunkData . SubChunks . Count ; subChunkIdx + + )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
LogSubChunkData subChunkData = chunkData . SubChunks [ subChunkIdx ] ;
if ( subChunkData . LineIndex > = newLineCount )
2021-04-29 15:10:34 -04:00
{
try
{
2022-05-27 09:33:00 -04:00
indexes . Add ( subChunkData . BuildIndex ( _logger ) ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
throw new Exception ( $"Failed to create index block - log {logFile.Id}, chunk {chunkIdx} ({logFile.Chunks.Count}), subchunk {subChunkIdx} ({chunkData.SubChunks.Count}), index lines: {newLineCount}, chunk index: {chunk.LineIndex}, subchunk index: {chunk.LineIndex + chunkData.SubChunkLineIndex[subChunkIdx]}, subchunk count: {subChunkData.LineCount}" , ex ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
newLength = subChunkData . Offset + subChunkData . Length ;
newLineCount = subChunkData . LineIndex + subChunkData . LineCount ;
2021-04-29 15:10:34 -04:00
}
}
}
// Try to update the log file
2022-03-23 14:50:23 -04:00
ILogFile ? newLogFile = logFile ;
if ( newLength > ( logFile . IndexLength ? ? 0 ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
LogIndexData index = LogIndexData . Merge ( indexes ) ;
_logger . LogDebug ( "Writing index for log {LogId} covering {Length} (index length {IndexLength})" , logFile . Id , newLength , index . GetSerializedSize ( ) ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
await WriteIndexAsync ( logFile . Id , newLength , index ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
while ( newLogFile ! = null & & newLength > ( newLogFile . IndexLength ? ? 0 ) )
2021-04-29 15:10:34 -04:00
{
2022-10-26 07:27:13 -04:00
newLogFile = await _logFiles . TryUpdateIndexAsync ( newLogFile , newLength , cancellationToken ) ;
2022-03-23 14:50:23 -04:00
if ( newLogFile ! = null )
2021-04-29 15:10:34 -04:00
{
break ;
}
2022-10-26 07:27:13 -04:00
newLogFile = await _logFiles . GetLogFileAsync ( logFile . Id , cancellationToken ) ;
2021-04-29 15:10:34 -04:00
}
}
2022-03-23 14:50:23 -04:00
return newLogFile ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Reads a chunk from storage
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">Log file to read from</param>
/// <param name="chunkIdx">The chunk to read</param>
2021-04-29 15:10:34 -04:00
/// <returns>Chunk data</returns>
2022-03-23 14:50:23 -04:00
private async Task < LogChunkData > ReadChunkAsync ( ILogFile logFile , int chunkIdx )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
ILogChunk chunk = logFile . Chunks [ chunkIdx ] ;
2021-04-29 15:10:34 -04:00
// Try to read the chunk data from storage
2022-03-23 14:50:23 -04:00
LogChunkData ? chunkData = null ;
2021-04-29 15:10:34 -04:00
try
{
// If the chunk is not yet complete, query the log builder
2022-03-23 14:50:23 -04:00
if ( chunk . Length = = 0 )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
chunkData = await _builder . GetChunkAsync ( logFile . Id , chunk . Offset , chunk . LineIndex ) ;
2021-04-29 15:10:34 -04:00
}
// Otherwise go directly to the log storage
2022-09-03 09:28:43 -04:00
chunkData ? ? = await _storage . ReadChunkAsync ( logFile . Id , chunk . Offset , chunk . LineIndex ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( ex , "Unable to read log {LogId} at offset {Offset}" , logFile . Id , chunk . Offset ) ;
2021-04-29 15:10:34 -04:00
}
// Get the minimum length and line count for the chunk
2022-03-23 14:50:23 -04:00
if ( chunkIdx + 1 < logFile . Chunks . Count )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
ILogChunk nextChunk = logFile . Chunks [ chunkIdx + 1 ] ;
2022-06-01 13:29:59 -04:00
chunkData = await RepairChunkDataAsync ( logFile , chunkIdx , chunkData , ( int ) ( nextChunk . Offset - chunk . Offset ) , nextChunk . LineIndex - chunk . LineIndex ) ;
2021-04-29 15:10:34 -04:00
}
else
{
2022-03-23 14:50:23 -04:00
if ( logFile . MaxLineIndex ! = null & & chunk . Length ! = 0 )
2021-04-29 15:10:34 -04:00
{
2022-06-01 13:29:59 -04:00
chunkData = await RepairChunkDataAsync ( logFile , chunkIdx , chunkData , chunk . Length , logFile . MaxLineIndex . Value - chunk . LineIndex ) ;
2021-04-29 15:10:34 -04:00
}
2022-09-03 09:28:43 -04:00
else
2021-04-29 15:10:34 -04:00
{
2022-09-03 09:28:43 -04:00
chunkData ? ? = await RepairChunkDataAsync ( logFile , chunkIdx , chunkData , 1024 , 1 ) ;
2021-04-29 15:10:34 -04:00
}
}
2022-03-23 14:50:23 -04:00
return chunkData ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Validates the given chunk data, and fix it up if necessary
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The log file instance</param>
/// <param name="chunkIdx">Index of the chunk within the logfile</param>
/// <param name="chunkData">The chunk data that was read</param>
/// <param name="length">Expected length of the data</param>
/// <param name="lineCount">Expected number of lines in the data</param>
2021-04-29 15:10:34 -04:00
/// <returns>Repaired chunk data</returns>
2022-06-01 13:29:59 -04:00
async Task < LogChunkData > RepairChunkDataAsync ( ILogFile logFile , int chunkIdx , LogChunkData ? chunkData , int length , int lineCount )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
int currentLength = 0 ;
int currentLineCount = 0 ;
if ( chunkData ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
currentLength = chunkData . Length ;
currentLineCount = chunkData . LineCount ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
if ( chunkData = = null | | currentLength < length | | currentLineCount < lineCount )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogWarning ( "Creating placeholder subchunk for log {LogId} chunk {ChunkIdx} (length {Length} vs expected {ExpLength}, lines {LineCount} vs expected {ExpLineCount})" , logFile . Id , chunkIdx , currentLength , length , currentLineCount , lineCount ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
List < LogSubChunkData > subChunks = new List < LogSubChunkData > ( ) ;
if ( chunkData ! = null & & chunkData . Length < length & & chunkData . LineCount < lineCount )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
subChunks . AddRange ( chunkData . SubChunks ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
LogText text = new LogText ( ) ;
text . AppendMissingDataInfo ( chunkIdx , logFile . Chunks [ chunkIdx ] . Server , length - currentLength , lineCount - currentLineCount ) ;
subChunks . Add ( new LogSubChunkData ( logFile . Type , currentLength , currentLineCount , text ) ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
ILogChunk chunk = logFile . Chunks [ chunkIdx ] ;
chunkData = new LogChunkData ( chunk . Offset , chunk . LineIndex , subChunks ) ;
2022-06-01 13:29:59 -04:00
try
{
await _storage . WriteChunkAsync ( logFile . Id , chunk . Offset , chunkData ) ;
}
catch ( Exception ex )
{
_logger . LogWarning ( ex , "Unable to put repaired log data for log {LogId} chunk {ChunkIdx}" , logFile . Id , chunkIdx ) ;
}
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
return chunkData ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Writes a set of chunks to the database
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFileId">Unique id of the log file</param>
/// <param name="offset">Offset of the chunk to write</param>
/// <param name="lineIndex">First line index of the chunk</param>
2021-04-29 15:10:34 -04:00
/// <returns>Chunk daata</returns>
2022-03-23 14:50:23 -04:00
private async Task < LogChunkData ? > WriteChunkAsync ( LogId logFileId , long offset , int lineIndex )
2021-04-29 15:10:34 -04:00
{
// Write the chunk to storage
2022-03-23 14:50:23 -04:00
LogChunkData ? chunkData = await _builder . GetChunkAsync ( logFileId , offset , lineIndex ) ;
if ( chunkData = = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogDebug ( "Log {LogId} offset {Offset} not found in log builder" , logFileId , offset ) ;
2021-04-29 15:10:34 -04:00
}
else
{
try
{
2022-03-23 14:50:23 -04:00
await _storage . WriteChunkAsync ( logFileId , chunkData . Offset , chunkData ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( ex , "Unable to write log {LogId} at offset {Offset}" , logFileId , chunkData . Offset ) ;
2021-04-29 15:10:34 -04:00
}
}
// Remove it from the log builder
try
{
2022-03-23 14:50:23 -04:00
await _builder . RemoveChunkAsync ( logFileId , offset ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( ex , "Unable to remove log {LogId} at offset {Offset} from log builder" , logFileId , offset ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
return chunkData ;
2021-04-29 15:10:34 -04:00
}
/// <summary>
/// Reads a chunk from storage
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">Log file to read from</param>
/// <param name="length">Length of the log covered by the index</param>
2021-04-29 15:10:34 -04:00
/// <returns>Chunk data</returns>
2022-03-23 14:50:23 -04:00
private async Task < LogIndexData ? > ReadIndexAsync ( ILogFile logFile , long length )
2021-04-29 15:10:34 -04:00
{
try
{
2022-03-23 14:50:23 -04:00
LogIndexData ? index = await _storage . ReadIndexAsync ( logFile . Id , length ) ;
return index ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( ex , "Unable to read log {LogId} index at length {Length}" , logFile . Id , length ) ;
2021-04-29 15:10:34 -04:00
return null ;
}
}
/// <summary>
/// Writes an index to the database
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFileId">Unique id of the log file</param>
/// <param name="length">Length of the data covered by the index</param>
/// <param name="index">Index to write</param>
2021-04-29 15:10:34 -04:00
/// <returns>Async task</returns>
2022-03-23 14:50:23 -04:00
private async Task WriteIndexAsync ( LogId logFileId , long length , LogIndexData index )
2021-04-29 15:10:34 -04:00
{
try
{
2022-03-23 14:50:23 -04:00
await _storage . WriteIndexAsync ( logFileId , length , index ) ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
catch ( Exception ex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
_logger . LogError ( ex , "Unable to write index for log {LogId}" , logFileId ) ;
2021-04-29 15:10:34 -04:00
}
}
/// <summary>
/// Determines if the user is authorized to perform an action on a particular template
/// </summary>
2022-03-23 14:50:23 -04:00
/// <param name="logFile">The template to check</param>
/// <param name="user">The principal to authorize</param>
2021-04-29 15:10:34 -04:00
/// <returns>True if the action is authorized</returns>
2022-03-23 14:50:23 -04:00
public static bool AuthorizeForSession ( ILogFile logFile , ClaimsPrincipal user )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
if ( logFile . SessionId ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
return user . HasClaim ( HordeClaimTypes . AgentSessionId , logFile . SessionId . Value . ToString ( ) ) ;
2021-04-29 15:10:34 -04:00
}
else
{
return false ;
}
}
/// <inheritdoc/>
2022-11-20 19:15:21 -05:00
public async Task < List < int > > SearchLogDataAsync ( ILogFile logFile , string text , int firstLine , int count , SearchStats searchStats , CancellationToken cancellationToken )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
Stopwatch timer = Stopwatch . StartNew ( ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
using IScope scope = GlobalTracer . Instance . BuildSpan ( "SearchLogDataAsync" ) . StartActive ( ) ;
scope . Span . SetTag ( "LogId" , logFile . Id . ToString ( ) ) ;
scope . Span . SetTag ( "Text" , text ) ;
scope . Span . SetTag ( "Count" , count . ToString ( CultureInfo . InvariantCulture ) ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
List < int > results = new List < int > ( ) ;
if ( count > 0 )
2021-04-29 15:10:34 -04:00
{
2022-11-20 19:15:21 -05:00
IAsyncEnumerable < int > enumerable = _settings . Value . FeatureFlags . EnableNewLogger ?
SearchLogDataInternalNewAsync ( logFile , text , firstLine , searchStats , cancellationToken ) :
SearchLogDataInternalAsync ( logFile , text , firstLine , searchStats ) ;
await using IAsyncEnumerator < int > enumerator = enumerable . GetAsyncEnumerator ( cancellationToken ) ;
2022-03-23 14:50:23 -04:00
while ( await enumerator . MoveNextAsync ( ) & & results . Count < count )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
results . Add ( enumerator . Current ) ;
2021-04-29 15:10:34 -04:00
}
}
2022-03-23 14:50:23 -04:00
_logger . LogDebug ( "Search for \"{SearchText}\" in log {LogId} found {NumResults}/{MaxResults} results, took {Time}ms ({@Stats})" , text , logFile . Id , results . Count , count , timer . ElapsedMilliseconds , searchStats ) ;
return results ;
2021-04-29 15:10:34 -04:00
}
2022-11-20 19:15:21 -05:00
async IAsyncEnumerable < int > SearchLogDataInternalNewAsync ( ILogFile logFile , string text , int firstLine , SearchStats searchStats , [ EnumeratorCancellation ] CancellationToken cancellationToken )
{
SearchTerm searchText = new SearchTerm ( text ) ;
TreeReader reader = await GetTreeReaderAsync ( cancellationToken ) ;
// Search the index
if ( logFile . LineCount > 0 )
{
LogNode ? root = await reader . ReadNodeAsync < LogNode > ( logFile . RefName , cancellationToken : cancellationToken ) ;
if ( root ! = null )
{
LogIndexNode index = await root . IndexRef . ExpandAsync ( reader , cancellationToken ) ;
await foreach ( int lineIdx in index . Search ( reader , firstLine , searchText , searchStats , cancellationToken ) )
{
yield return lineIdx ;
}
if ( root . Complete )
{
yield break ;
}
firstLine = root . LineCount ;
}
}
// Search any tail data we have
for ( ; ; )
{
Utf8String [ ] lines = await ReadTailAsync ( logFile , firstLine , cancellationToken ) ;
if ( lines . Length = = 0 )
{
break ;
}
for ( int idx = 0 ; idx < lines . Length ; idx + + )
{
if ( SearchTerm . FindNextOcurrence ( lines [ idx ] . Span , 0 , searchText ) ! = - 1 )
{
yield return firstLine + idx ;
}
}
firstLine + = lines . Length ;
}
}
async Task < Utf8String [ ] > ReadTailAsync ( ILogFile logFile , int index , CancellationToken cancellationToken )
{
const int BatchSize = 128 ;
string cacheKey = $"{logFile.Id}@{index}" ;
if ( _logFileCache . TryGetValue ( cacheKey , out Utf8String [ ] ? lines ) )
{
return lines ! ;
}
lines = ( await _logTailService . ReadAsync ( logFile . Id , index , BatchSize ) ) . ToArray ( ) ;
if ( logFile . Type = = LogType . Json )
{
LogChunkBuilder builder = new LogChunkBuilder ( lines . Sum ( x = > x . Length ) ) ;
foreach ( Utf8String line in lines )
{
builder . AppendJsonAsPlainText ( line . Span , _logger ) ;
}
lines = lines . ToArray ( ) ;
}
if ( lines . Length = = BatchSize )
{
int length = lines . Sum ( x = > x . Length ) ;
using ( ICacheEntry entry = _logFileCache . CreateEntry ( cacheKey ) )
{
entry . SetSlidingExpiration ( TimeSpan . FromMinutes ( 1.0 ) ) ;
entry . SetSize ( length ) ;
entry . SetValue ( lines ) ;
}
}
return lines ;
}
async IAsyncEnumerable < int > SearchLogDataInternalAsync ( ILogFile logFile , string text , int firstLine , SearchStats searchStats )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
SearchText searchText = new SearchText ( text ) ;
2021-04-29 15:10:34 -04:00
// Read the index for this log file
2022-03-23 14:50:23 -04:00
if ( logFile . IndexLength ! = null )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
LogIndexData ? indexData = await ReadIndexAsync ( logFile , logFile . IndexLength . Value ) ;
if ( indexData ! = null & & firstLine < indexData . LineCount )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
using IScope indexScope = GlobalTracer . Instance . BuildSpan ( "Indexed" ) . StartActive ( ) ;
indexScope . Span . SetTag ( "LineCount" , indexData . LineCount . ToString ( CultureInfo . InvariantCulture ) ) ;
2021-04-29 15:10:34 -04:00
2022-03-23 14:50:23 -04:00
foreach ( int lineIndex in indexData . Search ( firstLine , searchText , searchStats ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
yield return lineIndex ;
2021-04-29 15:10:34 -04:00
}
2022-03-23 14:50:23 -04:00
firstLine = indexData . LineCount ;
2021-04-29 15:10:34 -04:00
}
}
// Manually search through the rest of the log
2022-03-23 14:50:23 -04:00
int chunkIdx = logFile . Chunks . GetChunkForLine ( firstLine ) ;
for ( ; chunkIdx < logFile . Chunks . Count ; chunkIdx + + )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
ILogChunk chunk = logFile . Chunks [ chunkIdx ] ;
2021-04-29 15:10:34 -04:00
// Read the chunk data
2022-03-23 14:50:23 -04:00
LogChunkData chunkData = await ReadChunkAsync ( logFile , chunkIdx ) ;
if ( firstLine < chunkData . LineIndex + chunkData . LineCount )
2021-04-29 15:10:34 -04:00
{
// Find the first sub-chunk we're looking for
2022-03-23 14:50:23 -04:00
int subChunkIdx = 0 ;
if ( firstLine > chunk . LineIndex )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
subChunkIdx = chunkData . GetSubChunkForLine ( firstLine - chunk . LineIndex ) ;
2021-04-29 15:10:34 -04:00
}
// Search through the sub-chunks
2022-03-23 14:50:23 -04:00
for ( ; subChunkIdx < chunkData . SubChunks . Count ; subChunkIdx + + )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
LogSubChunkData subChunkData = chunkData . SubChunks [ subChunkIdx ] ;
if ( firstLine < subChunkData . LineIndex + subChunkData . LineCount )
2021-04-29 15:10:34 -04:00
{
// Create an index containing just this sub-chunk
2022-05-27 09:33:00 -04:00
LogIndexData index = subChunkData . BuildIndex ( _logger ) ;
2022-03-23 14:50:23 -04:00
foreach ( int lineIndex in index . Search ( firstLine , searchText , searchStats ) )
2021-04-29 15:10:34 -04:00
{
2022-03-23 14:50:23 -04:00
yield return lineIndex ;
2021-04-29 15:10:34 -04:00
}
}
}
}
}
}
}
}