Instrumenting PHP: A Minimalist Approach

I needed a quick way to measure performance and log errors in my Sift Science for WooCommerce plugin. I didn’t want to go back through all my code and embed logging and timing measurement statements, so I considered a more generic and lazy approach.

I decided to create a class that wraps the class I want to measure/monitor. Its constructor takes a class instance, it saves that instance. Then, for every function call to the wrapper class, the function in the underlying class is called and information is logged as needed. Here’s the class from the project:

https://github.com/Fermiac/woocommerce-siftscience/blob/master/includes/class-wc-siftscience-instrumentation.php

How it Works

The most interesting piece of code in this class is here:

public function __call( $name, $args ) {
  $metric = "{$this->prefix}_{$name}";
  $timer = $this->stats->create_timer( $metric );
  $error_timer = $this->stats->create_timer( "error_$metric" );
  try {
    $result = call_user_func_array( array( $this->subject, $name ), $args );
    $this->stats->save_timer( $timer );
    return $result;
  } catch ( Exception $exception ) {
    $this->logger->log_exception( $exception );
    throw $exception;
  }
}

It’s pretty straight forward; I use the PHP magic function __call for all method calls to the class. I initialize timers and then call the function of the class I’m wrapping. If it succeeds or fails I log the information I want and then I pass on the result or exception.

Drawbacks

The main drawback to this approach is that this wrapper class can’t be passed around the same way as the original class. That is, if you’re using PHP type hints, the wrapper class will not implement the same interface as the class it wraps. This also means that IDEs will have trouble auto-completing your code.

To compensate for this, I generally pass the original class to the constructors that need them. I then use the instrumentation class instance to plug into WordPress hooks. You lose a bit of detail in that you only get measurements on the outer surface of your plugin, but overall I’ve found this approach to be satisfactory. You can still calculate the total run time of your plugin and it’s nearly impossible for an exception to be thrown without it getting caught by the wrapper.

Conclusion

I found this approach easy and simple for collecting metrics and useful logs throughout my plugin. I may carve it out and share it as re-usable code one day, but for now the class has dependencies on other classes specific to my project. However, it wouldn’t be hard to copy the class and modify to your specific need.

Leave a Reply