Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Build output
bin/
obj/

# IDE / user files
.vs/
.vscode/
.idea/
*.user
*.suo

# Runtime artifacts produced by the app
*.db
*.db-shm
*.db-wal
alerts.log

# OS
.DS_Store
Thumbs.db
13 changes: 13 additions & 0 deletions HowToRun.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
If you ran 'dotnet run' right now, the app would:
1. Initialize SQLite (logs.db created, schema
applied).
2. Start IngestionHost, which starts
FileIngestionSource.
3. Stream src/apache_log.txt → parse → push to
channel → batch-write to SQLite.
4. Sit idle once the file is exhausted, waiting
for more producers (there are none).
5. GET /logs would return 404 because we haven't
mapped the endpoints yet.
6. The alert worker is registered but throws on
first tick because it's still a stub.
Comment on lines +12 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct documentation: AlertWorker is not registered.

The documentation states "The alert worker is registered but throws on first tick because it's still a stub," but Program.cs line 31 shows AlertWorker is commented out and not registered. The alert worker will not run and will not throw. Update the documentation to reflect that alerting is disabled.

📝 Proposed correction
-  6. The alert worker is registered but throws on
-  first tick because it's still a stub.
+  6. The alert worker is disabled (commented out in
+  Program.cs) as it remains a stub.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
6. The alert worker is registered but throws on
first tick because it's still a stub.
6. The alert worker is disabled (commented out in
Program.cs) as it remains a stub.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@HowToRun.md` around lines 12 - 13, The README incorrectly claims "The alert
worker is registered but throws on first tick because it's still a stub";
instead, update the documentation to state that AlertWorker is not registered
and alerting is disabled because the AlertWorker registration is commented out
in Program.cs (the commented call registering AlertWorker). Edit HowToRun.md to
replace that sentence with a note that AlertWorker is currently commented
out/not registered and therefore will not run or throw.

5 changes: 5 additions & 0 deletions LogAnalysis.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/LogAnalysis.System/LogAnalysis.System.csproj" />
</Folder>
</Solution>
212 changes: 212 additions & 0 deletions design.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/LogAnalysis.System/Alerting/AlertWorker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using LogAnalysis.System.Storage;

namespace LogAnalysis.System.Alerting;

public sealed class AlertWorker(
IEnumerable<IAlertRule> rules,
IEnumerable<IAlertSink> sinks,
ILogRepository repository,
IConfiguration config,
ILogger<AlertWorker> logger) : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken ct)
=> throw new NotImplementedException();
}
9 changes: 9 additions & 0 deletions src/LogAnalysis.System/Alerting/FileAlertSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace LogAnalysis.System.Alerting;

public sealed class FileAlertSink(
IConfiguration config,
ILogger<FileAlertSink> logger) : IAlertSink
{
public Task WriteAsync(Alert alert, CancellationToken ct)
=> throw new NotImplementedException();
}
12 changes: 12 additions & 0 deletions src/LogAnalysis.System/Alerting/IAlertRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using LogAnalysis.System.Storage;

namespace LogAnalysis.System.Alerting;

public interface IAlertRule
{
string Name { get; }

Task<Alert?> EvaluateAsync(ILogRepository repository, DateTimeOffset now, CancellationToken ct);
}

public sealed record Alert(string RuleName, string Detail, DateTimeOffset FiredAt);
6 changes: 6 additions & 0 deletions src/LogAnalysis.System/Alerting/IAlertSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LogAnalysis.System.Alerting;

public interface IAlertSink
{
Task WriteAsync(Alert alert, CancellationToken ct);
}
14 changes: 14 additions & 0 deletions src/LogAnalysis.System/Alerting/PatternRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using LogAnalysis.System.Storage;

namespace LogAnalysis.System.Alerting;

public sealed class PatternRule(
string name,
string substring,
TimeSpan window) : IAlertRule
{
public string Name => name;

public Task<Alert?> EvaluateAsync(ILogRepository repository, DateTimeOffset now, CancellationToken ct)
=> throw new NotImplementedException();
}
17 changes: 17 additions & 0 deletions src/LogAnalysis.System/Alerting/ThresholdRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using LogAnalysis.System.Domain;
using LogAnalysis.System.Storage;
using LogLevel = LogAnalysis.System.Domain.LogLevel;

namespace LogAnalysis.System.Alerting;

public sealed class ThresholdRule(
string name,
LogLevel level,
long threshold,
TimeSpan window) : IAlertRule
{
public string Name => name;

public Task<Alert?> EvaluateAsync(ILogRepository repository, DateTimeOffset now, CancellationToken ct)
=> throw new NotImplementedException();
}
21 changes: 21 additions & 0 deletions src/LogAnalysis.System/Api/QueryEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using LogAnalysis.System.Storage;

namespace LogAnalysis.System.Api;

public static class QueryEndpoints
{
public static IEndpointRouteBuilder MapQueryEndpoints(this IEndpointRouteBuilder app)
{
// GET /logs?startTime&endTime&level&service&limit&cursor
// GET /logs/aggregate?startTime&endTime&groupBy=level|service

// --- Production ingestion path (commented out for the demo) ----------
// POST /logs: accepts a JSON LogEntry, returns 202 Accepted, writes
// straight into the same LogChannel that FileIngestionSource uses.
// The pipeline downstream is source-agnostic, so enabling this is a
// one-method change. The demo replays src/apache_log.txt instead.
// ---------------------------------------------------------------------

return app;
}
}
7 changes: 7 additions & 0 deletions src/LogAnalysis.System/Domain/LogEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace LogAnalysis.System.Domain;

public sealed record LogEntry(
DateTimeOffset Timestamp,
LogLevel Level,
string Service,
string Message);
10 changes: 10 additions & 0 deletions src/LogAnalysis.System/Domain/LogLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace LogAnalysis.System.Domain;

public enum LogLevel
{
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Fatal = 4
}
39 changes: 39 additions & 0 deletions src/LogAnalysis.System/Ingestion/ApacheLogParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Globalization;
using System.Text.RegularExpressions;
using LogAnalysis.System.Domain;
using LogLevel = LogAnalysis.System.Domain.LogLevel;

namespace LogAnalysis.System.Ingestion;

public sealed partial class ApacheLogParser : ILogParser
{
private const string DateFormat = "dd/MMM/yyyy:HH:mm:ss zzz";
private const string ServiceTag = "apache";

[GeneratedRegex(
@"^\S+ \S+ \S+ \[(?<ts>[^\]]+)\] ""(?<req>[^""]*)"" (?<status>\d{3}) ",
RegexOptions.Compiled)]
private static partial Regex Pattern();

public bool CanParse(string line) => Pattern().IsMatch(line);

public LogEntry? Parse(string line)
{
var m = Pattern().Match(line);
if (!m.Success) return null;

if (!DateTimeOffset.TryParseExact(
m.Groups["ts"].Value, DateFormat,
CultureInfo.InvariantCulture, DateTimeStyles.None, out var ts))
{
ts = DateTimeOffset.UtcNow;
}
Comment on lines +25 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent fallback to current time masks data quality issues.

When timestamp parsing fails, the code silently substitutes DateTimeOffset.UtcNow, which can:

  • Make corrupted/malformed logs appear recent in queries and aggregations
  • Break temporal ordering and time-range filters
  • Trigger false alerts based on incorrect timestamps
  • Hide data quality problems that should be investigated

Consider logging a warning when the fallback occurs, or using a sentinel timestamp that's more obviously invalid.

🛡️ Proposed fix to add logging
 if (!DateTimeOffset.TryParseExact(
         m.Groups["ts"].Value, DateFormat,
         CultureInfo.InvariantCulture, DateTimeStyles.None, out var ts))
 {
+    // Log the issue but continue processing - better to have a bad timestamp than lose the log
+    // In production, consider: Dead-letter queue, metrics counter, or sentinel value
     ts = DateTimeOffset.UtcNow;
+    // Note: Would need ILogger injected into Parse or passed as parameter
 }

Alternatively, inject ILogger into the parser to enable proper warning logging.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!DateTimeOffset.TryParseExact(
m.Groups["ts"].Value, DateFormat,
CultureInfo.InvariantCulture, DateTimeStyles.None, out var ts))
{
ts = DateTimeOffset.UtcNow;
}
if (!DateTimeOffset.TryParseExact(
m.Groups["ts"].Value, DateFormat,
CultureInfo.InvariantCulture, DateTimeStyles.None, out var ts))
{
// Log the issue but continue processing - better to have a bad timestamp than lose the log
// In production, consider: Dead-letter queue, metrics counter, or sentinel value
ts = DateTimeOffset.UtcNow;
// Note: Would need ILogger injected into Parse or passed as parameter
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/LogAnalysis.System/Ingestion/ApacheLogParser.cs` around lines 25 - 30,
The parser currently falls back to DateTimeOffset.UtcNow when
DateTimeOffset.TryParseExact on m.Groups["ts"].Value fails, which silently masks
bad data; change the behavior in ApacheLogParser (where DateFormat and
m.Groups["ts"] are used) to either (a) inject an ILogger into the parser and
call logger.LogWarning including the raw timestamp string, the attempted
DateFormat and context (so callers can trace bad records) before setting a
sentinel value, or (b) set an obvious sentinel timestamp (e.g.,
DateTimeOffset.MinValue or a configurable sentinel) instead of UtcNow and log
the warning; ensure the log message includes m.Groups["ts"].Value and the
parsing failure so downstream systems can detect/data-quality issues.


