» /rd/lib.framework/Statistic/iStat.php

<?

/*
    Read iStat.md - https://github.com/homebase/radaris/blob/master/lib.framework/Statistic/iStat/iStat.md


    Use Statistic_iStat::$SKIP = 1; to suppress iStat.
    E.g. for local servers.

IDEAS:


4. Calc PFL page hits and speed
  calc slow05, slow1, slow2, slow2p, slow2pt (excess time)
  slow05 - number of items faster than 0.5 seconds
  slow1 -  number of items slower than 0.5 seconds, faster than 1

Admin Site @ Local -
  install

*/

/*
    CLI Mode statistics

    Core differences:

        * hit() - accumulates data in memory, every self::$timeout ~30sec flush it
            * flush() saves data instantly - not needed in most cases
        * provide useful stats() method to print accumulated data
        * tracks process data in instance-specific Mongo Collection
        * When UK is provided, disallows parallel execution with same UKs

*/



class Statistic_iStat_Cli extends Statistic_iStat {

    
// @TODO - per-process tracking
    //   Activated with grandStart call
    // @stats call

    // Save Hits timeout
    
public $timeout 30// seconds

    // RUNTIME CONFIG CONFIG
    
public $print_grand true;
    public 
$print_stats true;

    
// temp buffers
    
protected $hit_inc  = [];
    protected 
$hit_data = [];

    
// Process Tracking
    
public $Process;  // Statistic_iStat_Process
    
public $pid;      // Global Process ID

    /*
        UK - unique (within a stat node) process key
        only one process with specific UK can be executed in parallel
    */
    
PUBLIC function grandStart($uk 0) {
        
$this->Process = new Statistic_iStat_Process($this->name$uk);
        
$this->Process->start();
        
$this->pid $this->Process->id;
        
$this->log("grandStart pid:$this->pid");
        if (
$this->print_grand)
            echo 
Console::init("*** $this->name Grand Start pid:$this->pid");
    }

    PUBLIC function 
grandFinish(array $inc=[], array $data=[]) {
        
$this->flush();
        
$this->Process->finish();
        
$this->log("grandFinish pid:pid:$this->pid");
        if (
$this->print_grand) {
            
$this->Process->stats(); // print final stats
            
echo Console::init("*** $this->name Grand Finish pid:$this->pid");
        }
    }

    
// register + cache hits, flush hits, print stats
    
PUBLIC function hit($inc=[], array $data=[]) {
        if (! 
is_array($inc))
            
$inc = [$inc => 1];
        if (! isset(
$inc['hit']))
            
$inc['hit'] = 1;
        foreach (
$inc as $key => $v) {
            
$this->hit_inc[$key] = ($this->hit_inc[$key]??0) + $v;
        }
        
$this->sendMetric($inc);
        
$this->hit_data $data $this->hit_data;
        if (! 
once('iStat-save:'.$this->name$this->timeout))
            return;
        if (
$this->Process && $this->print_stats)
            
$this->Process->stats(); // print stats
        
$this->flush();
    }

    
// save, no empty saves
    
protected function save(array $inc=[], array $data=[]) {
        if (! 
$inc && ! $data)
            return;
        if (
$P $this->Process)
            
$P->save($inc$data);
        
parent::save($inc$data);

    }

    function 
flush() {
        
$this->save($this->hit_inc$this->hit_data);
        
$this->hit_inc = [];
        
$this->hit_data = [];
    }

    function  
__destruct () {
        
$this->flush();
    }
}


/**
    GO-API version of IStat
    Direct API Call every 30 seconds.
*/
class Statistic_iStat_Cli_GoApi extends Statistic_iStat_Cli {

    public 
$timeout 10// seconds - go api can handle any timeouts

    
protected function save(array $inc=[], array $data=[]) {
        if (
$P $this->Process)
            
$P->save($inc$data);
        
$this->goApiSave($inc$data);
    }

}

