feat(instance): метрики и healthcheck инстанса для дашборда
Раздел «Инстанс» получает живые показатели сервера (AGENT.md 7.19). - `internal/sysinfo`: загрузка процессора по cgroup v2 (`cpu.stat`/`cpu.max`) с откатом на `/proc/stat`, память по лимиту контейнера либо `/proc/meminfo`, место на разделе данных и размер базы вместе с журналом WAL; - `GET /api/v1/instance/metrics` (только администратор инстанса): метрики, состояние (`ok|warn|failed`), проверки app/database/disk/storage/voice/ gateway, версия и время работы; недоступные источники не ломают ответ, а попадают в проверку `metrics`; - тяжёлые проверки (БД, SFU, запись) кэшируются на 5 секунд: ручку можно опрашивать раз в секунду; - `voice.IssueRoomList` и `AdminClient.Ping` проверяют доступность RoomService и валидность ключей LiveKit (та же связка, что ломала модерацию); - у метрик свой лимит частоты, они исключены из общего лимита API (`RateLimiter.MiddlewareExcept`), иначе открытый раздел съедал бы половину бюджета запросов; - тесты: `internal/sysinfo` (cgroup, /proc, память, диск, размеры файлов) и `internal/server/api_metrics_test.go` (доступ, состав метрик, кэш, 429).
This commit is contained in:
+20
-2
@@ -110,17 +110,35 @@ func (c *AdminClient) ListParticipants(ctx context.Context, room string) ([]Part
|
||||
return response.Participants, nil
|
||||
}
|
||||
|
||||
// Ping проверяет доступность RoomService: healthcheck дашборда заодно
|
||||
// убеждается, что ключи LiveKit валидны (их неверность ломает модерацию).
|
||||
func (c *AdminClient) Ping(ctx context.Context) error {
|
||||
if !c.Enabled() {
|
||||
return ErrDisabled
|
||||
}
|
||||
token, err := c.issuer.IssueRoomList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.callWithToken(ctx, token, "ListRooms", map[string]any{}, nil)
|
||||
}
|
||||
|
||||
// call выполняет запрос к Twirp-методу RoomService с административным токеном
|
||||
// этой комнаты: LiveKit принимает `roomAdmin` только вместе с именем комнаты.
|
||||
func (c *AdminClient) call(ctx context.Context, room, method string, body any, out any) error {
|
||||
if !c.Enabled() {
|
||||
return ErrDisabled
|
||||
}
|
||||
payload, err := json.Marshal(body)
|
||||
token, err := c.issuer.IssueAdmin(room)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token, err := c.issuer.IssueAdmin(room)
|
||||
return c.callWithToken(ctx, token, method, body, out)
|
||||
}
|
||||
|
||||
// callWithToken выполняет запрос к Twirp-методу с готовым токеном.
|
||||
func (c *AdminClient) callWithToken(ctx context.Context, token, method string, body any, out any) error {
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+23
-14
@@ -87,6 +87,11 @@ func (i *TokenIssuer) Issue(room, identity, displayName string, grants Grants) (
|
||||
claims.NotBefore = now.Add(-10 * time.Second).Unix()
|
||||
claims.ExpiresAt = now.Add(i.ttl).Unix()
|
||||
|
||||
return i.sign(claims)
|
||||
}
|
||||
|
||||
// sign подписывает произвольные claims алгоритмом HS256.
|
||||
func (i *TokenIssuer) sign(claims any) (string, error) {
|
||||
header, err := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -142,20 +147,7 @@ func (i *TokenIssuer) IssueAdmin(room string) (string, error) {
|
||||
"roomAdmin": true,
|
||||
},
|
||||
}
|
||||
header, err := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
payload, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signingInput := encodeSegment(header) + "." + encodeSegment(payload)
|
||||
mac := hmac.New(sha256.New, []byte(i.apiSecret))
|
||||
if _, err := mac.Write([]byte(signingInput)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return signingInput + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil)), nil
|
||||
return i.sign(claims)
|
||||
}
|
||||
|
||||
// VerifyWebhook проверяет подпись вебхука LiveKit: это JWT, подписанный тем же
|
||||
@@ -272,6 +264,23 @@ func ResolveLiveKitURL(raw string) (string, error) {
|
||||
return strings.TrimSuffix(parsed.String(), "/"), nil
|
||||
}
|
||||
|
||||
// IssueRoomList подписывает служебный токен для запросов, которым не нужна
|
||||
// конкретная комната (проверка доступности RoomService в healthcheck).
|
||||
func (i *TokenIssuer) IssueRoomList() (string, error) {
|
||||
if !i.Enabled() {
|
||||
return "", ErrDisabled
|
||||
}
|
||||
return i.sign(map[string]any{
|
||||
"iss": i.apiKey,
|
||||
"sub": i.apiKey,
|
||||
"nbf": i.now().UTC().Add(-10 * time.Second).Unix(),
|
||||
"exp": i.now().UTC().Add(5 * time.Minute).Unix(),
|
||||
"video": map[string]any{
|
||||
"roomList": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// VoiceURLForClient отдаёт адрес LiveKit так, как его должен использовать
|
||||
// клиент: пусто, если голос не настроен.
|
||||
func (i *TokenIssuer) VoiceURLForClient(publicURL string) string {
|
||||
|
||||
Reference in New Issue
Block a user