389f5a0378
Communication Layer (WP-1–5): - 8 message patterns with correlation IDs, per-pattern timeouts - Central/Site communication actors, transport heartbeat config - Connection failure handling (no central buffering, debug streams killed) Data Connection Layer (WP-6–14, WP-34): - Connection actor with Become/Stash lifecycle (Connecting/Connected/Reconnecting) - OPC UA + LmxProxy adapters behind IDataConnection - Auto-reconnect, bad quality propagation, transparent re-subscribe - Write-back, tag path resolution with retry, health reporting - Protocol extensibility via DataConnectionFactory Site Runtime (WP-15–25, WP-32–33): - ScriptActor/ScriptExecutionActor (triggers, concurrent execution, blocking I/O dispatcher) - AlarmActor/AlarmExecutionActor (ValueMatch/RangeViolation/RateOfChange, in-memory state) - SharedScriptLibrary (inline execution), ScriptRuntimeContext (API) - ScriptCompilationService (Roslyn, forbidden API enforcement, execution timeout) - Recursion limit (default 10), call direction enforcement - SiteStreamManager (per-subscriber bounded buffers, fire-and-forget) - Debug view backend (snapshot + stream), concurrency serialization - Local artifact storage (4 SQLite tables) Health Monitoring (WP-26–28): - SiteHealthCollector (thread-safe counters, connection state) - HealthReportSender (30s interval, monotonic sequence numbers) - CentralHealthAggregator (offline detection 60s, online recovery) Site Event Logging (WP-29–31): - SiteEventLogger (SQLite, 6 event categories, ISO 8601 UTC) - EventLogPurgeService (30-day retention, 1GB cap) - EventLogQueryService (filters, keyword search, keyset pagination) 541 tests pass, zero warnings.
147 lines
5.5 KiB
C#
147 lines
5.5 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using ScadaLink.Commons.Messages.RemoteQuery;
|
|
|
|
namespace ScadaLink.SiteEventLogging;
|
|
|
|
/// <summary>
|
|
/// Processes event log queries locally against SQLite.
|
|
/// Supports filtering by event_type, time range, instance_id, severity,
|
|
/// and keyword search (LIKE on message and source).
|
|
/// Uses keyset pagination with continuation token (last event ID).
|
|
/// </summary>
|
|
public class EventLogQueryService : IEventLogQueryService
|
|
{
|
|
private readonly SiteEventLogger _eventLogger;
|
|
private readonly SiteEventLogOptions _options;
|
|
private readonly ILogger<EventLogQueryService> _logger;
|
|
|
|
public EventLogQueryService(
|
|
ISiteEventLogger eventLogger,
|
|
IOptions<SiteEventLogOptions> options,
|
|
ILogger<EventLogQueryService> logger)
|
|
{
|
|
_eventLogger = (SiteEventLogger)eventLogger;
|
|
_options = options.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
public EventLogQueryResponse ExecuteQuery(EventLogQueryRequest request)
|
|
{
|
|
try
|
|
{
|
|
var pageSize = request.PageSize > 0 ? request.PageSize : _options.QueryPageSize;
|
|
|
|
using var cmd = _eventLogger.Connection.CreateCommand();
|
|
var whereClauses = new List<string>();
|
|
var parameters = new List<SqliteParameter>();
|
|
|
|
// Keyset pagination: only return events with id > continuation token
|
|
if (request.ContinuationToken.HasValue)
|
|
{
|
|
whereClauses.Add("id > $afterId");
|
|
parameters.Add(new SqliteParameter("$afterId", request.ContinuationToken.Value));
|
|
}
|
|
|
|
if (request.From.HasValue)
|
|
{
|
|
whereClauses.Add("timestamp >= $from");
|
|
parameters.Add(new SqliteParameter("$from", request.From.Value.ToString("o")));
|
|
}
|
|
|
|
if (request.To.HasValue)
|
|
{
|
|
whereClauses.Add("timestamp <= $to");
|
|
parameters.Add(new SqliteParameter("$to", request.To.Value.ToString("o")));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.EventType))
|
|
{
|
|
whereClauses.Add("event_type = $eventType");
|
|
parameters.Add(new SqliteParameter("$eventType", request.EventType));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.Severity))
|
|
{
|
|
whereClauses.Add("severity = $severity");
|
|
parameters.Add(new SqliteParameter("$severity", request.Severity));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.InstanceId))
|
|
{
|
|
whereClauses.Add("instance_id = $instanceId");
|
|
parameters.Add(new SqliteParameter("$instanceId", request.InstanceId));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.KeywordFilter))
|
|
{
|
|
whereClauses.Add("(message LIKE $keyword OR source LIKE $keyword)");
|
|
parameters.Add(new SqliteParameter("$keyword", $"%{request.KeywordFilter}%"));
|
|
}
|
|
|
|
var whereClause = whereClauses.Count > 0
|
|
? "WHERE " + string.Join(" AND ", whereClauses)
|
|
: "";
|
|
|
|
// Fetch pageSize + 1 to determine if there are more results
|
|
cmd.CommandText = $"""
|
|
SELECT id, timestamp, event_type, severity, instance_id, source, message, details
|
|
FROM site_events
|
|
{whereClause}
|
|
ORDER BY id ASC
|
|
LIMIT $limit
|
|
""";
|
|
cmd.Parameters.AddWithValue("$limit", pageSize + 1);
|
|
foreach (var p in parameters)
|
|
cmd.Parameters.Add(p);
|
|
|
|
var entries = new List<EventLogEntry>();
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
entries.Add(new EventLogEntry(
|
|
Id: reader.GetInt64(0),
|
|
Timestamp: DateTimeOffset.Parse(reader.GetString(1)),
|
|
EventType: reader.GetString(2),
|
|
Severity: reader.GetString(3),
|
|
InstanceId: reader.IsDBNull(4) ? null : reader.GetString(4),
|
|
Source: reader.GetString(5),
|
|
Message: reader.GetString(6),
|
|
Details: reader.IsDBNull(7) ? null : reader.GetString(7)));
|
|
}
|
|
|
|
var hasMore = entries.Count > pageSize;
|
|
if (hasMore)
|
|
{
|
|
entries.RemoveAt(entries.Count - 1);
|
|
}
|
|
|
|
var continuationToken = entries.Count > 0 ? entries[^1].Id : (long?)null;
|
|
|
|
return new EventLogQueryResponse(
|
|
CorrelationId: request.CorrelationId,
|
|
SiteId: request.SiteId,
|
|
Entries: entries,
|
|
ContinuationToken: continuationToken,
|
|
HasMore: hasMore,
|
|
Success: true,
|
|
ErrorMessage: null,
|
|
Timestamp: DateTimeOffset.UtcNow);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to execute event log query: {CorrelationId}", request.CorrelationId);
|
|
return new EventLogQueryResponse(
|
|
CorrelationId: request.CorrelationId,
|
|
SiteId: request.SiteId,
|
|
Entries: [],
|
|
ContinuationToken: null,
|
|
HasMore: false,
|
|
Success: false,
|
|
ErrorMessage: ex.Message,
|
|
Timestamp: DateTimeOffset.UtcNow);
|
|
}
|
|
}
|
|
}
|