DbHandler::setCleanDivisor()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php declare(strict_types=1);
2
/**
3
 * Starlit Db Logging.
4
 *
5
 * @copyright Copyright (c) 2019 Starweb AB
6
 * @license   BSD 3-Clause
7
 */
8
9
namespace Starlit\Db\Logging;
10
11
use Monolog\Handler\AbstractProcessingHandler;
12
use Monolog\Logger;
13
use Starlit\Db\Db;
14
15
/**
16
 * Monolog handler to log to a database table.
17
 *
18
 * Use a table structure like this for compatibility:
19
 *
20
 * CREATE TABLE `log` (
21
 *   `log_entry_id` BIGINT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
22
 *   `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
23
 *   `channel` VARCHAR(64) NOT NULL,
24
 *   `level` VARCHAR(10) NOT NULL,
25
 *   `message` TEXT NOT NULL,
26
 *   PRIMARY KEY (`log_entry_id`),
27
 *   KEY `time` (`time`),
28
 *   KEY `channel` (`channel`),
29
 *   KEY `level` (`level`)
30
 * );
31
 *
32
 * @author Andreas Nilsson <http://github.com/jandreasn>
33
 */
34
class DbHandler extends AbstractProcessingHandler
35
{
36
    /**
37
     * @var Db
38
     */
39
    protected $db;
40
41
    /**
42
     * @var string
43
     */
44
    protected $table;
45
46
    /**
47
     * @var string[]
48
     */
49
    protected $additionalFields;
50
51
    /**
52
     * @var int
53
     */
54
    protected $maxEntries;
55
56
    /**
57
     * @var int
58
     */
59
    protected $cleanDivisor = 100;
60
61
    /**
62
     * Default probability of table being cleaned (1/100 = 1%).
63
     *
64
     * @var int
65
     */
66
    protected $cleanProbability = 1;
67
68
    /**
69
     * @param Db        $db
70
     * @param int       $maxEntries
71
     * @param string[]  $additionalFields
72
     * @param int       $level
73
     * @param bool      $bubble
74
     * @param string    $table
75
     */
76 6
    public function __construct(
77
        Db $db,
78
        int $maxEntries = null,
79
        array $additionalFields = [],
80
        int $level = Logger::DEBUG,
81
        bool $bubble = true,
82
        string $table = 'log'
83
    ) {
84 6
        parent::__construct($level, $bubble);
85
86 6
        $this->db = $db;
87 6
        $this->maxEntries = $maxEntries;
88 6
        $this->additionalFields = $additionalFields;
89 6
        $this->table = $table;
90 6
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 5
    protected function processRecord(array $record): array
96
    {
97 5
        $record = parent::processRecord($record);
98
99 5
        $record['additionalFieldsData'] = [];
100 5
        foreach ($this->additionalFields as $field) {
101 5
            foreach (['context', 'extra'] as $sourceKey) {
102 5
                if (isset($record[$sourceKey]) && array_key_exists($field, $record[$sourceKey])) {
103 1
                    $record['additionalFieldsData'][$field] = $record[$sourceKey][$field];
104 5
                    unset($record[$sourceKey][$field]);
105
                }
106
            }
107
        }
108
109 5
        return $record;
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 5
    protected function write(array $record): void
116
    {
117
        $dbData = [
118 5
            'channel' => $record['channel'],
119 5
            'level'   => $record['level_name'],
120 5
            'message' => $record['formatted'],
121 5
        ] + $record['additionalFieldsData'];
122
123 5
        $this->db->insert($this->table, $dbData);
124
125 5
        $this->clean($record['channel']);
126 5
    }
127
128 5
    protected function clean(string $channel): void
129
    {
130 5
        if ($this->maxEntries && mt_rand(1, $this->cleanDivisor) <= $this->cleanProbability) {
131 2
            $currentCount = $this->getChannelEntriesCount($channel);
132 2
            if ($currentCount > $this->maxEntries) {
133 1
                $entriesToDelete = $currentCount - $this->maxEntries;
134 1
                $this->deleteXOldestChannelEntries($channel, $entriesToDelete);
135
            }
136
        }
137 5
    }
138
139 2
    protected function getChannelEntriesCount(string $channel): int
140
    {
141 2
        return (int) $this->db->fetchValue(
142 2
            'SELECT COUNT(*) FROM `' . $this->table . '` WHERE `channel` = ?',
143 2
            [$channel]
144
        );
145
    }
146
147 1
    protected function deleteXOldestChannelEntries(string $channel, int $entriesToDelete): void
148
    {
149 1
        $this->db->exec(
150 1
            sprintf(
151 1
                'DELETE FROM `%s` WHERE `channel` = ?  ORDER BY `time` ASC LIMIT %d',
152 1
                $this->table,
153 1
                $entriesToDelete
154
            ),
155 1
            [$channel]
156
        );
157 1
    }
158
159 1
    public function clear(Logger $logger): void
160
    {
161 1
        $this->db->exec(sprintf('DELETE FROM `%s` WHERE `channel` = ?', $this->table), [$logger->getName()]);
162 1
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 5
    protected function getDefaultFormatter(): DbFormatter
168
    {
169 5
        return new DbFormatter();
170
    }
171
172
    /**
173
     * @see setCleanProbability()
174
     */
175 1
    public function setCleanDivisor(int $cleanDivisor): void
176
    {
177 1
        $this->cleanDivisor = $cleanDivisor;
178 1
    }
179
180
    /**
181
     * Sets the probability that log table is cleaned on log write.
182
     *
183
     * The clean probability together with clean divisor is used to calculate the probability.
184
     * With a clean probability of 1 and a divisor of 100, there's a 1% chance (1/100) the table
185
     * will be cleaned.
186
     */
187 3
    public function setCleanProbability(int $cleanProbability): void
188
    {
189 3
        $this->cleanProbability = $cleanProbability;
190 3
    }
191
}
192