diff --git a/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAircraftApi.cs b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAircraftApi.cs index 3e8f138..1152d71 100644 --- a/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAircraftApi.cs +++ b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAircraftApi.cs @@ -1,4 +1,5 @@ using BaseStationReader.Entities.Interfaces; +using BaseStationReader.Entities.Logging; using BaseStationReader.Entities.Tracking; namespace BaseStationReader.Logic.Api.AirLabs @@ -7,7 +8,7 @@ public class AirLabsAircraftApi : ExternalApiBase, IAircraftApi { private readonly string _baseAddress; - public AirLabsAircraftApi(ITrackerHttpClient client, string url, string key) : base(client) + public AirLabsAircraftApi(ITrackerLogger logger, ITrackerHttpClient client, string url, string key) : base(logger, client) { _baseAddress = $"{url}?api_key={key}"; } @@ -19,7 +20,9 @@ public AirLabsAircraftApi(ITrackerHttpClient client, string url, string key) : b /// public async Task?> LookupAircraft(string address) { - return await MakeApiRequest($"&hex={address}"); + Logger.LogMessage(Severity.Info, $"Looking up aircraft with address {address}"); + var properties = await MakeApiRequest($"&hex={address}"); + return properties; } /// @@ -52,8 +55,11 @@ public AirLabsAircraftApi(ITrackerHttpClient client, string url, string key) : b { ApiProperty.ModelICAO, apiResponse!["icao"]?.GetValue() ?? "" } }; } - catch + catch (Exception ex) { + var message = $"Error processing response: {ex.Message}"; + Logger.LogMessage(Severity.Error, message); + Logger.LogException(ex); properties = null; } } diff --git a/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAirlinesApi.cs b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAirlinesApi.cs index bbaf020..857eb9c 100644 --- a/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAirlinesApi.cs +++ b/src/BaseStationReader.Logic/Api/AirLabs/AirLabsAirlinesApi.cs @@ -1,5 +1,7 @@ using BaseStationReader.Entities.Interfaces; +using BaseStationReader.Entities.Logging; using BaseStationReader.Entities.Tracking; +using System.Text.Json.Nodes; namespace BaseStationReader.Logic.Api.AirLabs { @@ -7,7 +9,7 @@ public class AirLabsAirlinesApi : ExternalApiBase, IAirlinesApi { private readonly string _baseAddress; - public AirLabsAirlinesApi(ITrackerHttpClient client, string url, string key) : base(client) + public AirLabsAirlinesApi(ITrackerLogger logger, ITrackerHttpClient client, string url, string key) : base(logger, client) { _baseAddress = $"{url}?api_key={key}"; } @@ -19,6 +21,7 @@ public AirLabsAirlinesApi(ITrackerHttpClient client, string url, string key) : b /// public async Task?> LookupAirlineByIATACode(string iata) { + Logger.LogMessage(Severity.Info, $"Looking up airline with IATA code {iata}"); return await MakeApiRequest($"&iata_code={iata}"); } @@ -29,6 +32,7 @@ public AirLabsAirlinesApi(ITrackerHttpClient client, string url, string key) : b /// public async Task?> LookupAirlineByICAOCode(string icao) { + Logger.LogMessage(Severity.Info, $"Looking up airline with ICAO code {icao}"); return await MakeApiRequest($"&icao_code={icao}"); } @@ -55,13 +59,16 @@ public AirLabsAirlinesApi(ITrackerHttpClient client, string url, string key) : b // Extract the values into a dictionary properties = new() { - { ApiProperty.AirlineIATA, apiResponse!["iata_code"]!.GetValue() }, - { ApiProperty.AirlineICAO, apiResponse!["icao_code"]!.GetValue() }, - { ApiProperty.AirlineName, apiResponse!["name"]!.GetValue() }, + { ApiProperty.AirlineIATA, apiResponse!["iata_code"]?.GetValue() ?? "" }, + { ApiProperty.AirlineICAO, apiResponse!["icao_code"]?.GetValue() ?? "" }, + { ApiProperty.AirlineName, apiResponse!["name"]?.GetValue() ?? "" }, }; } - catch + catch (Exception ex) { + var message = $"Error processing response: {ex.Message}"; + Logger.LogMessage(Severity.Error, message); + Logger.LogException(ex); properties = null; } } diff --git a/src/BaseStationReader.Logic/Api/ExternalApiBase.cs b/src/BaseStationReader.Logic/Api/ExternalApiBase.cs index 519cc85..fabc8d5 100644 --- a/src/BaseStationReader.Logic/Api/ExternalApiBase.cs +++ b/src/BaseStationReader.Logic/Api/ExternalApiBase.cs @@ -1,4 +1,6 @@ using BaseStationReader.Entities.Interfaces; +using BaseStationReader.Entities.Logging; +using BaseStationReader.Entities.Tracking; using System.Text.Json.Nodes; namespace BaseStationReader.Logic.Api @@ -7,8 +9,11 @@ public abstract class ExternalApiBase { private readonly ITrackerHttpClient _client; - protected ExternalApiBase(ITrackerHttpClient client) + protected ITrackerLogger Logger { get; private set; } + + protected ExternalApiBase(ITrackerLogger logger, ITrackerHttpClient client) { + Logger = logger; _client = client; } @@ -35,12 +40,43 @@ protected ExternalApiBase(ITrackerHttpClient client) } } } - catch + catch (Exception ex) { + var message = $"Error calling {endpoint}: {ex.Message}"; + Logger.LogMessage(Severity.Error, message); + Logger.LogException(ex); node = null; } return node; } + + /// + /// Log the content of a properties dictionary resulting from an external API call + /// + /// + protected void LogProperties(Dictionary? properties) + { + // Check the properties dictionary isn't NULL + if (properties != null) + { + // Not a NULL dictionary, so iterate over all the properties it contains + foreach (var property in properties) + { + // Construct a message containing the property name and the value, replacing + // null values with "NULL" + var value = property.Value != null ? property.Value.ToString() : "NULL"; + var message = $"API property {property.Key.ToString()} = {value}"; + + // Log the message for this property + Logger.LogMessage(Severity.Info, message); + } + } + else + { + // Log the fact that the properties dictionary is NULL + Logger.LogMessage(Severity.Warning, "API lookup generated a NULL properties dictionary"); + } + } } } diff --git a/src/BaseStationReader.Tests/AirLabsAircraftApiTest.cs b/src/BaseStationReader.Tests/AirLabsAircraftApiTest.cs index 572c128..b181e17 100644 --- a/src/BaseStationReader.Tests/AirLabsAircraftApiTest.cs +++ b/src/BaseStationReader.Tests/AirLabsAircraftApiTest.cs @@ -21,8 +21,9 @@ public class AirLabsAircraftApiTest [TestInitialize] public void Initialise() { + var logger = new MockFileLogger(); _client = new MockTrackerHttpClient(); - _api = new AirLabsAircraftApi(_client, "", ""); + _api = new AirLabsAircraftApi(logger, _client, "", ""); } [TestMethod] diff --git a/src/BaseStationReader.Tests/AirLabsAirlinesApiTest.cs b/src/BaseStationReader.Tests/AirLabsAirlinesApiTest.cs index 59955f6..5c677d4 100644 --- a/src/BaseStationReader.Tests/AirLabsAirlinesApiTest.cs +++ b/src/BaseStationReader.Tests/AirLabsAirlinesApiTest.cs @@ -14,6 +14,8 @@ namespace BaseStationReader.Tests public class AirLabsAirlinesApiTest { private const string Response = "{\"response\": [{\"name\": \"Jet2.com\", \"iata_code\": \"LS\", \"icao_code\": \"EXS\"}]}"; + private const string NoIATACode = "{\"response\": [{\"name\": \"Jet2.com\", \"iata_code\": null, \"icao_code\": \"EXS\"}]}"; + private const string NoICAOCode = "{\"response\": [{\"name\": \"Jet2.com\", \"iata_code\": \"LS\", \"icao_code\": null}]}"; private MockTrackerHttpClient? _client = null; private IAirlinesApi? _api = null; @@ -21,8 +23,9 @@ public class AirLabsAirlinesApiTest [TestInitialize] public void Initialise() { + var logger = new MockFileLogger(); _client = new MockTrackerHttpClient(); - _api = new AirLabsAirlinesApi(_client, "", ""); + _api = new AirLabsAirlinesApi(logger, _client, "", ""); } [TestMethod] @@ -51,6 +54,32 @@ public void GetAirlineByICAOCodeTest() Assert.AreEqual("Jet2.com", properties[ApiProperty.AirlineName]); } + [TestMethod] + public void NoIATACodeTest() + { + _client!.AddResponse(NoIATACode); + var properties = Task.Run(() => _api!.LookupAirlineByICAOCode("EXS")).Result; + + Assert.IsNotNull(properties); + Assert.AreEqual(3, properties.Count); + Assert.AreEqual("", properties[ApiProperty.AirlineIATA]); + Assert.AreEqual("EXS", properties[ApiProperty.AirlineICAO]); + Assert.AreEqual("Jet2.com", properties[ApiProperty.AirlineName]); + } + + [TestMethod] + public void NoICAOCodeTest() + { + _client!.AddResponse(NoICAOCode); + var properties = Task.Run(() => _api!.LookupAirlineByICAOCode("EXS")).Result; + + Assert.IsNotNull(properties); + Assert.AreEqual(3, properties.Count); + Assert.AreEqual("LS", properties[ApiProperty.AirlineIATA]); + Assert.AreEqual("", properties[ApiProperty.AirlineICAO]); + Assert.AreEqual("Jet2.com", properties[ApiProperty.AirlineName]); + } + [TestMethod] public void InvalidJsonResponseTest() { diff --git a/src/BaseStationReader.Tests/AircraftLookupManagerTest.cs b/src/BaseStationReader.Tests/AircraftLookupManagerTest.cs index 4789f94..6f9607a 100644 --- a/src/BaseStationReader.Tests/AircraftLookupManagerTest.cs +++ b/src/BaseStationReader.Tests/AircraftLookupManagerTest.cs @@ -47,9 +47,10 @@ public void Initialise() _manufacturerId = Task.Run(() => manufacturerManager.AddAsync(ManufacturerName)).Result.Id; // Create the API wrappers + var logger = new MockFileLogger(); _client = new MockTrackerHttpClient(); - var airlinesApi = new AirLabsAirlinesApi(_client, "", ""); - var aircraftApi = new AirLabsAircraftApi(_client, "", ""); + var airlinesApi = new AirLabsAirlinesApi(logger, _client, "", ""); + var aircraftApi = new AirLabsAircraftApi(logger, _client, "", ""); // Finally, create a lookup manager _manager = new AircraftLookupManager(_airlines, _details, _models, airlinesApi, aircraftApi); diff --git a/src/BaseStationReader.UI/Models/AircraftLookupModel.cs b/src/BaseStationReader.UI/Models/AircraftLookupModel.cs index 0f4a14e..2ce89d0 100644 --- a/src/BaseStationReader.UI/Models/AircraftLookupModel.cs +++ b/src/BaseStationReader.UI/Models/AircraftLookupModel.cs @@ -1,5 +1,6 @@ using BaseStationReader.Data; using BaseStationReader.Entities.Config; +using BaseStationReader.Entities.Interfaces; using BaseStationReader.Entities.Lookup; using BaseStationReader.Logic.Api; using BaseStationReader.Logic.Api.AirLabs; @@ -14,7 +15,7 @@ public class AircraftLookupModel { private readonly AircraftLookupManager _lookupManager; - public AircraftLookupModel(TrackerApplicationSettings settings) + public AircraftLookupModel(ITrackerLogger logger, TrackerApplicationSettings settings) { // Create a database context var context = new BaseStationReaderDbContextFactory().CreateDbContext(Array.Empty()); @@ -31,8 +32,8 @@ public AircraftLookupModel(TrackerApplicationSettings settings) // Create the API wrappers var client = TrackerHttpClient.Instance; - var airlinesApi = new AirLabsAirlinesApi(client, airlinesUrl, key); - var aircraftApi = new AirLabsAircraftApi(client, aircraftUrl, key); + var airlinesApi = new AirLabsAirlinesApi(logger, client, airlinesUrl, key); + var aircraftApi = new AirLabsAircraftApi(logger, client, aircraftUrl, key); // Finally, create a lookup manager _lookupManager = new AircraftLookupManager(airlinesManager, detailsManager, modelsManager, airlinesApi, aircraftApi); diff --git a/src/BaseStationReader.UI/Models/LiveViewModel.cs b/src/BaseStationReader.UI/Models/LiveViewModel.cs index 72e8fe7..2b3787c 100644 --- a/src/BaseStationReader.UI/Models/LiveViewModel.cs +++ b/src/BaseStationReader.UI/Models/LiveViewModel.cs @@ -1,6 +1,8 @@ using BaseStationReader.Entities.Config; +using BaseStationReader.Entities.Events; using BaseStationReader.Entities.Expressions; using BaseStationReader.Entities.Interfaces; +using BaseStationReader.Entities.Logging; using BaseStationReader.Entities.Tracking; using BaseStationReader.Logic.Database; using BaseStationReader.Logic.Tracking; @@ -14,6 +16,7 @@ namespace BaseStationReader.UI.Models { public class LiveViewModel { + private ITrackerLogger? _logger = null; private ITrackerWrapper? _wrapper = null; public ObservableCollection TrackedAircraft { get; private set; } = new(); @@ -27,8 +30,12 @@ public class LiveViewModel /// public void Initialise(ITrackerLogger logger, TrackerApplicationSettings settings) { + _logger = logger; _wrapper = new TrackerWrapper(logger, settings); _wrapper.Initialise(); + _wrapper.AircraftAdded += OnAircraftAdded; + _wrapper.AircraftUpdated += OnAircraftUpdated; + _wrapper.AircraftRemoved += OnAircraftRemoved; } /// @@ -84,5 +91,35 @@ public void Refresh() // Update the observable collection from the filtered aircraft list TrackedAircraft = new ObservableCollection(aircraft); } + + /// + /// Handle the event raised when a new aircraft is detected + /// + /// + /// + private void OnAircraftAdded(object? sender, AircraftNotificationEventArgs e) + { + _logger!.LogMessage(Severity.Info, $"Added new aircraft {e.Aircraft.Address}"); + } + + /// + /// Handle the event raised when a new aircraft is updated + /// + /// + /// + private void OnAircraftUpdated(object? sender, AircraftNotificationEventArgs e) + { + _logger!.LogMessage(Severity.Debug, $"Updated aircraft {e.Aircraft.Address}"); + } + + /// + /// Handle the event raised when a new aircraft is removed + /// + /// + /// + private void OnAircraftRemoved(object? sender, AircraftNotificationEventArgs e) + { + _logger!.LogMessage(Severity.Debug, $"Removed aircraft {e.Aircraft.Address}"); + } } } diff --git a/src/BaseStationReader.UI/ViewModels/AircraftLookupWindowViewModel.cs b/src/BaseStationReader.UI/ViewModels/AircraftLookupWindowViewModel.cs index 15a861a..98656ed 100644 --- a/src/BaseStationReader.UI/ViewModels/AircraftLookupWindowViewModel.cs +++ b/src/BaseStationReader.UI/ViewModels/AircraftLookupWindowViewModel.cs @@ -1,4 +1,5 @@ using BaseStationReader.Entities.Config; +using BaseStationReader.Entities.Interfaces; using BaseStationReader.Entities.Lookup; using BaseStationReader.UI.Models; using ReactiveUI; @@ -12,10 +13,10 @@ public class AircraftLookupWindowViewModel : AircraftLookupCriteria public ReactiveCommand CloseCommand { get; private set; } - public AircraftLookupWindowViewModel(TrackerApplicationSettings settings, AircraftLookupCriteria? initialValues) + public AircraftLookupWindowViewModel(ITrackerLogger logger, TrackerApplicationSettings settings, AircraftLookupCriteria? initialValues) { // Set up the aircraft lookup model - _aircraftLookup = new AircraftLookupModel(settings); + _aircraftLookup = new AircraftLookupModel(logger, settings); // Populate from the initial values, if supplied Address = initialValues?.Address; diff --git a/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs b/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs index 7664f9b..3ee53c9 100644 --- a/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs +++ b/src/BaseStationReader.UI/ViewModels/MainWindowViewModel.cs @@ -26,6 +26,11 @@ public class MainWindowViewModel : ViewModelBase /// public TrackerApplicationSettings? Settings { get; set; } + /// + /// Logging provider + /// + public ITrackerLogger? Logger { get; set; } + /// /// True if the tracker is actively tracking /// @@ -119,7 +124,7 @@ public MainWindowViewModel() ShowAircraftLookupDialog = new Interaction(); ShowAircraftLookupCommand = ReactiveCommand.CreateFromTask(async () => { - var dialogViewModel = new AircraftLookupWindowViewModel(Settings!, AircraftLookupCriteria); + var dialogViewModel = new AircraftLookupWindowViewModel(Logger!, Settings!, AircraftLookupCriteria); var result = await ShowAircraftLookupDialog.Handle(dialogViewModel); return result; }); @@ -146,10 +151,8 @@ public MainWindowViewModel() /// /// Initialise the tracker /// - /// - /// - public void InitialiseTracker(ITrackerLogger logger, TrackerApplicationSettings settings) - => _liveView.Initialise(logger, settings); + public void InitialiseTracker() + => _liveView.Initialise(Logger!, Settings!); /// /// Start the tracker diff --git a/src/BaseStationReader.UI/Views/MainWindow.axaml.cs b/src/BaseStationReader.UI/Views/MainWindow.axaml.cs index f471a0a..ba16af0 100644 --- a/src/BaseStationReader.UI/Views/MainWindow.axaml.cs +++ b/src/BaseStationReader.UI/Views/MainWindow.axaml.cs @@ -12,6 +12,7 @@ using Avalonia.Threading; using BaseStationReader.Entities.Config; using BaseStationReader.Entities.Interfaces; +using BaseStationReader.Entities.Logging; using BaseStationReader.Entities.Tracking; using BaseStationReader.Logic.Configuration; using BaseStationReader.Logic.Logging; @@ -29,7 +30,7 @@ namespace BaseStationReader.UI.Views { public partial class MainWindow : ReactiveWindow { - private DispatcherTimer _timer = new DispatcherTimer(); + private readonly DispatcherTimer _timer = new DispatcherTimer(); private ITrackerLogger? _logger = null; private bool _aircraftLookupIsEnabled = false; @@ -54,13 +55,20 @@ private void OnLoaded(object? source, RoutedEventArgs e) // Set the title, based on the version set in the project properties Assembly assembly = Assembly.GetExecutingAssembly(); FileVersionInfo info = FileVersionInfo.GetVersionInfo(assembly.Location); - Title = $"Aircraft Database Viewer {info.FileVersion}"; + Title = $"Aircraft Tracker {info.FileVersion}"; // Load the settings and configure the logger ViewModel!.Settings = new TrackerConfigReader().Read("appsettings.json"); _logger = new FileLogger(); _logger.Initialise(ViewModel!.Settings!.LogFile, ViewModel!.Settings.MinimumLogLevel); + // Log the startup messages + _logger.LogMessage(Severity.Info, new string('=', 80)); + _logger.LogMessage(Severity.Info, Title); + + // Make the logger available to the view model + ViewModel.Logger = _logger; + // Configure the column titles and visibility ConfigureColumns(TrackedAircraftGrid); ConfigureColumns(DatabaseGrid); @@ -234,7 +242,7 @@ private void OnStartTracking(object source, RoutedEventArgs e) ViewModel!.LiveViewFilters = null; // Start tracking and perform an initial refresh - ViewModel!.InitialiseTracker(_logger!, ViewModel.Settings); + ViewModel!.InitialiseTracker(); ViewModel.StartTracking(); _timer.Start(); diff --git a/src/BaseStationReader.UI/appsettings.json b/src/BaseStationReader.UI/appsettings.json index f5e8b8c..79bd8c0 100644 --- a/src/BaseStationReader.UI/appsettings.json +++ b/src/BaseStationReader.UI/appsettings.json @@ -32,7 +32,7 @@ "ApiServiceKeys": [ { "Service": "AirLabs", - "Key": "4cbea986-cbb2-4abd-88f2-7b1a246db63f" + "Key": "" } ], "Columns": [