PepperDashCore
PepperDashCore copied to clipboard
[FEATURE]- Major update to Console Debugging (Discussion encouraged)
Is your feature request related to a problem? Please describe. The current console debugging mechanisms in the Debug static class has reached the limits of it's usefulness. The global debug level setting, especially when set to level 2 results in a quantity of messages that affects usability and readability in a negative way. We are at the point where we need effective ways to selectively enable and disable debug messages on a per device basis.
The idea here so far is that we would add console commands that allow users CRUD "Debug Contexts". Each DebugContext
would contain the information about what devices are part of that context and what debug level each device has. Then debugging can be enabled/disabled for each context.
Describe the solution you'd like
- Enable debugging on individual device basis
- Ability to easily turn on off per device
- Ability to set the level for each device
- Debugging contexts (groups of devices that get their debug settings set collectively)
- Contexts should contain a collection of device keys and each device should be able to have it's own level
- Should a device be able to be part of multiple contexts? Probably. The device itself it told it's highest debugging level and that determines which messages print
- Consider future web app visual interface for debugging via WS API or similar?
Challenges
- Aim to reduce runtime overhead for computation of whether to print statements or not.
- Maintain maximum flexibility while optimizing user friendliness
Implementation
- Create
IDebuggable
interface - Add overloads for Console that match
Debug.Console()
methods - Something like this:
public interface IDebuggable : IKeyed
{
int DebugLevel { get; }
void Console(uint level, string format, params object[] items);
void Console(uint level, IKeyed dev, string format, params object[] items);
void Console(uint level, ErrorLogLevel errorLogLevel, string format, params object[] items);
void ConsoleWithLog(uint level, string format, params object[] items);
void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items);
void LogError(ErrorLogLevel errorLogLevel, string str);
}
- Implement
IDebuggable
onDevice
- Have
IDebuggable
methods call corresponding methods inDebug
static class (maybe via interface extension methods?)
- Have
- Add console methods to Debug class to create/remove contexts and add/remove devices to contexts at specified levels
- The action of adding/removing a device from a context should set the
DebugLevel
value on any device that isIDebuggable
- The action of adding/removing a device from a context should set the
- Serious Console interaction involved
- Create a "pretty" console interaction interface to show current debug context status and what devices belong to what contexts at what level(s)
Here's some example code with a minimal level of abstraction for simplicity. (I wrote this as a normal console application since it's easier to toy with the code when you can test any code changes in less than a few seconds.)
public interface IDebugContext
{
int DebugLevel { get; set; }
bool DebugEnabled { get; set; }
}
public interface IDebugger
{
bool GlobalDebugEnable { get; set; }
int GlobalMaximumDebugLevel { get; set; }
void Write(IDebugContext ctx, int level, string data);
}
public class ConsoleDebugger : IDebugger
{
public bool GlobalDebugEnable { get; set; }
public int GlobalMaximumDebugLevel { get; set; }
public void Write(IDebugContext ctx, int level, string data)
{
if (GlobalDebugEnable && ctx.DebugEnabled && level <= Math.Min(ctx.DebugLevel, GlobalMaximumDebugLevel))
Console.WriteLine("[DEBUG] {0} {1}: {2}", ctx.GetType().Name, level, data);
}
}
public class DebuggableObject : IDebugContext
{
private readonly IDebugger _debugger;
public int DebugLevel { get; set; }
public bool DebugEnabled { get; set; }
public DebuggableObject(IDebugger debugger)
{
_debugger = debugger;
}
public void Test()
{
_debugger.Write(this, 1, "Level 1 debug message");
_debugger.Write(this, 2, "Level 2 debug message");
_debugger.Write(this, 3, "Level 3 debug message");
_debugger.Write(this, 4, "Level 4 debug message");
_debugger.Write(this, 5, "Level 5 debug message");
Console.WriteLine();
}
}
internal class Program
{
private static readonly ConsoleDebugger _consoleDebugger = new ConsoleDebugger {
GlobalMaximumDebugLevel = 4
};
public static void Main()
{
var obj1 = new DebuggableObject(_consoleDebugger) { DebugEnabled = true, DebugLevel = 1 };
var obj2 = new DebuggableObject(_consoleDebugger) { DebugEnabled = true, DebugLevel = 3 };
var obj3 = new DebuggableObject(_consoleDebugger) { DebugEnabled = true, DebugLevel = 5 };
Console.WriteLine("OBJ 1: [Debug Level {0}]", obj1.DebugLevel);
obj1.Test();
Console.WriteLine("OBJ 2: [Debug Level {0}]", obj2.DebugLevel);
obj2.Test();
Console.WriteLine("OBJ 3: [Debug Level {0}]", obj3.DebugLevel);
obj3.Test();
Console.WriteLine("Done...");
Console.ReadKey();
}
}
Here you can associate each DebuggableObject
with its own debug context. You can also have a global debug context set directly within the debugger class itself that takes precedence over any of the more scoped contexts. This way you can control things on a vast or more granular scale, whichever suits the requirements at troubleshooting time.
The benefit to attaching the context as part of the object itself is that you now have direct type information available via reflection if you need it. Reflection is slow though, so doing too much other than retrieving the name of the type may degrade performance at runtime.
This setup can branch out a few different ways... You can introduce a base class to encapsulate shared functionality between debuggers (i.e. the debug context checks), and you can even modify the IDebugContext
interface to hold all the contextual properties within an object for better transferability. For instance, you could encapsulate the context information in an object and have that as a property of the interface, then introduce a few methods that would allow you to take a debug context object and assign that to overwrite or copy it to another object that implements IDebugContext
.
We will be hosting a public Zoom meeting to encourage discussion on this topic on Thursday September 24th at 12p ET/3p PT. Details can be found on the Essentials Discord server.
Thoughts from Zoom discussion (September 24, 2020):
- Add ability to load contexts from config
- Print error log level with console message if specified
- Modify console print prefix to include not only deviceKey, verbosity level and error log level (severity) if specified.
- DebugSettings File:
- Segregate contexts from state
- State should include an object with the currently enabled context keys
- A separate object would contain a collection of all devices with a level > 0
- Implement a global debug expiry timer that gets modified by the last modification of any context
Is there a case to be made for storing the debug settings in a SQLite database instead of as a file on the processor?
@andrew-welker There may be some inherent benefits if the database was structured properly. For instance, now you could have commands or methods that retrieve stored debug messages of a certain criteria (i.e. debug level, messages from a particular context, etc). The SQLite database could still be a file, but in that case you'd probably want to do it in transactions though to reduce database write operations. (It also allows you to fallback.)
Tagging @jkdevito on this issue as I know he's been working on a solution.