var status = int.Parse(m.Groups["status"].Value, CultureInfo.InvariantCulture);
var level = status >= 500 ? LogLevel.Error
: status >= 400 ? LogLevel.Warn
: LogLevel.Info;

return new LogEntry(ts.ToUniversalTime(), level, ServiceTag, m.Groups["req"].Value);
}
}
44 changes: 44 additions & 0 deletions src/LogAnalysis.System/Ingestion/FileIngestionSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Runtime.CompilerServices;

namespace LogAnalysis.System.Ingestion;

public sealed class FileIngestionSource(
IngestionPipeline pipeline,
IConfiguration config,
ILogger<FileIngestionSource> logger) : IIngestionSource
{
public async Task RunAsync(CancellationToken ct)
{
var path = config["Ingestion:FilePath"];
if (string.IsNullOrWhiteSpace(path))
{
logger.LogInformation("FileIngestionSource disabled — no Ingestion:FilePath configured");
return;
}
if (!File.Exists(path))
{
logger.LogWarning("Log file not found: {Path}", path);
return;
}

logger.LogInformation("Replaying {Path}", path);

var count = 0;
await foreach (var line in ReadLinesAsync(path, ct))
{
await pipeline.IngestAsync(line, ct);
count++;
}

logger.LogInformation("File ingestion complete: {Count} line(s) from {Path}", count, path);
}

private static async IAsyncEnumerable<string> ReadLinesAsync(
string path, [EnumeratorCancellation] CancellationToken ct)
{
using var reader = new StreamReader(path);
string? line;
while ((line = await reader.ReadLineAsync(ct)) is not null)
yield return line;
}
}
6 changes: 6 additions & 0 deletions src/LogAnalysis.System/Ingestion/IIngestionSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LogAnalysis.System.Ingestion;

