# Logging

# Overview

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

- Simple syntax to embed logging in your source code
- Ability to selectively enable/disable logging at runtime
- Ability to selectively compile logging in or out of an scode image

# Log Levels

There are five logging severity levels:

- `Log.NONE`: all logging is disabled
- `Log.ERROR`: indicates a failure condition
- `Log.WARNING`: indicates an unexpected condition
- `Log.MESSAGE`: indicates something of interest
- `Log.TRACE`: used to embed debug tracing

# 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:

- `sys::Log.error`
- `sys::Log.warning`
- `sys::Log.message`
- `sys::Log.trace`

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:

- Don't add a trailing newline to your message, one will be added automatically
- Don't include any severity or log identity in your message string, this information is automatically included (the actual format is implementation specific)
- You are required to call the logging methods on the field define itself. For example you can't assign the log reference to a local variable, then use the local variable to call one of the logging methods - this will result in a compile time error. The reason for this restriction is that it enables the compiler to generate level checking and conditional compilation checking.

# 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