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
};
}
}