audit work, doctors work
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Doctor.Engine;
|
||||
using StellaOps.Doctor.Export;
|
||||
using StellaOps.Doctor.Output;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
@@ -27,6 +28,10 @@ public static class DoctorServiceCollectionExtensions
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IDoctorReportFormatter, MarkdownReportFormatter>());
|
||||
services.TryAddSingleton<ReportFormatterFactory>();
|
||||
|
||||
// Export services
|
||||
services.TryAddSingleton<ConfigurationSanitizer>();
|
||||
services.TryAddSingleton<DiagnosticBundleGenerator>();
|
||||
|
||||
// Ensure TimeProvider is registered
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace StellaOps.Doctor.Export;
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes configuration by removing sensitive values.
|
||||
/// </summary>
|
||||
public sealed class ConfigurationSanitizer
|
||||
{
|
||||
private const string RedactedValue = "***REDACTED***";
|
||||
|
||||
private static readonly HashSet<string> SensitiveKeys = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"password",
|
||||
"secret",
|
||||
"key",
|
||||
"token",
|
||||
"apikey",
|
||||
"api_key",
|
||||
"connectionstring",
|
||||
"connection_string",
|
||||
"credentials",
|
||||
"accesskey",
|
||||
"access_key",
|
||||
"secretkey",
|
||||
"secret_key",
|
||||
"private",
|
||||
"privatekey",
|
||||
"private_key",
|
||||
"cert",
|
||||
"certificate",
|
||||
"passphrase",
|
||||
"auth",
|
||||
"bearer",
|
||||
"jwt",
|
||||
"oauth",
|
||||
"client_secret",
|
||||
"clientsecret"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes the configuration, replacing sensitive values with [REDACTED].
|
||||
/// </summary>
|
||||
public SanitizedConfiguration Sanitize(IConfiguration configuration)
|
||||
{
|
||||
var sanitizedKeys = new List<string>();
|
||||
var values = SanitizeSection(configuration, string.Empty, sanitizedKeys);
|
||||
|
||||
return new SanitizedConfiguration
|
||||
{
|
||||
Values = values,
|
||||
SanitizedKeys = sanitizedKeys
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, object> SanitizeSection(
|
||||
IConfiguration config,
|
||||
string prefix,
|
||||
List<string> sanitizedKeys)
|
||||
{
|
||||
var result = new Dictionary<string, object>();
|
||||
|
||||
foreach (var section in config.GetChildren())
|
||||
{
|
||||
var fullKey = string.IsNullOrEmpty(prefix)
|
||||
? section.Key
|
||||
: $"{prefix}:{section.Key}";
|
||||
|
||||
var children = section.GetChildren().ToList();
|
||||
|
||||
if (children.Count == 0)
|
||||
{
|
||||
// Leaf value
|
||||
if (IsSensitiveKey(section.Key))
|
||||
{
|
||||
result[section.Key] = RedactedValue;
|
||||
sanitizedKeys.Add(fullKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[section.Key] = section.Value ?? "(null)";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Section with children
|
||||
result[section.Key] = SanitizeSection(section, fullKey, sanitizedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsSensitiveKey(string key)
|
||||
{
|
||||
// Check if any sensitive keyword is contained in the key
|
||||
foreach (var sensitiveKey in SensitiveKeys)
|
||||
{
|
||||
if (key.Contains(sensitiveKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a key appears to be sensitive.
|
||||
/// </summary>
|
||||
public static bool IsKeySensitive(string key) => IsSensitiveKey(key);
|
||||
}
|
||||
147
src/__Libraries/StellaOps.Doctor/Export/DiagnosticBundle.cs
Normal file
147
src/__Libraries/StellaOps.Doctor/Export/DiagnosticBundle.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using StellaOps.Doctor.Models;
|
||||
|
||||
namespace StellaOps.Doctor.Export;
|
||||
|
||||
/// <summary>
|
||||
/// Complete diagnostic bundle for support tickets and troubleshooting.
|
||||
/// </summary>
|
||||
public sealed record DiagnosticBundle
|
||||
{
|
||||
/// <summary>
|
||||
/// When this bundle was generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stella Ops version that generated this bundle.
|
||||
/// </summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment information.
|
||||
/// </summary>
|
||||
public required EnvironmentInfo Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The doctor report with all check results.
|
||||
/// </summary>
|
||||
public required DoctorReport DoctorReport { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sanitized configuration (secrets removed).
|
||||
/// </summary>
|
||||
public SanitizedConfiguration? Configuration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Recent log file contents (filename -> content).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Logs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// System resource information.
|
||||
/// </summary>
|
||||
public required SystemInfo SystemInfo { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Environment information for the diagnostic bundle.
|
||||
/// </summary>
|
||||
public sealed record EnvironmentInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Machine hostname.
|
||||
/// </summary>
|
||||
public required string Hostname { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Operating system description.
|
||||
/// </summary>
|
||||
public required string Platform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// .NET runtime version.
|
||||
/// </summary>
|
||||
public required string DotNetVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current process ID.
|
||||
/// </summary>
|
||||
public required int ProcessId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Working directory.
|
||||
/// </summary>
|
||||
public required string WorkingDirectory { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Process start time (UTC).
|
||||
/// </summary>
|
||||
public required DateTimeOffset StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environment name (Development, Production, etc.).
|
||||
/// </summary>
|
||||
public string? EnvironmentName { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System resource information.
|
||||
/// </summary>
|
||||
public sealed record SystemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Total available memory in bytes.
|
||||
/// </summary>
|
||||
public required long TotalMemoryBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Process working set in bytes.
|
||||
/// </summary>
|
||||
public required long ProcessMemoryBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of logical processors.
|
||||
/// </summary>
|
||||
public required int ProcessorCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Process uptime.
|
||||
/// </summary>
|
||||
public required TimeSpan Uptime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// GC heap size in bytes.
|
||||
/// </summary>
|
||||
public long GcHeapSizeBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of GC collections (Gen 0).
|
||||
/// </summary>
|
||||
public int Gen0Collections { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of GC collections (Gen 1).
|
||||
/// </summary>
|
||||
public int Gen1Collections { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of GC collections (Gen 2).
|
||||
/// </summary>
|
||||
public int Gen2Collections { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitized configuration with secrets removed.
|
||||
/// </summary>
|
||||
public sealed record SanitizedConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration values (secrets replaced with [REDACTED]).
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<string, object> Values { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of keys that were sanitized.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<string> SanitizedKeys { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Doctor.Engine;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Output;
|
||||
|
||||
namespace StellaOps.Doctor.Export;
|
||||
|
||||
/// <summary>
|
||||
/// Generates diagnostic bundles for support tickets.
|
||||
/// </summary>
|
||||
public sealed class DiagnosticBundleGenerator
|
||||
{
|
||||
private readonly DoctorEngine _engine;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IHostEnvironment? _hostEnvironment;
|
||||
private readonly ILogger<DiagnosticBundleGenerator> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new diagnostic bundle generator.
|
||||
/// </summary>
|
||||
public DiagnosticBundleGenerator(
|
||||
DoctorEngine engine,
|
||||
IConfiguration configuration,
|
||||
TimeProvider timeProvider,
|
||||
IHostEnvironment? hostEnvironment,
|
||||
ILogger<DiagnosticBundleGenerator> logger)
|
||||
{
|
||||
_engine = engine;
|
||||
_configuration = configuration;
|
||||
_timeProvider = timeProvider;
|
||||
_hostEnvironment = hostEnvironment;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a diagnostic bundle.
|
||||
/// </summary>
|
||||
public async Task<DiagnosticBundle> GenerateAsync(
|
||||
DiagnosticBundleOptions options,
|
||||
CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("Generating diagnostic bundle");
|
||||
|
||||
// Run full doctor check
|
||||
var report = await _engine.RunAsync(
|
||||
new DoctorRunOptions { Mode = DoctorRunMode.Full },
|
||||
cancellationToken: ct);
|
||||
|
||||
var sanitizer = new ConfigurationSanitizer();
|
||||
|
||||
var bundle = new DiagnosticBundle
|
||||
{
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
Version = GetVersion(),
|
||||
Environment = GetEnvironmentInfo(),
|
||||
DoctorReport = report,
|
||||
Configuration = options.IncludeConfig ? sanitizer.Sanitize(_configuration) : null,
|
||||
Logs = options.IncludeLogs ? await CollectLogsAsync(options, ct) : null,
|
||||
SystemInfo = CollectSystemInfo()
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Diagnostic bundle generated: {Passed} passed, {Failed} failed, {Warnings} warnings",
|
||||
report.Summary.Passed,
|
||||
report.Summary.Failed,
|
||||
report.Summary.Warnings);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports a diagnostic bundle to a ZIP file.
|
||||
/// </summary>
|
||||
public async Task<string> ExportToZipAsync(
|
||||
DiagnosticBundle bundle,
|
||||
string outputPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("Exporting diagnostic bundle to {OutputPath}", outputPath);
|
||||
|
||||
var directory = Path.GetDirectoryName(outputPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await using var zipStream = File.Create(outputPath);
|
||||
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
|
||||
|
||||
// Add doctor report as JSON
|
||||
await AddJsonEntryAsync(archive, "doctor-report.json", bundle.DoctorReport, ct);
|
||||
|
||||
// Add markdown summary
|
||||
var markdownFormatter = new MarkdownReportFormatter();
|
||||
var markdown = markdownFormatter.Format(bundle.DoctorReport, new DoctorOutputOptions
|
||||
{
|
||||
Verbose = true,
|
||||
IncludeRemediation = true,
|
||||
IncludeEvidence = true,
|
||||
IncludePassed = true
|
||||
});
|
||||
await AddTextEntryAsync(archive, "doctor-report.md", markdown, ct);
|
||||
|
||||
// Add environment info
|
||||
await AddJsonEntryAsync(archive, "environment.json", bundle.Environment, ct);
|
||||
|
||||
// Add system info
|
||||
await AddJsonEntryAsync(archive, "system-info.json", bundle.SystemInfo, ct);
|
||||
|
||||
// Add sanitized config if included
|
||||
if (bundle.Configuration is not null)
|
||||
{
|
||||
await AddJsonEntryAsync(archive, "config-sanitized.json", bundle.Configuration, ct);
|
||||
}
|
||||
|
||||
// Add logs if included
|
||||
if (bundle.Logs is not null)
|
||||
{
|
||||
foreach (var (name, content) in bundle.Logs)
|
||||
{
|
||||
await AddTextEntryAsync(archive, $"logs/{name}", content, ct);
|
||||
}
|
||||
}
|
||||
|
||||
// Add README
|
||||
var readme = GenerateReadme(bundle);
|
||||
await AddTextEntryAsync(archive, "README.md", readme, ct);
|
||||
|
||||
_logger.LogInformation("Diagnostic bundle exported to {OutputPath}", outputPath);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
private EnvironmentInfo GetEnvironmentInfo()
|
||||
{
|
||||
var process = Process.GetCurrentProcess();
|
||||
|
||||
return new EnvironmentInfo
|
||||
{
|
||||
Hostname = Environment.MachineName,
|
||||
Platform = RuntimeInformation.OSDescription,
|
||||
DotNetVersion = Environment.Version.ToString(),
|
||||
ProcessId = Environment.ProcessId,
|
||||
WorkingDirectory = Environment.CurrentDirectory,
|
||||
StartTime = process.StartTime.ToUniversalTime(),
|
||||
EnvironmentName = _hostEnvironment?.EnvironmentName
|
||||
};
|
||||
}
|
||||
|
||||
private SystemInfo CollectSystemInfo()
|
||||
{
|
||||
var gcInfo = GC.GetGCMemoryInfo();
|
||||
var process = Process.GetCurrentProcess();
|
||||
|
||||
return new SystemInfo
|
||||
{
|
||||
TotalMemoryBytes = gcInfo.TotalAvailableMemoryBytes,
|
||||
ProcessMemoryBytes = process.WorkingSet64,
|
||||
ProcessorCount = Environment.ProcessorCount,
|
||||
Uptime = _timeProvider.GetUtcNow() - process.StartTime.ToUniversalTime(),
|
||||
GcHeapSizeBytes = GC.GetTotalMemory(forceFullCollection: false),
|
||||
Gen0Collections = GC.CollectionCount(0),
|
||||
Gen1Collections = GC.CollectionCount(1),
|
||||
Gen2Collections = GC.CollectionCount(2)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> CollectLogsAsync(
|
||||
DiagnosticBundleOptions options,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var logs = new Dictionary<string, string>();
|
||||
|
||||
var logPaths = options.LogPaths ?? GetDefaultLogPaths();
|
||||
|
||||
foreach (var path in logPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await ReadRecentLinesAsync(path, options.MaxLogLines, ct);
|
||||
logs[Path.GetFileName(path)] = content;
|
||||
_logger.LogDebug("Collected log file: {Path}", path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read log file: {Path}", path);
|
||||
logs[Path.GetFileName(path)] = $"Error reading log file: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetDefaultLogPaths()
|
||||
{
|
||||
// Platform-specific log paths
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
return new[]
|
||||
{
|
||||
Path.Combine(appData, "StellaOps", "logs", "gateway.log"),
|
||||
Path.Combine(appData, "StellaOps", "logs", "scanner.log"),
|
||||
Path.Combine(appData, "StellaOps", "logs", "orchestrator.log")
|
||||
};
|
||||
}
|
||||
|
||||
return new[]
|
||||
{
|
||||
"/var/log/stellaops/gateway.log",
|
||||
"/var/log/stellaops/scanner.log",
|
||||
"/var/log/stellaops/orchestrator.log"
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<string> ReadRecentLinesAsync(
|
||||
string path,
|
||||
int maxLines,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
await using var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite);
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync(ct)) is not null)
|
||||
{
|
||||
lines.Add(line);
|
||||
if (lines.Count > maxLines)
|
||||
{
|
||||
lines.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
private static string GetVersion()
|
||||
{
|
||||
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
|
||||
var version = assembly.GetName().Version;
|
||||
return version?.ToString() ?? "unknown";
|
||||
}
|
||||
|
||||
private static async Task AddJsonEntryAsync<T>(
|
||||
ZipArchive archive,
|
||||
string entryName,
|
||||
T content,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
|
||||
await using var stream = entry.Open();
|
||||
await JsonSerializer.SerializeAsync(stream, content, JsonOptions, ct);
|
||||
}
|
||||
|
||||
private static async Task AddTextEntryAsync(
|
||||
ZipArchive archive,
|
||||
string entryName,
|
||||
string content,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
|
||||
await using var stream = entry.Open();
|
||||
await using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
await writer.WriteAsync(content.AsMemory(), ct);
|
||||
}
|
||||
|
||||
private static string GenerateReadme(DiagnosticBundle bundle)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("# Stella Ops Diagnostic Bundle");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Generated: {bundle.GeneratedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)} UTC");
|
||||
sb.AppendLine($"Version: {bundle.Version}");
|
||||
sb.AppendLine($"Hostname: {bundle.Environment.Hostname}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## Contents");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("- `doctor-report.json` - Full diagnostic check results");
|
||||
sb.AppendLine("- `doctor-report.md` - Human-readable report");
|
||||
sb.AppendLine("- `environment.json` - Environment information");
|
||||
sb.AppendLine("- `system-info.json` - System resource information");
|
||||
|
||||
if (bundle.Configuration is not null)
|
||||
{
|
||||
sb.AppendLine("- `config-sanitized.json` - Sanitized configuration (secrets removed)");
|
||||
}
|
||||
|
||||
if (bundle.Logs is not null && bundle.Logs.Count > 0)
|
||||
{
|
||||
sb.AppendLine("- `logs/` - Recent log files");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## Summary");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"- Passed: {bundle.DoctorReport.Summary.Passed}");
|
||||
sb.AppendLine($"- Info: {bundle.DoctorReport.Summary.Info}");
|
||||
sb.AppendLine($"- Warnings: {bundle.DoctorReport.Summary.Warnings}");
|
||||
sb.AppendLine($"- Failed: {bundle.DoctorReport.Summary.Failed}");
|
||||
sb.AppendLine($"- Skipped: {bundle.DoctorReport.Summary.Skipped}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## How to Use");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Share this bundle with Stella Ops support by:");
|
||||
sb.AppendLine("1. Creating a support ticket at https://support.stellaops.org");
|
||||
sb.AppendLine("2. Attaching this ZIP file");
|
||||
sb.AppendLine("3. Including any additional context about the issue");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("**Note:** This bundle has been sanitized to remove sensitive data.");
|
||||
sb.AppendLine("Review contents before sharing externally.");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace StellaOps.Doctor.Export;
|
||||
|
||||
/// <summary>
|
||||
/// Options for generating a diagnostic bundle.
|
||||
/// </summary>
|
||||
public sealed record DiagnosticBundleOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to include sanitized configuration in the bundle.
|
||||
/// </summary>
|
||||
public bool IncludeConfig { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include recent log files in the bundle.
|
||||
/// </summary>
|
||||
public bool IncludeLogs { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Duration of logs to include (from most recent).
|
||||
/// </summary>
|
||||
public TimeSpan LogDuration { get; init; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of log lines to include per file.
|
||||
/// </summary>
|
||||
public int MaxLogLines { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Log file paths to include.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? LogPaths { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include system information.
|
||||
/// </summary>
|
||||
public bool IncludeSystemInfo { get; init; } = true;
|
||||
}
|
||||
Reference in New Issue
Block a user