public interface IIngestionSource
{
Task RunAsync(CancellationToken ct);
}
9 changes: 9 additions & 0 deletions src/LogAnalysis.System/Ingestion/ILogParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using LogAnalysis.System.Domain;

namespace LogAnalysis.System.Ingestion;

public interface ILogParser
{
bool CanParse(string line);
LogEntry? Parse(string line);
}
33 changes: 33 additions & 0 deletions src/LogAnalysis.System/Ingestion/IngestionHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace LogAnalysis.System.Ingestion;

public sealed class IngestionHost(
IEnumerable<IIngestionSource> sources,
ILogger<IngestionHost> logger) : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken ct)
{
var tasks = sources.Select(s => RunSafe(s, ct)).ToArray();

if (tasks.Length == 0)
{
logger.LogInformation("No ingestion sources registered");
return Task.CompletedTask;
}

logger.LogInformation("Starting {Count} ingestion source(s)", tasks.Length);
return Task.WhenAll(tasks);
}

private async Task RunSafe(IIngestionSource source, CancellationToken ct)
{
try
{
await source.RunAsync(ct);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
logger.LogError(ex, "Ingestion source {Type} failed", source.GetType().Name);
}
}
}
32 changes: 32 additions & 0 deletions src/LogAnalysis.System/Ingestion/IngestionPipeline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using LogAnalysis.System.Pipeline;

