Модуль 7. Observability на минимальном стеке: OpenTelemetry, Jaeger, Cloud Monitoring

Цель модуля: Сделать Serverless Git-хостинг полностью прозрачным для эксплуатации без Prometheus и Grafana. Научить студента связывать логи, метрики и трейсы через OpenTelemetry. Использовать Cloud Monitoring SDK (не Prometheus-синтаксис). Настроить cost-алерты на free-tier лимиты. Освоить yc logging read как единственный способ отладки в Cloud Functions. Внедрить правило кардинальности метрик как архитектурный запрет и PII-маскирование через middleware. Показать путь миграции на Prometheus при переезде на VPS.

Стадия роста проекта: Продакшен 24/7. Система работает. Но пришло время доказать, что система действительно надёжна — и первое, что ломается — видимость.

Перед началом: Модули 1–6

Инструменты: OpenTelemetry, Jaeger, Cloud Monitoring SDK, Cloud Logging, Yandex Cloud CLI (yc), Terraform

Трудоёмкость: ~6 ч (теория 1.5 ч · практика 3.5 ч · cost-алерты + middleware 1 ч)

🔥 Инцидент: тишина, которая страшнее ошибок

Saga работает (Модуль 6), PR merge'ятся. Но в понедельник — письмо от дежурного: «Три PR зависли в статусе MERGE_FAILED за выходные. В логах тишина. А количество saga_merge_failed_total превысило 15, и никто не понимает, на каком шаге и почему».

Ты начинаешь копать. Логи есть, но без связи. Метрики: error rate почти нулевой — потому что MERGE_FAILED это не HTTP 500, а бизнес-решение. Трейсы: в Jaeger видны обрывки — трейс есть до Message Queue, а дальше пустота.

И ещё: в Cloud Monitoring — метка repo_id для каждого push. Количество временных рядов взлетело до 10 000. А в логах кто-то логирует email пользователя — утечка PII.

Три вопроса:

  1. «Что пошло не так?» — сквозные трейсы с trace_id через Message Queue.
  2. «Насколько всё плохо?» — правильные метрики в Cloud Monitoring.
  3. «Где причина в логах?» — структурированные JSON-логи через yc logging read.

Четвёртый вопрос: «Почему мониторинг тормозит?» — взрыв кардинальности из-за ID в labels метрик.

Невидимая система неуправляема. В Serverless это критично: нет SSH на сервер. Логи — единственный спасательный круг.

Сквозной User Story: «Вася нажал Merge PR #42. Saga прошла все шаги. В этом модуле OpenTelemetry связывает все переходы в один трейс, Cloud Monitoring алертит при MERGE_FAILED > 5 за 10 минут, cost-алерт срабатывает при превышении 10 RU/с в YDB.»

📖 Теория: Три кита Observability в Serverless

Глоссарий модуля:

Span / Trace — Span: одна операция; Trace: дерево Span-ов, склеенное общим trace_id.

Cloud Monitoring — управляемый мониторинг Yandex Cloud. Замена Prometheus для Serverless.

Cloud Monitoring SDK — yandex-cloud/go-sdk/monitoring для экспорта кастомных метрик. НЕ Prometheus-синтаксис.

Cloud Logging — управляемые логи Yandex Cloud.

yc logging read — CLI-команда для чтения логов. Единственный способ отладки в Serverless.

Cost-алерт — алерт на free-tier лимиты.

Cardinality (кардинальность) — число различных значений метки. repo_id = взрыв рядов. Архитектурный запрет: никаких ID в labels.

PII (Personally Identifiable Information) — персональные данные. Логировать нельзя. Маскирование через middleware.

OpenTelemetry (OTel) — единый стандарт трейсинга/метрик/логов.

traceparent — W3C Trace Context заголовок: 00-{trace_id}-{span_id}-{flags}.

7.1. Три кита Observability

100%
🖱 Перетащите • Колесо для зума

7.2. Сквозной trace_id через Message Queue

go
// Outbox Relay: trace_id → SQS message attributes
func publishToMQ(ctx context.Context, ev OutboxEvent) error {
    msg := &sqs.SendMessageInput{
        QueueUrl:    queueURL,
        MessageBody: aws.String(ev.Payload),
        MessageAttributes: map[string]types.MessageAttributeValue{
            "traceparent": {
                DataType:    aws.String("String"),
                StringValue: aws.String(ev.TraceID),
            },
            "event_id": {
                DataType:    aws.String("String"),
                StringValue: aws.String(ev.EventID),
            },
        },
    }
    _, err := sqsClient.SendMessage(ctx, msg)
    return err
}