/*

   Differences from Statistic_iStat

   * Profiler intergation
   * config APC caching

*/
class Statistic_iStat_Web extends Statistic_iStat {

    
// overload basic config
    // we'll cache config in APC
    
function config() { # config_hash
        
$key "iStat:".$this->name;
        
$C apcu_fetch($key);
        if (
$C)
            return 
$C;
        
$C parent::config();
        
apcu_store($key$C60);
        return 
$C;
    }

    
// Medium and Low Frequerency Events ONLY !!
    
PUBLIC function hit($inc=[], array $data=[]) {
        if (! 
$this->start)
            
Profiler::info("iStat/web(".$this->name.")"$inc + ['data' => $data]);
        
parent::hit($inc$data);
    }

    
// FOR Fast Web Processes
    //   APC bases hit() wrapper
    //   saves data once every 30 seconds
    // @params
    //   inc     [inc-key => amount]
    //   data    [any payload]  (to be stored as-is)
    //           @see trackMax/trackMin helpers - track 30-sec best/worst key's values. ex: longest-sql-query
    //   keys    "key1 key2 key3"  << (space delimited) list of all keys that should be saved!!
    //   flush   default(true) - save data to mongo. false - just save data to apc
    //           !! be careful - you must have at least one call with *true* for data to be saved
    // Important
    //   keys `hit`, `time`, `error` are always checked - never provide them in $keys
    //   Hits are cached for up to ONE HOUR with 30seconds save interval
    //    ^^ MEANS = at least ONE two hits in an Hour
    //   make sure event occur at least twice every hour
    //   Up to 30 seconds of data may be lost with php-fpm restart (apc cache lost)
    //   ONLY last $data is saved. examples last-invoice, worst-page
    //   $inc keys PRECISION is 0.001 !!! (apcu_inc only accepts ints)
    //   LIMITATION: you MUST always list ALL node keys you use in apcHit
    
PUBLIC function apcHit(array $inc=[], array $data=[], /* string */ $keys=""$flush true) {
        if (! isset(
$inc['hit']))
            
$inc['hit'] = 1;
        
$K 'AiStat:'.$this->name;
        foreach (
$inc as $k => $v) {
            
$v = (int) (1000 $v);
            
/* does NOT always work
            if (apcu_inc("$K/$k", $v) === false)
                apcu_store("$K/$k", $v, 3600);
            */
            
$c apcu_fetch("$K/$k");
            if (
$c !== false) {
                
# apcu_store("$K/$k", $v+$c, 3600); << race possible
                
apcu_inc("$K/$k"$v);
                
# \Log::text(" - inc $K/$k by $v");
            
} else {
                
apcu_store("$K/$k"$v3600);
                
# \Log::text(" - INIT $K/$k = $v");
            
}
        }
        if (! 
$flush)
            return;
        if (! 
once("once:$K"30))  // commit every 30 seconds
            
return;
        
// saving data
        
$this->apcFlush($keys$data);
    }

    
// You DO NOT need this call on web pages - only tests need it
    // flush APC cache
    
function apcFlush($keys$data=[]) {
        
$K 'AiStat:'.$this->name;
        
$inc = [];
        
$keys qw($keys);
        foreach (
qw("hit time error") as $k) {
            if (! 
in_array($k$keys))   // should be extra cautious, can't have key twice
                
$keys[] = $k;
        }
        foreach (
$keys as $k) {
            
$v apcu_fetch("$K/$k");
            if (
$v) {
                
$inc[$k] = round($v 10004);
                
apcu_dec("$K/$k"$v);
                
# apcu_store("$K/$k", 0, 3600);
            
}
        }
        
# \Log::text(" - FLUSH ".x2s($inc));
        
if ($inc || $data) {
            
$server fm("!^([^.]+)!"gethostname()); // short host name
            
$inc["server.$server"] = (int) @$inc['hit'];
            if (
$time = @$inc['time'])
                
$inc["server-time.$server"] = $time;
            
$this->save($inc$data);
        }
    }


    
// start + finish|error = HIT
    
PUBLIC function start() {
        
Profiler::inp("iStat::start(".$this->name.")", []); // show parent file:line in profiler
        
parent::start();
    }

    
// start -> finish  == HIT
    
PUBLIC function finish(array $inc=[], array $data=[]) {
        
Profiler::out(['inc' => $inc'data' => $data]);
        
parent::finish($inc$data);
    }

    PUBLIC function 
error(array $inc=[], array $data=[]) {
        
Profiler::alert("ERROR", ['inc' => $inc'data' => $data]);
        if (
$this->start) {
            
Profiler::out();
        }
        
parent::error($inc$data);
    }

}

// use I("Stat", "$stat_node_name") to instantiate
/*


    SPECIAL KEYS:

    hit  - number of hits / start+finish calls
    time - time taken by action (calculated automatically by start/stop)
    last   - time of last save
    data   - last data passed to '$set'

*/
class Statistic_iStat {
    public 
$key;       // Mongo Collection KEY
    
public $name;      // NODE Name

    
public /*array*/ $C = []; // Node Config. key => value

    
protected $start 0;        // start -> finish time tracking
    
protected $grand_start 0;  // grandStart -> grandFinish time tracking

    
static $log_first_errors 10// always log $log_first_errors errors

    
public $last_alert_time 0// time of last email sent

    
static $TEST 0// used by spartan test only
    
static $SKIP 0// used to SKIP iStat - Statistic_iStat_Skip() will be returned instead of normal class

    // Used by i(Stat) to find out what class to use
    
static function _class($args) { # classname to instantiate
        #if (! $args['_'])
        #    \Log::alert('stat node name requred');
        
if (! ($args['_'] ?? 0))
            return 
"Statistic_iStat_Base";
        if (
self::$TEST)
            return 
self::$TEST;
        if (
self::$SKIP)
            return new 
Statistic_iStat_Skip();
        if (
PHP_SAPI == 'cli') {
            if (isset(
$args['go-api']))
                return 
"Statistic_iStat_Cli_GoApi";
            return 
"Statistic_iStat_Cli";
        }
        if (isset(
$args['go-api'])) {
            if (
$args['go-api'] == 2)
                return 
"Statistic_iStat_Web_GoApi_Buffered";
            return 
"Statistic_iStat_Web_GoApi";
        }
        return 
"Statistic_iStat_Web";
    }

    
// setup/edit node
    // i('Stat', "my_hourly_node")->setup()
    // i('Stat', "my_hourly_node")->setup(['status' => 'forbidden']) << forbid(disable) node
    // i('Stat', "my_daily_node")->setup(['tp' => 'daily', 'status' => 'inactive'])
    /*
       Supported Parameters:
        tp: hourly (default) / daily / weekly
        status: active (default)/ inactive / forbidden / junk(deleted)
       Throws HB_TypeException if node name is not valid
    */
    
PUBLIC function setup(array $config=[]) { # stat_id

        //Validate node name. Throw exception if name is not valid
        
HB_Type::identifier($this->name);

        
$M M("statistic.istat");
        
$wh = ['name' => $this->name];
        if (! 
$M->one(['name' => $this->name])) {
            if (empty(
$wh['tp']))
                
$wh['tp'] = 'hourly';
            if (empty(
$wh['status']))
                
$wh['status'] = 'active';
            
$M->insert($wh + ['created' => time()]);
        }

        if (
$config) {
            
$M->set($wh$config);
        }
        return 
$M->one(['name' => $this->name]);
    }


    
// hit() - one hit
    // hit(['key1' => value, 'key2' => value]) - one hit + increment keys
    // hit([], ['last_invoice' => value]) - one hit + store value (replace value with the same name)
    
PUBLIC function hit($inc=[], array $data=[]) {
        if (! isset(
$inc['hit']))
            
$inc['hit'] = 1;
        
$this->sendMetric($inc);
        
$this->save($inc$data);
    }

    
// WEB api compatibility placeholder
    // lossy APC based hits for frequent WEB pages events tracking
    // Check Statistic_iStat_Web for actual implementation
    
PUBLIC function apcHit(array $inc=[], array $data=[], /* string */ $keys="") {
        
$this->hit($inc$data);
    }

    
// WEB api compatibility placeholder
    // lossy APC based hits for frequent WEB pages events tracking
    // Check Statistic_iStat_Web for actual implementation
    
function apcFlush($keys$data=[]) {
        
$this->save([], $data);
    }

    
// increase error count
    // does not update hits
    // does not update last
    // logs (static::$log_first_errors=10) first errors, then one error every 10 seconds
    // acts as finish
    
private $_errors=0;
    PUBLIC function 
error(array $inc=[], array $data=[]) {
        
$this->_error($inc$data);
        
// logging
        
$this->_errors++;
        if (
$this->_errors < static::$log_first_errors)
            
$this->log("Error: ".caller().( $data"\n  ".x2s($data) : ""));
        else {
            if (
$cnt once('iStatError:'.$this->name))
                
$this->log("got $cnt errors, last-error: ".caller().( $data"\n  ".x2s($data) : ""));
        }
    }

    
// increase error count
    // does not update hits
    // does not update last
    // acts as finish
    
PUBLIC function _error(array $inc=[], array $data=[]) {
        if (! isset(
$inc['error']))
            
$inc['error'] = 1;
        
$data['last'] = 0// no last update
        
$inc['hit'] = 0;
        
$this->hit($inc$data);
        
$this->start 0;
    }

    
// start + finish|error = HIT
    
PUBLIC function start() {
        
$this->start microtime(1);
    }

    
// start -> finish  == HIT
    
PUBLIC function finish(array $inc=[], array $data=[]) {
        if (! 
$this->start)
            
$this->log('finish w/o start''alert');
        
$this->hit($inc + ['time' => microtime(1) - $this->start], $data);
        
$this->start 0;
    }


    
// Grand Functions
    // mostly indended for CLI, but can be used everywhere
    
PUBLIC function grandStart() {
        
$this->grand_start microtime(1);
    }

    
// grandStart -> hits|start+finish -> grandFinish
    
PUBLIC function grandFinish(array $inc=[], array $data=[]) {
        if (! 
$this->grand_start)
            
$this->log('grandFinish w/o start''alert');
        
$this->save($inc + ['grandFinish' => 1], ['grandTime' => microtime(1) - $this->grand_start] + $data);
        
$this->grand_start 0;
    }

    
// use only when you want to save NOTHING (i.e. just update `last` value)
    // any save with data - always saves
    
function flush() {
        
$this->save(['flush' => 1]);
    }

    
// save data, no empty saves allowed
    // $data[last] overrides last, last=0 - does not update last value
    
protected function save(array $inc=[], array $data=[]) {
        if (! 
$inc && ! $data)
            return;
        
$last self::$TEST 'test' time();
        if (isset(
$data['last'])) {
            
$last $data['last'];
            unset(
$data['last']);
        }
        
Profiler::in_off("iStat", ['name' => $this->name'inc' => $inc'data' => $data]);
        
$n $this->name// key prefix
        
$_set $last ? [ "$n.last" => $last ] : [];
        foreach (
$data as $k => $v)
            
$_set["$n.data.$k"] = $v;
        
$_inc = [];
        foreach (
$inc as $k => $v)
            if (
$v)
                
$_inc["$n.$k"] = $v;
        
$tu $_set ? ['$inc' => $_inc'$set' => $_set] : ['$inc' => $_inc];

        try {
            
$this->M()->update($this->_id(), $tu);
        } catch(
\MongoDB\Driver\Exception\RuntimeException $ex) {
            if (
Debug::is_admin() || once(""60)) {
                
\Log::warning("iSTAT Mongo Save Exception: " $ex->getMessage());
            }
        }
        
Profiler::out();
    }

    
/**
     * Go-Api version of save
     */
    
function goApiSave(array $hits, array $data) {
        
$api_hits $api_data = [];
        foreach (
$hits as $key => $value) {
            
$api_hits[$this->name.".".$key] = $value;
        }
        foreach (
$data as $key => $value) {
            
$api_data[$this->name.".".$key] = $value;
        }
        
// v("goApiSave", $hits, $data);
        
i("go-api.name-server")->istat_hit($api_hits);
        if (
$api_data)
            
i("go-api.name-server")->istat_sdata($api_data);
    }

    
// Track MAX KEY Value
    // trackMax("key", $value, $data=[]) : store
    // trackMax("key") => value          : return value, reset stored value
    // APC is used as a storage, items are stored to 10Hours
    
PUBLIC function trackMax($key$value=null, array $data=[]) { # null | [value, $data]
        
$K 'apc:iStatMax:'.$this->name.":".$key;
        
$c apcu_fetch($K);
        if (
func_num_args() == 1) {
            
$d apcu_fetch($K."d");
            
apcu_delete($K);
            
apcu_delete($K."d");
            return [
$c$d];
        }
        if (
$value <= $c && $c !== false)
            return;
        
apcu_store($K$value36000);
        
apcu_store($K."d"$data36000);
        return;
    }

    
// Track MIN KEY Value
    // trackMin("key", $value, $data=[]) : store
    // trackMin("key") => value          : return value, reset stored value
    // APC is used as a storage, items are stored to 10Hours
    
PUBLIC function trackMin($key$value=null, array $data=[]) { # null | [value, $data]
        
$K 'apc:iStatMin:'.$this->name.":".$key;
        
$c apcu_fetch($K);
        if (
func_num_args() == 1) {
            
$d apcu_fetch($K."d");
            
apcu_delete($K);
            
apcu_delete($K."d");
            return [
$c$d];
        }
        if (
$value >= $c && $c !== false)
            return;
        
apcu_store($K$value36000);
        
apcu_store($K."d"$data36000);
        return;
    }

    
// tp - notice, warn, error, alert
    
function log($message$tp 'notice') {
        if (
$tp == 'notice')
            
$tp 'text';
        
\Log::$tp($this->name.": ".$message"istat");
    }

    
// SEMI-INTERNAL
    // changing stored data

    // get raw data for a given time
    // tp = hourly | daily | weekly
    
function _get($time=0$tp='hourly') {
        
$i i('Stat')->_get($time$tp);
        return (array) @
$i[$this->name];
    }

    
// '$set' - replace ALL measurments for a given time
    
function _set($time$tp, array $data) {
        
$d = [];
        foreach (
$data as $key => $v)
            
$d[$this->name.".".$key] = $v;
        
i('Stat')->_set($time$tp$d);
    }

    
// '$set' - remove measurments for a given time
    // $cnt = how many items to delete. $tp=hourly and $cnt=24 - delete whole day
    // i('Stat','test')->_unset(time(), 'daily', ['a2', 'a3'])
    
function _unset($time$tp, array $series$cnt=1) {
        
$d = [];
        foreach (
$series as $s)
            
$d[] = $this->name.".".$s;
        
i('Stat')->_unset($time$tp$d$cnt);
    }

    
/*


        ###  #     #  #######  #######  ######   #     #     #     #
         #   ##    #     #     #        #     #  ##    #    # #    #
         #   # #   #     #     #        #     #  # #   #   #   #   #
         #   #  #  #     #     #####    ######   #  #  #  #     #  #
         #   #   # #     #     #        #   #    #   # #  #######  #
         #   #    ##     #     #        #    #   #    ##  #     #  #
        ###  #     #     #     #######  #     #  #     #  #     #  #######


    */


    // current hour/day/week stats - ALL NODES
    
function _d($time 0) { # stats_hash
        
if ($time 315360000) { // 10 years
            
$time time() - $time;
        }
        return 
$this->M()->findOne($this->_id($time));
    }

    
// current Node Data : current hour/day/week stats
    
function d($node="") { # current stats_hash
        
$n = (array) @$this->_d()[$this->name];
        if (
$node)
            return @
$n[$node];
        if (
self::$TEST) {
            unset(
$n['server']);
            unset(
$n['server-time']);
        }
        return 
$n;
    }

    
// processed node items
    
function dd($time 0) {
        
$d = (array) $this->_d($time)[$this->name];
        
$node Statistic_iStat_Node::node($this->name);
        
$node->expand($d);
        
$node->derive($d);
        return 
$d;
    }

    
// clean up current stat - ALL NODES ARE DELETED = NEVER CALL !!
    /* debug only */ 
function _reset($id=0) {
        if (! 
$id$id $this->_id();
        if (
PHP_MAJOR_VERSION == || PHP_MAJOR_VERSION == 8){
            
$this->M()->replaceOne(["_id" => $id], ["_id" => $id]);
        }else{
            
$this->M()->update($id, []);
        }
    }


    
// mongo _id for now
    
function _id($time=0$tp='') { # mongo id for given type
        
if (self::$TEST)
            return 
1;
        if (! 
$time)
            
$time time();
        
$tp NVL($tp$this->C['tp']??''); // hourly / daily / weekly
        
switch ($tp) {
            case 
'hourly': return (int) sprintf("%02d%02d"date("d"$time), date("H"$time) );
            case 
'daily': return (int) date("z"$time); // 0..365
            
case 'weekly': return (int) date("W"$time); // 1..53
        
}
        
\Log::alert("unknown type='$tp' for stat node '$this->name'");
    }

    
// use I("Stat", $stat_name) to create
    
function __construct(array $p) {
        
$this->name $p["_"] ?? "";
        
error_if(! $this->name"series name required");
    }

    
// called by M() function
    // cache config in APC, read from Mongo
    // check status
    // throws Exception for disabled processes
    /* protected */ 
function _init() { # null | Exception
        
$this->$this->config();
        if ((
$this->C['status']??"") == 'forbidden')
            throw new 
Exception("$this->name stat node disabled");
    }

    function 
MN() { # Mongo Nodes Collection
        
return M("statistic.istat");
    }

    
// use $this->C instead
    
function config() { # config_hash
        
$r $this->MN()->findOne(['name' => $this->name]);
        if (isset(
$r['charts']))
            
$r['charts'] = str_replace("\r"""$r['charts']); // JS hates \r
        
return $r;
    }

    function 
configSet(array $set) {
        
$this->_configModify(['$set' => $set]);
    }

    
// !!! BE CAREFUL - this is Mongo UPDATE Wrapper - it will wipe your record
    // use '$set' or '$inc'
    // Used to store data in config node
    
function _configModify(array $update) {
        
$db $this->MN()->DB();
        if (
PHP_MAJOR_VERSION == || PHP_MAJOR_VERSION == 8) {
            
$cn $this->MN()->MC()->getCollectionName();
        } else {
            
$cn $this->MN()->MC()->getName();
        }
        
$r  $db->command(['findAndModify' => $cn,
                            
'query'  => ['name' => (string) $this->name],
                            
'update' => $update// ['$inc' => ['val' => $inc]],
                            
'new'    => true
                            
]);

        if (
PHP_MAJOR_VERSION == || PHP_MAJOR_VERSION == 8) {
            
$r iterator_to_array ($r);
            
$r reset($r);
        }

        if (
$r["ok"]==&& $r["value"]!==NULL)
            return 
$r["value"];
        throw new 
Exception("_configModify Failed: ".$r['errmsg']);
        
#if ($r["value"]===NULL || $r["errmsg"]=='No matching object found')
    
}

    function 
Admin() { # iStatAdmin
        
return i('StatAdmin'$this->name);
    }

    
// Collection structure
    // Hourly data stored in Monthly collection "istat_hourly_$ym" "node" => {(int)"DDHH" => data}
    // Daily data stored in Monthly collection "istat_daily_$y"    "node" => {(int) day   => data}
    // Weekly data is stored in collection "istat_weekly_$y"       "node" => {(int) week  => data}

    
function M_ID($time$tp='') { # [MCollection, (int) $id]
        
$time NVL($timetime());
        return [
$this->M(date('ymd'$time), $tp), $this->_id($time$tp)];
    }

    protected 
$M// ymd => Mongo Collection
    // $tp='' = default type
    
function M($ymd=""$tp='') { # Mongo Collection
        
if (! $this->C)
            
$this->_init();
        if (! 
$ymd)
            
$ymd date("ymd");
        
$K "$ymd-$tp";
        if (
$M $this->M[$K]??0)
            return 
$M;
        
// Create
        
$tp NVL($tp$this->C['tp']); // hourly / daily / weekly
        
switch ($tp) {
            case 
'hourly':
                
$p substr($ymd04);  # ONE MONTH
                
break;
            case 
'daily':
            case 
'weekly':
                
$p substr($ymd02);  # ONE YEAR
                
break;
            default:
                
\Log::alert("unknown type='$tp' for stat node '$this->name'");
        }
        if (
self::$TEST)
            
$p 'test';
        
$cname "statistic.istat_".$tp."_".$p;
        
$this->M[$K] = M($cname);
        if (
once("iStat:$tp".$ymd3600)) {
            
// check that table exists, create if needed
            // pre-populate with empty hourly/days/weeks records
            
if (! $this->M[$K]->one(1)) {
                
$m = [$this"m_setup_$tp"];
                
$m($this->M[$K], $ymd);
            }
        }
        return 
$this->M[$K];
    }

    private function 
m_setup_hourly($M$ymd) {
        
$days cal_days_in_month(CAL_GREGORIAN, (int) substr($ymd22), (int) ("20".substr($ymd02)));
        foreach (
Range(1$days) as $day) {
            foreach (
Range(023) as $hour)
                
$M->insert(['_id' => (int) sprintf("%02d%02d"$day$hour)]);
        }
        
$M->insert(['_id' => 1'created' => time()]);
    }

    
// date("z") 0..365(366)
    
private function m_setup_daily($M$ymd) {
        
$days date("z"mktime(0,0,0,12,31,(int) ("20".substr($ymd02)))) + 1;
        foreach (
Range(0$days) as $day)
            
$M->insert(['_id' => $day]);
        
$M->update(1, ['_setup' => time()]);
    }

    
// date("W") 1..53
    
private function m_setup_weekly($M) {
        foreach (
Range(153) as $week)
            
$M->insert(['_id' => $week]);
        
$M->update(1, ['_setup' => time()]);
    }

    protected function 
sendMetric(array $metrics): void
    
{
        foreach (
$metrics as $name => $value) {
            
$metric $this->prepareMetricName($this->name '.' $name);
            if (
$name === 'time') {
                
\Monitoring\Metric\Metrics::histogram($metric$value);
            } else {
                
\Monitoring\Metric\Metrics::inc($metric, [], $value);
            }
        }
    }

    private function 
prepareMetricName(string $metric): string
    
{
        return 
preg_replace(['/(\s|-)+/''/[^a-z0-9_.]+/'], ['_'''], strtolower($metric));
    }
}

/*
    sometime we do not want iStat
    e.g. profile-php command
*/
class Statistic_iStat_Skip {

    PUBLIC function 
setup(array $config=[]) { }

    PUBLIC function 
grandStart($uk 0) {    }

    PUBLIC function 
grandFinish(array $inc=[], array $data=[]) {    }

    
// register + cache hits, flush hits, print stats
    
PUBLIC function hit($inc=[], array $data=[]) {     }

    PUBLIC function 
apcHit(array $inc=[], array $data=[], /* string */ $keys=""$flush true) {    }

    PUBLIC function 
start() {    }

    
// start -> finish  == HIT
    
PUBLIC function finish(array $inc=[], array $data=[]) { }

    function 
save(array $inc=[], array $data=[]) { }

    function 
config() { }

    PUBLIC function 
trackMax($key$value=null, array $data=[]) {   }

    PUBLIC function 
trackMin($key$value=null, array $data=[]) {    }
    function 
dd($time 0) {return [];}
}




/*

   Differences from Statistic_iStat
   Direct/instant communication with IStat go-api implemented in rd-name-server

*/
class Statistic_iStat_Web_GoApi extends Statistic_iStat_Web {

    
// Medium and Low Frequerency Events ONLY !!
    
PUBLIC function hit($inc=[], array $data=[]) {
        if (! 
is_array($inc))
            
$inc = [$inc => 1];
        if (! isset(
$inc['hit']))
            
$inc['hit'] = 1;
        
$this->save($inc$data);
        
$this->sendMetric($inc);
    }

    
// Compatibility ONLY !! - direct api-socket write
    
protected function save(array $inc=[], array $data=[]) {
        
Profiler::info("iStat/api(".$this->name.")"$inc + ($data ? ['data' => $data] : []));
        
$this->goApiSave($inc$data);
    }

    
// Compatibility ONLY !! - direct api-socket write
    
PUBLIC function apcHit(array $inc=[], array $data=[], /* string */ $keys=""$flush true) {

        $this->hit($inc$data);

    }

    
// You DO NOT need this call on web pages - only tests need it
    // flush APC cache
    
function apcFlush($keys$data=[]) {
        
// do not need this anymore
    
}

}

/*

   Differences from Statistic_iStat
   Collect all Istat calls in memory, flushes on scipt end

*/
class Statistic_iStat_Web_GoApi_Buffered extends Statistic_iStat_Web_GoApi {

    static 
$HIT_BUFFER = [];
    static 
$DATA_BUFFER = [];

    
// collect all calls in memory
    
protected function save(array $inc=[], array $data=[]) {
        if (
Profiler::$enabled) {
            
$d = [];
            foreach (
$inc as $k => $v) {
                
$d[$k] = HB::number_format($v);
            }
            if (
$data)
                
$d['<b>data</b>'] = $data;
            
Profiler::info("iStat(".$this->name.")"$d);
        }
        foreach (
$inc as $k => $v) {
            
$idx $this->name.".".$k;
            if (empty(
self::$HIT_BUFFER[$idx])) {
                
self::$HIT_BUFFER[$idx] = $v;
            } else {
                
self::$HIT_BUFFER[$idx] += $v;
            }
        }
        foreach (
$data as $k => $v) {
            
self::$DATA_BUFFER[$this->name.".".$k] = $v;
        }
    }

    function 
__destruct() {
        
$api i("go-api.name-server");
        if (
self::$HIT_BUFFER) {
            
$api->istat_hit(self::$HIT_BUFFER);
            
self::$HIT_BUFFER = [];
        }
        if (
self::$DATA_BUFFER) {
            
$api->istat_sdata(self::$DATA_BUFFER);
            
self::$DATA_BUFFER = [];
        }
    }
}