Skip to content

Instantly share code, notes, and snippets.

@rmarscher
Created October 8, 2012 05:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rmarscher/3850852 to your computer and use it in GitHub Desktop.
Save rmarscher/3850852 to your computer and use it in GitHub Desktop.
MongoDB logging in Lithium PHP
<?php
namespace li3_common\analysis;
use lithium\action\Request;
use lithium\console\Request as ConsoleRequest;
use lithium\core\Environment;
use lithium\data\Connections;
use lithium\util\String;
use MongoDate;
use MongoId;
/**
* A class to enable logging of Mongo database commands
*/
class MongoLogger extends \lithium\core\StaticObject {
/**
* A cache of MongoDb source filters that have been set
*
* @var array
*/
protected static $_filters = array();
/**
* A list of database connections to log
*
* @var array
*/
protected static $_connections = array('default');
/**
* The `lithium\analysis\Logger` priority
*
* @var string
*/
protected static $_priority = 'debug';
/**
* Configurable classes to use for logging and MongoDb command exporting
*
* @var array
*/
protected static $_classes = array(
'logger' => 'lithium\analysis\Logger',
'exporter' => 'lithium\data\source\mongo_db\Exporter'
);
/**
* `String::insert`-style log formats
*
* Note: default formats can be found in the code inside
* `MongoLogger::config()` because they were too long to
* fit on one line here without breaking the coding standards.
*
* @see affin_common\analysis\MongoLogger::config()
* @var array
*/
protected static $_formats;
/**
* A reference to a `Request` object so we can prefix our logs
* with the url or command that the log is for
*
* @param object
*/
protected static $_request;
/**
* Configures the logger
*
* @param array $config Valid keys include:
* - `'classes'`: sets the `Logger` and `Exporter` classes
* - `'formats'`: updates the `String::insert()` style log formats
* - `'connections'`: an array of connection names to log
* - `'priority'`: Where to write logs. Defaults to `'debug'`
* - `'request'`: The request object. Defaults to null.
* @return boolean
*/
public static function config($config = array()) {
$defaultFormats = array(
"read" => "{:prefix} - {:dbConn}: db.{:collection}.find({:conditions}, " .
"{:fields}).sort({:sort}).limit({:limit}).skip({:skip})",
"update" => "{:prefix} - {:dbConn}: db.{:collection}.update({:conditions}, " .
"{:update}, {:upsert}, {:multiple})",
"delete" => "{:prefix} - {:dbConn}: db.{:collection}.remove({:conditions}, " .
"{:options})"
);
$defaults = array(
'classes' => static::$_classes,
'connections' => static::$_connections,
'formats' => static::$_formats ?: $defaultFormats,
'priority' => static::$_priority,
'request' => static::$_request
);
$config += $defaults;
foreach ($config as $property => $value) {
$property = "_" . $property;
static::$$property = $value;
}
return true;
}
/**
* Converts php mongo classes to their equivalent
* in native mongo javascript for use in the
* mongo shell
*
* @param mixed $value
* @return string
*/
public static function phpToMongoNative($value) {
$js = $value;
switch (true) {
case $value instanceof MongoDate:
$sec = $value->sec;
$js = 'ISODate("' . gmdate('Y-m-d\TH:i:s\Z', $sec) . '")';
break;
case $value instanceof MongoId:
$js = 'ObjectId("' . $value->{'$id'} . '")';
break;
}
return $js;
}
/**
* Removes quotes from native javascript objects that
* were escaped by json_encode
*
* @param string $json
* @return string
*/
public static function removeExtraQuotes($json) {
return preg_replace(
array('/"ISODate\(\\\"(.*)\\\"\)"/mU', '/\"ObjectId\(\\\"(.*)\\\"\)\"/mU'),
array('ISODate("$1")', 'ObjectId("$1")'),
$json
);
}
/**
* Uses the request to create a prefix to append to the beginning of the log lines.
*
* Either the request url or the li3 command
*
* @return string
*/
public static function prefix() {
$prefix = '';
switch (true) {
case !isset(static::$_request):
$prefix = '';
break;
case static::$_request instanceof \lithium\action\Request:
$prefix = static::$_request->url;
break;
case static::$_request instanceof \lithium\console\Request:
$prefix = "li3 " . implode(' ', static::$_request->argv);
break;
}
return $prefix;
}
/**
* Returns whether or not the logged has been enabled
*
* @todo is it possible to get a reference to those connection filters
* so we have the ability to turn off logging after they have been enabled?
*
* @return boolean
*/
public static function enabled() {
return !empty(static::$_filters);
}
/**
* Caches logging filters so we know which have been set so we
* have the option to disable them later
*
* @param string $connection Configured connection name
* @param string $method
* @param callback $filter
* @return boolean
*/
protected static function _cacheFilter($connection, $method, &$filter) {
if (!isset(static::$_filters[$connection])) {
static::$_filters[$connection] = array();
}
static::$_filters[$connection][$method] = &$filter;
return true;
}
/**
* Enables logging on the configured connections
*
* @see affin_common\analysis\MongoLogger::config()
* @param array $config The same options accepted by `MongoLogger::config()`
* @return boolean `true` on success, `false` if there was a problem
*/
public static function enable($config = array()) {
if (!empty($config)) {
static::config($config);
}
$connections = static::$_connections;
$classes = static::$_classes;
$formats = static::$_formats;
$priority = static::$_priority;
$logger = get_called_class();
$toMongoNative = function(&$value, $key) use ($logger) {
$value = $logger::phpToMongoNative($value);
};
$removeExtraQuotes = function($json) use ($logger) {
return $logger::removeExtraQuotes($json);
};
$prefix = static::prefix();
$use = compact(
'classes', 'formats', 'priority',
'prefix', 'toMongoNative', 'removeExtraQuotes'
);
foreach ($connections as $name) {
$conn = Connections::get($name);
$filter = function($self, $params, $chain) use ($use) {
extract($use);
$logger = $classes['logger'];
$query = $params['query'];
$options = $params['options'];
$args = $query->export($self);
$source = $args['source'];
$conditions = array();
if (isset($args['conditions'])) {
$conditions = $args['conditions'];
array_walk_recursive($conditions, $toMongoNative);
}
$logger::write(
$priority,
String::insert($formats['read'], array(
'prefix' => $prefix,
'dbConn' => $self->connection->$source,
'collection' => $source,
'conditions' => $removeExtraQuotes(json_encode((object) $conditions)),
'fields' => json_encode((object) $args['fields']),
'sort' => json_encode((object) $args['order']),
'limit' => (int) $args['limit'],
'skip' => (int) $args['offset']
))
);
return $chain->next($self, $params, $chain);
};
$conn->applyFilter('read', $filter);
static::_cacheFilter($name, 'read', $filter);
$filter = function($self, $params, $chain) use ($use) {
extract($use);
$logger = $classes['logger'];
$options = $params['options'];
$query = $params['query'];
$args = $query->export($self, array('keys' => array('conditions', 'source', 'data')));
$source = $args['source'];
$data = $args['data'];
$_exp = $classes['exporter'];
if ($query->entity()) {
$data = $_exp::get('update', $data);
}
$update = $query->entity() ? $_exp::toCommand($data) : $data;
$conditions = array();
if (isset($args['conditions'])) {
$conditions = $args['conditions'];
array_walk_recursive($conditions, $toMongoNative);
}
array_walk_recursive($update, $toMongoNative);
if ($options['multiple'] && !preg_grep('/^\$/', array_keys($update))) {
$update = array('$set' => $update);
}
$logger::write(
$priority,
String::insert($formats['update'], array(
'prefix' => $prefix,
'dbConn' => $self->connection->$source,
'collection' => $source,
'conditions' => $removeExtraQuotes(json_encode((object) $conditions)),
'update' => $removeExtraQuotes(json_encode((object) $update)),
'upsert' => (int) ((boolean) $options['upsert']),
'multiple' => (int) ((boolean) $options['multiple'])
))
);
return $chain->next($self, $params, $chain);
};
$conn->applyFilter('update', $filter);
static::_cacheFilter($name, 'read', $filter);
$filter = function($self, $params, $chain) use ($use) {
extract($use);
$logger = $classes['logger'];
$query = $params['query'];
$options = $params['options'];
$args = $query->export($self, array('keys' => array('source', 'conditions')));
$source = $args['source'];
$conditions = array();
if (isset($args['conditions'])) {
$conditions = $args['conditions'];
array_walk_recursive($conditions, $toMongoNative);
}
$logger::write(
$priority,
String::insert($formats['delete'], array(
'prefix' => $prefix,
'dbConn' => $self->connection->$source,
'collection' => $source,
'conditions' => $removeExtraQuotes(json_encode((object) $conditions)),
'options' => json_encode((object) $options)
))
);
return $chain->next($self, $params, $chain);
};
$conn->applyFilter('delete', $filter);
static::_cacheFilter($name, 'delete', $filter);
}
return true;
}
/**
* I'd love to be able to disable the logging mid-request, but it's just
* not possible unless we patch `lithium\core\Object` with a
* `removeFilter` method. Maybe I'll do that...
*
* @param array $connections (optional) A list of connections to remove
* the logging filters from. If omitted, removes from all connections
* @return boolean `true` on success, `false` if there was a problem
*/
public static function disable() {
foreach (static::_filters as $name => $filters) {
$conn = Connections::get($name);
foreach ($filters as $method => $filter) {
// $conn->removeFilter($method, $filter);
}
}
return false;
}
/**
* Retrieves a logger function suitable for use as a filter
* for `lithium\action\Dispatcher` and `lithium\console\Dispatcher`
*
* It makes sure that the `Environment` has been configured and if
* not and the `Request` is present in `$params`, it uses the `Request`
* object to set the `Environment`. This is necessary to make sure
* `Connections::get()` won't cause an error.
*
* @see affin_common\analysis\MongoLogger::enable()
* @see affin_common\analysis\MongoLogger::config()
* @param array $config The same options accepted by `MongoLogger::config()`
* @return callback
*/
public static function filter($config = array()) {
$logger = get_called_class();
$use = compact('config', 'logger');
return function($self, $params, $chain) use ($use) {
extract($use);
$prefix = '';
if (isset($params['request'])) {
$environment = Environment::get();
if ($environment == '') {
Environment::set($params['request']);
}
$config['request'] = $params['request'];
}
$logger::enable($config);
return $chain->next($self, $params, $chain);
};
}
}
?>
<?php
// usage:
use affin_common\analysis\MongoLogger;
use lithium\analysis\Logger;
Logger::config(array(
'default' => array('adapter' => 'File')
));
$dbLogger = MongoLogger::filter();
Filters::apply('lithium\action\Dispatcher', 'run', $dbLogger);
Filters::apply('lithium\console\Dispatcher', 'run', $dbLogger);
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment