Skip to content

Instantly share code, notes, and snippets.

@rpivo
Last active November 30, 2020 16:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rpivo/66482e5d00f127e4025afaa628c89674 to your computer and use it in GitHub Desktop.
Save rpivo/66482e5d00f127e4025afaa628c89674 to your computer and use it in GitHub Desktop.
Creating an Object Access Logger With Proxy and Reflect

Creating an Object Access Logger With Proxy and Reflect

You can create an access log for a specific object using Proxy and Reflect, which can keep track of all getting and setting of properties on the object.

Proxy is pretty crucial for this pattern, and although it's not necessary to use Reflect in this specific example, Proxy and Reflect were introduced to ECMAScript together and share many methods that complement each other (Proxy.get/Reflect.get, Proxy.apply/Reflect.apply, Proxy.construct/Reflect.construct, etc).

This is a really powerful pattern, and access logging is just one possible use case.

const obj = new Proxy({
  foo: 'bar',
  baz: () => console.log('hello from baz'),
  log: [],
}, {
  get(target, key) {
    target.log.push(`get ${key}`)

    if (key in target) return Reflect.get(target, key)
    else return 'default value'
  },
  set(target, key, value) {
    target.log.push(`set ${key}: ${value}`)
    return Reflect.set(target, key, value)
  }, 
})

// creates a log entry
console.log(obj.foo)

// creates a log entry
obj.cat = 'meow'

// reading a new property creates a log entry
const copyCat = obj.cat

// creates a log entry
obj.baz()

// creates a log entry
const thing = obj.thing // sets thing to 'default value'

// checking the log also creates a log entry!
console.log(obj.log)

/* at this point, obj.log is:
[
  'get foo',
  'set cat: meow',
  'get cat',
  'get baz',
  'get thing',
  'get log'
]
*/

In the above example, we create an object called obj that's an instance of Proxy. For the Proxy instance's first argument, we can pass in an object with default properties:

{
  foo: 'bar',
  baz: () => console.log('hello from baz'),
  log: [],
}

We give this Proxy instance get() and set() traps that will intercept all getting and setting of properties on the object.

{
  get(target, key) {
    target.log.push(`get ${key}`)
 
    if (key in target) return Reflect.get(target, key)
    else return 'default value'
  },
  set(target, key, value) {
    target.log.push(`set ${key}: ${value}`)
    return Reflect.set(target, key, value)
  }, 
}

Any time a property is read or set on obj, we push a new log to log:

target.log.push(`get ${key}`)

Another interesting thing we can easily implement with Proxy is to provide default values if the property doesn't exist on the object:

    if (key in target) return Reflect.get(target, key)
    else return 'default value'

if (key in target) return target[key] would also work here, but note that Proxy and Reflect share many related methods that complement each other, and this simple example doesn't show how powerful these two objects are together.

Now, any activity that occurs on the object will be logged:

// creates a log entry
console.log(obj.foo)

// creates a log entry
obj.cat = 'meow'

// reading a new property creates a log entry
const copyCat = obj.cat

// creates a log entry
obj.baz()

// creates a log entry
const thing = obj.thing // sets thing to 'default value'

// checking the log also creates a log entry!
console.log(obj.log)

/* at this point, obj.log is:
[
  'get foo',
  'set cat: meow',
  'get cat',
  'get baz',
  'get thing',
  'get log'
]
*/

We can also further distinguish the kinds of access by adding a call log type inside the get trap, and we can also pass the current value of the key into the log (or a default value if it's empty):

  get(target, key) {
    let value = 'empty'
    if (key in target) value = Reflect.get(target, key)
    target.log.push(`${typeof value === 'function' ? 'call' : 'get'} ${key}: ${value}`)
    return value
  },
  
  /* obj.log:
[
  'get foo: bar',
  'set cat: meow',
  'get cat: meow',
  "call baz: () => console.log('hello from baz')",
  'get thing: empty',
  "get log: get foo: bar,set cat: meow,get cat: meow,call baz: () => console.log('hello from baz'),get thing: empty"
]
  */

Note that the full log value is now printed when log is accessed, and the body of functions that were called is also printed, which is probably not wanted, but we can filter these out:

  get(target, key) {
    let value = 'empty'
    if (key in target) value = Reflect.get(target, key)

    const isFunction = typeof value === 'function'
    target.log.push(
      `${isFunction ? 'call' : 'get'} ${key}${isFunction || key === 'log' ? '' : `: ${value}`}`
    )

    return value
  },
  
  /* obj.log:
[
  'get foo: bar',
  'set cat: meow',
  'get cat: meow',
  'call baz',
  'get thing: empty',
  'get log'
]
  */

We can also add a timestamp to each log by adding a createLog function that handles timestamping, and passing logs to this function using Reflect.apply. createLog is an external function here, but it could also be a property on obj, which would keep all our logging logic inside the Proxy:

function createLog(logString) {
  this.log.push(`${new Date().toISOString()} / ${logString}`)
}

const obj = new Proxy({
  foo: 'bar',
  baz: () => console.log('hello from baz'),
  log: [],
}, {
  get(target, key) {
    let value = 'empty'
    if (key in target) value = Reflect.get(target, key)

    const isFunction = typeof value === 'function'
    const log = `${isFunction ? 'call' : 'get'} ${key}${isFunction || key === 'log' ? '' : `: ${value}`}`
    Reflect.apply(createLog, target, [log])

    return value
  },
  set(target, key, value) {
    Reflect.apply(createLog, target, [`set ${key}: ${value}`])
    return Reflect.set(target, key, value)
  }, 
})

/* obj.log:
[
  '2020-11-30T15:34:58.010Z / get foo: bar',
  '2020-11-30T15:34:58.017Z / set cat: meow',
  '2020-11-30T15:34:58.017Z / get cat: meow',
  '2020-11-30T15:34:58.017Z / call baz',
  '2020-11-30T15:34:58.017Z / get thing: empty',
  '2020-11-30T15:34:58.017Z / get log'
]
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment