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