using System.Collections.ObjectModel; using System.Text; using IronTelemetry.Client; namespace IronServices.Maui.Controls; /// /// A page for displaying captured app logs (exceptions, telemetry events) with share and clear functionality. /// Use within a NavigationPage for proper toolbar display. /// /// /// /// // Navigate to the log view /// await Navigation.PushAsync(new AppLogView /// { /// TelemetryClient = myTelemetryClient /// }); /// /// // Or with custom title /// await Navigation.PushAsync(new AppLogView /// { /// TelemetryClient = myTelemetryClient, /// Title = "Error Logs" /// }); /// /// public partial class AppLogView : ContentPage { private TelemetryClient? _telemetryClient; private readonly ObservableCollection _logItems = new(); private LogItem? _selectedItem; /// /// Creates a new AppLogView instance. /// public AppLogView() { InitializeComponent(); LogList.BindingContext = _logItems; } #region Bindable Properties /// /// The TelemetryClient to pull queued logs from. When set, automatically refreshes the log list. /// public static readonly BindableProperty TelemetryClientProperty = BindableProperty.Create( nameof(TelemetryClient), typeof(TelemetryClient), typeof(AppLogView), null, propertyChanged: OnTelemetryClientChanged); /// /// Gets or sets the TelemetryClient used to retrieve queued log items. /// public TelemetryClient? TelemetryClient { get => (TelemetryClient?)GetValue(TelemetryClientProperty); set => SetValue(TelemetryClientProperty, value); } private static void OnTelemetryClientChanged(BindableObject bindable, object oldValue, object newValue) { var view = (AppLogView)bindable; view._telemetryClient = newValue as TelemetryClient; view.RefreshLogs(); } /// /// Whether to show the share toolbar item. Default: true. /// public static readonly BindableProperty ShowShareButtonProperty = BindableProperty.Create( nameof(ShowShareButton), typeof(bool), typeof(AppLogView), true, propertyChanged: (b, o, n) => ((AppLogView)b).ShareToolbarItem.IsEnabled = (bool)n); /// /// Gets or sets whether the Share toolbar button is visible. /// public bool ShowShareButton { get => (bool)GetValue(ShowShareButtonProperty); set => SetValue(ShowShareButtonProperty, value); } /// /// Whether to show the clear toolbar item. Default: true. /// public static readonly BindableProperty ShowClearButtonProperty = BindableProperty.Create( nameof(ShowClearButton), typeof(bool), typeof(AppLogView), true, propertyChanged: (b, o, n) => ((AppLogView)b).ClearToolbarItem.IsEnabled = (bool)n); /// /// Gets or sets whether the Clear toolbar button is visible. /// public bool ShowClearButton { get => (bool)GetValue(ShowClearButtonProperty); set => SetValue(ShowClearButtonProperty, value); } #endregion #region Events /// /// Raised when logs are cleared via the Clear button. /// public event EventHandler? LogsCleared; /// /// Raised when logs are shared via the Share button. /// public event EventHandler? LogsShared; /// /// Raised when a log item is selected from the list. /// public event EventHandler? LogSelected; /// /// Raised when the log list is refreshed. /// public event EventHandler? LogsRefreshed; #endregion #region Public Methods /// /// Refresh the log list from the telemetry client's local log queue. /// Call this after adding new logs or when the queue may have changed. /// public void RefreshLogs() { _logItems.Clear(); if (_telemetryClient != null) { // Get all locally captured log items (not just failed ones) var localItems = _telemetryClient.GetLocalLogItems(); foreach (var item in localItems.OrderByDescending(i => i.Timestamp)) { _logItems.Add(LogItem.FromEnvelopeItem(item)); } } UpdateCount(); LogsRefreshed?.Invoke(this, EventArgs.Empty); } /// /// Add a log item manually (for local-only logging without TelemetryClient). /// /// Log type: "exception", "message", "info", "warning", etc. /// Title or exception type name. /// Log message or exception message. /// Optional stack trace for exceptions. public void AddLog(string type, string title, string message, string? stackTrace = null) { var item = new LogItem { Type = type, Title = title, Message = message, StackTrace = stackTrace, Timestamp = DateTime.UtcNow }; _logItems.Insert(0, item); UpdateCount(); } /// /// Add an exception as a log entry. /// /// The exception to log. public void AddException(Exception ex) { AddLog("exception", ex.GetType().Name, ex.Message, ex.StackTrace); } /// /// Clear all displayed logs. Does not clear the TelemetryClient's offline queue. /// public void ClearLogs() { _logItems.Clear(); HideDetail(); UpdateCount(); LogsCleared?.Invoke(this, EventArgs.Empty); } /// /// Clear all logs including the TelemetryClient's local log queue. /// public void ClearAllLogs() { _telemetryClient?.ClearLocalLogItems(); _telemetryClient?.OfflineQueue?.Clear(); ClearLogs(); } /// /// Export all logs as a formatted string suitable for sharing or saving. /// /// Formatted log export string. public string ExportLogs() { var sb = new StringBuilder(); sb.AppendLine($"=== App Logs Export ==="); sb.AppendLine($"Exported: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); sb.AppendLine($"Total items: {_logItems.Count}"); sb.AppendLine(); foreach (var item in _logItems) { sb.AppendLine($"--- [{item.Type.ToUpperInvariant()}] {item.Timestamp:yyyy-MM-dd HH:mm:ss UTC} ---"); sb.AppendLine($"Title: {item.Title}"); sb.AppendLine($"Message: {item.Message}"); if (!string.IsNullOrEmpty(item.StackTrace)) { sb.AppendLine("Stack Trace:"); sb.AppendLine(item.StackTrace); } sb.AppendLine(); } return sb.ToString(); } /// /// Get the current log items as a read-only list. /// public IReadOnlyList GetLogs() => _logItems.ToList().AsReadOnly(); /// /// Get the count of log items. /// public int LogCount => _logItems.Count; #endregion #region Event Handlers private void OnClearClicked(object? sender, EventArgs e) { ClearLogs(); } private void OnRefreshClicked(object? sender, EventArgs e) { RefreshLogs(); } private async void OnShareClicked(object? sender, EventArgs e) { if (_logItems.Count == 0) { await DisplayAlert("No Logs", "There are no logs to share.", "OK"); return; } var content = ExportLogs(); await Share.Default.RequestAsync(new ShareTextRequest { Title = "App Logs", Text = content }); LogsShared?.Invoke(this, EventArgs.Empty); } private async void OnLogSelected(object? sender, SelectionChangedEventArgs e) { if (e.CurrentSelection.FirstOrDefault() is LogItem item) { _selectedItem = item; ShowDetail(item); LogSelected?.Invoke(this, item); // Copy log entry to clipboard await CopyLogToClipboardAsync(item); } } /// /// Copies a log item's details to the clipboard. /// private async Task CopyLogToClipboardAsync(LogItem item) { var sb = new StringBuilder(); sb.AppendLine("=== Exception Log ==="); sb.AppendLine($"Type: {item.Type.ToUpperInvariant()}"); sb.AppendLine($"Time: {item.Timestamp:yyyy-MM-dd HH:mm:ss UTC}"); sb.AppendLine($"Title: {item.Title}"); sb.AppendLine(); sb.AppendLine("--- Message ---"); sb.AppendLine(item.Message); if (!string.IsNullOrEmpty(item.JourneyId)) { sb.AppendLine(); sb.AppendLine($"Journey ID: {item.JourneyId}"); } if (!string.IsNullOrEmpty(item.UserId)) { sb.AppendLine($"User ID: {item.UserId}"); } if (!string.IsNullOrEmpty(item.StackTrace)) { sb.AppendLine(); sb.AppendLine("--- Stack Trace ---"); sb.AppendLine(item.StackTrace); } await Clipboard.Default.SetTextAsync(sb.ToString()); // Show brief toast/feedback await MainThread.InvokeOnMainThreadAsync(async () => { var originalText = CountLabel.Text; CountLabel.Text = "Copied to clipboard!"; CountLabel.TextColor = Colors.Green; await Task.Delay(1500); CountLabel.Text = originalText; CountLabel.TextColor = Application.Current?.RequestedTheme == AppTheme.Dark ? Color.FromArgb("#9CA3AF") : Color.FromArgb("#6B7280"); }); } private void OnCloseDetailClicked(object? sender, EventArgs e) { HideDetail(); LogList.SelectedItem = null; } private async void OnCopyStackTraceClicked(object? sender, EventArgs e) { if (_selectedItem != null) { await CopyLogToClipboardAsync(_selectedItem); if (sender is Button button) { var originalText = button.Text; button.Text = "Copied!"; await Task.Delay(1000); button.Text = originalText; } } } #endregion #region Lifecycle protected override void OnAppearing() { base.OnAppearing(); RefreshLogs(); } #endregion #region Helper Methods private void UpdateCount() { CountLabel.Text = _logItems.Count == 1 ? "1 item" : $"{_logItems.Count} items"; } private void ShowDetail(LogItem item) { DetailTitle.Text = item.Title; DetailMessage.Text = item.Message ?? "No message available"; DetailStackTrace.Text = item.StackTrace ?? "No stack trace available"; DetailPanel.IsVisible = true; } private void HideDetail() { DetailPanel.IsVisible = false; _selectedItem = null; } #endregion } /// /// Represents a log item for display in AppLogView. /// public class LogItem { /// /// UTC timestamp when the log was captured. /// public DateTime Timestamp { get; set; } /// /// Log type: "exception", "message", "journey_start", "journey_end", "step_start", "step_end", "info", "warning". /// public string Type { get; set; } = "info"; /// /// Title or exception type name. /// public string Title { get; set; } = ""; /// /// Log message or exception message. /// public string Message { get; set; } = ""; /// /// Stack trace for exceptions. /// public string? StackTrace { get; set; } /// /// Associated journey ID if part of a user journey. /// public string? JourneyId { get; set; } /// /// Associated user ID. /// public string? UserId { get; set; } /// /// Formatted timestamp for display (local time, HH:mm:ss). /// public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss"); /// /// Short type label for display badge. /// public string TypeDisplay => Type switch { "exception" => "ERROR", "message" => "MSG", "journey_start" => "START", "journey_end" => "END", "step_start" => "STEP", "step_end" => "STEP", "warning" => "WARN", "info" => "INFO", _ => Type.ToUpperInvariant() }; /// /// Color for type badge. /// public Color TypeColor => Type switch { "exception" => Colors.Red, "warning" => Colors.Orange, "message" => Colors.Blue, "info" => Colors.Teal, "journey_start" => Colors.Green, "journey_end" => Colors.Green, "step_start" => Colors.Purple, "step_end" => Colors.Purple, _ => Colors.Gray }; /// /// Create a LogItem from a TelemetryClient EnvelopeItem. /// public static LogItem FromEnvelopeItem(EnvelopeItem item) { return new LogItem { Timestamp = item.Timestamp, Type = item.Type ?? "unknown", Title = item.ExceptionType ?? item.Name ?? item.Type ?? "Log Entry", Message = item.Message ?? "", StackTrace = item.StackTrace, JourneyId = item.JourneyId, UserId = item.UserId }; } }