Logging
The Coyote runtime provides a Microsoft.Coyote.Logging.ILogger interface for logging so that your
program output can be captured and included in coyote test tool output logs. The installed
ILogger can be accessed by the Logger property on the IActorRuntime interface or the Actor,
StateMachine and Monitor types.
The default implementation of the ILogger writes to the console when setting the --console
option in the coyote tool (or the Configuration.WithConsoleLoggingEnabled() configuration when
using the TestingEngine API).
The Coyote logging infrastructure decides when to log messages using the specified VerbosityLevel
and the individual LogSeverity of messages getting logged with the ILogger.Write and
ILogger.WriteLine methods (by default the LogSeverity of messages is set to LogSeverity.Info).
As long as the LogSeverity is equal or higher than the VerbosityLevel then the message will be
logged. By default, the VerbosityLevel is set to None, which means that no messages are logged,
but this can be easily customized using the --verbosity (or -v) option in the coyote tool (or
the Configuration.WithVerbosityEnabled() configuration when using the TestingEngine API)
Setting --verbosity in the command line will set the VerbosityLevel to VerbosityLevel.Info
which logs all messages with LogSeverity.Info and higher. Other allowed verbosity values are
error, warning, info, debug and exhaustive. For example, choosing --verbosity debug will
log all messages with LogSeverity.Debug and higher.
Installing up a custom logger
You can easily install your own logger by implementing the ILogger interface and replacing the
default logger by setting the Logger property on the IActorRuntime.
The following is an example of a custom ILogger implementation that captures all log output in a
StringBuilder:
using System.Text;
using Microsoft.Coyote.Logging;
class CustomLogger : ILogger
{
private readonly StringBuilder Builder;
private readonly VerbosityLevel VerbosityLevel;
private readonly object Lock;
public MemoryLogger(VerbosityLevel level)
{
this.Builder = new StringBuilder();
this.VerbosityLevel = level;
this.Lock = new object();
}
public void Write(string value) => this.Write(LogSeverity.Info, value);
public void Write(string format, object arg0) =>
this.Write(LogSeverity.Info, format, arg0);
public void Write(string format, object arg0, object arg1) =>
this.Write(LogSeverity.Info, format, arg0, arg1);
public void Write(string format, object arg0, object arg1, object arg2) =>
this.Write(LogSeverity.Info, format, arg0, arg1, arg2);
public void Write(string format, params object[] args) =>
this.Write(LogSeverity.Info, string.Format(format, args));
public void Write(LogSeverity severity, string value)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.Append(value);
}
}
}
public void Write(LogSeverity severity, string format, object arg0)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, arg0);
}
}
}
public void Write(LogSeverity severity, string format, object arg0, object arg1)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, arg0, arg1);
}
}
}
public void Write(LogSeverity severity, string format, object arg0, object arg1, object arg2)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, arg0, arg1, arg2);
}
}
}
public void Write(LogSeverity severity, string format, params object[] args)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, args);
}
}
}
public void WriteLine(string value) => this.WriteLine(LogSeverity.Info, value);
public void WriteLine(string format, object arg0) =>
this.WriteLine(LogSeverity.Info, format, arg0);
public void WriteLine(string format, object arg0, object arg1) =>
this.WriteLine(LogSeverity.Info, format, arg0, arg1);
public void WriteLine(string format, object arg0, object arg1, object arg2) =>
this.WriteLine(LogSeverity.Info, format, arg0, arg1, arg2);
public void WriteLine(string format, params object[] args) =>
this.WriteLine(LogSeverity.Info, string.Format(format, args));
public void WriteLine(LogSeverity severity, string value)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendLine(value);
}
}
}
public void WriteLine(LogSeverity severity, string format, object arg0)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, arg0);
this.Builder.AppendLine();
}
}
}
public void WriteLine(LogSeverity severity, string format, object arg0, object arg1)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, arg0, arg1);
this.Builder.AppendLine();
}
}
}
public void WriteLine(LogSeverity severity, string format, object arg0, object arg1, object arg2)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, arg0, arg1, arg2);
this.Builder.AppendLine();
}
}
}
public void WriteLine(LogSeverity severity, string format, params object[] args)
{
if (LogWriter.IsVerbose(severity, this.VerbosityLevel))
{
lock (this.Lock)
{
this.Builder.AppendFormat(format, args);
this.Builder.AppendLine();
}
}
}
public override string ToString()
{
lock (this.Lock)
{
return this.Builder.ToString();
}
}
public void Dispose()
{
lock (this.Lock)
{
this.Builder.Clear();
}
}
}
To replace the default logger, call the following IActorRuntime method:
runtime.Logger = new CustomLogger();
The above method replaces the previously installed logger with the specified one and returns the previously installed logger.
Note that the old ILogger might be disposable, so if you care about disposing the old logger at
the same time you may need to write this instead:
using (var oldLogger = runtime.Logger)
{
runtime.Logger = new CustomLogger();
}
You could write a custom ILogger to intercept all logging messages and send them to your favorite
logging service in Azure or even over a TCP socket.
You can also use one of the built-in loggers available in the Microsoft.Coyote.Logging namespace,
such as the MemoryLogger which is a thread-safe logger that writes the log in memory (you can
access it as a string by invoking MemoryLogger.ToString()) or the TextWriterLogger which
allows you to run an existing System.IO.TextWriter into an ILogger implementation.
Adding custom actor logging consumers
The IActorRuntime also provides a logging interface called IActorRuntimeLog that allows
consuming Actor and StateMachine activity and processing it in some custom way. When executing
actors, the runtime will call the IActorRuntimeLog interface to log various actions such as a new
Actor or StateMachine getting created or sending an Event to some actor.
You can implement your own IActorRuntimeLog consumer like this:
internal class CustomRuntimeLog : IActorRuntimeLog
{
public void OnCreateActor(ActorId id, ActorId creator)
{
// Add some custom logic.
}
public void OnEnqueueEvent(ActorId id, Event e)
{
// Add some custom logic.
}
// You can optionally override more actor logging methods.
}
You can then register the CustomRuntimeLog using the following IActorRuntime method:
runtime.RegisterLog(new CustomRuntimeLog());
You can register multiple IActorRuntimeLog objects in case you have consumers that are doing very
different things. The runtime will invoke each callback for every registered IActorRuntimeLog.
For example, see the ActorRuntimeLogGraphBuilder class which implements IActorRuntimeLog and
generates a directed graph representing all activities that happened during the execution of your
actors. See activity coverage for an example graph output. The coyote
test tool sets this up for you when you specify --actor-graph or --coverage activity command
line options.
See IActorRuntimeLog API documentation.
Customizing the text formatting when logging actor activities
You can also use the same IActorRuntimeLog feature to customize the text formatting when the
installed ILogger logs actor activity.
The default actor text formatting implementation is provided by the ActorRuntimeLogTextFormatter
base class which implements the IActorRuntimeLog interface and is responsible for formatting all
Actor and StateMachine activity as text and logging it using the installed Logger.
You can add your own subclass of ActorRuntimeLogTextFormatter using the RegisterLog method on
IActorRuntime. However, unlike other IActorRuntimeLog consumers, only a single
ActorRuntimeLogTextFormatter can exist and adding a new one will replace the previous text
formatter.
The following is an example of how to do this:
internal class CustomActorRuntimeLogTextFormatter : ActorRuntimeLogTextFormatter
{
public override void OnCreateActor(ActorId id, ActorId creator)
{
// Override to change the text to be logged.
this.Logger.WriteLine("Hello!");
}
public override void OnEnqueueEvent(ActorId id, Event e)
{
// Override to conditionally hide certain events from the log.
if (!(e is SecretEvent))
{
base.OnEnqueueEvent(id, e);
}
}
// You can optionally override more text formatting methods.
}
You can then replace the default ActorRuntimeLogTextFormatter with your new implementation using
the following IActorRuntime method:
runtime.RegisterLog(new CustomActorRuntimeLogTextFormatter());
The above method replaces the previously installed ActorRuntimeLogTextFormatter with the specified
one.
See ActorRuntimeLogTextFormatter documentation.