Sedona

Logging

Overview

The Sedona Framework includes a built-in logging facility with the following features:

Log Levels

There are five logging severity levels:

Log Definition

The sys::Log class is the primary API used for logging. The Log class is a const class like Type or Slot. This means you can't directly allocate instances yourself. Instead we use the define keyword to define a Log instance:

class MyService
{
  define Log log
}

Like other defines, the log is treated much like a static field. During compilation all the log instances are compiled into the scode as const data much like Kit instances. The APIs for log reflection follow a similar pattern to kit reflection:

class Sys
{
  define int logsLen
  const static Log[logsLen] logs
  static inline byte[logsLen] logLevels

  static Log log(int id)
  static Log findLog(Str qname)

}

const class Log
{
  define int NONE = 0
  define int ERROR = 1
  define int WARNING = 2
  define int MESSAGE = 3
  define int TRACE = 4

  int level()
  bool isError()
  bool isWarning()
  bool isMessage()
  bool isTrace()

  OutStream error(Str msg)
  OutStream warning(Str msg)
  OutStream message(Str msg)
  OutStream trace(Str msg)

  const short id
  const Str qname
}

Note that the logging levels are stored separately from the Log instances themselves. This is because the log objects are readonly and stored in the code section. The log levels must be in dynamic memory to allow runtime modification.

Log Naming

All logs are identified by a qname (qualified name), which is based on the define field's qname:

  1. If the define field is named "log", then the log qname is the qname of the declaring type
  2. If the define field ends in "Log", then the log qname is the field's qname minus the "Log" suffix
  3. If none of the above applies, then log's qname is the field's qname

For example this class in a kit named "acme":

class MyService
{
  define Log log     // log qname is "acme::MyService"
  define Log reqLog  // log qname is "acme::MyService.req"
  define Log stuff   // log qname is "acme::MyService.stuff"
}

By convention, your primary Log definition should be named "log" to match rule 1. Sub-logging for extra tracing should use names with a "Log" suffix.

Logging

To embed logging into your code, call one of following the logging methods:

All of the logging methods take a Str and return an OutStream, which permits string interpolation:

// these lines of code
log.message("Started!")
log.error("Cannot open port=$port")
log.trace("Received $numBytes bytes from $addr")

// would print something like
-- MESSAGE [acme::MyServer] Started!
-- ERROR [acme::MyServer] Cannot open port 8080
-- TRACE [acme::MyServer] Received 5 bytes from 32

A few points to note:

Runtime Configuration

Log levels are stored in the Sys.logLevels field. This array is indexed by Log.id and stores the current level as a number between 0 and 4 (Log.NONE to Log.TRACE):

// change my logging level to trace
Sys.logLevels[log.id] = Log.TRACE

The current level defines the maximum severity that is logged. For example a level or WARNING will log calls to error and warning, but not calls to message and trace. All log levels default to MESSAGE on startup.

The compiler automatically inserts code that jumps over a logging statement if the current log level is set lower. This means the method call and any embedded expressions are efficiently skipped:

// this code
log.trace("This is the ${count++} time")

// is semantically equivalent to
if (log.isTrace())
  log.trace("This is the ${count++} time")

WebService Spy Page

If a Sedona device is running the web::WebService, then you can use the spy page at "<device IP>/spy/logs" to change the log levels for a SVM at runtime.

Compile-time Configuration

TODO: this feature is not implemented yet