From c8d8d9d3d2c616276d07a35ce57f1033f05692e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20M=C3=BCller?= Date: Fri, 13 Feb 2026 17:40:33 +0100 Subject: [PATCH 1/5] fix: recurring exceptions when sending tuio messages fails --- .../src/Core/Tuio/Components/TuioSender.cs | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/library/src/Core/Tuio/Components/TuioSender.cs b/library/src/Core/Tuio/Components/TuioSender.cs index 4732547c..f730dbfb 100644 --- a/library/src/Core/Tuio/Components/TuioSender.cs +++ b/library/src/Core/Tuio/Components/TuioSender.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Concurrent; using System.IO; using System.Net.Sockets; using System.Net.WebSockets; using System.Runtime.Serialization.Formatters.Binary; +using System.Text; using System.Threading; using System.Threading.Tasks; using CoreOSC; @@ -18,17 +20,18 @@ namespace ReFlex.Core.Tuio.Components public class TuioSender : ITuioSender { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private readonly ConcurrentDictionary _loggedSendErrors = new ConcurrentDictionary(); /// /// Client for sending TUIO Messages to Server using UDP /// protected UdpClient UdpClient; - + /// /// Client Interface for sending TUIO Messages to Server using TCP. (use for better test abilities) /// protected ITcpClient TcpClient; - + /// /// Client Interface for sending TUIO Messages to Server using WebSocket protocol. (use for better test abilities) /// @@ -37,6 +40,45 @@ public class TuioSender : ITuioSender private string _serverAddress; private int _serverPort; + private void LogErrorOnce(Exception exc, string methodName, string message = null) + { + if (exc == null) + { + return; + } + + var errorKey = $"{methodName}:{BuildExceptionSignature(exc)}"; + if (!_loggedSendErrors.TryAdd(errorKey, 0)) + { + return; + } + + if (string.IsNullOrWhiteSpace(message)) + { + Log.Error(exc); + return; + } + + Log.Error(exc, message); + } + + private static string BuildExceptionSignature(Exception exc) + { + var builder = new StringBuilder(); + var current = exc; + + while (current != null) + { + builder.Append(current.GetType().FullName); + builder.Append(':'); + builder.Append(current.Message); + builder.Append('|'); + current = current.InnerException; + } + + return builder.ToString(); + } + /// /// Specifies whether a valid has been provided and sending is enabled. /// @@ -78,6 +120,7 @@ public virtual void Initialize(TuioConfiguration config) _serverAddress = config.ServerAddress; _serverPort = config.ServerPort; + _loggedSendErrors.Clear(); IsInitialized = true; } @@ -96,7 +139,7 @@ public virtual async Task SendUdp(OscBundle bundle) } catch (Exception exc) { - Log.Error(exc, $"Error sending osc message via UDP"); + LogErrorOnce(exc, nameof(SendUdp), "Error sending osc message via UDP"); } } @@ -115,7 +158,7 @@ public virtual async Task SendTcp(OscBundle bundle) } catch (Exception exc) { - Log.Error(exc); + LogErrorOnce(exc, nameof(SendTcp)); } } @@ -146,7 +189,7 @@ public virtual async Task SendWebSocket(OscBundle bundle) } catch (Exception exc) { - Log.Error(exc); + LogErrorOnce(exc, nameof(SendWebSocket)); } } @@ -165,7 +208,14 @@ public virtual async Task SendWebSocket(OscBundle bundle) /// to be transmitted protected virtual async Task SendOscMessageUdp(OscMessage msg) { - await UdpClient.SendMessageAsync(msg); + try + { + await UdpClient.SendMessageAsync(msg); + } + catch (Exception exc) + { + LogErrorOnce(exc, nameof(SendOscMessageUdp)); + } } /// @@ -190,7 +240,7 @@ protected virtual async Task SendOscMessageTcp(OscMessage msg) } catch (Exception exc) { - Log.Error(exc); + LogErrorOnce(exc, nameof(SendOscMessageTcp)); } } @@ -218,7 +268,7 @@ await WsClient.SendAsync(new ArraySegment(mStream.ToArray()), } catch (Exception exc) { - Log.Error(exc); + LogErrorOnce(exc, nameof(SendOscMessageWebSocket)); } } @@ -237,8 +287,9 @@ await WsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close connection" UdpClient?.Dispose(); TcpClient?.Dispose(); WsClient?.Dispose(); + _loggedSendErrors.Clear(); IsInitialized = false; } } -} \ No newline at end of file +} From 2bea5c39671011ece6d3b7882757a5a58484acd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20M=C3=BCller?= Date: Fri, 13 Feb 2026 18:39:50 +0100 Subject: [PATCH 2/5] move to separate class --- library/src/Core/Common/Util/LogUtilities.cs | 91 +++++++++++++++++++ .../src/Core/Tuio/Components/TuioSender.cs | 57 ++++-------- 2 files changed, 108 insertions(+), 40 deletions(-) create mode 100644 library/src/Core/Common/Util/LogUtilities.cs diff --git a/library/src/Core/Common/Util/LogUtilities.cs b/library/src/Core/Common/Util/LogUtilities.cs new file mode 100644 index 00000000..f6aaba43 --- /dev/null +++ b/library/src/Core/Common/Util/LogUtilities.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Concurrent; +using System.Text; + +namespace ReFlex.Core.Common.Util +{ + public static class LogUtilities + { + /// + /// Logs an exception only once for the same source, method and exception signature. + /// + /// Thread-safe cache that stores signatures of already logged errors. + /// Exception to log. + /// Logical source (for example class name) used for scoping deduplication. + /// Method name used for scoping deduplication. + /// Logging callback used when no custom message is provided. + /// Logging callback used when a custom message is provided. + /// Optional custom message for the log entry. + /// Thrown when required arguments are null. + public static void LogErrorOnce( + ConcurrentDictionary loggedErrors, + Exception exception, + string sourceName, + string methodName, + Action logError, + Action logErrorWithMessage, + string message = null) + { + if (loggedErrors == null) + throw new ArgumentNullException(nameof(loggedErrors)); + if (logError == null) + throw new ArgumentNullException(nameof(logError)); + if (logErrorWithMessage == null) + throw new ArgumentNullException(nameof(logErrorWithMessage)); + + if (exception == null) + { + return; + } + + var errorKey = BuildErrorKey(sourceName, methodName, exception); + if (!loggedErrors.TryAdd(errorKey, 0)) + { + return; + } + + if (string.IsNullOrWhiteSpace(message)) + { + logError(exception); + return; + } + + logErrorWithMessage(exception, message); + } + + /// + /// Creates a key that uniquely identifies an error context for deduplicated logging. + /// + /// Logical source (for example class name). + /// Method name where the exception occurred. + /// Exception to create the key from. + /// Deterministic key based on source, method and exception signature. + private static string BuildErrorKey(string sourceName, string methodName, Exception exception) + { + return $"{sourceName}:{methodName}:{BuildExceptionSignature(exception)}"; + } + + /// + /// Builds a deterministic signature from the exception chain. + /// This includes each exception type and message, including inner exceptions. + /// + /// Exception to convert to a signature. + /// Signature string that can be used for deduplication. + private static string BuildExceptionSignature(Exception exception) + { + var builder = new StringBuilder(); + var current = exception; + + while (current != null) + { + builder.Append(current.GetType().FullName); + builder.Append(':'); + builder.Append(current.Message); + builder.Append('|'); + current = current.InnerException; + } + + return builder.ToString(); + } + } +} diff --git a/library/src/Core/Tuio/Components/TuioSender.cs b/library/src/Core/Tuio/Components/TuioSender.cs index f730dbfb..648899c4 100644 --- a/library/src/Core/Tuio/Components/TuioSender.cs +++ b/library/src/Core/Tuio/Components/TuioSender.cs @@ -4,7 +4,6 @@ using System.Net.Sockets; using System.Net.WebSockets; using System.Runtime.Serialization.Formatters.Binary; -using System.Text; using System.Threading; using System.Threading.Tasks; using CoreOSC; @@ -12,6 +11,7 @@ using NLog; using ReFlex.Core.Common.Adapter; using ReFlex.Core.Common.Interfaces; +using ReFlex.Core.Common.Util; using ReFlex.Core.Tuio.Interfaces; using ReFlex.Core.Tuio.Util; @@ -40,43 +40,14 @@ public class TuioSender : ITuioSender private string _serverAddress; private int _serverPort; - private void LogErrorOnce(Exception exc, string methodName, string message = null) + private void LogError(Exception exception) { - if (exc == null) - { - return; - } - - var errorKey = $"{methodName}:{BuildExceptionSignature(exc)}"; - if (!_loggedSendErrors.TryAdd(errorKey, 0)) - { - return; - } - - if (string.IsNullOrWhiteSpace(message)) - { - Log.Error(exc); - return; - } - - Log.Error(exc, message); + Log.Error(exception); } - private static string BuildExceptionSignature(Exception exc) + private void LogErrorWithMessage(Exception exception, string message) { - var builder = new StringBuilder(); - var current = exc; - - while (current != null) - { - builder.Append(current.GetType().FullName); - builder.Append(':'); - builder.Append(current.Message); - builder.Append('|'); - current = current.InnerException; - } - - return builder.ToString(); + Log.Error(exception, message); } /// @@ -139,7 +110,8 @@ public virtual async Task SendUdp(OscBundle bundle) } catch (Exception exc) { - LogErrorOnce(exc, nameof(SendUdp), "Error sending osc message via UDP"); + LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendUdp), LogError, + LogErrorWithMessage, "Error sending osc message via UDP"); } } @@ -158,7 +130,8 @@ public virtual async Task SendTcp(OscBundle bundle) } catch (Exception exc) { - LogErrorOnce(exc, nameof(SendTcp)); + LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendTcp), LogError, + LogErrorWithMessage); } } @@ -189,7 +162,8 @@ public virtual async Task SendWebSocket(OscBundle bundle) } catch (Exception exc) { - LogErrorOnce(exc, nameof(SendWebSocket)); + LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendWebSocket), + LogError, LogErrorWithMessage); } } @@ -214,7 +188,8 @@ protected virtual async Task SendOscMessageUdp(OscMessage msg) } catch (Exception exc) { - LogErrorOnce(exc, nameof(SendOscMessageUdp)); + LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendOscMessageUdp), + LogError, LogErrorWithMessage); } } @@ -240,7 +215,8 @@ protected virtual async Task SendOscMessageTcp(OscMessage msg) } catch (Exception exc) { - LogErrorOnce(exc, nameof(SendOscMessageTcp)); + LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendOscMessageTcp), + LogError, LogErrorWithMessage); } } @@ -268,7 +244,8 @@ await WsClient.SendAsync(new ArraySegment(mStream.ToArray()), } catch (Exception exc) { - LogErrorOnce(exc, nameof(SendOscMessageWebSocket)); + LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), + nameof(SendOscMessageWebSocket), LogError, LogErrorWithMessage); } } From d9597e2b6ae0a88417bc4415b39d847cc5df605e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20M=C3=BCller?= Date: Fri, 13 Feb 2026 18:44:00 +0100 Subject: [PATCH 3/5] refactoring: move dictionary to utilities --- library/src/Core/Common/Util/LogUtilities.cs | 38 ++++++++++++++++--- .../src/Core/Tuio/Components/TuioSender.cs | 18 ++++----- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/library/src/Core/Common/Util/LogUtilities.cs b/library/src/Core/Common/Util/LogUtilities.cs index f6aaba43..1d2e32e8 100644 --- a/library/src/Core/Common/Util/LogUtilities.cs +++ b/library/src/Core/Common/Util/LogUtilities.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Concurrent; +using System.Linq; using System.Text; namespace ReFlex.Core.Common.Util { public static class LogUtilities { + private static readonly ConcurrentDictionary LoggedErrors = new ConcurrentDictionary(); + /// /// Logs an exception only once for the same source, method and exception signature. /// - /// Thread-safe cache that stores signatures of already logged errors. /// Exception to log. /// Logical source (for example class name) used for scoping deduplication. /// Method name used for scoping deduplication. @@ -18,7 +20,6 @@ public static class LogUtilities /// Optional custom message for the log entry. /// Thrown when required arguments are null. public static void LogErrorOnce( - ConcurrentDictionary loggedErrors, Exception exception, string sourceName, string methodName, @@ -26,8 +27,10 @@ public static void LogErrorOnce( Action logErrorWithMessage, string message = null) { - if (loggedErrors == null) - throw new ArgumentNullException(nameof(loggedErrors)); + if (sourceName == null) + throw new ArgumentNullException(nameof(sourceName)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); if (logError == null) throw new ArgumentNullException(nameof(logError)); if (logErrorWithMessage == null) @@ -39,7 +42,7 @@ public static void LogErrorOnce( } var errorKey = BuildErrorKey(sourceName, methodName, exception); - if (!loggedErrors.TryAdd(errorKey, 0)) + if (!LoggedErrors.TryAdd(errorKey, 0)) { return; } @@ -53,6 +56,31 @@ public static void LogErrorOnce( logErrorWithMessage(exception, message); } + /// + /// Clears deduplication entries. + /// If is provided, only entries for this source are removed. + /// If it is null or whitespace, all entries are removed. + /// + /// Optional source name filter. + public static void ClearLoggedErrors(string sourceName = null) + { + if (string.IsNullOrWhiteSpace(sourceName)) + { + LoggedErrors.Clear(); + return; + } + + var sourcePrefix = $"{sourceName}:"; + var keys = LoggedErrors.Keys + .Where(key => key.StartsWith(sourcePrefix, StringComparison.Ordinal)) + .ToList(); + + foreach (var key in keys) + { + LoggedErrors.TryRemove(key, out _); + } + } + /// /// Creates a key that uniquely identifies an error context for deduplicated logging. /// diff --git a/library/src/Core/Tuio/Components/TuioSender.cs b/library/src/Core/Tuio/Components/TuioSender.cs index 648899c4..d08e6d9f 100644 --- a/library/src/Core/Tuio/Components/TuioSender.cs +++ b/library/src/Core/Tuio/Components/TuioSender.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.IO; using System.Net.Sockets; using System.Net.WebSockets; @@ -20,7 +19,6 @@ namespace ReFlex.Core.Tuio.Components public class TuioSender : ITuioSender { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private readonly ConcurrentDictionary _loggedSendErrors = new ConcurrentDictionary(); /// /// Client for sending TUIO Messages to Server using UDP @@ -91,7 +89,7 @@ public virtual void Initialize(TuioConfiguration config) _serverAddress = config.ServerAddress; _serverPort = config.ServerPort; - _loggedSendErrors.Clear(); + LogUtilities.ClearLoggedErrors(nameof(TuioSender)); IsInitialized = true; } @@ -110,7 +108,7 @@ public virtual async Task SendUdp(OscBundle bundle) } catch (Exception exc) { - LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendUdp), LogError, + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendUdp), LogError, LogErrorWithMessage, "Error sending osc message via UDP"); } } @@ -130,7 +128,7 @@ public virtual async Task SendTcp(OscBundle bundle) } catch (Exception exc) { - LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendTcp), LogError, + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendTcp), LogError, LogErrorWithMessage); } } @@ -162,7 +160,7 @@ public virtual async Task SendWebSocket(OscBundle bundle) } catch (Exception exc) { - LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendWebSocket), + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendWebSocket), LogError, LogErrorWithMessage); } } @@ -188,7 +186,7 @@ protected virtual async Task SendOscMessageUdp(OscMessage msg) } catch (Exception exc) { - LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendOscMessageUdp), + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageUdp), LogError, LogErrorWithMessage); } } @@ -215,7 +213,7 @@ protected virtual async Task SendOscMessageTcp(OscMessage msg) } catch (Exception exc) { - LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), nameof(SendOscMessageTcp), + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageTcp), LogError, LogErrorWithMessage); } } @@ -244,7 +242,7 @@ await WsClient.SendAsync(new ArraySegment(mStream.ToArray()), } catch (Exception exc) { - LogUtilities.LogErrorOnce(_loggedSendErrors, exc, nameof(TuioSender), + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageWebSocket), LogError, LogErrorWithMessage); } } @@ -264,7 +262,7 @@ await WsClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close connection" UdpClient?.Dispose(); TcpClient?.Dispose(); WsClient?.Dispose(); - _loggedSendErrors.Clear(); + LogUtilities.ClearLoggedErrors(nameof(TuioSender)); IsInitialized = false; } From 641e2a49c674c5164de2d3f5907ff6ed418bcfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20M=C3=BCller?= Date: Fri, 13 Feb 2026 19:02:00 +0100 Subject: [PATCH 4/5] added tests --- test/Library/Tuio.Test/LogUtilitiesTest.cs | 224 +++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 test/Library/Tuio.Test/LogUtilitiesTest.cs diff --git a/test/Library/Tuio.Test/LogUtilitiesTest.cs b/test/Library/Tuio.Test/LogUtilitiesTest.cs new file mode 100644 index 00000000..53a1f1dd --- /dev/null +++ b/test/Library/Tuio.Test/LogUtilitiesTest.cs @@ -0,0 +1,224 @@ +using System; +using NUnit.Framework; +using ReFlex.Core.Common.Util; + +namespace ReFlex.Core.Tuio.Test +{ + [TestFixture] + public class LogUtilitiesTest + { + [Test] + public void TestNoAdditionalLogWhenKeyExists() + { + // Use a unique source scope so tests stay isolated even with shared static cache. + var sourceName = CreateUniqueSourceName(nameof(TestNoAdditionalLogWhenKeyExists)); + var methodName = "SendUdp"; + var logCount = 0; + + try + { + // First occurrence should be logged. + LogUtilities.LogErrorOnce( + new InvalidOperationException("send failed"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + // Same exception signature in same source/method should be suppressed. + LogUtilities.LogErrorOnce( + new InvalidOperationException("send failed"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + // Exactly one log entry is expected. + Assert.That(logCount, Is.EqualTo(1)); + } + finally + { + // Cleanup test-specific cache entries. + LogUtilities.ClearLoggedErrors(sourceName); + } + } + + [Test] + public void TestClearAllowsLoggingAgain() + { + // Unique source keeps this test independent from other runs. + var sourceName = CreateUniqueSourceName(nameof(TestClearAllowsLoggingAgain)); + var methodName = "SendTcp"; + var logCount = 0; + + try + { + // First log call should pass through. + LogUtilities.LogErrorOnce( + new InvalidOperationException("connection failed"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++, + "custom message"); + + // Reset deduplication state for this source. + LogUtilities.ClearLoggedErrors(sourceName); + + // After reset, same error must be logged again. + LogUtilities.LogErrorOnce( + new InvalidOperationException("connection failed"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++, + "custom message"); + + // One log before and one log after clear. + Assert.That(logCount, Is.EqualTo(2)); + } + finally + { + // Cleanup test-specific cache entries. + LogUtilities.ClearLoggedErrors(sourceName); + } + } + + [Test] + public void TestDifferentMessagesAndExceptionTypesAreLoggedOnceEach() + { + // Unique source keeps this test independent from other runs. + var sourceName = CreateUniqueSourceName(nameof(TestDifferentMessagesAndExceptionTypesAreLoggedOnceEach)); + var methodName = "SendOscMessageTcp"; + var logCount = 0; + + try + { + // Distinct signatures: message, type and inner-exception chain. + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message B"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new ArgumentException("message A"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A", new Exception("inner A")), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + // Repeat all variants; duplicates should be suppressed. + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message B"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new ArgumentException("message A"), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A", new Exception("inner A")), + sourceName, + methodName, + _ => logCount++, + (_, __) => logCount++); + + // Four unique signatures should produce four logs total. + Assert.That(logCount, Is.EqualTo(4)); + } + finally + { + // Cleanup test-specific cache entries. + LogUtilities.ClearLoggedErrors(sourceName); + } + } + + [Test] + public void TestSameErrorIsLoggedOncePerMethod() + { + // Unique source keeps this test independent from other runs. + var sourceName = CreateUniqueSourceName(nameof(TestSameErrorIsLoggedOncePerMethod)); + var firstMethod = "SendTcp"; + var secondMethod = "SendOscMessageTcp"; + var logCount = 0; + + try + { + // Same error in first method: only first call logs. + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + firstMethod, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + firstMethod, + _ => logCount++, + (_, __) => logCount++); + + Assert.That(logCount, Is.EqualTo(1)); + + // Same error in different method should log once again (method is part of the key). + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + secondMethod, + _ => logCount++, + (_, __) => logCount++); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + secondMethod, + _ => logCount++, + (_, __) => logCount++); + + // One log per method is expected. + Assert.That(logCount, Is.EqualTo(2)); + } + finally + { + // Cleanup test-specific cache entries. + LogUtilities.ClearLoggedErrors(sourceName); + } + } + + private static string CreateUniqueSourceName(string testName) + { + // A per-test unique source avoids cross-test interference through the static cache. + return $"{nameof(LogUtilitiesTest)}.{testName}.{Guid.NewGuid()}"; + } + } +} From 113f97eee751aa0d623c62e6f4983d8dc89a6306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20M=C3=BCller?= Date: Fri, 13 Feb 2026 19:11:47 +0100 Subject: [PATCH 5/5] refactoring: encapsulate Logging Functionality in LogUtilities --- library/src/Core/Common/Common.csproj | 1 + library/src/Core/Common/Util/LogUtilities.cs | 14 +- .../src/Core/Tuio/Components/TuioSender.cs | 28 +- test/Library/Tuio.Test/LogUtilitiesTest.cs | 311 ++++++++---------- 4 files changed, 151 insertions(+), 203 deletions(-) diff --git a/library/src/Core/Common/Common.csproj b/library/src/Core/Common/Common.csproj index 6f260d34..38f9ba7d 100644 --- a/library/src/Core/Common/Common.csproj +++ b/library/src/Core/Common/Common.csproj @@ -24,6 +24,7 @@ + diff --git a/library/src/Core/Common/Util/LogUtilities.cs b/library/src/Core/Common/Util/LogUtilities.cs index 1d2e32e8..b2cddec7 100644 --- a/library/src/Core/Common/Util/LogUtilities.cs +++ b/library/src/Core/Common/Util/LogUtilities.cs @@ -2,11 +2,13 @@ using System.Collections.Concurrent; using System.Linq; using System.Text; +using NLog; namespace ReFlex.Core.Common.Util { public static class LogUtilities { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private static readonly ConcurrentDictionary LoggedErrors = new ConcurrentDictionary(); /// @@ -15,26 +17,18 @@ public static class LogUtilities /// Exception to log. /// Logical source (for example class name) used for scoping deduplication. /// Method name used for scoping deduplication. - /// Logging callback used when no custom message is provided. - /// Logging callback used when a custom message is provided. /// Optional custom message for the log entry. /// Thrown when required arguments are null. public static void LogErrorOnce( Exception exception, string sourceName, string methodName, - Action logError, - Action logErrorWithMessage, string message = null) { if (sourceName == null) throw new ArgumentNullException(nameof(sourceName)); if (methodName == null) throw new ArgumentNullException(nameof(methodName)); - if (logError == null) - throw new ArgumentNullException(nameof(logError)); - if (logErrorWithMessage == null) - throw new ArgumentNullException(nameof(logErrorWithMessage)); if (exception == null) { @@ -49,11 +43,11 @@ public static void LogErrorOnce( if (string.IsNullOrWhiteSpace(message)) { - logError(exception); + Log.Error(exception); return; } - logErrorWithMessage(exception, message); + Log.Error(exception, message); } /// diff --git a/library/src/Core/Tuio/Components/TuioSender.cs b/library/src/Core/Tuio/Components/TuioSender.cs index d08e6d9f..c44eb689 100644 --- a/library/src/Core/Tuio/Components/TuioSender.cs +++ b/library/src/Core/Tuio/Components/TuioSender.cs @@ -38,16 +38,6 @@ public class TuioSender : ITuioSender private string _serverAddress; private int _serverPort; - private void LogError(Exception exception) - { - Log.Error(exception); - } - - private void LogErrorWithMessage(Exception exception, string message) - { - Log.Error(exception, message); - } - /// /// Specifies whether a valid has been provided and sending is enabled. /// @@ -108,8 +98,7 @@ public virtual async Task SendUdp(OscBundle bundle) } catch (Exception exc) { - LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendUdp), LogError, - LogErrorWithMessage, "Error sending osc message via UDP"); + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendUdp), "Error sending osc message via UDP"); } } @@ -128,8 +117,7 @@ public virtual async Task SendTcp(OscBundle bundle) } catch (Exception exc) { - LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendTcp), LogError, - LogErrorWithMessage); + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendTcp)); } } @@ -160,8 +148,7 @@ public virtual async Task SendWebSocket(OscBundle bundle) } catch (Exception exc) { - LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendWebSocket), - LogError, LogErrorWithMessage); + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendWebSocket)); } } @@ -186,8 +173,7 @@ protected virtual async Task SendOscMessageUdp(OscMessage msg) } catch (Exception exc) { - LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageUdp), - LogError, LogErrorWithMessage); + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageUdp)); } } @@ -213,8 +199,7 @@ protected virtual async Task SendOscMessageTcp(OscMessage msg) } catch (Exception exc) { - LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageTcp), - LogError, LogErrorWithMessage); + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageTcp)); } } @@ -242,8 +227,7 @@ await WsClient.SendAsync(new ArraySegment(mStream.ToArray()), } catch (Exception exc) { - LogUtilities.LogErrorOnce(exc, nameof(TuioSender), - nameof(SendOscMessageWebSocket), LogError, LogErrorWithMessage); + LogUtilities.LogErrorOnce(exc, nameof(TuioSender), nameof(SendOscMessageWebSocket)); } } diff --git a/test/Library/Tuio.Test/LogUtilitiesTest.cs b/test/Library/Tuio.Test/LogUtilitiesTest.cs index 53a1f1dd..2f9f40b7 100644 --- a/test/Library/Tuio.Test/LogUtilitiesTest.cs +++ b/test/Library/Tuio.Test/LogUtilitiesTest.cs @@ -1,4 +1,7 @@ using System; +using NLog; +using NLog.Config; +using NLog.Targets; using NUnit.Framework; using ReFlex.Core.Common.Util; @@ -7,40 +10,61 @@ namespace ReFlex.Core.Tuio.Test [TestFixture] public class LogUtilitiesTest { + private LoggingConfiguration _previousConfiguration; + private MemoryTarget _memoryTarget; + + [SetUp] + public void Setup() + { + // Store current configuration to restore it after each test. + _previousConfiguration = LogManager.Configuration; + + // Capture all log entries in memory so assertions can validate deduplication behavior. + _memoryTarget = new MemoryTarget("LogUtilitiesMemoryTarget") + { + Layout = "${level}|${message}|${exception:format=Type,Message}" + }; + + var config = new LoggingConfiguration(); + config.AddRuleForAllLevels(_memoryTarget); + LogManager.Configuration = config; + LogManager.ReconfigExistingLoggers(); + } + + [TearDown] + public void TearDown() + { + // Ensure static deduplication state does not leak into the next test. + LogUtilities.ClearLoggedErrors(); + + // Restore original logging configuration for the rest of the test suite. + LogManager.Configuration = _previousConfiguration; + LogManager.ReconfigExistingLoggers(); + + _memoryTarget?.Dispose(); + } + [Test] public void TestNoAdditionalLogWhenKeyExists() { // Use a unique source scope so tests stay isolated even with shared static cache. var sourceName = CreateUniqueSourceName(nameof(TestNoAdditionalLogWhenKeyExists)); var methodName = "SendUdp"; - var logCount = 0; - try - { - // First occurrence should be logged. - LogUtilities.LogErrorOnce( - new InvalidOperationException("send failed"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - // Same exception signature in same source/method should be suppressed. - LogUtilities.LogErrorOnce( - new InvalidOperationException("send failed"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - // Exactly one log entry is expected. - Assert.That(logCount, Is.EqualTo(1)); - } - finally - { - // Cleanup test-specific cache entries. - LogUtilities.ClearLoggedErrors(sourceName); - } + // First occurrence should be logged. + LogUtilities.LogErrorOnce( + new InvalidOperationException("send failed"), + sourceName, + methodName); + + // Same exception signature in same source/method should be suppressed. + LogUtilities.LogErrorOnce( + new InvalidOperationException("send failed"), + sourceName, + methodName); + + // Exactly one log entry is expected. + Assert.That(_memoryTarget.Logs.Count, Is.EqualTo(1)); } [Test] @@ -49,39 +73,28 @@ public void TestClearAllowsLoggingAgain() // Unique source keeps this test independent from other runs. var sourceName = CreateUniqueSourceName(nameof(TestClearAllowsLoggingAgain)); var methodName = "SendTcp"; - var logCount = 0; - try - { - // First log call should pass through. - LogUtilities.LogErrorOnce( - new InvalidOperationException("connection failed"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++, - "custom message"); - - // Reset deduplication state for this source. - LogUtilities.ClearLoggedErrors(sourceName); - - // After reset, same error must be logged again. - LogUtilities.LogErrorOnce( - new InvalidOperationException("connection failed"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++, - "custom message"); - - // One log before and one log after clear. - Assert.That(logCount, Is.EqualTo(2)); - } - finally - { - // Cleanup test-specific cache entries. - LogUtilities.ClearLoggedErrors(sourceName); - } + // First log call should pass through. + LogUtilities.LogErrorOnce( + new InvalidOperationException("connection failed"), + sourceName, + methodName, + "custom message"); + + // Reset deduplication state for this source. + LogUtilities.ClearLoggedErrors(sourceName); + + // After reset, same error must be logged again. + LogUtilities.LogErrorOnce( + new InvalidOperationException("connection failed"), + sourceName, + methodName, + "custom message"); + + // One log before and one log after clear. + Assert.That(_memoryTarget.Logs.Count, Is.EqualTo(2)); + Assert.That(_memoryTarget.Logs[0], Does.Contain("custom message")); + Assert.That(_memoryTarget.Logs[1], Does.Contain("custom message")); } [Test] @@ -90,76 +103,51 @@ public void TestDifferentMessagesAndExceptionTypesAreLoggedOnceEach() // Unique source keeps this test independent from other runs. var sourceName = CreateUniqueSourceName(nameof(TestDifferentMessagesAndExceptionTypesAreLoggedOnceEach)); var methodName = "SendOscMessageTcp"; - var logCount = 0; - try - { - // Distinct signatures: message, type and inner-exception chain. - LogUtilities.LogErrorOnce( - new InvalidOperationException("message A"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new InvalidOperationException("message B"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new ArgumentException("message A"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new InvalidOperationException("message A", new Exception("inner A")), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - // Repeat all variants; duplicates should be suppressed. - LogUtilities.LogErrorOnce( - new InvalidOperationException("message A"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new InvalidOperationException("message B"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new ArgumentException("message A"), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new InvalidOperationException("message A", new Exception("inner A")), - sourceName, - methodName, - _ => logCount++, - (_, __) => logCount++); - - // Four unique signatures should produce four logs total. - Assert.That(logCount, Is.EqualTo(4)); - } - finally - { - // Cleanup test-specific cache entries. - LogUtilities.ClearLoggedErrors(sourceName); - } + // Distinct signatures: message, type and inner-exception chain. + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A"), + sourceName, + methodName); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message B"), + sourceName, + methodName); + + LogUtilities.LogErrorOnce( + new ArgumentException("message A"), + sourceName, + methodName); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A", new Exception("inner A")), + sourceName, + methodName); + + // Repeat all variants; duplicates should be suppressed. + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A"), + sourceName, + methodName); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message B"), + sourceName, + methodName); + + LogUtilities.LogErrorOnce( + new ArgumentException("message A"), + sourceName, + methodName); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("message A", new Exception("inner A")), + sourceName, + methodName); + + // Four unique signatures should produce four logs total. + Assert.That(_memoryTarget.Logs.Count, Is.EqualTo(4)); } [Test] @@ -169,50 +157,31 @@ public void TestSameErrorIsLoggedOncePerMethod() var sourceName = CreateUniqueSourceName(nameof(TestSameErrorIsLoggedOncePerMethod)); var firstMethod = "SendTcp"; var secondMethod = "SendOscMessageTcp"; - var logCount = 0; - try - { - // Same error in first method: only first call logs. - LogUtilities.LogErrorOnce( - new InvalidOperationException("same error"), - sourceName, - firstMethod, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new InvalidOperationException("same error"), - sourceName, - firstMethod, - _ => logCount++, - (_, __) => logCount++); - - Assert.That(logCount, Is.EqualTo(1)); - - // Same error in different method should log once again (method is part of the key). - LogUtilities.LogErrorOnce( - new InvalidOperationException("same error"), - sourceName, - secondMethod, - _ => logCount++, - (_, __) => logCount++); - - LogUtilities.LogErrorOnce( - new InvalidOperationException("same error"), - sourceName, - secondMethod, - _ => logCount++, - (_, __) => logCount++); - - // One log per method is expected. - Assert.That(logCount, Is.EqualTo(2)); - } - finally - { - // Cleanup test-specific cache entries. - LogUtilities.ClearLoggedErrors(sourceName); - } + // Same error in first method: only first call logs. + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + firstMethod); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + firstMethod); + + // Same error in different method should log once again (method is part of the key). + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + secondMethod); + + LogUtilities.LogErrorOnce( + new InvalidOperationException("same error"), + sourceName, + secondMethod); + + // One log per method is expected. + Assert.That(_memoryTarget.Logs.Count, Is.EqualTo(2)); } private static string CreateUniqueSourceName(string testName)