// Консьюмер: восстанавливаем контекст из message attributes
func (c *Consumer) handle(ctx context.Context, msg SQSMessage) error {
    traceparent := msg.MessageAttributes["traceparent"]
    ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier{
        "traceparent": traceparent,
    })
    ctx, span := otel.Tracer("saga-orchestrator").Start(ctx, "ProcessMerge")
    defer span.End()
    // ... бизнес-логика
}

Пример трейса для PR merge:

[API] POST /api/pr/42/merge              350ms  ← корневой span
  ├── [git-gateway] git merge             180ms
  │     ├── [NFS] write packfile           120ms
  │     └── [YDB] UPDATE refs               15ms
  ├── [YDB] INSERT pr_saga                 10ms
  └── [Outbox] INSERT outbox_events         8ms

[Timer] saga-orchestrator tick             50ms  ← новый трейс (Timer Trigger)
  ├── [YDB] SELECT stuck PRs               12ms
  ├── [YDB] UPDATE state=NOTIFYING          8ms
  └── [MQ] Publish notify command            5ms

[Timer] saga-orchestrator tick             45ms  ← следующий tick
  ├── [YDB] UPDATE state=INDEXING           8ms
  └── [Search] Update index                  30ms  ← здесь тормозит!

Критично: Timer Trigger создаёт новый трейс. Связь между трейсами — через pr_id в логах и trace_id в SQS message attributes.

7.3. Cloud Monitoring SDK: метрики и алерты

В Serverless нет Prometheus. Используем Cloud Monitoring SDK:

go
// ❌ АНТИПАТТЕРН: Prometheus-синтаксис (не работает в Serverless)
var sagaTransitionsTotal = promauto.NewCounterVec(...)

// ✅ ПРАВИЛЬНО: yandex-cloud/go-sdk/monitoring
import (
    "github.com/yandex-cloud/go-genproto/yandex/cloud/monitoring/v1"
    monitoring "github.com/yandex-cloud/go-sdk/monitoring"
)

type MetricsExporter struct {
    sdk *yc.SDK
}

func (m *MetricsExporter) ExportSagaTransition(fromState, toState string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    _, err := m.sdk.Monitoring().Metrics().Publish(ctx, &monitoring.PublishMetricsRequest{
        FolderId: os.Getenv("YC_FOLDER_ID"),
        Metrics: []*monitoring.Metric{
            {
                Name: "saga_transitions_total",
                Labels: map[string]string{
                    "from_state": fromState,
                    "to_state":   toState,
                },
                Type:     monitoring.Metric_COUNTER,
                Value:    1,
                Metadata: &monitoring.MetricMetadata{},
            },
        },
    })
    return err
}

func (m *MetricsExporter) ExportMergeFailed(failedStep string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    _, err := m.sdk.Monitoring().Metrics().Publish(ctx, &monitoring.PublishMetricsRequest{
        FolderId: os.Getenv("YC_FOLDER_ID"),
        Metrics: []*monitoring.Metric{
            {
                Name: "saga_merge_failed_total",
                Labels: map[string]string{
                    "failed_step": failedStep,
                },
                Type:     monitoring.Metric_COUNTER,
                Value:    1,
                Metadata: &monitoring.MetricMetadata{},
            },
        },
    })
    return err
}

func (m *MetricsExporter) ExportDLQCount(count int) error {
    // ... аналогично для dlq_messages_count
}

Алерты в Terraform:

hcl
# Cost-алерт: YDB RU/с
resource "yandex_monitoring_alert" "ydb_ru" {
  name = "ydb-ru-exceeds-free-tier"
  conditions {
    metric {
      name      = "ydb.serverless.ru_per_second"
      threshold = 8
      duration  = 300
    }
  }
  notification_channels = [yandex_monitoring_notification_channel.telegram.id]
}

# Бизнес-алерт: MERGE_FAILED > 5 за 10 минут
resource "yandex_monitoring_alert" "saga_merge_failed" {
  name = "saga-merge-failed-spike"
  conditions {
    metric {
      name      = "saga_merge_failed_total"
      threshold = 5
      duration  = 600
    }
  }
  notification_channels = [yandex_monitoring_notification_channel.telegram.id]
}

