Realtime In-Place Logger

#codesnippet #javascript

Useful for analysis of data that's changing in real time. Prints properties of interest in-place to the terminal, preventing the need to keep up with quickly scrolling log lines.

See also: the same code in Python.

const clearLines = (lines) => {
  for (let i = 0; i < lines; i++) {
    process.stdout.write('\x1b[2K') // Clear the current line
    process.stdout.write('\x1b[A') // Move cursor up one line
  }
  process.stdout.write('\x1b[2K') // Clear the previous line
}

class Singleton {
  constructor() {
    if (this.constructor.instance) {
      return this.constructor.instance
    }
    this.constructor.instance = this
  }
}

class RealtimeLogger extends Singleton {
  constructor(numProperties) {
    super()
    if (!this._data) {
      this.numProperties = numProperties
      this.hasPrinted = false
      this._data = {}
    }
  }

  get(key) {
    return this._data[key]
  }

  set(key, value) {
    this._data[key] = value
  }

  print() {
    const keys = Object.keys(this._data)
    if (keys.length !== this.numProperties) {
      console.warn(`Warning: Set num_properties to ${keys.length}`)
    }
    if (this.hasPrinted) {
      clearLines(this.numProperties)
    }

    for (let i = 0; i < this.numProperties; i++) {
      const k = keys[i]
      console.log(`${k}: ${this._data[k]}`)
    }

    this.hasPrinted = true
  }
}

Example

const viz = new RealtimeLogger(2)
viz.set('my variable', 42)
viz.set('Something else', 50)
viz.print()

for (let i = 0; i < 6; i++) {
  viz.set('my variable', i)
  viz.print()
}

Prints the following. Notice that, despite the fact that print() is invoked 6 times, there are only two log lines. This is because viz only has 2 properties, and they're printed in-place upon each invocation.

my variable: 5
Something else: 50