feat: Implement container logs functionality in InstancesViewModel
- Added properties for managing container logs, including log entries, service filters, and auto-refresh options. - Introduced commands for refreshing logs, toggling auto-refresh, and closing the logs panel. - Implemented log fetching logic with error handling and status messages. - Integrated log display in the InstancesView with a dedicated logs panel. feat: Enhance navigation to Instances page with auto-selection - Added method to navigate to the Instances page and auto-select an instance based on abbreviation. feat: Update SettingsViewModel to load and save Bitwarden configuration - Integrated Bitwarden configuration loading from IOptions and saving to appsettings.json. - Added properties for Bitwarden instance project ID and connection status. - Updated UI to reflect Bitwarden settings and connection status. feat: Add advanced options for instance creation - Introduced a new expander in CreateInstanceView for advanced options, including purging stale volumes. feat: Improve InstanceDetailsWindow with pending setup banner - Added a banner to indicate pending setup for Xibo OAuth credentials, with editable fields for client ID and secret. fix: Update appsettings.json to include Bitwarden configuration structure - Added Bitwarden section to appsettings.json for storing configuration values. chore: Update Docker Compose template with health checks - Added health check configuration for web service in template.yml to ensure service availability. refactor: Drop AppSettings table from database - Removed AppSettings table and related migration files as part of database cleanup. feat: Create ServiceLogEntry DTO for log management - Added ServiceLogEntry class to represent individual log entries from Docker services.
This commit is contained in:
@@ -6,6 +6,7 @@ using OTSSignsOrchestrator.Core.Configuration;
|
||||
using OTSSignsOrchestrator.Core.Models.DTOs;
|
||||
using OTSSignsOrchestrator.Core.Models.Entities;
|
||||
using OTSSignsOrchestrator.Core.Services;
|
||||
using ServiceLogEntry = OTSSignsOrchestrator.Core.Models.DTOs.ServiceLogEntry;
|
||||
|
||||
namespace OTSSignsOrchestrator.Desktop.Services;
|
||||
|
||||
@@ -441,6 +442,121 @@ public class SshDockerCliService : IDockerCliService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<ServiceLogEntry>> GetServiceLogsAsync(string stackName, string? serviceName = null, int tailLines = 200)
|
||||
{
|
||||
EnsureHost();
|
||||
|
||||
// Determine which services to fetch logs for
|
||||
List<string> serviceNames;
|
||||
if (!string.IsNullOrEmpty(serviceName))
|
||||
{
|
||||
serviceNames = new List<string> { serviceName };
|
||||
}
|
||||
else
|
||||
{
|
||||
var services = await InspectStackServicesAsync(stackName);
|
||||
serviceNames = services.Select(s => s.Name).ToList();
|
||||
}
|
||||
|
||||
var allEntries = new List<ServiceLogEntry>();
|
||||
foreach (var svcName in serviceNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cmd = $"docker service logs --timestamps --no-trunc --tail {tailLines} {svcName} 2>&1";
|
||||
var (exitCode, stdout, _) = await _ssh.RunCommandAsync(_currentHost!, cmd, TimeSpan.FromSeconds(15));
|
||||
|
||||
if (exitCode != 0 || string.IsNullOrWhiteSpace(stdout))
|
||||
{
|
||||
_logger.LogDebug("No logs returned for service {Service} (exit={ExitCode})", svcName, exitCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse each line. Docker service logs format with --timestamps:
|
||||
// <timestamp> <service>.<replica>.<taskid>@<node> | <message>
|
||||
// or sometimes just:
|
||||
// <timestamp> <service>.<replica>.<taskid> <message>
|
||||
foreach (var line in stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var entry = ParseLogLine(line, svcName, stackName);
|
||||
if (entry != null)
|
||||
allEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch logs for service {Service}", svcName);
|
||||
}
|
||||
}
|
||||
|
||||
return allEntries.OrderBy(e => e.Timestamp).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a single line from <c>docker service logs --timestamps</c> output.
|
||||
/// </summary>
|
||||
private static ServiceLogEntry? ParseLogLine(string line, string serviceName, string stackName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return null;
|
||||
|
||||
// Format: "2026-02-25T14:30:45.123456789Z service.replica.taskid@node | message"
|
||||
// The timestamp is always the first space-delimited token when --timestamps is used.
|
||||
var firstSpace = line.IndexOf(' ');
|
||||
if (firstSpace <= 0)
|
||||
return new ServiceLogEntry
|
||||
{
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Source = serviceName,
|
||||
ServiceName = StripStackPrefix(serviceName, stackName),
|
||||
Message = line
|
||||
};
|
||||
|
||||
var timestampStr = line[..firstSpace];
|
||||
var rest = line[(firstSpace + 1)..].TrimStart();
|
||||
|
||||
// Try to parse the timestamp
|
||||
if (!DateTimeOffset.TryParse(timestampStr, out var timestamp))
|
||||
{
|
||||
// If timestamp parsing fails, treat the whole line as the message
|
||||
return new ServiceLogEntry
|
||||
{
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Source = serviceName,
|
||||
ServiceName = StripStackPrefix(serviceName, stackName),
|
||||
Message = line
|
||||
};
|
||||
}
|
||||
|
||||
// Split source and message on the pipe separator
|
||||
var source = serviceName;
|
||||
var message = rest;
|
||||
var pipeIndex = rest.IndexOf('|');
|
||||
if (pipeIndex >= 0)
|
||||
{
|
||||
source = rest[..pipeIndex].Trim();
|
||||
message = rest[(pipeIndex + 1)..].TrimStart();
|
||||
}
|
||||
|
||||
return new ServiceLogEntry
|
||||
{
|
||||
Timestamp = timestamp,
|
||||
Source = source,
|
||||
ServiceName = StripStackPrefix(serviceName, stackName),
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips the stack name prefix from a fully-qualified service name.
|
||||
/// e.g. "acm-cms-stack_acm-web" → "acm-web"
|
||||
/// </summary>
|
||||
private static string StripStackPrefix(string serviceName, string stackName)
|
||||
{
|
||||
var prefix = stackName + "_";
|
||||
return serviceName.StartsWith(prefix) ? serviceName[prefix.Length..] : serviceName;
|
||||
}
|
||||
|
||||
private void EnsureHost()
|
||||
{
|
||||
if (_currentHost == null)
|
||||
|
||||
Reference in New Issue
Block a user