• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 20822145205

08 Jan 2026 03:30PM UTC coverage: 87.146% (-0.2%) from 87.309%
20822145205

push

github

tyevco
feat(logging): Add comprehensive logging integration across Editor, TestBridge, and MCP

This commit implements a complete logging integration that enables:
- MCP Resources/Tools for AI agents to browse and query logs
- Debug session log capture
- Editor Console with unified query interface
- Ring buffer bounded storage for all use cases

- Add ILogQueryable interface for queryable log providers
- Add LogEntry, LogQuery, and LogStats records
- Add RingBufferLogProvider with bounded storage and query support
- Enhance TestLogProvider with ILogQueryable

- Add ILogController interface and LogControllerImpl
- Add LogEntrySnapshot, LogStatsSnapshot, LogQueryDto for IPC transport
- Add LogCommandHandler for IPC log operations (getCount, query, getStats, getRecent, clear)
- Add RemoteLogController for client-side log access
- Include LogStats in WorldStats for MCP visibility
- Add LogQueryable option to TestBridgeOptions

- Add LogResources: stats, recent, by-level, by-category
- Add LogTools: log_get_stats, log_get_count, log_get_recent, log_get_errors,
  log_get_by_level, log_get_by_category, log_search, log_query, log_clear

- Add LogCapture for capturing logs during debug sessions
- Wire LogCapture into DebugPlugin with configurable options

- Update EditorLogProvider to implement ILogQueryable
- Add Query() and GetStats() methods
- Add LogEntryExtensions for convenience methods
- Wire EditorLogProvider to editor's TestBridge for MCP access
- Delete duplicate LogEntry class (use shared KeenEyes.Logging.LogEntry)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

9196 of 12362 branches covered (74.39%)

Branch coverage included in aggregate %.

556 of 944 new or added lines in 27 files covered. (58.9%)

3 existing lines in 2 files now uncovered.

159050 of 180700 relevant lines covered (88.02%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.99
/src/KeenEyes.Debugging/DebugPlugin.cs
1
using KeenEyes.Capabilities;
2
using KeenEyes.Debugging.Timeline;
3
using KeenEyes.Logging;
4

5
namespace KeenEyes.Debugging;
6

7
/// <summary>
8
/// Plugin that adds debugging and profiling capabilities to a KeenEyes world.
9
/// </summary>
10
/// <remarks>
11
/// <para>
12
/// The DebugPlugin provides comprehensive debugging tools including system profiling,
13
/// entity inspection, memory tracking, and GC allocation monitoring. These tools use
14
/// SystemHooks for automatic integration without modifying individual systems.
15
/// </para>
16
/// <para>
17
/// All debugging features are exposed through extension APIs that can be retrieved
18
/// using <see cref="IWorld.GetExtension{T}"/>. Features can be enabled or disabled
19
/// individually through the configuration options.
20
/// </para>
21
/// </remarks>
22
/// <example>
23
/// <code>
24
/// // Install with default options (all features enabled)
25
/// using var world = new World();
26
/// world.InstallPlugin(new DebugPlugin());
27
///
28
/// // Access debugging tools
29
/// var profiler = world.GetExtension&lt;Profiler&gt;();
30
/// var inspector = world.GetExtension&lt;EntityInspector&gt;();
31
/// var memoryTracker = world.GetExtension&lt;MemoryTracker&gt;();
32
/// var gcTracker = world.GetExtension&lt;GCTracker&gt;();
33
/// var queryProfiler = world.GetExtension&lt;QueryProfiler&gt;();
34
/// var timeline = world.GetExtension&lt;TimelineRecorder&gt;();
35
///
36
/// // Run your simulation
37
/// world.Update(0.016f);
38
///
39
/// // Print profiling report
40
/// foreach (var profile in profiler.GetAllSystemProfiles())
41
/// {
42
///     Console.WriteLine($"{profile.Name}: {profile.AverageTime.TotalMilliseconds:F2}ms");
43
/// }
44
/// </code>
45
/// </example>
46
/// <param name="options">Configuration options for the debug plugin.</param>
47
public sealed class DebugPlugin(DebugOptions? options = null) : IWorldPlugin
1✔
48
{
49
    private readonly DebugOptions options = options ?? new DebugOptions();
1✔
50
    private EventSubscription? profilingHook;
51
    private EventSubscription? gcTrackingHook;
52
    private EventSubscription? timelineHook;
53
    private LogCapture? logCapture;
54

55
    /// <summary>
56
    /// Gets the name of this plugin.
57
    /// </summary>
58
    public string Name => "Debug";
1✔
59

60
    /// <inheritdoc />
61
    public void Install(IPluginContext context)
62
    {
63
        // Install DebugController (always available, provides debug mode toggle)
64
        var debugController = new DebugController(options.InitialDebugMode);
1✔
65
        context.SetExtension(debugController);
1✔
66

67
        // Wire up optional callback for debug mode changes (enables logging integration)
68
        if (options.OnDebugModeChanged is not null)
1✔
69
        {
70
            debugController.DebugModeChanged += (_, isDebug) => options.OnDebugModeChanged(isDebug);
×
71
        }
72

73
        // Install EntityInspector if inspection capability is available (no performance overhead)
74
        if (context.TryGetCapability<IInspectionCapability>(out var inspectionCapability) && inspectionCapability is not null)
1✔
75
        {
76
            context.TryGetCapability<IHierarchyCapability>(out var hierarchyCapability);
1✔
77
            var inspector = new EntityInspector(context.World, inspectionCapability, hierarchyCapability);
1✔
78
            context.SetExtension(inspector);
1✔
79
        }
80

81
        // Get statistics capability for MemoryTracker and QueryProfiler
82
        if (context.TryGetCapability<IStatisticsCapability>(out var statsCapability) && statsCapability is not null)
1✔
83
        {
84
            var memoryTracker = new MemoryTracker(statsCapability);
1✔
85
            context.SetExtension(memoryTracker);
1✔
86

87
            // Install QueryProfiler if enabled
88
            if (options.EnableQueryProfiling)
1✔
89
            {
90
                var queryProfiler = new QueryProfiler(statsCapability);
1✔
91
                context.SetExtension(queryProfiler);
1✔
92
            }
93
        }
94

95
        // Get the system hook capability for profiling, GC tracking, and timeline
96
        ISystemHookCapability? hookCapability = null;
1✔
97
        if ((options.EnableProfiling || options.EnableGCTracking || options.EnableTimeline) && !context.TryGetCapability<ISystemHookCapability>(out hookCapability))
1✔
98
        {
99
            throw new InvalidOperationException(
1✔
100
                "DebugPlugin requires ISystemHookCapability for profiling, GC tracking, or timeline. " +
1✔
101
                "Disable these options or provide a world that supports system hooks.");
1✔
102
        }
103

104
        // Conditionally install profiling
105
        if (options.EnableProfiling && hookCapability is not null)
1✔
106
        {
107
            var profiler = new Profiler();
1✔
108
            context.SetExtension(profiler);
1✔
109

110
            profilingHook = hookCapability.AddSystemHook(
1✔
111
                beforeHook: (system, dt) => profiler.BeginSample(system.GetType().Name),
1✔
112
                afterHook: (system, dt) => profiler.EndSample(system.GetType().Name),
1✔
113
                phase: options.ProfilingPhase
1✔
114
            );
1✔
115
        }
116

117
        // Conditionally install GC tracking
118
        if (options.EnableGCTracking && hookCapability is not null)
1✔
119
        {
120
            var gcTracker = new GCTracker();
1✔
121
            context.SetExtension(gcTracker);
1✔
122

123
            gcTrackingHook = hookCapability.AddSystemHook(
1✔
124
                beforeHook: (system, dt) => gcTracker.BeginTracking(system.GetType().Name),
1✔
125
                afterHook: (system, dt) => gcTracker.EndTracking(system.GetType().Name),
1✔
126
                phase: options.GCTrackingPhase
1✔
127
            );
1✔
128
        }
129

130
        // Conditionally install timeline recording
131
        if (options.EnableTimeline && hookCapability is not null)
1✔
132
        {
133
            var timelineRecorder = new TimelineRecorder(options.TimelineMaxFrames);
1✔
134
            context.SetExtension(timelineRecorder);
1✔
135

136
            timelineHook = hookCapability.AddSystemHook(
1✔
137
                beforeHook: (system, dt) => timelineRecorder.BeginRecording(system.GetType().Name),
1✔
138
                afterHook: (system, dt) => timelineRecorder.EndRecording(system.GetType().Name, dt),
1✔
139
                phase: options.TimelinePhase
1✔
140
            );
1✔
141
        }
142

143
        // Conditionally install log capture
144
        if (options.EnableLogCapture)
1✔
145
        {
NEW
146
            logCapture = new LogCapture(options.LogQueryable, options.LogCaptureMaxEntries);
×
NEW
147
            context.SetExtension(logCapture);
×
148

149
            // Auto-start capture when debug mode is enabled (if configured)
NEW
150
            if (options.AutoStartLogCaptureOnDebugMode)
×
151
            {
NEW
152
                var controller = context.GetExtension<DebugController>();
×
NEW
153
                if (controller is not null)
×
154
                {
NEW
155
                    controller.DebugModeChanged += (_, isDebug) =>
×
NEW
156
                    {
×
NEW
157
                        if (isDebug && logCapture is not null && !logCapture.IsCapturing)
×
NEW
158
                        {
×
NEW
159
                            logCapture.StartCapture();
×
NEW
160
                        }
×
NEW
161
                        else if (!isDebug && logCapture is not null && logCapture.IsCapturing)
×
NEW
162
                        {
×
NEW
163
                            logCapture.StopCapture();
×
NEW
164
                        }
×
NEW
165
                    };
×
166
                }
167
            }
168
        }
169
    }
1✔
170

171
    /// <inheritdoc />
172
    public void Uninstall(IPluginContext context)
173
    {
174
        // Dispose hooks
175
        profilingHook?.Dispose();
1✔
176
        gcTrackingHook?.Dispose();
1✔
177
        timelineHook?.Dispose();
1✔
178

179
        // Remove extensions
180
        context.RemoveExtension<DebugController>();
1✔
181
        context.RemoveExtension<EntityInspector>();
1✔
182
        context.RemoveExtension<MemoryTracker>();
1✔
183

184
        if (options.EnableProfiling)
1✔
185
        {
186
            context.RemoveExtension<Profiler>();
1✔
187
        }
188

189
        if (options.EnableGCTracking)
1✔
190
        {
191
            context.RemoveExtension<GCTracker>();
1✔
192
        }
193

194
        if (options.EnableQueryProfiling)
1✔
195
        {
196
            context.RemoveExtension<QueryProfiler>();
1✔
197
        }
198

199
        if (options.EnableTimeline)
1✔
200
        {
201
            context.RemoveExtension<TimelineRecorder>();
1✔
202
        }
203

204
        if (options.EnableLogCapture)
1✔
205
        {
NEW
206
            logCapture?.Dispose();
×
NEW
207
            context.RemoveExtension<LogCapture>();
×
208
        }
209
    }
1✔
210
}
211

212
/// <summary>
213
/// Configuration options for the debug plugin.
214
/// </summary>
215
/// <remarks>
216
/// These options control which debugging features are enabled and their behavior.
217
/// Features that are disabled have zero performance overhead.
218
/// </remarks>
219
public sealed record DebugOptions
220
{
221
    /// <summary>
222
    /// Gets or initializes the initial debug mode state.
223
    /// </summary>
224
    /// <remarks>
225
    /// <para>
226
    /// When debug mode is enabled, debugging components may perform additional
227
    /// diagnostics, capture more detailed information, or enable verbose logging.
228
    /// </para>
229
    /// <para>
230
    /// The debug mode can be toggled at runtime via <see cref="DebugController"/>.
231
    /// Default is false.
232
    /// </para>
233
    /// </remarks>
234
    public bool InitialDebugMode { get; init; } = false;
1✔
235

236
    /// <summary>
237
    /// Gets or initializes a value indicating whether system profiling is enabled.
238
    /// </summary>
239
    /// <remarks>
240
    /// When enabled, the profiler tracks execution time for all systems and provides
241
    /// detailed timing metrics. Default is true.
242
    /// </remarks>
243
    public bool EnableProfiling { get; init; } = true;
1✔
244

245
    /// <summary>
246
    /// Gets or initializes a value indicating whether GC allocation tracking is enabled.
247
    /// </summary>
248
    /// <remarks>
249
    /// When enabled, tracks memory allocations per system to identify allocation hotspots.
250
    /// This has minimal overhead but may not capture all allocations in multi-threaded scenarios.
251
    /// Default is true.
252
    /// </remarks>
253
    public bool EnableGCTracking { get; init; } = true;
1✔
254

255
    /// <summary>
256
    /// Gets or initializes the phase filter for profiling hooks.
257
    /// </summary>
258
    /// <remarks>
259
    /// If specified, profiling will only track systems in the specified phase.
260
    /// If null, all systems in all phases are profiled. Default is null (profile all phases).
261
    /// </remarks>
262
    public SystemPhase? ProfilingPhase { get; init; } = null;
1✔
263

264
    /// <summary>
265
    /// Gets or initializes the phase filter for GC tracking hooks.
266
    /// </summary>
267
    /// <remarks>
268
    /// If specified, GC tracking will only monitor systems in the specified phase.
269
    /// If null, all systems in all phases are tracked. Default is null (track all phases).
270
    /// </remarks>
271
    public SystemPhase? GCTrackingPhase { get; init; } = null;
1✔
272

273
    /// <summary>
274
    /// Gets or initializes a value indicating whether query profiling is enabled.
275
    /// </summary>
276
    /// <remarks>
277
    /// When enabled, the query profiler provides access to cache statistics and allows
278
    /// manual timing of individual queries. Unlike system profiling, query timing requires
279
    /// manual instrumentation. Default is true.
280
    /// </remarks>
281
    public bool EnableQueryProfiling { get; init; } = true;
1✔
282

283
    /// <summary>
284
    /// Gets or initializes a value indicating whether timeline recording is enabled.
285
    /// </summary>
286
    /// <remarks>
287
    /// When enabled, the timeline recorder captures detailed execution history for all
288
    /// systems, allowing frame-by-frame analysis and export for external visualization.
289
    /// Default is false (due to memory overhead of storing history).
290
    /// </remarks>
291
    public bool EnableTimeline { get; init; } = false;
1✔
292

293
    /// <summary>
294
    /// Gets or initializes the maximum number of frames to keep in timeline history.
295
    /// </summary>
296
    /// <remarks>
297
    /// Older frames are automatically discarded to manage memory usage. Default is 300
298
    /// (approximately 5 seconds at 60fps).
299
    /// </remarks>
300
    public int TimelineMaxFrames { get; init; } = 300;
1✔
301

302
    /// <summary>
303
    /// Gets or initializes the phase filter for timeline recording.
304
    /// </summary>
305
    /// <remarks>
306
    /// If specified, timeline will only record systems in the specified phase.
307
    /// If null, all systems in all phases are recorded. Default is null (record all phases).
308
    /// </remarks>
309
    public SystemPhase? TimelinePhase { get; init; } = null;
1✔
310

311
    /// <summary>
312
    /// Gets or initializes a callback invoked when debug mode changes.
313
    /// </summary>
314
    /// <remarks>
315
    /// <para>
316
    /// Use this callback to integrate with logging systems. The callback receives
317
    /// the new debug mode state (true = debug mode enabled, false = disabled).
318
    /// </para>
319
    /// <para>
320
    /// Example usage with KeenEyes.Logging:
321
    /// <code>
322
    /// var options = new DebugOptions
323
    /// {
324
    ///     OnDebugModeChanged = (isDebug) =>
325
    ///     {
326
    ///         logManager.MinimumLevel = isDebug ? LogLevel.Debug : LogLevel.Info;
327
    ///     }
328
    /// };
329
    /// world.InstallPlugin(new DebugPlugin(options));
330
    /// </code>
331
    /// </para>
332
    /// <para>
333
    /// Default is null (no callback).
334
    /// </para>
335
    /// </remarks>
336
    public Action<bool>? OnDebugModeChanged { get; init; } = null;
1✔
337

338
    /// <summary>
339
    /// Gets or initializes a value indicating whether log capture is enabled.
340
    /// </summary>
341
    /// <remarks>
342
    /// <para>
343
    /// When enabled, the <see cref="LogCapture"/> extension is installed, allowing
344
    /// capture of log entries during debug sessions for later analysis.
345
    /// </para>
346
    /// <para>
347
    /// Default is false. Set <see cref="LogQueryable"/> to provide the log source.
348
    /// </para>
349
    /// </remarks>
350
    public bool EnableLogCapture { get; init; } = false;
1✔
351

352
    /// <summary>
353
    /// Gets or initializes the log queryable provider to capture logs from.
354
    /// </summary>
355
    /// <remarks>
356
    /// Required when <see cref="EnableLogCapture"/> is true. Pass a reference to your
357
    /// RingBufferLogProvider or other <see cref="ILogQueryable"/> implementation.
358
    /// </remarks>
NEW
359
    public ILogQueryable? LogQueryable { get; init; } = null;
×
360

361
    /// <summary>
362
    /// Gets or initializes the maximum number of log entries to capture.
363
    /// </summary>
364
    /// <remarks>
365
    /// Older entries are discarded when this limit is reached. Default is 10,000.
366
    /// </remarks>
367
    public int LogCaptureMaxEntries { get; init; } = 10_000;
1✔
368

369
    /// <summary>
370
    /// Gets or initializes a value indicating whether log capture should automatically
371
    /// start when debug mode is enabled and stop when debug mode is disabled.
372
    /// </summary>
373
    /// <remarks>
374
    /// <para>
375
    /// When true, log capture is tied to the debug mode toggle, making it easy to
376
    /// capture logs during debugging sessions without manual start/stop calls.
377
    /// </para>
378
    /// <para>
379
    /// Default is true. Set to false for manual control over log capture.
380
    /// </para>
381
    /// </remarks>
382
    public bool AutoStartLogCaptureOnDebugMode { get; init; } = true;
1✔
383
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc