Created
June 19, 2012 13:29
-
-
Save jeffjrare/2954179 to your computer and use it in GitHub Desktop.
WIP: Simple denormalized event real time logging
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * [_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'); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * [_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'); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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'); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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