1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 
<?php declare(strict_types=1);

    use Daphnie\Collector;

    /**
     * Copyright (C) Apis Networks, Inc - All Rights Reserved.
     *
     * Unauthorized copying of this file, via any medium, is
     * strictly prohibited without consent. Any dissemination of
     * material herein is prohibited.
     *
     * For licensing inquiries email <licensing@apisnetworks.com>
     *
     * Written by Matt Saladna <matt@apisnetworks.com>, May 2017
     */

    class Telemetry_Module extends Module_Skeleton
    {
        // @var Collector
        private $collector;

        public function __construct()
        {
            parent::__construct();
            $this->exportedFunctions = [
                '*'        => PRIVILEGE_ADMIN,
                'get'      => PRIVILEGE_ADMIN|PRIVILEGE_SITE,
                'range'    => PRIVILEGE_SITE|PRIVILEGE_ADMIN,
                'has'      => PRIVILEGE_SITE|PRIVILEGE_ADMIN
            ];
            if (!TELEMETRY_ENABLED) {
                $this->exportedFunctions = ['*' => PRIVILEGE_NONE];
            }
        }

        /**
         * Get latest metric value
         *
         * Performs a partial scan in last 12 hours
         *
         * @param string|array $metric metric name (dot notation)
         * @param int|null $site_id
         * @return int|int[]|null
         */
        public function get($metric, int $site_id = null)
        {
            if ($this->permission_level & (PRIVILEGE_USER | PRIVILEGE_SITE)) {
                if ($site_id && $site_id !== $this->site_id) {
                    error('Cannot specify site ID');
                    return null;
                }
                $site_id = $this->site_id;
            }

            return $this->getCollector()->get($metric, $site_id);
        }

        /**
         * Metric exists
         *
         * @param string $metric
         * @return bool
         */
        public function has(string $metric): bool
        {
            return null !== \Daphnie\MetricBroker::resolve($metric);
        }

        /**
         * Get metric range
         *
         * @param          $metric
         * @param int      $begin  when negative, now minus $begin
         * @param int|null $end
         * @param int|null $site_id
         * @param string|bool $summable sum (bool) or interval ranges to sum as (string)
         * @return int[]|int|null
         */
        public function range($metric, int $begin, ?int $end = null, int $site_id = null, $summable = true)
        {
            if ($this->permission_level & (PRIVILEGE_USER | PRIVILEGE_SITE)) {
                if ($site_id && $site_id !== $this->site_id) {
                    error('Cannot specify site ID');

                    return null;
                }
                $site_id = $this->site_id;
            }

            return $this->getCollector()->range($metric, $begin, $end, $site_id, $summable);

        }

        /**
         * Get metric histogram
         *
         * @param          $metric
         * @param array    $bins
         * @param int      $begin
         * @param int|null $end
         * @param int|null $site_id
         * @return int[]|int|null
         */
        public function histogram($metric, array $bins, int $begin, ?int $end = null, int $site_id = null)
        {
            if ($this->permission_level & (PRIVILEGE_USER | PRIVILEGE_SITE)) {
                if ($site_id && $site_id !== $this->site_id) {
                    error('Cannot specify site ID');

                    return null;
                }
                $site_id = $this->site_id;
            }

            return $this->getCollector()->histogram($metric, $begin, $end, $site_id, $specifier);

        }

        /**
         * Get collector instance
         *
         * @return Collector
         */
        private function getCollector(): Collector
        {
            if (!isset($this->collector)) {
                $this->collector = new Collector(\PostgreSQL::pdo());
            }

            return $this->collector;
        }

        /**
         * Drop metric value from database
         *
         * @param string $metric metric to discard
         * @param bool   $rekey  rekey attribute metadata on next run
         * @return bool
         */
        public function drop_metric(string $metric, bool $rekey = false): bool
        {
            if (null === ($id = $this->getCollector()->metricAsId($metric))) {
                return false;
            }

            $db = \PostgreSQL::pdo();

            $table = $rekey ? 'metric_attributes' : 'metrics';
            $chunker = new \Daphnie\Chunker($db);
            $chunker->decompressRange(null);
            $stmt = $db->prepare("DELETE FROM $table WHERE attr_id = :attr_id");
            $ret = $stmt->execute([':attr_id' => $id]);
            $chunker->release();
            return $ret ?: error('Failed to drop metric %(metric)s: %(err)s',
                ['metric' => $metric, 'err' => array_get($stmt->errorInfo(), 2, '')]
            );
        }

        /**
         * Timescale chunk statistics
         *
         * @return array
         */
        public function chunks(): array
        {
            return (new \Daphnie\Chunker(\PostgreSQL::pdo()))->getChunkStats();
        }

        /**
         * Get all metric symbols
         *
         * @return array
         */
        public function metrics(): array
        {
            return array_keys($this->getCollector()->all());
        }

        /**
         * Get metric compression usage
         *
         * @return array
         */
        public function db_compression_usage(): array {
            $pg = PostgreSQL::pdo();
            $res = $pg->query("SELECT * FROM timescaledb_information.compressed_hypertable_stats WHERE hypertable_name::varchar = 'metrics'");
            if (!$res) {
                return [];
            }

            $rec = array_get($res->fetchAll(\PDO::FETCH_ASSOC), 0, []);

            foreach ($rec as $k => $v) {
                if (substr($k, -6) === '_bytes') {
                    $rec[$k] = \Formatter::changeBytes($v);
                }
            }

            return (array)$rec;
        }

        /**
         * Get metric usage
         *
         * @return array
         */
        public function db_usage(): array
        {
            $pg = PostgreSQL::pdo();
            $res = $pg->query("
            SELECT ht.id,
                ht.schema_name AS table_schema,
                ht.table_name,
                t.tableowner AS table_owner,
                ht.num_dimensions,
                ( SELECT count(1) AS count
                       FROM _timescaledb_catalog.chunk ch
                      WHERE ch.hypertable_id = ht.id) AS num_chunks,
                bsize.table_bytes,
                bsize.index_bytes,
                bsize.toast_bytes,
                bsize.total_bytes
               FROM _timescaledb_catalog.hypertable ht
                 LEFT JOIN pg_tables t ON ht.table_name = t.tablename AND ht.schema_name = t.schemaname
                 LEFT JOIN LATERAL hypertable_relation_size(
                    CASE
                        WHEN has_schema_privilege(ht.schema_name::text, 'USAGE'::text) THEN format('%I.%I'::text, ht.schema_name, ht.table_name)
                        ELSE NULL::text
                    END::regclass) bsize(table_bytes, index_bytes, toast_bytes, total_bytes) ON true
            WHERE table_name = 'metrics'");
            if (!$res) {
                return [];
            }

            $rec = array_get($res->fetchAll(\PDO::FETCH_ASSOC), 0);

            foreach ($rec as $k => $v) {
                if (substr($k, -6) === '_bytes') {
                    $rec[$k] = \Formatter::changeBytes($v);
                }
            }

            return (array)$rec;
        }

        /**
         * Decompress all chunks
         *
         * Note: reinitialize_compression() must be called after this
         *
         * @return bool
         */
        public function decompress_all(): bool
        {
            $pg = PostgreSQL::pdo();
            $chunker = new \Daphnie\Chunker($pg);
            if (null === $chunker->decompressRange(null)) {
                return false;
            }

            // block automatic recompression
            $chunker->clearTransientRecompressionChunks();

            return true;
        }

        /**
         * Reinitialize suspended compression
         *
         * @return bool
         */
        public function reinitialize_compression(): bool
        {
            $pg = PostgreSQL::pdo();
            $chunker = new \Daphnie\Chunker($pg);
            foreach ($chunker->getJobs() as $job) {
                if (!$chunker->resumeJob($job['job_id'])) {
                    return false;
                }
            }

            return true;
        }

        public function _cron(Cronus $cron)
        {
            $collector = $this->getCollector();
            foreach ($collector->getAnonymousCollections() as $collection) {
                $collection->log($collector);
            }

            /**
             * Prevent losing configuration settings in allkeys-lru purge
             */
            $cache = \Cache_Global::spawn();
            $cache->get(CONFIGURATION_KEY);
            \Lararia\JobDaemon::snapshot();
        }
    }