namespace LogAnalysis.System.Ingestion;

public sealed class IngestionPipeline(
IEnumerable<ILogParser> parsers,
LogChannel channel,
ILogger<IngestionPipeline> logger)
{
private readonly ILogParser[] _parsers = parsers.ToArray();

public async ValueTask IngestAsync(string line, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(line)) return;

foreach (var parser in _parsers)
{
if (!parser.CanParse(line)) continue;

var entry = parser.Parse(line);
if (entry is null) continue;

await channel.Writer.WriteAsync(entry, ct);
return;
}

logger.LogDebug("No parser matched line: {Preview}", Preview(line));
}

private static string Preview(string line) =>
line.Length <= 80 ? line : line[..80];
}
14 changes: 14 additions & 0 deletions src/LogAnalysis.System/LogAnalysis.System.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>LogAnalysis.System</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
</ItemGroup>
Comment on lines +4 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable Microsoft.Data.Sqlite version compatible with TargetFramework net10.0, and are there any support or behavior caveats when staying on 9.0.0 with .NET 10?

💡 Result:

The latest stable version of Microsoft.Data.Sqlite is 10.0.5, released on 2026-03-12, which explicitly targets net10.0 and is fully compatible with TargetFramework net10.0 [1][2]. Version 9.0.x versions (latest patch 9.0.14) rely on netstandard2.0 targeting, which works with net10.0 but can cause runtime assembly loading issues if not directly referenced in the net10.0 project (e.g., when pulled transitively from netstandard libraries), requiring explicit PackageReference in the executable project [3]. Staying on 9.0.0 specifically (an early 9.0 release from late 2024) with.NET 10 misses bug fixes, security patches, and performance improvements from later 9.0.x and 10.0.x releases, and lacks native net10.0 optimizations like DateOnly/TimeOnly support without polyfills [2]. Upgrading to 10.0 introduces breaking changes in DateTimeOffset handling: textual timestamps without offset are now assumed UTC (previously local), DateTimeOffset to REAL columns are converted to UTC before writing, and GetDateTime on offset timestamps returns UTC with DateTimeKind.Utc [4]. No ongoing support issues for 9.0.x on net10.0 beyond resolution caveats, but 10.0 is recommended for best compatibility [2].

Citations:


Upgrade Microsoft.Data.Sqlite to version 10.0.5 for explicit net10.0 support.

Version 9.0.0 is outdated (latest in 9.x series is 9.0.14) and uses netstandard2.0 targeting. Version 10.0.5 (released March 2026) explicitly targets net10.0 and provides native optimizations, security patches, and performance improvements. Note that upgrading introduces breaking changes in DateTimeOffset handling: textual timestamps without offset are now assumed UTC (previously local), and GetDateTime on offset timestamps returns UTC with DateTimeKind.Utc. Review these changes against your codebase before upgrading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/LogAnalysis.System/LogAnalysis.System.csproj` around lines 4 - 12, Update
the Microsoft.Data.Sqlite package reference in the LogAnalysis.System.csproj
from 9.0.0 to 10.0.5 to pick up explicit net10.0 support and native
improvements, then audit code that parses or reads timestamps: search for usages
of Microsoft.Data.Sqlite (package), calls to reader.GetDateTime and any
DateTimeOffset parsing/ToLocalTime conversions and ensure textual timestamps
without offsets are treated as UTC (or explicitly specify Kind) and that
GetDateTime results are handled as DateTimeKind.Utc (convert to local if needed)
to avoid behavioral breakage from the library change.


</Project>
20 changes: 20 additions & 0 deletions src/LogAnalysis.System/Pipeline/LogChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Threading.Channels;
using LogAnalysis.System.Domain;

namespace LogAnalysis.System.Pipeline;

public sealed class LogChannel
{
private const int Capacity = 10_000;

private readonly Channel<LogEntry> _channel = Channel.CreateBounded<LogEntry>(
new BoundedChannelOptions(Capacity)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});

public ChannelWriter<LogEntry> Writer => _channel.Writer;
public ChannelReader<LogEntry> Reader => _channel.Reader;
}
Loading