Мой опыт установки Sentry self-hosted

от автора

Привет! Меня зовут Даниил Ткаченко, я веб‑разработчик в ИТ‑компании «Активика». В статье я поделюсь опытом развёртывания Sentry self‑hosted для высоконагруженного проекта. Несмотря на обилие материалов по SaaS‑версии, актуальных гайдов по self‑hosted‑установке почти нет — особенно с учётом современных требований к производительности и отказоустойчивости.

Мы столкнулись с рядом проблем: нестабильностью на базовом хостинге, отсутствием перехвата HTTP‑ошибок и быстрым заполнением диска. Под катом разберу каждую проблему, покажу код решений и дам рекомендации для тех, кто планирует развернуть Sentry самостоятельно.

Статья будет полезна разработчикам и DevOps‑инженерам без опыта работы с self‑hosted Sentry.

Проблема 1. Нестабильность на базовом хостинге

Всё началось с того, что на нашем базовом хостинге возникли ошибки: Sentry стабильно не запускался из‑за особенностей виртуализации. Нельзя было даже развернуть инструмент. Поэтому мы переехали на другой хостинг — и тогда всё получилось.

Но тут меня тоже поджидали проблемы: память быстро забивалась, и не просто логами в том же ClickHouse или файлами реплеев, а логами в Kafka.

За время тестовых подключений к демоверсии проекта (а ей пользуется ограниченное число пользователей для тестирования новых функций) Sentry падал два раза, а демо-версия проекта грузилась очень долго. 

Когда Sentry падал, сайт грузился очень долго — около 30–40 секунд. Это происходило потому, что клиент пытался не только загрузить JS‑скрипты с внешних CDN, но и отправить события на сервер Sentry. Запросы зависали на таймаутах, блокируя загрузку страницы.

Конфигурация сервера, которую использовали: 32 ГБ RAM, 4 ядра CPU, 40 ГБ SSD. Установили до ажиотажа с ценами на память — потом пришлось уменьшить RAM до 16-24 ГБ, увеличить SSD до 100ГБ и добавить swap-файл, чтобы не переплачивать. 

Лайфхаки для ускорения загрузки сайта, когда сентри не доступен

Даже при self‑hosted решении возможны сбои: DDoS‑атаки, перезагрузка кластера, ошибки ПО. Делюсь проверенными мной способами минимизировать их влияние, чтобы избежать длительной загрузки и работы сайта, даже при полном отказе Sentry.

Хак 1. Локальная урезанная версия JS‑библиотеки

Я скачал JS-библиотеку Sentry и хранил её локально в урезанной версии. Теперь никаких внешних запросов — загрузка мгновенная и без зависаний.

Хак 2.  Таймауты на соединения

Добавляем код с таймаутами на соединения поверх SDK, который прерывает запросы к Sentry, если они длятся дольше 300 миллисекунд.