# Cost-алерт: Cloud Functions invocations
resource "yandex_monitoring_alert" "functions_invocations" {
  name = "functions-invocations-exceed-free-tier"
  conditions {
    metric {
      name      = "functions.invocations.count"
      threshold = 800000
      duration  = 3600
    }
  }
  notification_channels = [yandex_monitoring_notification_channel.telegram.id]
}

# Outbox-алерт: pending-события накапливаются
resource "yandex_monitoring_alert" "outbox_pending" {
  name = "outbox-pending-accumulating"
  conditions {
    metric {
      name      = "outbox_pending_count"
      threshold = 50
      duration  = 300
    }
  }
  notification_channels = [yandex_monitoring_notification_channel.telegram.id]
}

# DLQ-алерт: сообщения в DLQ
resource "yandex_monitoring_alert" "dlq_messages" {
  name = "dlq-messages-accumulating"
  conditions {
    metric {
      name      = "dlq_messages_count"
      threshold = 5
      duration  = 300
    }
  }
  notification_channels = [yandex_monitoring_notification_channel.telegram.id]
}

# Алерт на взрыв кардинальности
resource "yandex_monitoring_alert" "cardinality_explosion" {
  name = "cardinality-explosion"
  conditions {
    metric {
      name      = "monitoring.series.count"
      threshold = 5000  # > 5000 рядов — тревога
      duration  = 300
    }
  }
  notification_channels = [yandex_monitoring_notification_channel.telegram.id]
}

Дашборд в Terraform:

hcl
resource "yandex_monitoring_dashboard" "git_hosting" {
  name = "git-hosting-dashboard"

  widgets {
    title = "Saga Transitions"
    chart {
      targets {
        query = "saga_transitions_total"
      }
    }
  }

  widgets {
    title = "Merge Failed"
    chart {
      targets {
        query = "saga_merge_failed_total"
      }
    }
  }

  widgets {
    title = "Outbox Pending"
    chart {
      targets {
        query = "outbox_pending_count"
      }
    }
  }

  widgets {
    title = "YDB RU/s"
    chart {
      targets {
        query = "ydb.serverless.ru_per_second"
      }
    }
  }
}

7.4. yc logging read: единственный спасательный круг

bash
# Прочитать логи git-gateway за последний час
yc logging read \
  --folder-id=b1g... \
  --log-group-id=e9b... \
  --filter='function_name="git-hosting-git-gateway"' \
  --since="1h" \
  --limit=100

# Прочитать все переходы Saga для PR #42
yc logging read \
  --folder-id=b1g... \
  --filter='pr_id="42" AND message="saga_transition"' \
  --since="3d"

7.5. Корреляция: Jaeger → логи

  1. В Jaeger находишь трейс PR #42, копируешь trace_id
  2. Читаешь логи через CLI:
bash
yc logging read \
  --folder-id=b1g... \
  --filter='trace_id="abc123..."' \
  --since="1h"

Результат:

json
{
  "timestamp": "2024-01-15T10:00:15Z",
  "level": "INFO",
  "trace_id": "abc123...",
  "span_id": "789xyz",
  "pr_id": "42",
  "repo_id": "repo-1",
  "saga_state": "INDEXING",
  "from_state": "NOTIFYING",
  "to_state": "INDEXING",
  "reason": "notify_ok",
  "attempts": 0,
  "duration_ms": 45,
  "message": "saga_transition",
  "service": "saga-orchestrator",
  "function_id": "git-hosting-saga-orchestrator"
}

7.6. Кардинальность метрик (архитектурный запрет) и PII-маскирование

Правило кардинальности метрик

Правило: Никаких ID (repo_id, user_id, pr_id, event_id, commit_sha) в labels метрик. Только категории и статусы.

Почему: Каждый уникальный label = новый временной ряд. 10 000 репозиториев × метрика с repo_id = 10 000 рядов = взрыв стоимости Cloud Monitoring и деградация производительности.

Чек-лист кардинальности:

markdown
## Чек-лист кардинальности метрик

- [ ] Все labels — категории (state, status, error_type, from_state, to_state)
- [ ] Нет ID (repo_id, user_id, pr_id, event_id, commit_sha) в labels
- [ ] Максимум 5 labels на метрику
- [ ] Количество уникальных значений на label < 100
- [ ] Протестировано: при 10 000 сущностей рядов < 1000

Примеры:

go
// ❌ АНТИПАТТЕРН: repo_id в labels
metricsExporter.ExportPush(repoID, userID) // 10 000 рядов!

// ✅ ПРАВИЛЬНО: только категории
metricsExporter.ExportSagaTransition("MERGING", "NOTIFYING") // ~20 рядов
metricsExporter.ExportMergeFailed("INDEXING")                // ~5 рядов
metricsExporter.ExportConflictRetries("refs", 3)             // ~10 рядов

В ADR-SV7 добавить: «Кардинальность как архитектурный запрет: никаких ID в labels. При > 5000 рядов — ревью всех метрик».

PII-маскирование как middleware

Вынести PII-маскирование в общий middleware, который используется во всех Cloud Functions:

go
// internal/observability/pii_middleware.go
package observability

import (
    "context"
    "log/slog"
)

// piiKeys — список ключей, которые нужно маскировать
var piiKeys = map[string]bool{
    "email":        true,
    "phone":        true,
    "password":     true,
    "card_number":  true,
    "cvv":          true,
    "ssn":          true,
    "passport":     true,
    "access_token": true,
}

// NewPIISafeLogger создаёт logger с автоматическим PII-маскированием
func NewPIISafeLogger(base *slog.Logger) *slog.Logger {
    return slog.New(&piiHandler{Handler: base.Handler()})
}

type piiHandler struct {
    slog.Handler
}

func (h *piiHandler) Handle(ctx context.Context, r slog.Record) error {
    // Перебираем атрибуты и маскируем PII
    newAttrs := make([]slog.Attr, 0, r.NumAttrs())
    r.Attrs(func(a slog.Attr) bool {
        if piiKeys[a.Key] {
            a.Value = slog.StringValue("***")
        }
        newAttrs = append(newAttrs, a)
        return true
    })
    // Создаём новую запись с маскированными атрибутами
    newRecord := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
    newRecord.Add(newAttrs...)
    return h.Handler.Handle(ctx, newRecord)
}

Использование во всех Cloud Functions:

go
// cmd/api/main.go
func main() {
    logger := observability.NewPIISafeLogger(slog.Default())
    slog.SetDefault(logger)

    // Теперь все логи автоматически маскируют PII
    slog.Info("user login", "email", user.Email, "user_id", user.ID)
    // Вывод: user login email=*** user_id=abc123
}

Тест PII-маскирования:

go
func TestPIIMasking(t *testing.T) {
    var buf bytes.Buffer
    base := slog.New(slog.NewJSONHandler(&buf, nil))
    logger := observability.NewPIISafeLogger(base)

    logger.Info("test", "email", "user@example.com", "user_id", "123")

    output := buf.String()
    if strings.Contains(output, "user@example.com") {
        t.Error("PII not masked: email leaked")
    }
    if !strings.Contains(output, "***") {
        t.Error("PII masking not applied")
    }
    if !strings.Contains(output, "123") {
        t.Error("non-PII field incorrectly masked")
    }
}

Валидация traceparent (W3C Trace Context)

go
// internal/observability/trace_validation.go
package observability

import (
    "fmt"
    "regexp"
)

var traceparentRegex = regexp.MustCompile(`^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$`)

// ValidateTraceparent проверяет формат W3C Trace Context
func ValidateTraceparent(traceparent string) error {
    if traceparent == "" {
        return fmt.Errorf("empty traceparent")
    }
    if !traceparentRegex.MatchString(traceparent) {
        return fmt.Errorf("invalid traceparent format: %s", traceparent)
    }
    return nil
}

// Middleware для валидации trace контекста
func ValidateTraceContext(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        traceparent := r.Header.Get("traceparent")
        if traceparent != "" {
            if err := ValidateTraceparent(traceparent); err != nil {
                slog.Warn("invalid traceparent",
                    "error", err,
                    "traceparent", traceparent,
                )
                // Не блокируем запрос, но логируем warning
            }
        }
        next.ServeHTTP(w, r)
    })
}

Использование в Cloud Functions:

go
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/pr", handlePR)

    // Middleware применяется ко всем запросам
    handler := observability.ValidateTraceContext(mux)
    http.ListenAndServe(":8080", handler)
}

