ILIAS  trunk Revision v12.0_alpha-1227-g7ff6d300864
class.ilSessionStatistics.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22{
23 private const int SLOT_SIZE = 15;
24
27 private static ?ilDBStatement $raw_data_statement = null;
28
29 public static function isActive(): bool
30 {
31 global $DIC;
32
34 $ilSetting = $DIC['ilSetting'];
35
36 return (bool) $ilSetting->get('session_statistics', '1');
37 }
38
39 public static function createRawEntry(string $a_session_id, int $a_session_type, int $a_timestamp, int $a_user_id): void
40 {
41 global $DIC;
42
44 $ilDB = $DIC['ilDB'];
45
46 if (!$a_user_id || !$a_session_id || !self::isActive()) {
47 return;
48 }
49
50 // #9669: if a session was destroyed and somehow the session id is still
51 // in use there will be a id-collision for the raw-entry
52 $ilDB->replace(
53 'usr_session_stats_raw',
54 [
55 'session_id' => [ilDBConstants::T_TEXT, $a_session_id]
56 ],
57 [
58 'type' => [ilDBConstants::T_INTEGER, $a_session_type],
59 'start_time' => [ilDBConstants::T_INTEGER, $a_timestamp],
60 'user_id' => [ilDBConstants::T_INTEGER, $a_user_id]
61 ]
62 );
63 }
64
69 public static function closeRawEntry($a_session_id, ?int $a_context = null, $a_expired_at = null): void
70 {
71 global $DIC;
72
74 $ilDB = $DIC['ilDB'];
75
76 if (!self::isActive()) {
77 return;
78 }
79
80 // single entry
81 if (!is_array($a_session_id)) {
82 if ($a_expired_at) {
83 $end_time = $a_expired_at;
84 } else {
85 $end_time = time();
86 }
87 $sql = 'UPDATE usr_session_stats_raw' .
88 ' SET end_time = ' . $ilDB->quote($end_time, ilDBConstants::T_INTEGER);
89 if ($a_context) {
90 $sql .= ', end_context = ' . $ilDB->quote($a_context, ilDBConstants::T_INTEGER);
91 }
92 $sql .= ' WHERE session_id = ' . $ilDB->quote($a_session_id, ilDBConstants::T_TEXT) .
93 ' AND end_time IS NULL';
94 $ilDB->manipulate($sql);
95 }
96 // batch closing
97 elseif (!$a_expired_at) {
98 $sql = 'UPDATE usr_session_stats_raw' .
99 ' SET end_time = ' . $ilDB->quote(time(), ilDBConstants::T_INTEGER);
100 if ($a_context) {
101 $sql .= ', end_context = ' . $ilDB->quote($a_context, ilDBConstants::T_INTEGER);
102 }
103 $sql .= ' WHERE ' . $ilDB->in('session_id', $a_session_id, false, ilDBConstants::T_TEXT) .
104 ' AND end_time IS NULL';
105 $ilDB->manipulate($sql);
106 }
107 // batch with individual timestamps
108 else {
109 foreach ($a_session_id as $id => $ts) {
110 $sql = 'UPDATE usr_session_stats_raw' .
111 ' SET end_time = ' . $ilDB->quote($ts, ilDBConstants::T_INTEGER);
112 if ($a_context) {
113 $sql .= ', end_context = ' . $ilDB->quote($a_context, ilDBConstants::T_INTEGER);
114 }
115 $sql .= ' WHERE session_id = ' . $ilDB->quote($id, ilDBConstants::T_TEXT) .
116 ' AND end_time IS NULL';
117 $ilDB->manipulate($sql);
118 }
119 }
120 }
121
126 private static function getCurrentSlot(int $a_now): ?array
127 {
128 global $DIC;
129
131 $ilDB = $DIC['ilDB'];
132
133 // get latest slot in db
134 $sql = 'SELECT MAX(slot_end) previous_slot_end FROM usr_session_stats';
135 $res = $ilDB->query($sql);
136 $row = $ilDB->fetchAssoc($res);
137 $previous_slot_end = $row['previous_slot_end'];
138
139 // no previous slot? calculate last complete slot
140 // should we use minimum session raw date instead? (problem: table lock)
141 if (!$previous_slot_end) {
142 $slot = (int) (floor(date('i') / self::SLOT_SIZE));
143 // last slot of previous hour
144 if (!$slot) {
145 $current_slot_begin = mktime((int) date('H', $a_now) - 1, 60 - self::SLOT_SIZE, 0);
146 }
147 // "normalize" to slot
148 else {
149 $current_slot_begin = mktime((int) date('H', $a_now), ($slot - 1) * self::SLOT_SIZE, 0);
150 }
151 } else {
152 $current_slot_begin = $previous_slot_end + 1;
153 }
154
155 $current_slot_end = $current_slot_begin + (60 * self::SLOT_SIZE) - 1;
156
157 // no complete slot: nothing to do yet
158 if ($current_slot_end < $a_now) {
159 return [$current_slot_begin, $current_slot_end];
160 }
161
162 return null;
163 }
164
165 private static function getNumberOfActiveRawSessions(int $a_time): int
166 {
167 global $DIC;
168
170 $ilDB = $DIC['ilDB'];
171
172 return (int) $ilDB->fetchAssoc(
173 $ilDB->execute(
174 self::getNumberOfActiveRawSessionsPreparedStatement(),
175 [$a_time, $a_time]
176 )
177 )['counter'];
178 }
179
183 private static function getRawData(int $a_begin, int $a_end): Generator
184 {
185 global $DIC;
186
188 $ilDB = $DIC['ilDB'];
189
190 $res = $ilDB->execute(
191 self::getRawDataPreparedStatement(),
192 [$a_end, $a_begin]
193 );
194 while ($row = $ilDB->fetchAssoc($res)) {
195 yield $row;
196 }
197 }
198
203 private static function createNewAggregationSlot(int $a_now): ?array
204 {
205 global $DIC;
206
208 $ilDB = $DIC['ilDB'];
209
210 $ilAtomQuery = $ilDB->buildAtomQuery();
211 $ilAtomQuery->addTableLock('usr_session_stats');
212
213 $ilAtomQuery->addQueryCallable(function (ilDBInterface $ilDB) use ($a_now, &$slot) {
214 // if we had to wait for the lock, no current slot should be returned here
215 $slot = self::getCurrentSlot($a_now);
216 if (!is_array($slot)) {
217 $slot = null;
218 return;
219 }
220
221 // save slot to mark as taken
222 $fields = [
223 'slot_begin' => [ilDBConstants::T_INTEGER, $slot[0]],
224 'slot_end' => [ilDBConstants::T_INTEGER, $slot[1]],
225 ];
226 $ilDB->insert('usr_session_stats', $fields);
227 });
228
229 $ilAtomQuery->run();
230
231 return $slot;
232 }
233
234 public static function aggregateRaw(int $a_now): void
235 {
236 if (!self::isActive()) {
237 return;
238 }
239
240 $slot = self::createNewAggregationSlot($a_now);
241 while (is_array($slot)) {
242 self::aggregateRawHelper($slot[0], $slot[1]);
243 $slot = self::createNewAggregationSlot($a_now);
244 }
245
246 // #12728
247 self::deleteAggregatedRaw($a_now);
248 }
249
250 private static function getNumberOfActiveRawSessionsPreparedStatement(): ilDBStatement
251 {
252 if (self::$number_of_active_raw_sessions_statement === null) {
253 global $DIC;
254
256 $ilDB = $DIC['ilDB'];
257
258 self::$number_of_active_raw_sessions_statement = $ilDB->prepare(
259 'SELECT COUNT(*) counter FROM usr_session_stats_raw '
260 . 'WHERE (end_time IS NULL OR end_time >= ?) '
261 . 'AND start_time <= ? '
264 );
265 }
266
268 }
269
270 private static function getAggregatedRawDataPreparedStatement(): ilDBStatement
271 {
272 if (!self::$aggregated_raw_data_statement) {
273 global $DIC;
274
276 $ilDB = $DIC['ilDB'];
277
278 self::$aggregated_raw_data_statement = $ilDB->prepareManip(
279 'UPDATE usr_session_stats '
280 . 'SET active_min = ?, '
281 . 'active_max = ?, '
282 . 'active_avg = ?, '
283 . 'active_end = ?, '
284 . 'opened = ?, '
285 . 'closed_manual = ?, '
286 . 'closed_expire = ?, '
287 . 'closed_login = ?, '
288 . 'closed_misc = ? '
289 . 'WHERE slot_begin = ? AND slot_end = ?',
290 [
302 ]
303 );
304 }
305
307 }
308
309 private static function getRawDataPreparedStatement(): ilDBStatement
310 {
311 if (!self::$raw_data_statement) {
312 global $DIC;
313
315 $ilDB = $DIC['ilDB'];
316
317 self::$raw_data_statement = $ilDB->prepare(
318 'SELECT start_time, end_time, end_context FROM usr_session_stats_raw' .
319 ' WHERE start_time <= ?' .
320 ' AND (end_time IS NULL OR end_time >= ?)' .
322 ' ORDER BY start_time',
324 );
325 }
327 }
328
329 private static function aggregateRawHelper(int $a_begin, int $a_end): void
330 {
331 global $DIC;
332
334 $ilDB = $DIC['ilDB'];
335
336 // "relevant" closing types
337 $separate_closed = [
341 ];
342
343 // gather/process data (build event timeline)
344 $events = [];
345 $closed_counter = $events;
346 $opened_counter = 0;
347 foreach (self::getRawData($a_begin, $a_end) as $item) {
348 // open/close counters are _not_ time related
349
350 // we could filter for undefined/invalid closing contexts
351 // and ignore those items, but this would make any debugging
352 // close to impossible
353 // "closed_other" would have been a good idea...
354
355 // session opened
356 if ($item['start_time'] >= $a_begin) {
357 $opened_counter++;
358 $events[$item['start_time']][] = 1;
359 }
360 // session closed
361 if ($item['end_time'] && $item['end_time'] <= $a_end) {
362 if (in_array($item['end_context'], $separate_closed, true)) {
363 if (!isset($closed_counter[$item['end_context']])) {
364 $closed_counter[$item['end_context']] = 0;
365 }
366
367 $closed_counter[$item['end_context']]++;
368 } else {
369 $closed_counter[0] = ($closed_counter[0] ?? 0) + 1;
370 }
371 $events[$item['end_time']][] = -1;
372 }
373 }
374
375 // initializing active statistical values
376 $active_begin = self::getNumberOfActiveRawSessions($a_begin - 1);
377 $active_avg = $active_begin;
378 $active_max = $active_begin;
379 $active_min = $active_begin;
380 $active_end = $active_begin;
381
382 // parsing events / building averages
383 if (count($events)) {
384 $last_update_avg = $a_begin - 1;
385 $slot_seconds = self::SLOT_SIZE * 60;
386 $active_avg = 0;
387
388 // parse all open/closing events
389 ksort($events);
390 foreach ($events as $ts => $actions) {
391 // actions which occur in the same second are "merged"
392 foreach ($actions as $action) {
393 // max
394 if ($action > 0) {
395 $active_end++;
396 }
397 // min
398 else {
399 $active_end--;
400 }
401 }
402
403 // max
404 if ($active_end > $active_max) {
405 $active_max = $active_end;
406 }
407
408 // min
409 if ($active_end < $active_min) {
410 $active_min = $active_end;
411 }
412
413 // avg
414 $diff = $ts - $last_update_avg;
415 $active_avg += $diff / $slot_seconds * $active_end;
416 $last_update_avg = $ts;
417 }
418
419 // add up to end of slot if needed
420 if ($last_update_avg < $a_end) {
421 $diff = $a_end - $last_update_avg;
422 $active_avg += $diff / $slot_seconds * $active_end;
423 }
424
425 $active_avg = round($active_avg);
426 }
427 unset($events);
428
429 $ilDB->execute(
430 self::getAggregatedRawDataPreparedStatement(),
431 [
432 $active_min,
433 $active_max,
434 $active_avg,
435 $active_end,
436 $opened_counter,
437 (int) ($closed_counter[ilSession::SESSION_CLOSE_USER] ?? 0),
438 (int) ($closed_counter[ilSession::SESSION_CLOSE_EXPIRE] ?? 0),
439 (int) ($closed_counter[ilSession::SESSION_CLOSE_LOGIN] ?? 0),
440 (int) ($closed_counter[0] ?? 0),
441 $a_begin,
442 $a_end
443 ]
444 );
445 }
446
447 private static function deleteAggregatedRaw(int $a_now): void
448 {
449 global $DIC;
450
452 $ilDB = $DIC['ilDB'];
453
454 // we are rather defensive here - 7 days BEFORE current aggregation
455 $cut = $a_now - (60 * 60 * 24 * 7);
456
457 $ilDB->manipulate(
458 'DELETE FROM usr_session_stats_raw' .
459 ' WHERE start_time <= ' . $ilDB->quote($cut, ilDBConstants::T_INTEGER)
460 );
461 }
462
466 public static function getNumberOfSessionsByType(int $a_from, int $a_to): array
467 {
468 global $DIC;
469
471 $ilDB = $DIC['ilDB'];
472
473 $sql = 'SELECT SUM(opened) opened, SUM(closed_manual) closed_manual,' .
474 ' SUM(closed_expire) closed_expire,' .
475 ' SUM(closed_login) closed_login, SUM(closed_misc) closed_misc' .
476 ' FROM usr_session_stats' .
477 ' WHERE slot_end > ' . $ilDB->quote($a_from, ilDBConstants::T_INTEGER) .
478 ' AND slot_begin < ' . $ilDB->quote($a_to, ilDBConstants::T_INTEGER);
479 $res = $ilDB->query($sql);
480
481 return $ilDB->fetchAssoc($res);
482 }
483
487 public static function getActiveSessions(int $a_from, int $a_to): array
488 {
489 global $DIC;
490
492 $ilDB = $DIC['ilDB'];
493
494 $sql = 'SELECT slot_begin, slot_end, active_min, active_max, active_avg' .
495 ' FROM usr_session_stats' .
496 ' WHERE slot_end > ' . $ilDB->quote($a_from, ilDBConstants::T_INTEGER) .
497 ' AND slot_begin < ' . $ilDB->quote($a_to, ilDBConstants::T_INTEGER) .
498 ' ORDER BY slot_begin';
499 $res = $ilDB->query($sql);
500
501 $all = [];
502 while ($row = $ilDB->fetchAssoc($res)) {
503 $all[] = array_map(intval(...), $row);
504 }
505
506 return $all;
507 }
508
512 public static function getLastAggregation(): ?int
513 {
514 global $DIC;
515
516 $ilDB = $DIC['ilDB'];
517
518 $sql = 'SELECT MAX(slot_end) latest FROM usr_session_stats';
519 $res = $ilDB->query($sql);
520 $row = $ilDB->fetchAssoc($res);
521 if ($row['latest'] !== null) {
522 return (int) $row['latest'];
523 }
524
525 //TODO check if return null as timestamp causes issues
526 return null;
527 }
528}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
static array $session_types_controlled
static getLastAggregation()
Get timestamp of last aggregation.
static ilDBStatement $raw_data_statement
static ilDBStatement $number_of_active_raw_sessions_statement
static ilDBStatement $aggregated_raw_data_statement
const int SESSION_CLOSE_LOGIN
const int SESSION_CLOSE_EXPIRE
const int SESSION_CLOSE_USER
Interface ilDBInterface.
Interface ilDBStatement.
$res
Definition: ltiservices.php:69
global $ilSetting
Definition: privfeed.php:31
global $DIC
Definition: shib_login.php:26