yiisoft / yii2-debug

Debug Extension for Yii 2
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
202 stars 149 forks source link

Store data in a cache #495

Closed SamMousa closed 1 year ago

SamMousa commented 1 year ago

What steps will reproduce the problem?

Try running an app on a cluster. If you have multiple nodes running the debug data for each tag is not available everywhere.

What's expected?

Some way to solve that, which is easily possible.

What do you get instead?

1/n of your requests for the debugger will fail due to the data not existing.

Proof of concept

<?php
declare(strict_types=1);

namespace collecthor\components;

use yii\base\InvalidConfigException;
use yii\caching\CacheInterface;
use yii\debug\FlattenException;
use yii\debug\LogTarget;
use yii\di\Instance;

class DebugCacheLogTarget extends LogTarget
{
    /** @var CacheInterface */
    public $cache;

    public function init()
    {
        parent::init();
        if (!isset($this->cache)) {
            throw new InvalidConfigException("A cache must be configured");
        }
        $this->cache = Instance::ensure($this->cache, CacheInterface::class);
    }

    /**
     * Exports log messages to a specific destination.
     * Child classes must implement this method.
     * @throws \yii\base\Exception
     */
    public function export()
    {
        $summary = $this->collectSummary();
        $data = [];
        $exceptions = [];
        foreach ($this->module->panels as $id => $panel) {
            try {
                $panelData = $panel->save();
                if ($id === 'profiling') {
                    $summary['peakMemory'] = $panelData['memory'];
                    $summary['processingTime'] = $panelData['time'];
                }
                $data[$id] = serialize($panelData);
            } catch (\Exception $exception) {
                $exceptions[$id] = new FlattenException($exception);
            }
        }
        $data['summary'] = $summary;
        $data['exceptions'] = $exceptions;

        $this->save($this->tag, $data);
        $this->updateIndex($summary);
    }

    private function retrieve(string $tag): array
    {
        return $this->cache->get(['YII2_DEBUG' . $tag]);
    }
    private function save(string $tag, array $data): void
    {
        $this->cache->set(['YII2_DEBUG' . $tag], $data);

    }
    private function remove(string $tag): void
    {
        $this->cache->delete(['YII2_DEBUG' . $tag]);
    }
    private function updateIndex($summary): void
    {
        $key = ['YII2_DEBUG_INDEX', $this->module->dataPath];
        // We have a race condition here that could cause us to lose entries. This is acceptable.
        $manifest = $this->cache->exists('YII2_DEBUG_INDEX') ? $this->cache->get('YII2_DEBUG_INDEX') : [];
        $manifest[$this->tag] = $summary;

        $this->cache->set($key, $this->truncate($manifest));

    }

    /**
     * Remove entries exceeding the history size from the manifest.
     * @param array $manifest
     * @return array
     */
    private function truncate(array $manifest): array
    {
        $result = [];
        foreach($manifest as $tag => $entry) {
            if (count($result) <= $this->module->historySize) {
                $result[$tag] = $entry;
            } else {
                $this->remove($tag);
                // We do not support the mail panel and thus its files are not deleted.
            }

        }
        return $result;
    }

    public function loadManifest(): array
    {
        $key = ['YII2_DEBUG_INDEX', $this->module->dataPath];
        $result = $this->cache->get($key);
        return $result ?: [];
    }

    public function loadTagToPanels($tag): array
    {
        if (!is_string($tag)) {
            throw new \Exception('Tag must be a string');
        }
        $data = $this->retrieve($tag);
        $exceptions = $data['exceptions'];
        foreach ($this->module->panels as $id => $panel) {
            if (isset($data[$id])) {
                $panel->tag = $tag;
                $panel->load(unserialize($data[$id]));
            } else {
                unset($this->module->panels[$id]);
            }
            if (isset($exceptions[$id])) {
                $panel->setError($exceptions[$id]);
            }
        }

        return $data;
    }

}

This is a proof of concept implementation; if anyone wants to pick this up and transform it into a PR please feel free to! I won't have the time to polish it properly.

This will work with any cache implementation. One could argue that the default implementation could even be adapted to use a FileCache. By using a common CacheInterface for storage one implementation could serve both use cases. I've tested this using a Redis cache.

samdark commented 1 year ago

Isn't it the same as https://github.com/yiisoft/yii2-debug/pull/489?

SamMousa commented 1 year ago

Yes it is, I had not seen that one. Seems to be a more complete implementation so I'll just close this!

samdark commented 1 year ago

Reviewing it will help greatly.