G-SERVICE Docs
Архитектура ISP (OSS/BSS)

Observability и Мониторинг

Метрики, логирование, трейсинг, алертинг и SLA для платформы ISP.

Observability и Мониторинг

Телеком-система работает 24/7, и любой сбой напрямую влияет на абонентов. Observability — это не "мониторинг на дашборде", а способность понять, что происходит внутри системы по её внешним сигналам.

Почему ISP нужна продвинутая observability: В отличие от типичного SaaS, ISP-платформа имеет критичные зависимости от физического оборудования (OLT, BRAS, RADIUS), которое может отказывать непредсказуемо. Dunning-процесс, ошибочно заблокировавший платящего абонента — это прямой churn. Задержка provisioning > 5 минут — звонок в техподдержку. Каждый сигнал должен быть связан с бизнес-контекстом: не просто "HTTP 500", а "абонент X не может подключиться к сети после оплаты".

Три столпа Observability

Loading diagram...

Логирование (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()),
            )
        })
    }
}

Правила логирования

  • Уровни: DEBUGINFOWARNERRORFATAL.
  • Библиотека: log/slog (Go 1.21+, стандартная библиотека). Не используем logrus, zapslog достаточно и поддерживается 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_totalGaugeКоличество активных абонентов
isp_new_connections_totalCounterНовые подключения за период
isp_churn_rateGaugeПроцент оттока абонентов
isp_revenue_total_rubCounterВыручка (суммарные платежи)
isp_arpu_rubGaugeСредний доход на абонента (ARPU)
isp_overdue_accounts_totalGaugeКоличество счетов с задолженностью
isp_dunning_blocked_totalGaugeАбоненты, заблокированные за неуплату

Технические метрики

МетрикаТипОписание
isp_http_requests_totalCounterОбщее число HTTP-запросов (по сервису, методу, коду)
isp_http_request_duration_secondsHistogramВремя обработки запроса (p50, p95, p99)
isp_rabbitmq_messages_published_totalCounterОтправлено сообщений в RabbitMQ
isp_rabbitmq_queue_depthGaugeГлубина очереди RabbitMQ (critical!)
isp_provisioning_duration_secondsHistogramВремя провижининга (от команды до результата)
isp_provisioning_errors_totalCounterОшибки провижининга (по типу)
isp_radius_auth_totalCounterRADIUS авторизации (accept/reject)
isp_radius_auth_duration_secondsHistogramВремя ответа RADIUS
isp_active_sessions_totalGaugeАктивные PPPoE/IPoE сессии
isp_db_connections_activeGaugeАктивные соединения с БД
isp_outbox_pending_eventsGaugeНеотправленные события в outbox

Сетевые метрики

МетрикаТипОписание
isp_bras_cpu_usage_percentGaugeЗагрузка CPU на BRAS
isp_bras_sessions_totalGaugeАктивные сессии на BRAS
isp_olt_port_utilization_percentGaugeУтилизация портов OLT
isp_traffic_bpsGaugeТрафик (in/out) по узлу/клиенту
isp_packet_loss_percentGaugeПотери пакетов

Distributed Tracing

Инструментация (OpenTelemetry SDK)

Каждый сервис инструментирован через OpenTelemetry SDK:

  • HTTP/ConnectRPC: Автоматическая инструментация входящих/исходящих запросов.
  • RabbitMQ: Трейсинг через traceparent header в 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Деградация сервиса, ошибки > SLA15 мин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 (для команды)

СервисAvailabilityLatency p99Error Budget (30d)
API Gateway99.95%200ms21.6 мин
Customer Core99.9%300ms43.2 мин
Billing & Finance99.95%500ms21.6 мин
Provisioning99.9%2s43.2 мин
RADIUS (FreeRADIUS)99.99%50ms4.3 мин
RabbitMQ99.99%4.3 мин

Error Budget Policy

  • Budget > 50%: Нормальная разработка, деплои в любое время.
  • Budget 20–50%: Только проверенные изменения, обязательный canary deploy.
  • Budget < 20%: Заморозка фич, фокус на стабильности и инцидентах.
  • Budget = 0%: Полный freeze, только hotfixes.

Дашборды (Grafana)

Рекомендуемый набор дашбордов:

  1. Executive Overview — ARPU, churn, активные абоненты, выручка.
  2. Service Health — Availability, latency, error rate по каждому сервису.
  3. RabbitMQ Pipeline — Queue depth, throughput, DLQ depth.
  4. RADIUS & Sessions — Auth rate, reject rate, active sessions, CoA stats.
  5. Provisioning — Success/failure rate, duration, DLQ depth.
  6. Network — Трафик BRAS, утилизация портов OLT, сессии по узлам.
  7. Billing — Платежи, задолженности, dunning pipeline.
  8. 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

Ссылки по теме

On this page