Data Bus and Contracts

The logging framework is designed to be able to serve as a data bus to carry business data, which enables an elegant solution to billing.

However, unlike diagnostic data, business data demands a much higher standard for clarity, reliability and stability of the protocol. The malleability of the logging format fails to meet the requirements.

Where the open format falls short

Explicit contracts

The best practice is to define a class for the domain data, and one module that owns the event name, the encoder, and the decoder:

class SearchCall(vendor: String, queries: Int, vendorCost: Int)

section SearchLog
  def searchedEvent: String = "myagent.tools.search"

  def searched(call: SearchCall): Unit receives logger =
    logger.logFields(searchedEvent, encode(call))

  def encode(call: SearchCall): Map[String, Value] =
    Map:
      "vendor"     ~ call.vendor
      "queries"    ~ call.queries
      "vendorCost" ~ call.vendorCost

  def decode(entry: Entry): SearchCall = ...
end

The encoder and decoder are located in the same file, so they cannot drift apart. Also, the usage site uses the class directly, which is well-documented and any breaking changes would be caught by the compiler.

The point is not ceremony. It is that the contract can now only be broken on purpose.

Contracts in the framework

The framework follows its own rule for the two records something is usually built on: TurnLog, which the transcript is read back from, and Usage, which billing is computed from.

See also