7.7. Миграция на Prometheus при переезде на VPS

СервисServerless (MVP)VPS / K8s (рост)Что меняется
МетрикиCloud Monitoring SDKPrometheus + GrafanaOTLP-экспортер → Prometheus scraper
ЛогиCloud LoggingLoki / ELKФормат JSON тот же
ТрейсыJaeger (OTLP)Jaeger (OTLP)Не меняется
АлертыCloud Monitoring AlertsAlertmanagerПравила те же

Ключевой инсайт: OpenTelemetry абстрагирует экспорт. При переезде меняется только OTLP-экспортер. Код метрик НЕ меняется.

7.8. Интеграционный тест на trace_id

go
//go:build integration

func TestTracePropagation_EndToEnd(t *testing.T) {
    req, _ := http.NewRequest("POST", "/api/pr/42/merge", nil)
    req.Header.Set("traceparent", "00-abc123def456789012345678abcdef90-1234567890abcdef-01")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    time.Sleep(10 * time.Second)

    msg, err := sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
        QueueUrl:              queueURL,
        MessageAttributeNames: []string{"traceparent"},
    })
    if err != nil {
        t.Fatalf("receive message: %v", err)
    }

    traceAttr := msg.Messages[0].MessageAttributes["traceparent"]
    if traceAttr == nil || *traceAttr.StringValue != "00-abc123def456789012345678abcdef90-1234567890abcdef-01" {
        t.Errorf("trace_id not propagated through MQ")
    }
}

7.9. Этот паттерн в архетипах

АрхетипКлючевой трейсБизнес-метрикаПочему критично
ФинтехПеревод: API → Saga → KYC → БДsaga_compensation_totalКомпенсации — бизнес-решения
Git-хостингPush: Smart HTTP → Outbox → Webhookoutbox_pending_count, saga_merge_failed_totalВебхуки теряются тихо
МессенджерСообщение: Send → Deliver → Readdelivery_latency_p99Недоставлено
ЛентаЗапрос: API → Кэш → БД → CDNcache_hit_ratioЛавина на БД

7.10. Компромиссы (Trade-offs)

РешениеПлюсМинусКогда выбирать
Cloud Monitoring SDK (Serverless)Управляемый; интеграция с YCНет PromQL; привязка к YCServerless MVP
Prometheus + Grafana (VPS)Полный контрольНужно обслуживатьРост > 1k RPS
Cloud Logging + yc CLIУправляемыйТолько через CLIServerless
OpenTelemetry (OTLP)Единый стандарт; миграция без изменения кодаOverheadВсегда
Cost-алерты на free-tierЗащита от неожиданного счётаЛожные срабатыванияВсегда для Serverless
Кардинальность как запретНет взрыва рядовОграничение детализацииВсегда
PII-middleware по умолчаниюЗащита данныхДоп. overhead на логированиеВсегда

Вывод: для Serverless MVP — Cloud Monitoring SDK + Cloud Logging + OTel + кардинальность + PII-middleware. При переезде на VPS — замена OTLP-экспортера. См. ADR-SV7.

🛠 Практические задания

Задача 1: OpenTelemetry + Jaeger ✅ Готово, когда: [ ] OTel SDK инициализирован во всех 4 Cloud Functions [ ] trace_id сохраняется в SQS message attributes [ ] Консьюмер восстанавливает контекст [ ] В Jaeger видно единое дерево трейса

Задача 2: Cloud Monitoring метрики + алерты ✅ Готово, когда: [ ] Метрики через Cloud Monitoring SDK (не Prometheus) [ ] Низкая кардинальность: нет repo_id, user_id [ ] Алерты в Terraform: YDB RU/с > 8, MERGE_FAILED > 5, invocations > 800k, outbox_pending > 50, DLQ > 5, cardinality > 5000 [ ] Дашборд в Terraform

Задача 3: yc logging read + PII masking ✅ Готово, когда: [ ] yc logging read --filter='pr_id="42"' показывает полную историю [ ] PII маскируются на *** через middleware [ ] Логи содержат trace_id, pr_id, saga_state

Задача 4: Правило кардинальности + чек-лист ✅ Готово, когда: [ ] Все метрики проверены: нет ID в labels [ ] Чек-лист кардинальности пройден [ ] Алерт cardinality_explosion в Terraform

