oslg

A logger, initially for picky OpenStudio Measure developers who wish to select what gets logged to which target (e.g. OpenStudio runner vs custom JSON files). Yet oslg has no OpenStudio dependency; it can be integrated within any other environment. Just add:

gem "oslg", git: "https://github.com/rd2/oslg", branch: "main"

... in a v2.x bundled development environment "Gemfile" (or instead as a gemspec dependency), and then run:

bundle install (or 'bundle update')

OpenStudio & EnergyPlus

In most cases, critical (and many non-critical) OpenStudio anomalies will be caught by EnergyPlus at the start of a simulation. Standalone applications (e.g. Apply Measures Now) or SDK based iterative solutions can't rely on EnergyPlus to catch such errors - and somehow warn users of potentially invalid results. oslg provides developers a means to log warnings, as well as non-fatal & fatal errors, that may eventually put OpenStudio's (or EnergyPlus') internal processes at risk. Developers are free to decide how to harness oslg as they see fit, e.g. output logged WARNING messages to the OpenStudio runner, while writing out DEBUG messages to a bug report file.

As a Ruby module, one can access oslg by extending a Measure module or class:

module M
  extend OSlg
  ...
end

Ordered oslg levels (or CONSTANTS), from benign to severe:

DEBUG
INFO
WARN
ERROR
FATAL

DEBUG messages aren't benign at all, but are certainly less informative for the typical Measure user.

Initially, oslg sets 2 global internal attributes: level (INFO) and status (< DEBUG). The level attribute is a user-set threshold below which less severe logs (e.g. DEBUG) are ignored. For instance, if level were reset to DEBUG (e.g. M.reset(M::DEBUG)), then all DEBUG messages would also be logged. The status attribute is reset with each new log entry when the latter's log level is more severe than its predecessors (e.g. status == M::FATAL if there is a single log entry registered as FATAL). To check the curent oslg status (true or false):

M.debug?
M.warn?
M.error?
M.fatal?

It's sometimes not a bad idea to rely on a clean slate (e.g. within RSpecs). The following purges all previous logs and resets level (INFO) and status (< DEBUG) - use with caution in production code!

M.clean!

EnergyPlus will run with e.g. out-of-range material or fluid properties, while logging ERROR messages in the process. It remains up to users to decide what to do with simulation results. We recommend something similar with oslg. For instance, we suggest logging as FATAL any error that should halt Measure processes and prevent OpenStudio from launching an EnergyPlus simulation. This could be missing or poorly formatted files.

M.log(M::FATAL, "Missing input JSON file")

Consider logging non-fatal ERROR messages when encountering invalid OpenStudio file entries, i.e. well-defined, yet invalid vis-à-vis EnergyPlus limitations. The invalid object could be simply ignored, while the Measure pursues its (otherwise valid) calculations ... with OpenStudio ultimately launching an EnergyPlus simulation. If a simulation indeed ran (ultimately a go/no-go decision made by the EnergyPlus simulation engine), it would be up to users to decide if simulation results were valid or useful, given the context - maybe based on oslg logged messages. In short, non-fatal ERROR logs should ideally point to bad input (that users can fix).

M.log(M::ERROR, "Measure won't process MASSLESS materials")

A WARNING could be triggered from inherit limitations of the underlying Measure scope or methodology (something users may have little knowledge of beforehand). For instance, surfaces the size of dinner plates are often artifacts of poor 3D modelling. It's usually not a good idea to have such small surfaces in an OpenStudio model, but neither OpenStudio nor EnergyPlus will necessarily warn users of such occurrences. It's up to users to decide on the suitable course of action.

M.log(M::WARN, "Surface area < 100cm2")

There's also the possibility of logging INFO-rmative messages for users, e.g. the final state of a Measure variable before exiting.

M.log(M::INFO, "Envelope compliant to prescriptive code requirements")

Finally, a number of sanity checks are likely warranted to ensure Ruby doesn't crash (e.g., invalid access to uninitialized variables), especially for lower-level functions. We suggest implementing safe fallbacks when this occurs, but DEBUG errors could nonetheless be logged to signal buggy code.

M.log(M::DEBUG, "Hash? expecting Array (method)")

All log entries are stored in a single Ruby Array, with each individual log entry as a Ruby Hash with 2 keys :level and :message, e.g.:

M.logs.each do |log|
  puts "Uh-oh: #{log[:message]}" if log[:level] > M::INFO
end

These logs can be first mapped to other structures (then edited), depending on output targets.

Preset log templates

Typically, developers would first catch bad input, log an error message and possibly exit by returning an object (e.g. false, nil), such as:

unless var.is_a?(Array)
  M.log(M::DEBUG, "#{var.class}? expecting Array (method)")
  return false
end

The following are oslg one-liner methods that log & return in one go. These are for some of the most common checks OpenStudio SDK Ruby developers are likely to need. The methods require valid arguments for oslg to actually log. Although often expecting either strings or integers as arguments, the methods will try to convert other types to strings (e.g. classes, numbers, even entire arrays) or integers if possible.


invalid: for logging e.g. nilled objects or out-of-scope variables:

return M.invalid("area", "sum", 0, M::FATAL, false) if area > 1000000

This logs a FATAL error message informing users that an out-of-scope argument, 'area', was caught while running method 'sum', and then exits by returning false. The logged message would be:

"Invalid 'area' (sum)"

The 3rd parameter (e.g. 0) is ignored unless > 0 - a useful option when asserting method arguments:

def sum(areas, units)
  return M.invalid("areas", "sum", 1) unless areas.respond_to?(:to_f)
  return M.invalid("units", "sum", 2) unless units.respond_to?(:to_s)
  ...
end

... would generate the following if both areas and units arguments were, for instance, nilled:

"Invalid 'areas' arg #1 (sum)"
"Invalid 'units' arg #2 (sum)"

The first 2 invalid method parameters (faulty object ID, calling method ID) are required. The remaining 3 parameters are optional; in such cases, invalid level defaults to DEBUG, and invalid returns nil.


mismatch: for logging incompatible instances vs classes:

return M.mismatch("area", area, Float, "sum") unless area.is_a?(Numeric)

If 'area' were for example a String, mismatch would generate the following DEBUG log message (before returning nil):

"'area' String? expecting Float (sum)"

These 4 mismatch parameters are required (an object ID, a valid Ruby object, the mismatched Ruby class, and the calling method ID). As a safeguard, oslg will NOT log a mismatch if the object is an actual instance of the class. As with invalid, there are 2 optional terminal parameters (e.g. M::FATAL, false).


hashkey: for logging missing Hash keys:

return M.hashkey("floor area", floor, :area, "sum") unless floor.key?(:area)

If the Hash floor does not hold :area as one of its keys, then hashkey would generate the following DEBUG log message (before returning nil):

"Missing 'area' key in 'floor' Hash (sum)"

Similar to mismatch, the method hashkey requires 4 parameters (a Hash ID, a valid Ruby Hash, the missing key, and the calling method ID). There are also 2 optional terminal parameters (e.g. M::FATAL, false).


empty: for logging empty Enumerable (e.g. Array, Hash) instances or uninitialized Boost optionals (e.g. an uninitialized ThermalZone object of an OpenStudio Space):

return M.empty("zone", "conditioned?", M::FATAL, false) if space.thermalZone.empty?

An empty (i.e. uninitialized) thermalZone would generate the following FATAL error log message (before returning false):

"Empty 'zone' (conditioned?)"

Again, the first 2 parameters are required; the last 2 are optional.


zero: for logging zero'ed (or nearly-zero'ed) values:

M.zero("floor area", "sum", M::FATAL, false) if floor[:area].abs < TOL

... generating the following FATAL error log message (before returning false):

"Zero 'floor area' (sum)"

And again, the first 2 parameters are required; the last 2 are optional.


negative: for logging negative (< 0) values:

M.negative("floor area", "sum", M::FATAL, false) if floor[:area] < 0

... generating this FATAL log message (before returning false):

"Negative 'floor area' (sum)"

You guessed it: the first 2 parameters are required; the last 2 as optionals.


Look up the full oslg API here.