Перейти к содержанию

Кардинальность и безопасные атрибуты

Мы добавили три полезные метрики, но стоимость наблюдения зависит не только от частоты измерений. Каждая новая комбинация labels создаёт отдельную временную серию. Разберём кардинальность и ограничим атрибуты «Пульса». Одновременно уточним правила данных: секреты, тела запросов, query и персональные сведения не должны собираться в сигналы, даже если их можно скрыть на дашборде позднее.

Кардинальность — число различных значений или их сочетаний. Для маршрута с двумя устойчивыми шаблонами она мала; для полного URL с UUID задания растёт с каждым запросом. Для гистограммы каждая комбинация дополнительно образует набор bucket, count и sum. Поэтому невинный label user_id способен умножить объём хранилища гораздо сильнее, чем увеличение числа измерений в уже существующей серии.

Ограниченный словарь

Для HTTP-метрик сохраняем только service, route и status class. В «Пульсе» это два сервисных имени, несколько маршрутов и пять возможных классов ответа. Конкретный UUID остаётся в прикладной базе; trace ID остаётся в trace context и коррелированных логах. Нельзя получить возможность поиска отдельного случая, не заплатив за соответствующий сигнал, поэтому общие метрики сознательно не выполняют эту задачу.

Модуль attributes.mjs фиксирует допустимые значения event-атрибутов и ограничивает числовую попытку. Неизвестное поле отбрасывается. Это более устойчивый подход, чем перечислить все возможные названия секретов, которые разработчик может случайно добавить позднее. Однако защита зависит от использования модуля: прямой вызов стороннего logger или автоматическая инструментация способны создать дополнительные данные вне этого wrapper.

Полный файл attributes.mjs состояния 06:

const values={
  route:new Set(['/v1/reports','/health','unmatched']),
  status_class:new Set(['1xx','2xx','3xx','4xx','5xx']),
  error_code:new Set(['db_timeout','operation_failed']),
  outcome:new Set(['queued','done','lease_lost'])
};
export function safeAttributes(input) {
  const out={};
  for(const [key,value] of Object.entries(input)) {
    if(values[key]?.has(value)) out[key]=value;
    if(key==='attempt' && Number.isSafeInteger(value) && value>0) out[key]=Math.min(value,100);
  }
  return out;
}

Важен контроль значений, а не только имён. Поле route может иметь безопасное имя, но содержать полный URL вместо шаблона. Поэтому словарь разрешает именно /v1/reports, /health и unmatched. Поле error_code получает один из ограниченных серверных кодов, а не текст исключения. Такая проверка делает изменение контракта явным: новый outcome нужно осознанно добавить в словарь и документацию.

Номер попытки полезен в логах как подробность конкретной работы. Он не добавляется к метрике HTTP и не используется как индексный label Loki. Не всякое высококардинальное поле запрещено во всех сигналах: trace ID нужен для поиска отдельного случая. Запрет касается бессистемного переноса этих идентификаторов в метрики и индексные метки, где они создают неоправданно много серий или потоков.

Ресурс и операция

Resource описывает источник: service name, версию приложения и окружение. Эти значения меняются при выпуске или развёртывании, а не при каждом запросе. Атрибут span описывает отдельную операцию. Если записать tenant ID в resource процесса, который обслуживает множество компаний, последующие сигналы могут ошибочно получить чужую идентичность. Для multi-tenant наблюдения нужна отдельная ограниченная модель доступа и фильтрации.

Мы отключили автоматические resource detectors, поэтому случайные host ID и environment values не добавляются в выбранный ресурс. Это помогает удержать небольшой словарь лаборатории. Для эксплуатации сведения о регионе или экземпляре могут быть необходимы, но их выбирают по диагностическому вопросу. Не следует автоматически превращать все environment variables в attributes: среди них находятся passwords, токены и строки соединений.

Метрика включает собственный bounded service, а Collector позднее не копирует все resource attributes в labels. Если включить resource_to_telemetry_conversion без анализа, можно неожиданно добавить service.instance.id, hostname или версию, умножающую число серий. Версия полезна для сравнения выпусков, но её влияние на запросы и retention нужно планировать. В нашем дашборде используется стабильное поле service.

Данные, которые не собираем

Authorization и cookie несут право доступа и исключены из событий. Тела запросов и ответы могут содержать документы, поэтому их не записываем автоматически. Query может содержать поисковую строку или временную подпись; вместо полного URL сохраняем route template. SQL statement и параметры остаются в закрытой среде базы. Email, телефон, имя сотрудника и содержимое отчёта не нужны для диагностики показанного пути.

Хеширование персонального значения не делает его автоматически безопасным label. Словарь email может быть восстановлен перебором, а уникальный хеш всё равно создаёт ту же высокую кардинальность. Если необходима расследовательская связь, её проектируют с конкретным сроком хранения и правами отдельно от общих метрик. Для текущей задачи достаточно trace ID, который не обозначает пользователя по модели приложения.

Collector позже удалит несколько известных опасных атрибутов как второй рубеж. Это не исправляет небезопасный body лога и не является универсальной очисткой PII. Нельзя сначала отправить весь request в collector, а затем назвать систему безопасной по наличию delete action для Authorization. Главная граница находится в месте создания данных, где приложение знает, что поле означает и нужно ли оно для сигнала.

Оценка стоимости

Предположим, есть три маршрута и пять классов статуса. Это небольшой верхний предел сочетаний для одного service. Если добавить тысячу user ID и тысячу динамических UUID, теоретическое пространство резко увеличится, даже если реальные сочетания встречаются не все. Для histogram каждое сочетание порождает несколько bucket-серий. Такой расчёт объясняет, почему высокая кардинальность быстро влияет на память exporter, сеть и диски backend.

Ограничение числа серий в backend полезно как аварийный рубеж, но не заменяет правильный словарь. При достижении лимита часть новых данных будет отклонена, и оператор потеряет наблюдение как раз при росте нагрузки. Поэтому сначала ограничиваем labels в приложении, затем наблюдаем число series и отказов экспорта. Стоимость и корректность здесь связаны: переполненный путь телеметрии делает выводы дашборда ненадёжными.

Для самостоятельного разбора измените только мысленную модель: вместо route template записывается req.url. Назовите два последствия — рост серий и возможность попадания query с секретом. Затем сопоставьте trace ID в логах и metrics labels. В первом случае он позволяет найти единичный trace; во втором превращает общий counter в огромное множество одноразовых counters, которые трудно агрегировать и дорого хранить.

Шестое состояние добавляет явный словарь event-атрибутов к уже ограниченным метрикам и ресурсам. Оно не заявлено как проверка отсутствия PII в запущенной системе: приложение не исполнялось. При будущей лаборатории нужно просмотреть реальные payload всех трёх сигналов и подтвердить, что новая инструментация не обошла wrapper. Следующий урок покажет путь доставки этих заранее ограниченных данных через Collector.

Предыдущий урок: Метрики нагрузки и задержки · Следующий урок: OpenTelemetry Collector и OTLP

Оглавление серии · Исходники всех состояний.

Документация: Prometheus: labels, Loki OTLP labels.