Skip to content

Instantly share code, notes, and snippets.

@jeffjrare
Created June 19, 2012 13:29
Show Gist options
  • Select an option

  • Save jeffjrare/2954179 to your computer and use it in GitHub Desktop.

Select an option

Save jeffjrare/2954179 to your computer and use it in GitHub Desktop.
WIP: Simple denormalized event real time logging
<?php
require_once("../../secret_includes/connect.php");
$s=0;
$e=0;
$startTime = microtime(true);
Dp_Stats_Realtime::LoadSample();
for($i=32800;$i<=34000;$i++){
$somethingDone = Dp_Stats_Realtime::PushStat(new Dp_Habitation($i), 'single_view');
if($somethingDone){
$s++;
}else{
$e++;
}
}
var_dump($s, $e);
echo "<br/><br/>".(microtime(true)-$startTime)." seconds";
//print_nice(Dp_Stats_Realtime::PullStat(new Dp_Habitation(32901), 'single_view'));
/**
* [_NormalizationMapping description]
* @access private
* @return array
*/
private static function _NormalizationMapping()
{
return array(
'hour' => 'H',
'day_hour' => 'd H',
'month_hour' => 'm H',
'month_day_hour' => 'm-d H',
'year_hour' => 'Y H',
'year_month' => 'Y-m H',
'year_month_day_hour' => 'Y-m-d H',
'month' => 'm',
'year' => 'Y',
'year_month' => 'Y-m',
'day' => 'd',
'month_day' => 'm-d',
'year_day' => 'Y-d',
'year_month_day' => 'Y-m-d',
'weekday' => 'N',
'month_weekday' => 'm-N',
'year_weekday' => 'Y-N',
'year_month_weekday' => 'Y-m-N');
}
/**
* [_NormalizationMapping description]
* @access private
* @return array
*/
private static function _NormalizationMapping()
{
return array(
'hour' => 'H',
'day_hour' => 'd H',
'month_hour' => 'm H',
'month_day_hour' => 'm-d H',
'year_hour' => 'Y H',
'year_month' => 'Y-m H',
'year_month_day_hour' => 'Y-m-d H',
'month' => 'm',
'year' => 'Y',
'year_month' => 'Y-m',
'day' => 'd',
'month_day' => 'm-d',
'year_day' => 'Y-d',
'year_month_day' => 'Y-m-d',
'weekday' => 'N',
'month_weekday' => 'm-N',
'year_weekday' => 'Y-N',
'year_month_weekday' => 'Y-m-N');
}
<?php
/**
* [_NormalizationMapping description]
* @access private
* @return array
*/
private static function _NormalizationMapping()
{
return array(
'hour' => 'H',
'day_hour' => 'd H',
'month_hour' => 'm H',
'month_day_hour' => 'm-d H',
'year_hour' => 'Y H',
'year_month' => 'Y-m H',
'year_month_day_hour' => 'Y-m-d H',
'month' => 'm',
'year' => 'Y',
'year_month' => 'Y-m',
'day' => 'd',
'month_day' => 'm-d',
'year_day' => 'Y-d',
'year_month_day' => 'Y-m-d',
'weekday' => 'N',
'month_weekday' => 'm-N',
'year_weekday' => 'Y-N',
'year_month_weekday' => 'Y-m-N');
}
<?php
/**
*
* NoSQL realtime stats collector (Redis | Cassandra)
*
* - Everything static, once loaded, we keep it in RAM
* - Push or pull what you want
* - Organize your stats with event types
* - Everything is stored in denormalized time, easily browseable/pluggeable into a stats grabber (webapp, etc.)
*
* @author jeffgirard
*
*/
class Dp_Stats_Realtime
{
/**
* Parameters
*/
const STORE = 'redis';
const ON_FAILURE_RETRY = 3;
private static $_storeConn;
private static $_environment = 'dev';
private static $_acceptedClasses = array();
private static $_acceptedEvents = array();
private static $_eventsPerClasses = array();
private static $_setupIsChecked=false;
private static $_acceptedStores = array('redis', 'cassandra');
private static $_failureRetry = 0;
public static function LoadSample()
{
self::SetAcceptedClasses(array('Dp_Habitation'));
self::SetAcceptedEvents(array(
'single_view',
'search_result',
'favorited',
'dwl_bg'));
self::SetEventsPerClass('Dp_Habitation', array(
'single_view',
'search_result',
'favorited',
'dwl_bg'));
}
/**
* [_AcceptedClasses description]
* @access private
* @return array
*/
private static function _AcceptedClasses()
{
return self::$_acceptedClasses;
}
/**
* [_EventsPerClasses description]
* @access private
* @param [type] $obj [description]
* @return array
*/
private static function _EventsPerClass($className)
{
return self::$_eventsPerClasses[$className];
}
/**
* [_NormalizationMapping description]
* @access private
* @return array
*/
private static function _NormalizationMapping()
{
return array(
'hour' => 'H',
'day_hour' => 'd H',
'month_hour' => 'm H',
'month_day_hour' => 'm-d H',
'year_hour' => 'Y H',
'year_month' => 'Y-m H',
'year_month_day_hour' => 'Y-m-d H',
'month' => 'm',
'year' => 'Y',
'year_month' => 'Y-m',
'day' => 'd',
'month_day' => 'm-d',
'year_day' => 'Y-d',
'year_month_day' => 'Y-m-d',
'weekday' => 'N',
'month_weekday' => 'm-N',
'year_weekday' => 'Y-N',
'year_month_weekday' => 'Y-m-N');
}
/**
* [_GetKeyNameByClass description]
* @access private
* @param [type] $obj [description]
* @return string
*/
private static function _GetKeyNameByClass($className)
{
return trim(strtolower($className));
}
/**
* [PushStat description]
* @access public
* @param [type] $obj [description]
* @param [type] $event [description]
* @param [type] $customTimestamp=null [description]
* @return bool
*/
public static function PushStat($obj, $event, $customTimestamp=null, $classNameWhenId=null)
{
$somethingDone = false;
$className = $classNameWhenId ? $classNameWhenId : get_class($obj);
if(!$obj || !in_array($className, self::_AcceptedClasses())) throw new Exception("Error Processing Request", 1);
if(!in_array($event, self::_EventsPerClass($className))) throw new Exception("Error Processing Request", 1);
if(is_int($obj)){
$uId = $obj;
}else{
$uId = $obj->getId();
}
self::_SetupCheck();
try{
self::_HandleStoreConnection();
$denormalizedData = self::_GetDenormalized($customTimestamp);
$keyNameObjectPart = self::_GetKeyNameByClass($className);
if($uId){
foreach($denormalizedData as $keyPart => $ref){
if(self::STORE == 'redis'){
$somethingDone = self::_PushStatRedis($keyNameObjectPart, $event, $keyPart, $uId, $ref);
}elseif(self::STORE == 'cassandra'){
$somethingDone = self::_PushStatCassandra($keyNameObjectPart, $event, $keyPart, $uId, $ref);
}
}
}
}catch(Exception $e){
self::_HandleException($e);
}
return $somethingDone;
}
/**
* [PullStat description]
* @access public
* @param [type] $obj [description]
* @param [type] $event [description]
* @return array
*/
public static function PullStat($obj, $event)
{
$arrStats = array();
$className = $classNameWhenId ? $classNameWhenId : get_class($obj);
if(!$obj || !in_array($className, self::_AcceptedClasses())) throw new Exception("Error Processing Request", 1);
if(!in_array($event, self::_EventsPerClass($className))) throw new Exception("Error Processing Request", 1);
self::_SetupCheck();
if(is_int($obj)){
$uId = $obj;
}else{
$uId = $obj->getId();
}
try{
self::_HandleStoreConnection();
$denormalizedData = self::_GetDenormalized($customTimestamp);
$keyNameObjectPart = self::_GetKeyNameByClass($obj);
if($uId){
foreach($denormalizedData as $keyPart => $ref){
if(self::STORE == 'redis'){
$arrStats[$keyPart] = self::_PullStatRedis($keyNameObjectPart, $event, $keyPart, $uId);
}elseif(self::STORE == 'cassandra'){
$arrStats[$keyPart] = self::_PullStatCassandra($keyNameObjectPart, $event, $keyPart, $uId);
}
}
}
}catch(Exception $e){
self::_HandleException($e);
}
return $arrStats;
}
/**
* [_PushStatRedis description]
* @access private
* @param [type] $keyNameObjectPart [description]
* @param [type] $event [description]
* @param [type] $keyPart [description]
* @param [type] $objId [description]
* @param [type] $value [description]
* @return bool
*/
private static function _PushStatRedis($keyNameObjectPart, $event, $keyPart, $objId, $ref)
{
$keyName = $keyNameObjectPart.":{$event}:{$objId}:{$keyPart}";
//self::$_storeConn->del($keyName);
try{
self::$_storeConn->hincrby($keyName, $ref, 1);
}catch(Exception $e){
self::_HandleRedisFailure($e);
self::_PushStatRedis($keyNameObjectPart, $event, $keyPart, $objId, $ref);
}
return true;
}
/**
* [_PullStatRedis description]
* @access private
* @param [type] $keyNameObjectPart [description]
* @param [type] $event [description]
* @param [type] $keyPart [description]
* @param array
*/
private static function _PullStatRedis($keyNameObjectPart, $event, $keyPart, $objId)
{
$keyName = $keyNameObjectPart.":{$event}:{$objId}:{$keyPart}";
try{
return self::$_storeConn->hgetall($keyName);
}catch(Exception $e){
self::_HandleRedisFailure($e);
self::_PullStatRedis($keyNameObjectPart, $event, $keyPart, $objId);
}
}
/**
* [_PushStatCassandra description]
* @access private
* @param [type] $keyNameObjectPart [description]
* @param [type] $event [description]
* @param [type] $keyPart [description]
* @param [type] $objId [description]
* @param [type] $ref [description]
*/
private static function _PushStatCassandra($keyNameObjectPart, $event, $keyPart, $objId, $ref)
{
throw new Exception('Not yet implemented');
}
/**
* [_PullStatCassandra description]
* @access private
* @param [type] $keyNameObjectPart [description]
* @param [type] $event [description]
* @param [type] $keyPart [description]
* @param [type] $objId [description]
*/
private static function _PullStatCassandra($keyNameObjectPart, $event, $keyPart, $objId)
{
throw new Exception('Not yet implemented');
}
/**
* [_GetDenormalized description]
* @access private
* @param [type] $customTimestamp=null [description]
* @return array
*/
private static function _GetDenormalized($customTimestamp=null)
{
if( !$customTimestamp ){
$customTimestamp = time();
}
$arrDenormalized = array();
foreach(self::_NormalizationMapping() as $denormName => $denormFormat){
$arrDenormalized[$denormName] = date($denormFormat, $customTimestamp);
}
return $arrDenormalized;
}
/**
* [SetEnvironment description]
* @access public
* @param string $envName [description]
*/
public static function SetEnvironment($envName)
{
self::$_environment = strtolower(trim($envName));
}
/**
* [SetAcceptedClasses description]
* @access public
* @param [type] $arrClasses [description]
*/
public static function SetAcceptedClasses($arrClasses)
{
self::$_acceptedClasses = $arrClasses;
}
/**
* [AddAcceptedClasses description]
* @access public
* @param [type] $class [description]
*/
public static function AddAcceptedClass($class)
{
self::$_acceptedClasses[] = $class;
}
/**
* [SetAcceptedEvents description]
* @access public
* @param [type] $arrEvents [description]
*/
public static function SetAcceptedEvents($arrEvents)
{
self::$_acceptedEvents = $arrEvents;
}
/**
* [AddAcceptedEvent description]
* @access public
* @param [type] $event [description]
*/
public static function AddAcceptedEvent($event)
{
self::$_acceptedEvents[] = $event;
}
/**
* [SetEventsPerClasses description]
* @access public
* @param Array $arrEventsPerClasses [description]
*/
public static function SetEventsPerClasses(Array $arrEventsPerClasses)
{
self::$_eventsPerClasses = $arrEventsPerClasses;
}
/**
* [SetEventsPerClass description]
* @access public
* @param [type] $className [description]
* @param Array $arrEvents [description]
*/
public static function SetEventsPerClass($className, Array $arrEvents)
{
self::$_eventsPerClasses[$className] = $arrEvents;
}
/**
* [AddEventsPerClass description]
* @access public
* @param [type] $className [description]
* @param [type] $event [description]
*/
public static function AddEventsPerClass($className, $event)
{
self::$_eventsPerClasses[$className][] = $event;
}
/**
* [InDev description]
* @access public
* @return bool
*/
public static function InDev()
{
return self::$_environment == 'dev';
}
/**
* [InProd description]
* @access public
* @return bool
*/
public static function InProd()
{
return !self::InDev();
}
/**
* [_HandleStoreConnection description]
* @access private
* @return bool
*/
private static function _HandleStoreConnection()
{
if(!self::$_storeConn){
include 'partage/library/Predis/Predis.php';
self::$_storeConn = new Predis_Client();
}
return true;
}
private static function _HandleRedisFailure($e)
{
if(self::$_failureRetry <= self::ON_FAILURE_RETRY && !self::$_storeConn || !self::$_storeConn->isConnected()){
self::$_storeConn = new Predis_Client();
self::$_failureRetry++;
}else{
throw $e;
}
}
private static function _HandleCassandraFailure()
{
throw new Exception('Not yet implemented');
}
/**
* [_HandleException description]
* @access private
* @param [type] $e [description]
* @return bool
*/
private static function _HandleException($e)
{
if(self::InDev()){
throw $e;
}else{
// Todo: Log to system exception handler
}
return true;
}
/**
* [_SetupCheck description]
* @access private
*/
private static function _SetupCheck()
{
if( !self::$_setupIsChecked ){
if(!self::$_acceptedEvents || empty(self::$_acceptedEvents)){
throw new Exception('Invalid setup for: acceptedEvents');
}
if(!self::$_acceptedClasses || empty(self::$_acceptedClasses)){
throw new Exception('Invalid setup for: acceptedClasses');
}
if(!self::$_eventsPerClasses || empty(self::$_eventsPerClasses)){
throw new Exception('Invalid setup for: eventsPerClasses');
}
if( !self::_NormalizationMapping() || count(self::_NormalizationMapping()) == 0 ){
throw new Exception('Invalid base configuration for: _NormalizationMapping()');
}
if(!in_array(self::STORE, self::$_acceptedStores)){
throw new Exception('Invalid parameter STORE for: '.(self::STORE ? self::STORE : '<null>'));
}
if(!method_exists(__CLASS__, '_PushStat' . ucfirst(self::STORE))){
throw new Exception('Missing expected method: _PushStat' . ucfirst(self::STORE));
}
if(!method_exists(__CLASS__, '_PullStat' . self::STORE)){
throw new Exception('Missing expected method: _PullStat' . ucfirst(self::STORE));
}
self::$_setupIsChecked = true;
}
}
}
<?php
require_once("../../secret_includes/connect.php");
Dp_Stats_Realtime::LoadSample();
for($i=32800;$i<=34000;$i++){
Dp_Stats_Realtime::PushStat(new Dp_Habitation($i), 'single_view');
}
print_nice(Dp_Stats_Realtime::PullStat(new Dp_Habitation(32900), 'single_view'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment