<?
/*
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, $C, 60);
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", $v, 3600);
# \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 / 1000, 4);
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, $value, 36000);
apcu_store($K."d", $data, 36000);
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, $value, 36000);
apcu_store($K."d", $data, 36000);
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 == 7 || 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->C = $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 == 7 || 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 == 7 || PHP_MAJOR_VERSION == 8) {
$r = iterator_to_array ($r);
$r = reset($r);
}
if ($r["ok"]==1 && $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($time, time());
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($ymd, 0, 4); # ONE MONTH
break;
case 'daily':
case 'weekly':
$p = substr($ymd, 0, 2); # 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".$ymd, 3600)) {
// 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($ymd, 2, 2), (int) ("20".substr($ymd, 0, 2)));
foreach (Range(1, $days) as $day) {
foreach (Range(0, 23) 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($ymd, 0, 2)))) + 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(1, 53) 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) {
}
// 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 = [];
}
}
}