7.5. Демонстрация - Улучшаем логирование
Шаг 1
Давайте посмотрим на данные, с которыми нам придётся работать. Нам нужно объединить 1 и 3, 2 и 4 записи в одну, а также слить последнюю и третью с конца записи.
import {log} from "./logger";
const logs = [
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 0,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 0,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 3,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 5,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 10,
},
{
"message": "Uncaught RangeError: Maximum call stack size exceeded",
"timestamp": 14,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 15,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 18,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 21,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 22,
},
];
log(logs);
Шаг 2
Сперва в целом отфильтруем частые сообщения. Не будем при этом трогать предыдущие, с которыми надо их слить. Сделаем это при помощи Map — будем писать туда записи с текстом сообщения как ключ, а также временем его прибытия как значение. Тогда при каждом новом сообщении мы сможем за O(1) проверить, встречалось ли оно ранее и насколько давно.
import {log} from "./logger";
function rateLimit(logs) {
const encountered = new Map();
return logs.filter(({message, timestamp}) => {
const previousEncounterTimestamp = encountered.get(message);
encountered.set(message, timestamp);
// оставляем элемент в массиве, если он произошёл впервые, либо сильно позже предыдущего
return previousEncounterTimestamp === undefined || previousEncounterTimestamp < timestamp - 5;
})
}
const logs = [
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 0,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 0,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 3,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 5,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 10,
},
{
"message": "Uncaught RangeError: Maximum call stack size exceeded",
"timestamp": 14,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 15,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 18,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 21,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 22,
},
];
Шаг 3
А теперь, помимо времени последней встречи, будем сохранять и место последней встречи. Чтобы мутировать составляющийся массив, нам нужно заменить filter на более наивную конструкцию. Теперь вместо игнорирования частых логов будем идти по сохранённому индексу к последнему подобному и добавлять туда x2.
import {log} from "./logger";
function rateLimit(logs) {
const encountered = new Map();
const rateLimited = [];
for (const {message, timestamp} of logs) {
const {timestamp: previousEncounterTimestamp, last: lastEncounter} = encountered.get(message) || {};
// если элемент произошёл впервые, либо сильно позже предыдущего
if (previousEncounterTimestamp === undefined || previousEncounterTimestamp < timestamp - 5) {
// по сообщению-ключу положим время встречи этого сообщения и место, где оно лежит
encountered.set(message, {timestamp, last: rateLimited.length});
// и оставляем элемент в массиве
rateLimited.push({message, timestamp});
// а если мы такое уже встречали...
} else {
// то обновим время последней встречи
encountered.set(message, {timestamp, last: lastEncounter});
// и обновим последнее подобное сообщение, чтобы отразить, что оно дублируется
rateLimited[lastEncounter].message += ' x2';
}
}
return rateLimited;
}
const logs = [
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 0,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 0,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 3,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 5,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 10,
},
{
"message": "Uncaught RangeError: Maximum call stack size exceeded",
"timestamp": 14,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 15,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 18,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 21,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 22,
},
];
log(rateLimit(logs));
Шаг 4
И наш код работал бы замечательно, если бы мы не дублировали во втором логе x2 x2. Нужно починить наш алгоритм так, чтобы он выдавал вместо этого x3. Это можно сделать двумя простыми способами. Первый — использовать регулярное выражение, которое будет доставать из логов x(\d+)$ и заменять его на нужное, но регулярные выражения довольно «прожорливы» по времени вычисления. Второй — положить количество прошлых встреч в наш «кеш» логов, но для этого понадобится выделять больше памяти. Опять мы пришли к ситуации, когда нужно чем-то пожертвовать. Давайте предположим, что сейчас скорость работы для системы критичнее занимаемого места и реализуем второй вариант.
import {log} from "./logger";
function rateLimit(logs) {
const encountered = new Map();
const rateLimited = [];
for (const {message, timestamp} of logs) {
const {timestamp: previousEncounterTimestamp, last: lastEncounter, occurences} = encountered.get(message) || {};
// если элемент произошёл впервые, либо сильно позже предыдущего
if (previousEncounterTimestamp === undefined || previousEncounterTimestamp < timestamp - 5) {
// по сообщению-ключу положим время встречи этого сообщения и место, где оно лежит
encountered.set(message, {timestamp, last: rateLimited.length, occurences: 1});
// и оставляем элемент в массиве
rateLimited.push({message, timestamp});
// а если мы такое уже встречали...
} else {
// то обновим время последней встречи
encountered.set(message, {timestamp, last: lastEncounter, occurences: occurences + 1});
// и обновим последнее подобное сообщение, чтобы отразить, что оно дублируется
rateLimited[lastEncounter].message = `${message} x${occurences + 1}`;
}
}
return rateLimited;
}
const logs = [
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 0,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 0,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 3,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 5,
},
{
"message": "TypeError: 'undefined' is not an object",
"timestamp": 10,
},
{
"message": "Uncaught RangeError: Maximum call stack size exceeded",
"timestamp": 14,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 15,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 18,
},
{
"message": "Cannot read property 'score' of undefined",
"timestamp": 21,
},
{
"message": "ReferenceError: event is not defined",
"timestamp": 22,
},
];
log(rateLimit(logs));
File logger.js
const logTable = document.getElementById('logs');
export function log(logs) {
const body = document.createElement('tbody');
for (const log of logs) {
const timestamp = document.createElement('td');
timestamp.innerText = log.timestamp;
const message = document.createElement('td');
message.innerText = log.message;
const row = document.createElement('tr');
row.appendChild(timestamp);
row.appendChild(message);
body.appendChild(row);
}
logTable.appendChild(body);
}