Observability и Мониторинг
Метрики, логирование, трейсинг, алертинг и SLA для платформы ISP.
Observability и Мониторинг
Телеком-система работает 24/7, и любой сбой напрямую влияет на абонентов. Observability — это не "мониторинг на дашборде", а способность понять, что происходит внутри системы по её внешним сигналам.
Почему ISP нужна продвинутая observability: В отличие от типичного SaaS, ISP-платформа имеет критичные зависимости от физического оборудования (OLT, BRAS, RADIUS), которое может отказывать непредсказуемо. Dunning-процесс, ошибочно заблокировавший платящего абонента — это прямой churn. Задержка provisioning > 5 минут — звонок в техподдержку. Каждый сигнал должен быть связан с бизнес-контекстом: не просто "HTTP 500", а "абонент X не может подключиться к сети после оплаты".
Три столпа Observability
Логирование (Structured Logs)
Стандарт формата
Все сервисы пишут логи в JSON (structured logging):
{
"timestamp": "2024-01-15T12:00:00.123Z",
"level": "INFO",
"service": "billing-service",
"trace_id": "abc123def456",
"span_id": "span-789",
"correlation_id": "order-2024-001",
"message": "Payment processed successfully",
"context": {
"account_id": "acc-123",
"amount": 500.0,
"payment_id": "pay-456"
}
}Go: настройка slog (стандартная библиотека Go 1.21+)
// internal/pkg/logger/logger.go
package logger
import (
"log/slog"
"os"
)
func New(service string, env string) *slog.Logger {
var handler slog.Handler
opts := &slog.HandlerOptions{
Level: parseLevel(env),
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Маскирование ПДн
switch a.Key {
case "passport":
v := a.Value.String()
if len(v) > 4 {
a.Value = slog.StringValue("***" + v[len(v)-4:])
}
case "phone":
v := a.Value.String()
if len(v) > 4 {
a.Value = slog.StringValue("***" + v[len(v)-4:])
}
}
return a
},
}
if env == "production" {
handler = slog.NewJSONHandler(os.Stdout, opts)
} else {
handler = slog.NewTextHandler(os.Stdout, opts)
}
return slog.New(handler).With(
slog.String("service", service),
slog.String("env", env),
)
}
func parseLevel(env string) slog.Level {
if env == "production" {
return slog.LevelInfo
}
return slog.LevelDebug
}Middleware: внедрение correlation_id и trace_id
// internal/pkg/middleware/logging.go
func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
span := trace.SpanFromContext(ctx)
reqLogger := logger.With(
slog.String("trace_id", span.SpanContext().TraceID().String()),
slog.String("span_id", span.SpanContext().SpanID().String()),
slog.String("correlation_id", r.Header.Get("X-Correlation-ID")),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
)
start := time.Now()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
next.ServeHTTP(ww, r.WithContext(context.WithValue(ctx, loggerKey, reqLogger)))
reqLogger.Info("request completed",
slog.Int("status", ww.Status()),
slog.Duration("duration", time.Since(start)),
slog.Int("bytes", ww.BytesWritten()),
)
})
}
}Правила логирования
- Уровни:
DEBUG→INFO→WARN→ERROR→FATAL. - Библиотека:
log/slog(Go 1.21+, стандартная библиотека). Не используемlogrus,zap—slogдостаточно и поддерживается core team. - Формат: JSON в production, Text в development.
- Персональные данные: Никогда не логируем ПДн в открытом виде. Маскирование через
ReplaceAttr:passport: ***1234,phone: ***7890. - Correlation ID: Обязателен в каждой лог-записи для сквозного трейсинга.
- Trace ID / Span ID: Автоматически из OpenTelemetry context.
- Retention: 30 дней hot (Loki), 1 год cold (S3).
- Запрещено:
fmt.Println,log.Fatal(кроме main),panicдля бизнес-ошибок.
Метрики (Prometheus)
Бизнес-метрики (Business KPIs)
| Метрика | Тип | Описание |
|---|---|---|
isp_active_subscribers_total | Gauge | Количество активных абонентов |
isp_new_connections_total | Counter | Новые подключения за период |
isp_churn_rate | Gauge | Процент оттока абонентов |
isp_revenue_total_rub | Counter | Выручка (суммарные платежи) |
isp_arpu_rub | Gauge | Средний доход на абонента (ARPU) |
isp_overdue_accounts_total | Gauge | Количество счетов с задолженностью |
isp_dunning_blocked_total | Gauge | Абоненты, заблокированные за неуплату |
Технические метрики
| Метрика | Тип | Описание |
|---|---|---|
isp_http_requests_total | Counter | Общее число HTTP-запросов (по сервису, методу, коду) |
isp_http_request_duration_seconds | Histogram | Время обработки запроса (p50, p95, p99) |
isp_rabbitmq_messages_published_total | Counter | Отправлено сообщений в RabbitMQ |
isp_rabbitmq_queue_depth | Gauge | Глубина очереди RabbitMQ (critical!) |
isp_provisioning_duration_seconds | Histogram | Время провижининга (от команды до результата) |
isp_provisioning_errors_total | Counter | Ошибки провижининга (по типу) |
isp_radius_auth_total | Counter | RADIUS авторизации (accept/reject) |
isp_radius_auth_duration_seconds | Histogram | Время ответа RADIUS |
isp_active_sessions_total | Gauge | Активные PPPoE/IPoE сессии |
isp_db_connections_active | Gauge | Активные соединения с БД |
isp_outbox_pending_events | Gauge | Неотправленные события в outbox |
Сетевые метрики
| Метрика | Тип | Описание |
|---|---|---|
isp_bras_cpu_usage_percent | Gauge | Загрузка CPU на BRAS |
isp_bras_sessions_total | Gauge | Активные сессии на BRAS |
isp_olt_port_utilization_percent | Gauge | Утилизация портов OLT |
isp_traffic_bps | Gauge | Трафик (in/out) по узлу/клиенту |
isp_packet_loss_percent | Gauge | Потери пакетов |
Distributed Tracing
Инструментация (OpenTelemetry SDK)
Каждый сервис инструментирован через OpenTelemetry SDK:
- HTTP/ConnectRPC: Автоматическая инструментация входящих/исходящих запросов.
- RabbitMQ: Трейсинг через
traceparentheader в AMQP properties. - Database: Инструментация SQL-запросов (время, запрос, таблица).
- RADIUS: Кастомные спаны для auth/acct запросов.
Go: инициализация OpenTelemetry
// internal/pkg/otel/setup.go
package otel
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/propagation"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func Setup(ctx context.Context, serviceName, version string) (shutdown func(context.Context) error, err error) {
res, _ := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
semconv.ServiceVersion(version),
semconv.DeploymentEnvironment("production"),
),
)
// Traces → OTel Collector → Tempo
traceExporter, _ := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))), // 10% sampling
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
// Metrics → Prometheus scrape
promExporter, _ := prometheus.New()
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(promExporter),
)
otel.SetMeterProvider(mp)
return func(ctx context.Context) error {
_ = tp.Shutdown(ctx)
_ = mp.Shutdown(ctx)
return nil
}, nil
}RabbitMQ: propagation через AMQP headers
// internal/pkg/rabbitmq/tracing.go
// Inject trace context в AMQP headers при publish
func InjectTraceContext(ctx context.Context, headers amqp.Table) amqp.Table {
if headers == nil {
headers = amqp.Table{}
}
otel.GetTextMapPropagator().Inject(ctx, &amqpHeaderCarrier{headers})
return headers
}
// Extract trace context из AMQP headers при consume
func ExtractTraceContext(ctx context.Context, headers amqp.Table) context.Context {
return otel.GetTextMapPropagator().Extract(ctx, &amqpHeaderCarrier{headers})
}
type amqpHeaderCarrier struct{ h amqp.Table }
func (c *amqpHeaderCarrier) Get(key string) string {
v, ok := c.h[key]
if !ok { return "" }
s, _ := v.(string)
return s
}
func (c *amqpHeaderCarrier) Set(key, value string) { c.h[key] = value }
func (c *amqpHeaderCarrier) Keys() []string {
keys := make([]string, 0, len(c.h))
for k := range c.h { keys = append(keys, k) }
return keys
}Пример трейса: "Подключение нового абонента"
[Trace: order-2024-001] Duration: 2d 4h 15m
├── [Customer Core] Create Customer 12ms
├── [Inventory] Check Availability 45ms
├── [OMS] Create Order 23ms
├── [FSM] Schedule Installation 180ms
│ └── [External] Wait for field work 2d 4h
├── [OMS] Process Completion 15ms
├── [Provisioning] Activate Service 1.2s
│ ├── [AAA] Create RADIUS Profile 34ms
│ ├── [Inventory] Bind Port 28ms
│ └── [NAS] Verify Session 850ms
└── [Notification] Send Welcome Email 120msАлертинг
Уровни severity
| Severity | Описание | Время реакции | Канал |
|---|---|---|---|
critical | Массовый сбой, абоненты без связи | 5 мин | PagerDuty + звонок |
high | Деградация сервиса, ошибки > SLA | 15 мин | Telegram + email |
medium | Аномалия, потенциальная проблема | 1 час | Telegram |
low | Информационное, тренды | 1 рабочий день | Email / Jira |
Ключевые алерты
# Critical
- alert: RadiusAuthDown
expr: up{job="freeradius"} == 0
for: 1m
severity: critical
description: 'RADIUS сервер не отвечает — абоненты не могут подключиться'
- alert: BillingQueueDepthHigh
expr: isp_rabbitmq_queue_depth{queue="billing.q.commands"} > 10000
for: 5m
severity: critical
description: 'Billing отстаёт от потока событий — возможна задержка платежей'
# High
- alert: ProvisioningErrorRateHigh
expr: rate(isp_provisioning_errors_total[5m]) > 0.1
for: 10m
severity: high
description: 'Более 10% провижининг-команд завершаются ошибкой'
- alert: OutboxBacklog
expr: isp_outbox_pending_events > 100
for: 10m
severity: high
description: 'Outbox relay отстаёт — события не публикуются в RabbitMQ'
# Medium
- alert: HighSessionCount
expr: isp_active_sessions_total > 50000
for: 15m
severity: medium
description: 'Нетипично высокое количество сессий на BRAS'SLA и SLO
Внешние SLA (для абонентов)
| Метрика | Цель | Измерение |
|---|---|---|
| Доступность интернета | 99.5% | (1 - downtime / total_time) × 100 |
| Время подключения нового абонента | ≤ 3 дня | От заявки до активации |
| Время восстановления | ≤ 4 часа | От заявки до восстановления связи |
| Скорость (от заявленной) | ≥ 80% | Средняя скорость / тариф × 100 |
Внутренние SLO (для команды)
| Сервис | Availability | Latency p99 | Error Budget (30d) |
|---|---|---|---|
| API Gateway | 99.95% | 200ms | 21.6 мин |
| Customer Core | 99.9% | 300ms | 43.2 мин |
| Billing & Finance | 99.95% | 500ms | 21.6 мин |
| Provisioning | 99.9% | 2s | 43.2 мин |
| RADIUS (FreeRADIUS) | 99.99% | 50ms | 4.3 мин |
| RabbitMQ | 99.99% | — | 4.3 мин |
Error Budget Policy
- Budget > 50%: Нормальная разработка, деплои в любое время.
- Budget 20–50%: Только проверенные изменения, обязательный canary deploy.
- Budget < 20%: Заморозка фич, фокус на стабильности и инцидентах.
- Budget = 0%: Полный freeze, только hotfixes.
Дашборды (Grafana)
Рекомендуемый набор дашбордов:
- Executive Overview — ARPU, churn, активные абоненты, выручка.
- Service Health — Availability, latency, error rate по каждому сервису.
- RabbitMQ Pipeline — Queue depth, throughput, DLQ depth.
- RADIUS & Sessions — Auth rate, reject rate, active sessions, CoA stats.
- Provisioning — Success/failure rate, duration, DLQ depth.
- Network — Трафик BRAS, утилизация портов OLT, сессии по узлам.
- Billing — Платежи, задолженности, dunning pipeline.
- Incidents — Открытые алерты, MTTR, timeline инцидентов.
Incident Runbook (шаблон)
Каждый критический алерт должен иметь привязанный runbook — пошаговую инструкцию для дежурного инженера.
Шаблон runbook
# Runbook: [Название алерта]
## Severity: critical / high
## Сервис: [billing-service / provisioning / radius / ...]
### Симптомы
- Что видит дежурный (алерт, дашборд, жалобы абонентов)
### Вероятные причины
1. [Причина 1] — как проверить
2. [Причина 2] — как проверить
3. [Причина 3] — как проверить
### Диагностика
1. Проверить дашборд: [ссылка на Grafana]
2. Проверить логи: `{service="xxx"} |= "error"` в Grafana Loki
3. Проверить трейсы: [ссылка на Tempo, фильтр по service]
4. Проверить RabbitMQ: Management UI → Queues → [queue_name]
### Действия
1. **Если [причина 1]:** [конкретные шаги]
2. **Если [причина 2]:** [конкретные шаги]
3. **Если неизвестно:** эскалация → [команда/канал]
### Эскалация
- **Через 15 мин без прогресса:** @on-call-lead
- **Через 30 мин:** @team-lead + @cto (если critical)
### После инцидента
- [ ] Заполнить postmortem
- [ ] Создать задачу на устранение root cause
- [ ] Обновить этот runbook если нужноПример: RadiusAuthDown
# Runbook: RadiusAuthDown
## Severity: critical
## Сервис: freeradius
### Симптомы
- Алерт `RadiusAuthDown` в Alertmanager
- Абоненты не могут подключиться (PPPoE/IPoE reject)
- Дашборд RADIUS: auth_total = 0
### Диагностика
1. `kubectl get pods -n radius` — pod запущен?
2. `kubectl logs -n radius freeradius-0 --tail=100` — ошибки?
3. Проверить порт 1812/UDP: `nc -zu freeradius-svc 1812`
4. Проверить связь с PostgreSQL: `kubectl exec freeradius-0 -- radiusd -XC`
### Действия
1. **Pod CrashLoopBackOff:** `kubectl describe pod` → fix config/secrets
2. **DB connection refused:** проверить PostgreSQL pod, PgBouncer
3. **OOM killed:** увеличить memory limit, проверить утечки
4. **Cert expired (если TLS):** обновить сертификаты
### Эскалация
- 5 мин без прогресса → звонок network team
- Если нужен rollback RADIUS config → @radius-adminСсылки по теме
- Безопасность и аудит: СОРМ, логирование ПДн, шифрование — Безопасность.
- Бизнес-процессы: Workflow'ы, таймауты, SLA шагов — Workflows.
- RabbitMQ: Топология exchanges/queues, DLQ — API-контракты.
- Технологии: Prometheus, Grafana, Loki, Tempo, OTel — Технологический стек.
- Термины: SLA, SLO, Error Budget, DLQ, Outbox — Глоссарий.