18 KiB
TODO
Completed
- скип лист коллекторов в конфиге
- кэш для метрик
- fetch + build
- wrapper cache
- клиент кинетика поддерживает сессию
- Механизм остановки background updaters: Context-based cancellation с graceful shutdown
- Background updater для всех метрик: Реализован generic updater для всех коллекторов
- Работает для system, hotspot, internet, interface, interface_stats, process
- Учитывает
skip_collectorsдля каждого устройства - Graceful shutdown через context cancellation и WaitGroup
High Priority
Critical Issues
(Пусто - все критические проблемы решены или перенесены в планируемые фичи)
Configuration & Architecture
- Хардкод пути к конфигу: Нет CLI флагов для указания файла конфигурации
- Device timeout не учитывается: HTTP клиент использует хардкод 5s таймаут
- Добавить версионирование
Medium Priority
Features
- Привести в порядок логгирование, добавить разные уровни (Error, Info, Debug), добавить в конфиг параметр отображения логов.
- медиа коллектор
- обработка ошибок для offline устройств (улучшенная)
- Метрики ошибок экспортера: Экспортировать метрики о работе самого экспортера
keenetic_scrape_success{device, collector}- успешность последнего scrapekeenetic_scrape_duration_seconds{device, collector}- длительность сбораkeenetic_scrape_errors_total{device, collector}- счетчик ошибок- Позволит настраивать алерты на проблемы со сбором метрик
- Улучшит observability и диагностику проблем
- Строго типизированный кеш с дженериками: Улучшение type safety
- ✅ Текущее состояние: Добавлены runtime проверки type assertions с обработкой ошибок
- 🎯 Цель: Переделать на
TypedCacheсCacheEntry[T any]для compile-time type safety - Уберет необходимость runtime проверок и сделает код более безопасным
- См. вариант 1 из предыдущих обсуждений
Security & Production Readiness
- Credentials в git: config.yaml содержит тестовые пароли
- Для production: использовать environment variables или secrets manager
Testing
- Нет тестов: Нулевое покрытие тестами
- Unit tests для коллекторов
- Integration tests для Keenetic клиента
- Tests для кеширования
Low Priority
Keenetic Client Code Quality Issues
Найдено при анализе internal/keenetic/client.go (2025-10-16)
Critical Issues (безопасность/корректность)
-
Race condition при повторной аутентификации (
client.go:249-261)- Проблема: Если несколько горутин одновременно получат HTTP 401, все они параллельно вызовут
authenticate(). Это создаст множественные конкурирующие запросы к/authendpoint. - Локация: Метод
doGet(), блок обработкиStatusUnauthorized - Последствия: Непредсказуемое поведение, лишние запросы к API, возможные конфликты сессий
- Решение: Использовать
sync.Mutexилиsync.Onceдля синхронизации процесса re-authentication - Пример:
var authMutex sync.Mutex authMutex.Lock() defer authMutex.Unlock() if err := c.authenticate(ctx, c.login, c.password); err != nil { ... }
- Проблема: Если несколько горутин одновременно получат HTTP 401, все они параллельно вызовут
-
Race condition на полях Client (
client.go:127-133)- Проблема: Поля
login,password,Hostnameзаписываются в методах (authenticate(),fetchHostname()), которые могут вызываться из разных горутин без синхронизации - Локация: Структура
Clientи методы, изменяющие её поля - Последствия: Data races, неопределенное поведение, возможные паники
- Решение: Добавить
sync.RWMutexдля защиты полей или сделать их write-once (инициализация только в конструкторе) - Детекция:
go test -raceпокажет эти гонки
- Проблема: Поля
-
Отсутствие ограничения размера тела ответа при ошибках (
client.go:264)- Проблема:
io.ReadAll(resp.Body)читает всё тело без ограничений при HTTP ошибках - Локация: Блок обработки
resp.StatusCode >= 400в методеdoGet() - Последствия:
- Потенциальная DoS атака через огромное тело ответа
- OOM если сервер вернет гигабайты данных
- Чувствительные данные могут попасть в логи
- Решение: Использовать
io.LimitReader(resp.Body, maxBodySize)(например, 1MB) - Пример:
limitedBody := io.LimitReader(resp.Body, 1024*1024) // 1MB max data, err := io.ReadAll(limitedBody) if err != nil { return fmt.Errorf("failed to read error body: %w", err) }
- Проблема:
-
Недостаточная валидация входных данных
- Проблема в
NewClient()(client.go:135-145): Не проверяется что URL имеет scheme (http/https) и host - Проблема в
Init()иauthenticate(): Нет проверки чтоloginиpasswordне пустые - Локация: Конструктор и методы инициализации
- Последствия: Невалидные клиенты могут быть созданы, приводя к ошибкам позже
- Решение: Добавить валидацию в
NewClient():if parsed.Scheme != "http" && parsed.Scheme != "https" { return nil, fmt.Errorf("URL must have http or https scheme") } if parsed.Host == "" { return nil, fmt.Errorf("URL must have a host") } - Решение для credentials: Проверять в
Init()перед вызовомauthenticate()
- Проблема в
Serious Issues (надежность)
-
Игнорируется timeout из конфигурации (
client.go:139)- Проблема: HTTP клиент всегда использует хардкод
Timeout: 5 * time.Second, несмотря на то что в config.yaml есть полеtimeoutдля каждого устройства - Локация: Инициализация
httpClientвNewClient() - Последствия: Невозможно настроить timeout для медленных устройств или быстрых сетей
- Решение: Принимать timeout как параметр в
NewClient()или добавить методSetTimeout() - Связано: High Priority issue "Device timeout не учитывается"
- Проблема: HTTP клиент всегда использует хардкод
-
Игнорирование ошибки создания cookiejar (
client.go:136)- Проблема:
jar, _ := cookiejar.New(nil)игнорирует потенциальную ошибку - Локация: Инициализация в
NewClient() - Последствия: Нарушение best practice, теоретически может скрыть проблему
- Решение: Обрабатывать ошибку:
jar, err := cookiejar.New(nil) if err != nil { return nil, fmt.Errorf("failed to create cookie jar: %w", err) }
- Проблема:
-
GetConnectedInterfaceStats() молча игнорирует ошибки (
client.go:355-358)- Проблема: При ошибке получения статистики интерфейса просто
continue, без логирования - Локация: Цикл по интерфейсам в
GetConnectedInterfaceStats() - Последствия: Невозможно отличить "интерфейс disconnected" от "ошибка API"
- Решение: Логировать ошибки или возвращать частичные результаты с map ошибок:
type InterfaceStatsResult struct { Stats map[string]InterfaceStats Errors map[string]error }
- Проблема: При ошибке получения статистики интерфейса просто
-
GetProcessInfo() молча пропускает невалидные записи (
client.go:298-300)- Проблема: При ошибке
json.Unmarshalпростоcontinue, нет информации о количестве пропущенных процессов - Локация: Цикл парсинга процессов в
GetProcessInfo() - Последствия: Тихая потеря данных, сложно диагностировать проблемы с API
- Решение: Добавить счетчик ошибок или логирование
- Проблема: При ошибке
-
Контекст не проверяется внутри длительных операций (
client.go:343-363)- Проблема: В
GetConnectedInterfaceStats()делается N запросов в цикле, ноctx.Done()не проверяется между итерациями - Локация: Цикл по интерфейсам
- Последствия: Невозможно отменить долгую операцию, если context был cancelled
- Решение: Добавить проверку:
for _, iface := range interfaces { select { case <-ctx.Done(): return nil, ctx.Err() default: } // ... continue processing }
- Проблема: В
Design Issues (проектирование)
-
Неэффективная повторная аутентификация
- Проблема: При каждом 401 делается полный цикл (GET /auth для получения challenge + POST /auth), даже если challenge еще валиден
- Локация: Логика re-authentication в
doGet() - Последствия: Лишние запросы к API, увеличенная latency
- Решение: Кешировать challenge с разумным TTL или использовать только POST при re-auth
-
Отсутствие retry логики
- Проблема: При временных сетевых ошибках (connection reset, timeout) сразу возвращается ошибка
- Локация: Все API методы
- Последствия: Низкая устойчивость к временным проблемам сети
- Решение: Добавить configurable retry с exponential backoff для idempotent операций
- Библиотеки:
github.com/cenkalti/backoff/v4или встроенный механизм
-
Нет метрик и observability
- Проблема: Невозможно отследить важные события:
- Количество re-authentication попыток
- Время выполнения запросов (latency по endpoint)
- Частота ошибок по типам и endpoint
- Размер ответов API
- Локация: Весь клиент
- Последствия: Сложно диагностировать проблемы в production, нет visibility
- Решение: Интегрировать с Prometheus metrics или добавить structured logging
- Связано: Medium Priority issue "Метрики ошибок экспортера"
- Проблема: Невозможно отследить важные события:
-
Отсутствие метода Cleanup/Close
- Проблема:
ClientвладеетhttpClientс connections pool и cookiejar, но нет способа явно освободить ресурсы - Локация: Структура
Client - Последствия: Goroutine leaks в тестах, невозможность graceful cleanup
- Решение: Добавить метод:
func (c *Client) Close() error { c.httpClient.CloseIdleConnections() return nil }
- Проблема:
Code Quality Issues (качество кода)
-
Утечка ресурсов при двойном defer (
client.go:247, 260)- Проблема:
resp, err := doRequest() defer resp.Body.Close() // defer на первый resp if resp.StatusCode == http.StatusUnauthorized { _ = resp.Body.Close() // явное закрытие resp, err = doRequest() // переприсваивание переменной defer resp.Body.Close() // defer на второй resp } - Локация: Метод
doGet() - Последствия: Путаница, потенциальное двойное закрытие, плохая читаемость
- Решение: Использовать именованные переменные (
resp1,resp2) или рефакторить логику
- Проблема:
-
Смешанный язык в комментариях (
client.go:250)- Проблема: Комментарий
// закрываем перед повторомна русском языке - Локация: Различные места в файле
- Нарушение: CLAUDE.md требует все комментарии на английском
- Решение: Перевести все комментарии на английский
- Проблема: Комментарий
-
Потенциальная утечка credentials в логах (
client.go:264)- Проблема: В error message включается
fullURL.String(), который может содержать query параметры с токенами - Локация: Обработка ошибок в
doGet() - Последствия: Чувствительные данные могут попасть в логи
- Решение: Sanitize URL перед логированием или использовать
fullURL.Redacted()
- Проблема: В error message включается
-
Недостаточная типизация для статусов
- Проблема: Поля типа string для статусов легко опечатать:
Connected string `json:"connected"` // "yes"/"no" State string `json:"state"` Link string `json:"link"` - Локация: Структуры моделей данных
- Последствия: Легко сделать ошибку при сравнении ("Yes" vs "yes")
- Решение: Использовать custom types с константами:
type ConnectionStatus string const ( ConnectionStatusYes ConnectionStatus = "yes" ConnectionStatusNo ConnectionStatus = "no" )
- Проблема: Поля типа string для статусов легко опечатать:
-
Неиспользуемые поля в структурах
- Проблема:
InterfaceStats.LastOverflow(client.go:118) не используется нигде - Локация: Определения структур
- Последствия: Мертвый код, увеличивает cognitive load
- Решение: Удалить или задокументировать зачем поле сохранено
- Проблема:
-
Магические числа
- Проблема:
5 * time.Second(client.go:139),400(client.go:263) - Локация: Различные места
- Последствия: Плохая читаемость и maintainability
- Решение: Использовать именованные константы:
const ( defaultHTTPTimeout = 5 * time.Second httpErrorStatusCode = 400 )
- Проблема:
Общие рекомендации
- Добавить линтеры:
golangci-lintс конфигом покажет большинство этих проблем - Race detector:
go test -raceдля всех тестов - Structured logging: Заменить
fmt.Errorfна proper logging с levels - Error wrapping: Везде используется, это хорошо (Go 1.13+ style)
- Context usage: В целом правильно, но не проверяется в циклах