имя файла отправителя убрано из журнала приёма

- расширение приводится к перечню известных форматов прежде метки метрики:
  страница метрик открыта, и хвост имени уезжал на неё дословно
- проверки приёма перехватывают все три потока журнала и читают реестр метрик,
  каждая падает при снятии того, что сторожит
This commit is contained in:
av
2026-08-11 16:37:58 +03:00
parent ffa36e96c7
commit bd6001cd8d
18 changed files with 1797 additions and 20 deletions
+69
View File
@@ -0,0 +1,69 @@
package metrics
import (
"strconv"
"strings"
)
// OtherFormatLabel — значение метки для всего, чего нет в перечне known-форматов.
const OtherFormatLabel = "other"
// knownFormats — закрытый перечень расширений, которые допускаются меткой.
//
// Состав: пути, которые выдаёт Telegram (голосовое приходит как
// `voice/file_N.oga`, кружок — с `.mp4`), плюс форматы, доезжающие приёмом по
// HTTP, плюс собственное умолчание сервиса на случай имени без расширения.
// Списку, по которому бот отбирает **документы** (`isAudioDocument`), перечень
// намеренно не равен: тот судит по типу содержимого и своим списком пользуется
// лишь когда типа нет, а сюда попадает и то, что приходит другими путями.
// Сведение двух списков в один уронило бы основной вход сервиса в `other`.
var knownFormats = map[string]struct{}{
"mp3": {},
"wav": {},
"ogg": {},
"oga": {},
"opus": {},
"flac": {},
"m4a": {},
"aac": {},
"wma": {},
"mp4": {},
"mkv": {},
"mov": {},
"avi": {},
"webm": {},
"audio": {}, // умолчание сервиса, когда расширения в имени не было
}
// FormatLabel приводит расширение к виду, годному для метки метрики.
//
// Расширение приходит из имени, которое дал отправитель, и потому может быть
// чем угодно: имя `запись.тайное-слово` отдаёт `тайное-слово`. Страница метрик
// открыта, то есть метка — поверхность пошире журнала. Незнакомое значение
// заменяется одним общим: это закрывает и утечку куска имени, и рост числа
// временных рядов, которым иначе распоряжается анонимный отправитель.
//
// Имени файла на диске это не касается — там расширение остаётся пришедшим.
// ObserveInputFileSize записывает размер принятой записи. Расширение приводится
// здесь, а не у вызывающего: сырая точка употребления — это место, где хвост
// имени отправителя однажды снова уедет наружу, и проверка у вызывающего этого
// не заметит.
func ObserveInputFileSize(ext string, size int64) {
InputFileSizeHistogram.WithLabelValues(FormatLabel(ext)).Observe(float64(size))
}
// ObserveConversionDuration записывает длительность конвертации. Исходный формат
// приводится по той же причине, что и в приёме.
func ObserveConversionDuration(srcExt, targetFormat string, failed bool, seconds float64) {
ConversionDurationHistogram.
WithLabelValues(FormatLabel(srcExt), targetFormat, strconv.FormatBool(failed)).
Observe(seconds)
}
func FormatLabel(ext string) string {
normalized := strings.ToLower(strings.TrimPrefix(ext, "."))
if _, ok := knownFormats[normalized]; ok {
return normalized
}
return OtherFormatLabel
}
+117
View File
@@ -0,0 +1,117 @@
package metrics
import (
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
)
// Метка метрики уезжает на страницу, которая отдаётся без проверки отправителя,
// поэтому судим здесь ровно одно: что наружу выходит только известное значение.
func TestFormatLabel(t *testing.T) {
testCases := []struct {
name string
ext string
want string
}{
{name: "известное расширение с точкой", ext: ".mp3", want: "mp3"},
{name: "известное расширение без точки", ext: "mp3", want: "mp3"},
{name: "регистр приводится", ext: ".MP3", want: "mp3"},
{name: "умолчание сервиса", ext: ".audio", want: "audio"},
// Голосовое из Telegram приходит путём вида `voice/file_N.oga`, кружок —
// с `.mp4`. Это основной вход сервиса: усечение перечня до списка, по
// которому бот отбирает документы, схлопнуло бы его в общее значение.
{name: "голосовое из Telegram", ext: ".oga", want: "oga"},
{name: "видеокружок из Telegram", ext: ".mp4", want: "mp4"},
{name: "opus", ext: ".opus", want: "opus"},
// Значение сверяется с литералом, а не с самой константой: сверка с
// константой утверждала бы тавтологию, а спека нормирует слово `other`
// дословно.
{name: "хвост имени отправителя", ext: ".тайное-слово", want: "other"},
{name: "часть даты в имени", ext: ".08", want: "other"},
{name: "пустое", ext: "", want: "other"},
{name: "одна точка", ext: ".", want: "other"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := FormatLabel(tc.ext); got != tc.want {
t.Errorf("FormatLabel(%q) = %q, ожидалось %q", tc.ext, got, tc.want)
}
})
}
}
// labelValues собирает значения меток названного семейства из общего реестра.
func labelValues(t *testing.T, family string) []string {
families, err := prometheus.DefaultGatherer.Gather()
if err != nil {
t.Fatalf("не удалось прочитать реестр метрик: %v", err)
}
var values []string
for _, mf := range families {
if mf.GetName() != family {
continue
}
for _, m := range mf.GetMetric() {
for _, label := range m.GetLabel() {
values = append(values, label.GetValue())
}
}
}
return values
}
// Метку конвертации приёмом по HTTP не достать: шаг живёт в воркере, и своего
// окружения у него нет. Поэтому обёртка судится здесь — по реестру, а не по
// чистой функции: сырое употребление гистограммы мимо обёртки и есть то место,
// где хвост имени отправителя однажды снова уедет наружу.
func TestObserveConversionDurationLabelsAreKnown(t *testing.T) {
const tail = "CONVLEAKMARKER5X8"
ObserveConversionDuration("."+tail, "ogg", false, 1)
values := labelValues(t, "transcriber_conversion_duration_seconds")
if len(values) == 0 {
t.Fatal("метрика длительности конвертации не заполнилась")
}
assertTailAbsentAndOtherPresent(t, values, tail)
}
// Та же проверка для метки приёма — на случай, если обёртку обойдут только с
// одной стороны.
func TestObserveInputFileSizeLabelsAreKnown(t *testing.T) {
const tail = "INPUTLEAKMARKER5X8"
ObserveInputFileSize("."+tail, 42)
values := labelValues(t, "transcriber_input_file_size_bytes")
if len(values) == 0 {
t.Fatal("метрика размера принятой записи не заполнилась")
}
assertTailAbsentAndOtherPresent(t, values, tail)
}
// assertTailAbsentAndOtherPresent судит значения метки по двум признакам сразу.
// Одного «хвоста нет» мало: приведение, ослабленное до смены регистра, хвост
// пропустило бы, а поиск заглавного маркера в строчном значении его не нашёл бы.
// Поэтому сравнение регистронезависимое, и рядом стоит второй признак — что
// незнакомое расширение вообще доехало до общего значения.
func assertTailAbsentAndOtherPresent(t *testing.T, values []string, tail string) {
t.Helper()
for _, v := range values {
if strings.Contains(strings.ToLower(v), strings.ToLower(tail)) {
t.Fatalf("метка несёт хвост имени, данного отправителем: %q", v)
}
}
for _, v := range values {
if v == "other" {
return
}
}
t.Fatal("незнакомое расширение не приведено к общему значению")
}