Это предотвращает переполнение очереди запросов в браузере и сохраняет отзывчивость сайта.

     const SENTRY_TIMEOUT_MS = 300;        function makeCustomTimeoutTransport(options) {            // The 'makeRequest' function is the core where you control the fetch call.            function makeRequest(request) {                const controller = new AbortController();                const timeoutId = setTimeout(() => controller.abort(), SENTRY_TIMEOUT_MS); // 3-second timeout                const requestOptions = {                    body: request.body,                    method: 'POST',                    referrerPolicy: 'origin',                    headers: options.headers,                    signal: controller.signal, // <-- Connect the AbortController                    ...options.fetchOptions,                };                // Return the fetch promise. It will be rejected if it times out.                return fetch(options.url, requestOptions)                    .then(response => {                        clearTimeout(timeoutId); // Clear timer on success                        return {                            statusCode: response.status,                            headers: {                                'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),                                'retry-after': response.headers.get('Retry-After'),                            },                        };                    })                    .catch(error => {                        clearTimeout(timeoutId);                        if (error.name === 'AbortError') {                            console.warn('Sentry event dropped due to send timeout.');                            error.message = 'Sentry event dropped due to send timeout.';                        }                        throw error; // Re-throw to let the SDK handle the failure                    });            }            // Use the official helper to build a compliant transport.            return Sentry.createTransport(options, makeRequest);        }        Sentry.init({            transport: makeCustomTimeoutTransport,…        });

Эти изменения мы протестировали на проде. 

Результат: после внедрения перестало забиваться лог бесконечными попытками достучаться до Sentry.

Проблема 2. Отсутствие перехвата HTTP‑ошибок

Оказалось, что Sentry из коробки не ловит HTTP-ошибки на асинхронных запросах — картинки 404, сломанные API-вызовы или страницы проходят мимо. Я написал небольшое расширение, примерно на 100 строк JS, которое:

  • перехватывает запросы через fetch и XMLHttpRequest

  • отслеживает ошибки (коды 4xx, 5xx)

  • отправляет информацию в Sentry с деталями запроса и ответа

Ключевые возможности расширения:

  • фильтрация по URL и кодам статусов

  • ограничение размера данных (MAX_CAPTURE_LENGTH = 1000 байт)

  • санитизация небезопасных заголовков (cookie, authorization и т. д.)

  • поддержка PII (персональных данных) при необходимости

// sentry-http-integration.js/** * Расширенная интеграция для перехвата HTTP-запросов и отправки информации об ошибках в Sentry. * Основана на официальной реализации httpClientIntegration из Sentry, но с дополнительными возможностями * для более детального анализа запросов и ответов. * * @see Официальная документация Sentry: https://docs.sentry.io/platforms/javascript/ */class SentryHttpDataIntegration {    /**     * Уникальный идентификатор интеграции.     * @type {string}     */    static id = 'SentryHttpDataIntegration';    /**     * Имя интеграции.     * @type {string}     */    name = 'SentryHttpDataIntegration';    /**     * Максимальная длина захватываемых данных в байтах.     * Используется для предотвращения отправки слишком больших данных в Sentry.     * @type {number}     */    static MAX_CAPTURE_LENGTH = 1000;    /**     * Список "небезопасных" заголовков, доступ к которым ограничен в браузерах.     * @type {Array<string>}     * @private     */    static _RESTRICTED_HEADERS = [        'set-cookie',        'set-cookie2',        'cookie2',        'cookie',        'authorization',        'proxy-authorization',        'sec-',        'proxy-'    ];    /**     * Настройки для фильтрации HTTP-запросов, по которым будут отправляться события.     * @type {Object}     * @property {Array<Array<number>|number>} failedRequestStatusCodes - Коды HTTP-статусов, которые считаются ошибками     * @property {Array<string|RegExp>} failedRequestTargets - URL-шаблоны для отслеживания ошибок     * @private     */    _options = {        failedRequestStatusCodes: [[500, 599]],        failedRequestTargets: [/.*/],    };    /**     * Конструктор класса.     * @param {Object} options - Настройки интеграции     * @param {Array<Array<number>|number>} [options.failedRequestStatusCodes] - Коды HTTP-статусов для отслеживания     * @param {Array<string|RegExp>} [options.failedRequestTargets] - URL-шаблоны для отслеживания     */    constructor(options = {}) {        this._options = {            ...this._options,            ...options,        };    }    /**     * Устанавливает перехватчики для fetch и XMLHttpRequest.     * Этот метод вызывается Sentry при инициализации интеграции.     */    setupOnce() {        this._wrapFetch();        this._wrapXHR();    }    /**     * Проверяет, должен ли запрос быть обработан на основе его URL и статуса ответа.     * @param {number} status - Код HTTP-статуса     * @param {string} url - URL запроса     * @returns {boolean} - true, если запрос соответствует критериям для отправки в Sentry     * @private     */    _shouldCaptureResponse(status, url) {        // Не обрабатываем запросы к самому Sentry        if (this._isSentryRequest(url)) {            return false;        }        return (            this._isInStatusCodeRange(status) &&            this._isUrlInTargets(url)        );    }    /**     * Проверяет, соответствует ли код статуса заданным диапазонам ошибок.     * @param {number} status - Код HTTP-статуса     * @returns {boolean} - true, если статус входит в диапазоны ошибок     * @private     */    _isInStatusCodeRange(status) {        return this._options.failedRequestStatusCodes.some(range => {            if (typeof range === 'number') {                return status === range;            }            return status >= range[0] && status <= range[1];        });    }    /**     * Проверяет, соответствует ли URL заданным шаблонам.     * @param {string} url - URL запроса     * @returns {boolean} - true, если URL соответствует хотя бы одному шаблону     * @private     */    _isUrlInTargets(url) {        return this._options.failedRequestTargets.some(target => {            if (typeof target === 'string') {                return url.includes(target);            }            return target.test(url);        });    }    /**     * Проверяет, является ли запрос запросом к Sentry.     * @param {string} url - URL запроса     * @returns {boolean} - true, если запрос направлен к Sentry     * @private     */    _isSentryRequest(url) {        // Простая проверка на запросы к Sentry        // В реальной реализации может использоваться isSentryRequestUrl из @sentry/core        return url.includes('sentry.io') || url.includes('ingest.sentry.io');    }    /**     * Перехватывает нативный fetch API для отслеживания запросов.     * Заменяет глобальную функцию fetch на свою обертку, которая:     * - сохраняет информацию о запросе     * - выполняет оригинальный запрос     * - перехватывает ошибки и ответы с кодом ошибки (4xx, 5xx)     * - отправляет информацию в Sentry в случае ошибки     * @private     */    _wrapFetch() {        if (typeof window.fetch !== 'function') {            console.warn('Fetch API не поддерживается в этом окружении');            return;        }        const originalFetch = window.fetch;        const self = this;        window.fetch = async function (input, init) {            // Получаем метод запроса (GET по умолчанию)            const method = init?.method ?? 'GET';            // Создаем объект Request для унификации работы с разными форматами параметров            const request = self._getRequest(input, init);            // Захватываем данные запроса, если они есть            const requestData = init?.body                ? await self._captureRequestBody(init.body)                : null;            // Извлекаем заголовки запроса, если sendDefaultPii активен            let requestHeaders;            if (self._shouldSendDefaultPii()) {                try {                    requestHeaders = self._extractFetchHeaders(request.headers);                    // Удаляем небезопасные заголовки                    self._sanitizeHeaders(requestHeaders);                } catch (e) {                    // Игнорируем ошибки при извлечении заголовков                }            }            try {                // Выполняем оригинальный запрос                const response = await originalFetch.apply(this, arguments);                // Если ответ соответствует критериям ошибки                if (self._shouldCaptureResponse(response.status, response.url)) {                    // Извлекаем заголовки ответа, если активен sendDefaultPii                    let responseHeaders;                    if (self._shouldSendDefaultPii()) {                        try {                            responseHeaders = self._extractFetchHeaders(response.headers);                            // Удаляем небезопасные заголовки                            self._sanitizeHeaders(responseHeaders);                        } catch (e) {                            // Игнорируем ошибки при извлечении заголовков                        }                    }                    // Захватываем тело ответа                    const responseData = await self._captureResponseBody(response.clone());                    // Отправляем информацию об ошибке в Sentry                    self._captureError({                        method,                        url: response.url,                        requestData,                        requestHeaders,                        responseData,                        responseHeaders,                        status: response.status                    });                }                return response;            } catch (error) {                // В случае сетевой ошибки отправляем информацию в Sentry                self._captureError({                    method,                    url: request.url,                    requestData,                    requestHeaders,                    error: error.message,                    status: ''                });                // Передаем ошибку дальше                throw error;            }        };    }    /**     * Создает объект Request из параметров fetch.     * @param {RequestInfo} input - URL или объект Request     * @param {RequestInit} [init] - Опции запроса     * @returns {Request} - Объект Request     * @private     */    _getRequest(input, init) {        if (!init && input instanceof Request) {            return input;        }        // Если тело оригинального Request уже использовано, просто возвращаем его        if (input instanceof Request && input.bodyUsed) {            return input;        }        return new Request(input, init);    }    /**     * Перехватывает XMLHttpRequest для отслеживания запросов.     * Модифицирует прототип XMLHttpRequest, заменяя методы open и send     * для сбора информации о запросах и перехвата ошибок.     * @private     */    _wrapXHR() {        if (typeof XMLHttpRequest === 'undefined') {            console.warn('XMLHttpRequest не поддерживается в этом окружении');            return;        }        const originalSend = XMLHttpRequest.prototype.send;        const originalOpen = XMLHttpRequest.prototype.open;        const self = this;        // Сохраняем метод и URL запроса через перехват open        XMLHttpRequest.prototype.open = function (method, url) {            this._method = method;            this._url = url;            this._requestHeaders = {};            return originalOpen.apply(this, arguments);        };        // Перехватываем setRequestHeader для сохранения заголовков        const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;        XMLHttpRequest.prototype.setRequestHeader = function (name, value) {            if (!this._requestHeaders) {                this._requestHeaders = {};            }            this._requestHeaders[name] = value;            return originalSetRequestHeader.apply(this, arguments);        };        // Перехватываем метод send для отслеживания запросов        XMLHttpRequest.prototype.send = function (body) {            // Захватываем данные запроса            const requestData = self._captureSyncData(body);            // Санитизируем заголовки запроса            let requestHeaders;            if (self._shouldSendDefaultPii() && this._requestHeaders) {                requestHeaders = {...this._requestHeaders};                self._sanitizeHeaders(requestHeaders);            }            // Добавляем обработчик события завершения запроса            this.addEventListener('loadend', () => {                // Если статус ответа указывает на ошибку и URL соответствует критериям                if (self._shouldCaptureResponse(this.status, this.responseURL ?? this._url)) {                    // Безопасно получаем заголовки ответа                    let responseHeaders;                    if (self._shouldSendDefaultPii()) {                        try {                            // Используем безопасный метод получения заголовков ответа                            responseHeaders = self._getXHRResponseHeaders(this);                            // Дополнительно санитизируем заголовки                            self._sanitizeHeaders(responseHeaders);                        } catch (e) {                            // Игнорируем ошибки при извлечении заголовков                        }                    }                    // Захватываем тело ответа                    const responseData = self._captureSyncXHRResponseData(this);                    // Отправляем информацию об ошибке в Sentry                    self._captureError({                        method: this._method ?? 'GET',                        url: this.responseURL ?? this._url,                        requestData,                        requestHeaders,                        responseData,                        responseHeaders,                        status: this.status                    });                }            });            // Вызываем оригинальный метод send            return originalSend.call(this, body);        };    }    /**     * Удаляет небезопасные заголовки из объекта заголовков.     * @param {Object} headers - Объект с заголовками     * @private     */    _sanitizeHeaders(headers) {        if (!headers) return;        // Удаляем небезопасные заголовки        Object.keys(headers).forEach(key => {            const lowerKey = key.toLowerCase();            if (SentryHttpDataIntegration._RESTRICTED_HEADERS.some(                restrictedHeader => lowerKey === restrictedHeader || lowerKey.startsWith(restrictedHeader)            )) {                delete headers[key];            }        });    }    /**     * Безопасно захватывает тело ответа XHR.     * @param {XMLHttpRequest} xhr - Объект XMLHttpRequest     * @returns {string|Object|null} - Захваченные данные ответа     * @private     */    _captureSyncXHRResponseData(xhr) {        try {            // Безопасно получаем тело ответа, обрабатывая разные типы ответов            let responseData = xhr.response;            // Проверяем тип ответа и при необходимости конвертируем            if (responseData) {                if (typeof responseData === 'object') {                    try {                        // Для объектов попробуем преобразовать их в строку JSON                        responseData = JSON.stringify(responseData).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);                    } catch (e) {                        // Если не удалось преобразовать, вернем тип объекта                        responseData = `[${Object.prototype.toString.call(responseData)}]`;                    }                } else if (typeof responseData === 'string') {                    // Ограничиваем длину строки                    responseData = responseData.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);                } else {                    // Для других типов данных преобразуем их в строку                    responseData = String(responseData).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);                }            }            return responseData;        } catch (e) {            return '[unable to capture response data]';        }    }    /**     * Проверяет, нужно ли отправлять персональные данные (PII).     * @returns {boolean} - true, если нужно отправлять PII     * @private     */    _shouldSendDefaultPii() {        // В реальной реализации это зависит от настроек Sentry        // Здесь просто возвращаем true для демонстрации        return true;    }    /**     * Извлекает заголовки из объекта Headers.     * @param {Headers} headers - Объект Headers     * @returns {Object} - Объект с заголовками     * @private     */    _extractFetchHeaders(headers) {        const result = {};        if (headers instanceof Headers) {            headers.forEach((value, key) => {                // Проверяем, не является ли заголовок небезопасным                if (!SentryHttpDataIntegration._RESTRICTED_HEADERS.some(                    restrictedHeader => key.toLowerCase() === restrictedHeader ||                        key.toLowerCase().startsWith(restrictedHeader)                )) {                    result[key] = value;                }            });        }        return result;    }    /**     * Извлекает заголовки ответа из XMLHttpRequest.     * @param {XMLHttpRequest} xhr - Объект XMLHttpRequest     * @returns {Object} - Объект с заголовками     * @private     */    _getXHRResponseHeaders(xhr) {        const headers = xhr.getAllResponseHeaders();        const result = {};        if (!headers) {            return result;        }        headers.split('\r\n').forEach(line => {            if (!line) return;            const separatorIndex = line.indexOf(': ');            if (separatorIndex > 0) {                const key = line.substring(0, separatorIndex);                const value = line.substring(separatorIndex + 2);                // Проверяем, не является ли заголовок небезопасным                if (!SentryHttpDataIntegration._RESTRICTED_HEADERS.some(                    restrictedHeader => key.toLowerCase() === restrictedHeader ||                        key.toLowerCase().startsWith(restrictedHeader)                )) {                    result[key] = value;                }            }        });        return result;    }    /**     * Вспомогательные методы для захвата данных     */    /**     * Захватывает и обрабатывает тело запроса для последующей отправки в Sentry.     * Поддерживает различные типы данных: FormData, Blob, Response и строки.     * @param {*} body - Тело запроса     * @returns {Promise<Object|string|null>} - Обработанные данные запроса     * @private     */    async _captureRequestBody(body) {        try {            // Обработка данных FormData            if (body instanceof FormData) {                return Object.fromEntries(body.entries());            }            // Обработка объектов, поддерживающих метод text() (Request, Response, Blob)            if (typeof body.text === 'function') {                const text = await body.text();                return text.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);            }            // Обработка остальных типов данных            return String(body).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);        } catch {            // В случае ошибки возвращаем информативную строку            return '[unable to capture request body]';        }    }    /**     * Захватывает и обрабатывает тело ответа для последующей отправки в Sentry.     * @param {Response} response - Объект Response     * @returns {Promise<string>} - Текстовое представление тела ответа     * @private     */    async _captureResponseBody(response) {        try {            // Пытаемся получить тело как текст            const text = await response.text();            return text.slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);        } catch (e) {            try {                // Если не удалось получить как текст, пробуем работать с типом ответа                const contentType = response.headers.get('content-type');                if (contentType && contentType.includes('application/json')) {                    // Для JSON пытаемся получить и преобразовать данные                    const clone = response.clone();                    const json = await clone.json();                    return JSON.stringify(json).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);                }                if (contentType && contentType.includes('text/')) {                    // Для текстовых форматов пытаемся еще раз получить текст                    const clone = response.clone();                    return (await clone.text()).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);                }                // Для бинарных данных возвращаем информацию о типе                return `[binary data: ${contentType ?? 'unknown type'}]`;            } catch {                return '[unable to capture response body]';            }        }    }    /**     * Синхронно захватывает данные для XMLHttpRequest запросов.     * @param {*} data - Данные для захвата     * @returns {Object|string|null} - Обработанные данные     * @private     */    _captureSyncData(data) {        try {            if (!data) return null;            // Обработка данных FormData            if (data instanceof FormData) {                return Object.fromEntries(data.entries());            }            // Обработка данных Blob            if (data instanceof Blob) {                return `[Blob data: ${data.type ?? 'unknown type'}, size: ${data.size} bytes]`;            }            // Обработка данных ArrayBuffer            if (data instanceof ArrayBuffer) {                return `[ArrayBuffer data: size: ${data.byteLength} bytes]`;            }            // Если данные - объект, пытаемся преобразовать в JSON            if (typeof data === 'object' && data !== null) {                try {                    return JSON.stringify(data).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);                } catch (e) {                    return `[object: ${Object.prototype.toString.call(data)}]`;                }            }            // Обработка остальных типов данных            return String(data).slice(0, SentryHttpDataIntegration.MAX_CAPTURE_LENGTH);        } catch (e) {            return '[unable to capture data]';        }    }    /**     * Получает размер ответа из заголовка Content-Length.     * @param {Object} headers - Заголовки ответа     * @returns {number|undefined} - Размер ответа в байтах или undefined     * @private     */    _getResponseSizeFromHeaders(headers) {        if (!headers) return undefined;        const contentLength = headers['Content-Length'] ?? headers['content-length'];        if (contentLength) {            return parseInt(contentLength, 10);        }        return undefined;    }    /**     * Формирует и отправляет информацию об ошибке в Sentry.     * @param {Object} details - Детали ошибки     * @param {string} details.method - HTTP метод запроса     * @param {string} details.url - URL запроса     * @param {*} details.requestData - Данные запроса     * @param {Object} [details.requestHeaders] - Заголовки запроса     * @param {number} [details.status] - Статус ответа (если есть)     * @param {*} [details.responseData] - Данные ответа (если есть)     * @param {Object} [details.responseHeaders] - Заголовки ответа     * @param {string} [details.error] - Сообщение об ошибке (если есть)     * @private     */    _captureError(details) {        // Создаем объект ошибки с сообщением из details.error или на основе статуса ответа        const error = new Error(details.error ?? `HTTP Error ${details.status ?? ''}`);        // Формируем сообщение об ошибке        const message = `HTTP Client Error: ${details.method} ${details.url} ${details.status ?? ''}`;        // Формируем событие для Sentry        const event = {            message,            exception: {                values: [                    {                        type: 'Error',                        value: message,                    },                ],            },            request: {                url: details.url,                method: details.method,                headers: details.requestHeaders,                data: details.requestData,            },            contexts: {                response: {                    status_code: details.status,                    headers: details.responseHeaders ?? {},                    data: details.responseData ?? {},                    body_size: this._getResponseSizeFromHeaders(details.responseHeaders),                },            },        };        // Добавляем механизм исключения        this._addExceptionMechanism(event);        // Отправляем событие в Sentry        Sentry.captureException(error, {            contexts: event.contexts,            request: event.request,            tags: {                'http.status_code': details.status ?? '',                'http.method': details.method,            },        });    }    /**     * Добавляет механизм исключения к событию.     * @param {Object} event - Событие Sentry     * @private     */    _addExceptionMechanism(event) {        if (event.exception && event.exception.values && event.exception.values[0]) {            event.exception.values[0].mechanism = {                type: 'http.client',                handled: false,            };        }    }}/** * Универсальный экспорт класса интеграции. * Поддерживает как CommonJS модули (Node.js), так и браузерную среду. */if (typeof module !== 'undefined' && module.exports) {    module.exports = SentryHttpDataIntegration;} else {    window.SentryHttpDataIntegration = SentryHttpDataIntegration;}

Проблема 3. Быстрое заполнение диска

Основная проблема — быстрое заполнение диска. 40 ГБ заполнялись за часы при включённом трейсинге и метриках. Sentry накапливает данные в ClickHouse, Kafka и Postgres.

Если всё-таки Sentry заполнил всё пространство, но повышать объём хранилища вы не хотите, то можно удалить некоторые docker-volume. Например, sentry-kafka, sentry-seaweedfs, возможно даже sentry-data. После их удаления запустите скрипт установки Sentry, и он заново создаст нужные docker volume. 

Решения для снижения нагрузки:

  1. Снижение срока хранения событий. Установили срок хранения событий в системе до 2-4 дней (в файле .env: SENTRY_EVENT_RETENTION_DAYS). Для хранения 14 дней потребуется 200+ ГБ.

  2. Отключение ненужных трейсингов:

    • браузерные спаны

    • ошибки от сторонних CDN (например, таймауты Google Fonts)

    • фоновые задачи (вкладка Performance в настройках проекта)

3. Оптимизация Kafka:

  • уменьшили retention событий до минимума — данные обрабатываются и удаляются.

Такие у нас настройки для контейнера Kafka: 

KAFKA_LOG_RETENTION_HOURS: «1»

KAFKA_MESSAGE_MAX_BYTES: «10000000» #10MB or bust

KAFKA_MAX_REQUEST_SIZE: «10000000» #10MB on requests apparently too

CONFLUENT_SUPPORT_METRICS_ENABLE: «false»

KAFKA_LOG_RETENTION_BYTES: «10737418240» # 10 GiB (server default; помните про «на партицию»)

KAFKA_LOG_SEGMENT_BYTES: «268435456» # 256 MiB сегмент, чтобы быстрее закрывались и удалялись

KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS: «300000» # каждые 5 минут проверять old segments

KAFKA_LOG_SEGMENT_DELETE_DELAY_MS: «60000» # задержка удаления сегмента

KAFKA_LOG_CLEANUP_POLICY: «delete»

KAFKA_LOG_CLEANER_ENABLE: true

Вот ещё ссылка на доку: https://develop.sentry.dev/self-hosted/troubleshooting/kafka/#reducing-disk-usage

  1. Вручную чистили ClickHouse: TRUNCATE на старых таблицах вроде spans_local, transactions_local. 

  2. Настроили cron для ежедневной очистки файлов, которые хранятся больше нашего срока в системе: find /var/lib/docker/volumes/sentry-data/_data -type f -mtime +3 -delete

Заключение 

Развёртывание Sentry self‑hosted — задача нетривиальная, особенно для высоконагруженных проектов. В ходе работы мы:

  • устранили зависания сайта из‑за недоступности Sentry (локальная JS‑библиотека + таймауты);

  • расширили мониторинг ошибок (интеграция для перехвата HTTP‑ошибок);

  • снизили нагрузку на диск (оптимизация Kafka, ClickHouse, настройка retention).

Рекомендации для тех, кто планирует развернуть Sentry self‑hosted:

  1. Начинайте с конфигурации, которая соответствует нагрузке (минимум 16 ГБ RAM, 4 ядра CPU).

  2. Сразу локализуйте JS‑библиотеку Sentry — это предотвратит зависания сайта.

  3. Настройте таймауты для запросов к Sentry — 300–500 мс достаточно для большинства сценариев.

  4. Отключайте ненужные трейсинги и метрики — это сэкономит место и ресурсы.

  5. Регулярно проверяйте использование диска и настраивайте retention данных.

  6. Тестируйте отказоустойчивость — симулируйте сбои Sentry и проверяйте поведение приложения.

Если у вас остались вопросы или есть идеи для улучшения — пишите в комментариях! 

ссылка на оригинал статьи https://habr.com/ru/articles/1030358/