1111
1212// ReSharper disable UnusedType.Global
1313
14+ using System . Collections . Concurrent ;
1415using System . Diagnostics ;
1516using System . Runtime . CompilerServices ;
1617
@@ -28,10 +29,23 @@ namespace Core.MemoryLog
2829 /// All logging work and parameter evaluation is skipped at compile time.
2930 /// In Debug builds, calls are routed to <see cref="Backend"/>,
3031 /// which can be swapped at runtime.
32+ /// Some Exceptions though WarningOnce and ErrorOnce are not compiled out, but they will only log once per call site.
3133 /// </para>
3234 /// </summary>
3335 public static class DebugLogger
3436 {
37+
38+ /// <summary>
39+ /// The logged keys
40+ /// </summary>
41+ private static readonly ConcurrentDictionary < string , byte > LoggedKeys = new ( ) ;
42+
43+ /// <summary>
44+ /// Resets the LoggedKeys dictionary, allowing all "Once" log methods to log again.
45+ /// Escape Hatches: This is useful for unit tests that need to verify logging behavior across multiple test cases.
46+ /// </summary>
47+ public static void ResetOnce ( ) => LoggedKeys . Clear ( ) ;
48+
3549 /// <summary>
3650 /// Gets or sets the backend logger instance used for debug logging.
3751 /// <para>
@@ -126,5 +140,113 @@ public static void Debug(
126140 var prefix = $ "{ Path . GetFileName ( file ) } :{ line } { caller } → ";
127141 Backend . LogDebug ( prefix + message , args ) ;
128142 }
143+
144+ /// <summary>
145+ /// Writes a debug-level message **only once** per call site during runtime.
146+ /// </summary>
147+ /// <param name="message">The message.</param>
148+ /// <param name="caller">The caller.</param>
149+ /// <param name="file">The file.</param>
150+ /// <param name="line">The line.</param>
151+ /// <param name="args">The arguments.</param>
152+ [ Conditional ( "DEBUG" ) ]
153+ public static void DebugOnce (
154+ string message ,
155+ [ CallerMemberName ] string caller = "" ,
156+ [ CallerFilePath ] string file = "" ,
157+ [ CallerLineNumber ] int line = 0 ,
158+ params object [ ] args )
159+ {
160+ if ( LoggedKeys . TryAdd ( $ "{ file } :{ line } ", 0 ) )
161+ {
162+ Backend . LogDebug ( message , args ) ;
163+ }
164+ }
165+
166+ /// <summary>
167+ /// Writes a trace-level message **only once** per call site during runtime.
168+ /// </summary>
169+ /// <param name="message">The message.</param>
170+ /// <param name="caller">The caller.</param>
171+ /// <param name="file">The file.</param>
172+ /// <param name="line">The line.</param>
173+ /// <param name="args">The arguments.</param>
174+ [ Conditional ( "DEBUG" ) ]
175+ public static void TraceOnce (
176+ string message ,
177+ [ CallerMemberName ] string caller = "" ,
178+ [ CallerFilePath ] string file = "" ,
179+ [ CallerLineNumber ] int line = 0 ,
180+ params object [ ] args )
181+ {
182+ if ( LoggedKeys . TryAdd ( $ "{ file } :{ line } ", 0 ) )
183+ {
184+ Backend . LogTrace ( message , args ) ;
185+ }
186+ }
187+
188+ /// <summary>
189+ /// Writes an information-level message **only once** per call site during runtime.
190+ /// </summary>
191+ /// <param name="message">The message.</param>
192+ /// <param name="caller">The caller.</param>
193+ /// <param name="file">The file.</param>
194+ /// <param name="line">The line.</param>
195+ /// <param name="args">The arguments.</param>
196+ [ Conditional ( "DEBUG" ) ]
197+ public static void InfoOnce (
198+ string message ,
199+ [ CallerMemberName ] string caller = "" ,
200+ [ CallerFilePath ] string file = "" ,
201+ [ CallerLineNumber ] int line = 0 ,
202+ params object [ ] args )
203+ {
204+ if ( LoggedKeys . TryAdd ( $ "{ file } :{ line } ", 0 ) )
205+ {
206+ Backend . LogInformation ( message , args ) ;
207+ }
208+ }
209+
210+ /// <summary>
211+ /// Writes a warning-level message **only once** per call site during runtime.
212+ /// </summary>
213+ /// <param name="message">The message.</param>
214+ /// <param name="caller">The caller.</param>
215+ /// <param name="file">The file.</param>
216+ /// <param name="line">The line.</param>
217+ /// <param name="args">The arguments.</param>
218+ public static void WarnOnce (
219+ string message ,
220+ [ CallerMemberName ] string caller = "" ,
221+ [ CallerFilePath ] string file = "" ,
222+ [ CallerLineNumber ] int line = 0 ,
223+ params object [ ] args )
224+ {
225+ if ( LoggedKeys . TryAdd ( $ "{ file } :{ line } ", 0 ) )
226+ {
227+ Backend . LogWarning ( message , args ) ;
228+ }
229+ }
230+
231+ /// <summary>
232+ /// Writes an error-level message **only once** per call site during runtime.
233+ /// </summary>
234+ /// <param name="message">The message.</param>
235+ /// <param name="caller">The caller.</param>
236+ /// <param name="file">The file.</param>
237+ /// <param name="line">The line.</param>
238+ /// <param name="args">The arguments.</param>
239+ public static void ErrorOnce (
240+ string message ,
241+ [ CallerMemberName ] string caller = "" ,
242+ [ CallerFilePath ] string file = "" ,
243+ [ CallerLineNumber ] int line = 0 ,
244+ params object [ ] args )
245+ {
246+ if ( LoggedKeys . TryAdd ( $ "{ file } :{ line } ", 0 ) )
247+ {
248+ Backend . LogError ( message , args ) ;
249+ }
250+ }
129251 }
130252}
0 commit comments