Задача 5: Валидация traceparent ✅ Готово, когда: [ ] Middleware ValidateTraceContext применяется ко всем запросам [ ] Невалидный traceparent логируется как warning, но не блокирует запрос [ ] Тест на валидный и невалидный traceparent

Задача 6: План миграции на Prometheus ✅ Готово, когда: [ ] ADR-SV7: «Cloud Monitoring vs Prometheus» [ ] Описано, что меняется: OTLP-экспортер [ ] Описано, что НЕ меняется: код метрик, трейсы, алерты

⚠️ Антипаттерн для разбора

go
func (h *Handler) MergePR(w http.ResponseWriter, r *http.Request) {
    slog.Info("merge started")  // ❌ Нет trace_id и pr_id
    mergeTotal.WithLabelValues(repoID).Inc()  // ❌ Взрыв кардинальности
    slog.Info("user merge", "email", user.Email)  // ❌ PII без маскирования
    // ❌ Нет cost-алертов
    // ❌ Нет валидации traceparent
}

🧩 Проверь себя

1. Какое правило кардинальности метрик?
2. PII в логах — это...
3. Зачем валидировать traceparent?
4. Почему repo_id в labels метрики — антипаттерн?
5. Какой SDK использовать для метрик в Serverless?
6. Почему trace_id «обрывается» при прохождении через Message Queue?
7. Как прочитать логи Cloud Function в Serverless?
8. Timer Trigger создаёт...
9. Что меняется при миграции с Cloud Monitoring на Prometheus?
10. Cost-алерт на YDB RU/с должен срабатывать при...

🤖 Инструкция для LLM — Режим Модуля 7

Привет! Я начинаю Модуль 7 курса «Git Hosting на Go: Serverless MVP на Yandex Cloud».

Дай короткий ответ (2–3 предложения): почему trace_id обрывается при прохождении через Message Queue, и как его передать через SQS message attributes.

Выдай задание:

  1. Внедрить OpenTelemetry во все 4 Cloud Functions.
  2. Настроить Cloud Monitoring метрики через SDK и алерты в Terraform.
  3. Написать скрипт yc logging read для отладки PR #42.
  4. Внедрить правило кардинальности метрик как архитектурный запрет.
  5. Вынести PII-маскирование в middleware для всех функций.
  6. Добавить валидацию traceparent.
  7. Написать ADR-SV7: миграция Cloud Monitoring → Prometheus.

Покажи код Антипаттерна и попроси объяснить.

НЕ ПИШИ за меня. Дождись моего ответа.

📝 Чек-лист ментора

  • Логи: структурированы (JSON), содержат trace_id, pr_id, saga_state.
  • Trace Propagation: trace_id передаётся через SQS message attributes.
  • Timer Trigger: новый трейс, связь через pr_id.
  • Jaeger: видно дерево вызовов от API до saga-orchestrator.
  • Метрики: Cloud Monitoring SDK (не Prometheus), низкая кардинальность.
  • Кардинальность: чек-лист пройден, нет ID в labels, алерт cardinality_explosion.
  • PII Masking: middleware во всех функциях, email/phone/password → ***.
  • Валидация traceparent: middleware, warning при невалидном формате.
  • Cost-алерты: YDB RU/с > 8, invocations > 800k, outbox_pending > 50, MERGE_FAILED > 5, DLQ > 5.
  • Дашборд в Terraform.
  • yc logging read: студент умеет читать логи.
  • Корреляция: Jaeger → trace_id → yc logging read.
  • План миграции: ADR-SV7.
  • Интеграционный тест на trace_id.
  • Mermaid-диаграмма: передача trace_id через HTTP и MQ.

🔗 Мостик к Модулю 8 (Defense)

«Отлично, теперь мы видим всю систему как на ладони. Но как это всё задеплоить в продакшене? Как защитить секреты и проверить зависимости на уязвимости? Что будет, если NFS упадёт? В Модуле 8 мы соберём всё вместе: Terraform, GitHub Actions, Lockbox, govulncheck, DR-план с git fsck, авто-ротация HMAC, Cost-отчёт. А в финале — защита перед комитетом.»

Сквозной User Story: «Вася нажал Merge PR #42. Saga прошла все шаги, Observability алертит. В Модуле 8 мы задеплоим всё через Terraform + GitHub Actions, защитим секреты через Lockbox, и Вася сделает свой финальный push в production.»