docs: split integrator guide (Diplodoc) vs internal docs + add build-docs CI

Чёткое разделение audiences:
- docs/ — consumer-facing only, Diplodoc-built. Public contract для
  интеграторов (Альтум, BI, third-party).
- docs-internal/ — operator runbooks, status snapshots, legacy tech pages.
  НЕ в Diplodoc build, НЕ публикуются.

Изменения:
- Move docs/{ops,status,tech} → docs-internal/{ops,status,tech}
- docs/toc.yaml: убраны секции Operations, Архитектура (legacy), Status
- docs/index.md: убран tab "оператор / on-call", focus на integrators
- docs/README.md: rewritten — scope только integrator guide, editorial
  guidelines (no leak internal architecture, stable API contract)
- .yfm: убран явный ignore status/* (теперь не нужно — папка переехала)
- docs-internal/README.md: index для внутренней документации с rationale
  разделения

Scrubbing implementation details из Diplodoc страниц:
- events.md: убраны Strimzi/cluster.142 references, OutboxPoller детали,
  internal alert names (OrdinisOutboxLagHigh, OrdinisOutboxDLQNonEmpty),
  metric names (nsi_outbox_*). Заменены на consumer-observable contract.
- webhooks.md: убраны metric names (nsi_webhook_ssrf_rejected_total),
  alert table (OrdinisWebhookHighFailureRate и т.д.). Retry policy
  переписана на user-side perspective.
- errors.md: убран alert name OrdinisReadApiErrorBudgetBurnFast, Tempo
  endpoint URL, internal connection details.
- x-references.md: OrphanReferenceScanner → general "Ordinis периодически
  детектирует". Убраны metric/alert names. Cascade roadmap без link на
  internal design doc.
- auth.md: убран file path ScopeContext.java.
- bundle-cuod.md: убран Maven module path.
- caching.md: cascade design link заменён на text reference.

CI:
- .gitlab-ci.yml: новый job build-docs (stage: build) — Node 20 alpine,
  pnpm install --frozen-lockfile + pnpm build, artifact docs/_dist на
  неделю. Triggers: auto на docs/**, либо manual web. Готов к follow-up
  pages-deploy job когда host решён.
- Новый pattern .docs-changes для rules.

Build verified clean: 13 HTML pages, 6.9M, no broken links/refs.
This commit is contained in:
Zimin A.N.
2026-05-07 23:27:03 +03:00
parent d5268b9395
commit bcfb07f547
23 changed files with 232 additions and 215 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ Ordinis использует трёхуровневый доступ к слов
Альтернативный синтаксис ролей (для compat с другими сервисами в realm):
суффикс `-PUBLIC|INTERNAL|RESTRICTED` (например `altum-PUBLIC`,
`geoportal-INTERNAL`). Маппинг определяется в `ordinis-auth/.../ScopeContext.java`.
`geoportal-INTERNAL`). Маппинг встроен в auth layer Ordinis.
{% note info %}
+3 -2
View File
@@ -77,8 +77,9 @@ graph LR
I --supportedLevelCodes--> PL
```
(Это subset, для full графа см. отдельные schemas в коде:
`ordinis-cuod-bundle/src/main/resources/cuod/schemas/`.)
(Это subset. Полный граф можно получить через `GET /dictionaries` +
inspection schemas через `GET /dictionaries/{name}` — каждый возвращает
JSON Schema с x-references маркерами.)
## Типичные сценарии
+6 -4
View File
@@ -24,7 +24,10 @@ useQuery({
})
```
### Server-side кэш (Caffeine + Spring)
### Server-side кэш (на consumer side)
Любая стандартная in-memory cache library с TTL — Caffeine, Guava,
Redis с EXPIRE и т.д. Пример (Spring + Caffeine):
```java
@Cacheable(value = "ordinis-records", key = "#dictionary + ':' + #lang")
@@ -170,14 +173,13 @@ referencing records. Consumer должен слушать events и invalidate
def on_event(event):
cache.delete(f"ordinis:{event['dictionary']}:byKey:{event['businessKey']}")
# NEW v2: cascade source — заодно invalidate dependents
# NEW v2: cascade source — заодно invalidate источник
if event.get('cascadeSource'):
# Источник тоже нужно invalidate (мы видим только закрытие dependent)
src = event['cascadeSource']
cache.delete(f"ordinis:{src['dictionary']}:byKey:{src['businessKey']}")
```
См. design doc `dict-relationships-v2` (внутренний artifact).
Spec появится перед deployment v2.
## Stale cache на 5xx / timeout
+8 -8
View File
@@ -97,9 +97,9 @@ Ordinis следует стандартам REST с расширениями:
| Code | Описание |
|---|---|
| `internal_unexpected` | Server bug — escalate с traceId |
| `db_unavailable` | Postgres недоступен |
| `vault_unavailable` | Vault sealed либо unreachable |
| `kafka_publish_failed` | Outbox не смог publish (но запись в БД OK — eventually consistent) |
| `db_unavailable` | БД недоступна — retry с long backoff |
| `dependent_unavailable` | Зависимый сервис (config, secrets) недоступен — retry |
| `event_publish_failed` | Запись в БД ОК, но события не отправлены — eventually consistent (events доедут позже) |
## Rate limits
@@ -114,9 +114,8 @@ limits через resource constraints:
| Concurrent JWT verifications | bounded heap (~5k/sec практический максимум) |
При превышении (типично DDoS-like patterns):
- HTTP `429 Too Many Requests` после ingress rate limit (ingress nginx default).
- HTTP `429 Too Many Requests` после ingress rate limit.
- HTTP `503 Service Unavailable` при exhausting connection pool.
- `5xx error rate spike` → alert `OrdinisReadApiErrorBudgetBurnFast`.
### Recommended client-side limits
@@ -155,13 +154,14 @@ def retry_with_backoff(
## Tracing
Каждый response имеет `X-Trace-Id` header и `traceId` field в error body.
Ordinis эмитит OpenTelemetry traces в Tempo (`tempo.monitoring:4317`).
При эскалации Ordinis team:
1. Скопируй `traceId` из response либо логов.
2. Создай issue с воспроизведением.
3. Ordinis team query'ит Tempo: `traceId="abc123-..."`.
4. Trace покажет full path: ingress → read-api → postgres → response.
3. Ordinis team поднимет trace по этому ID — full path запроса виден.
Без `traceId` investigation занимает существенно больше времени —
обязательно сохраняй его при ошибках.
## Дальше
+28 -56
View File
@@ -12,53 +12,27 @@ Ordinis публикует все изменения справочников в
| `ordinis.cuod.events.internal` | INTERNAL | INTERNAL словари — нужна role `ordinis-internal`+. |
| `ordinis.cuod.events.restricted` | RESTRICTED | RESTRICTED — role `ordinis-restricted`. |
Topology: 1 broker (single-node на cluster.142), 3+ partitions per topic
(historical setup имел 12 partitions, при cluster recreate 2026-05-07
spec задал 3; existing topics annotated `strimzi.io/managed=false`,
broker partitions сохранены), `cleanup.policy: delete`,
`retention.ms: 604800000` (7 дней).
Параметры топиков (для consumer config):
- `cleanup.policy: delete`
- `retention.ms: 604800000` (7 дней)
- Multi-partition (точное число — спросить Ordinis team при registration,
partition count может меняться по operational reasons).
## Подключение
### Адреса bootstrap
### Bootstrap address + creds
| Среда | Bootstrap servers | Auth |
|---|---|---|
| prod | `ordinis-kafka-kafka-bootstrap.ordinis-prod:9092` (TLS+SASL: `:9093`) | SCRAM-SHA-512 |
| staging | `ordinis-kafka-kafka-bootstrap.ordinis-staging:9092` | SCRAM-SHA-512 |
Запросить у Ordinis team:
- Bootstrap servers (внутренний DNS на per-environment basis)
- SASL credentials (SCRAM-SHA-512 username + password)
- Topic ACL (Read + Describe на нужные топики)
- Consumer group prefix (для group ACL)
### Получение SASL креденшелов
Auth: **SASL/SCRAM-SHA-512**. TLS опциональный (отдельный listener).
Запросить у Ordinis team создание Kafka user (Strimzi `KafkaUser` CR):
```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaUser
metadata:
name: altum-consumer
namespace: ordinis-prod
labels:
strimzi.io/cluster: ordinis-kafka
spec:
authentication:
type: scram-sha-512
authorization:
type: simple
acls:
- resource:
type: topic
name: ordinis.cuod.events.public
patternType: literal
operations: [Read, Describe]
- resource:
type: group
name: altum-consumer-
patternType: prefix
operations: [Read]
```
Strimzi user-operator создаст Secret `altum-consumer` с полями `username`,
`password`. Используй их в Kafka client config.
Креденшелы выдаются per-consumer service. Не share между сервисами —
ACL и audit за каждым.
## Schema события
@@ -143,18 +117,17 @@ each businessKey изолирован.
## At-least-once семантика
Outbox pattern гарантирует **at-least-once** delivery:
1. Writer пишет запись в БД + outbox table в одной транзакции.
2. OutboxPoller (scheduled @1s) читает unpublished rows, отправляет в Kafka.
3. После Kafka ACK — отмечает row как published (`published_at` set).
Ordinis использует **outbox pattern** — изменение БД и публикация в Kafka
атомарны через transactional outbox. Гарантия: **at-least-once**.
Что это значит для consumer'а:
- Можешь получить **дубликат** события если poller crashed после ACK но до
`published_at` write. Idempotency у consumer'а — must.
- При network partition между Kafka и poller — сообщения накапливаются в
outbox (alert `OrdinisOutboxLagHigh` срабатывает > 100 events).
- Можешь получить **дубликат** события (rare, при partial failures на
стороне Ordinis). Idempotency у consumer'а — must.
- При недоступности Kafka события накапливаются в Ordinis side и
доставляются после восстановления — **events не теряются**.
- Order — гарантирован per partition key (`dictionary:businessKey`),
не глобально.
## Idempotency
@@ -167,13 +140,12 @@ if not seen:
redis.setex(f"ordinis:event:...", 86400, "1")
```
## DLQ (Dead-letter queue)
## Persistent failures (Ordinis side)
Если событие не публикуется > 1000 retry попыток (network issues,
Kafka unavailable) — переходит в DLQ table в Ordinis. Не теряется, но
требует ручного разбора.
Симптом: alert `OrdinisOutboxDLQNonEmpty` → check `/api/v1/admin/audit?action=outbox.dlq`.
Если событие не публикуется > 1000 retry попыток (multi-day Kafka outage),
оно складывается в DLQ внутри Ordinis. Не теряется, но требует ручного
разбора Ordinis team. Consumer не видит таких событий до их
re-publishing — но это очень редкий сценарий.
## Топик для discovery
+7 -18
View File
@@ -50,8 +50,7 @@ Webhook URLs валидируются перед отправкой:
- **Запрещено:** non-HTTPS scheme. Только `https://`.
- **Запрещено:** URL > 2048 chars.
Метрика `nsi_webhook_ssrf_rejected_total` increments при попытках. См.
prometheus rule `OrdinisWebhookSsrfRejected`.
При нарушении registration возвращает 422 + список причин.
## Payload
@@ -155,10 +154,11 @@ Consumer должен ответить `2xx` в течение 10 секунд:
| 4 | через 10 мин |
| 5 | через 1ч |
| 6-10 | каждые 6ч |
| 1000+ | DLQ → ручной разбор |
| После всех попыток | Ordinis team помечает delivery как failed для ручного разбора |
После 1000 неудач delivery попадает в DLQ (`/api/v1/admin/webhooks/deliveries/dlq`).
Alert: `OrdinisWebhookDLQNonEmpty`.
Если consumer недоступен > 1000 retry попыток — Ordinis team будет
расследовать. Долгосрочное unavailability стоит coordinate (planned
maintenance window) чтобы не накапливать orphan deliveries.
## Test ping
@@ -196,19 +196,8 @@ Response:
сложить в очередь.
3. **HMAC verify первым** — до парсинга JSON.
4. **Metrics** — track received / processed / errors для своего side.
Сравнивай с Ordinis metrics при расследовании.
5. **Whitelist Ordinis IP** на firewall если строгие правила. IP cluster.142
= `192.168.100.142`, но в production будет ingress IP. Спросить у Ordinis
team при registration.
## Alerting (Ordinis side)
| Alert | Trigger |
|---|---|
| `OrdinisWebhookHighFailureRate` | failure rate > 50% за 5 мин |
| `OrdinisWebhookDLQNonEmpty` | events в DLQ > 0 |
| `OrdinisWebhookDispatcherIdle` | 0 deliveries при > 0 active subscriptions |
| `OrdinisWebhookSsrfRejected` | SSRF guard сработал > 0 раз за 15 мин |
5. **Whitelist Ordinis IP** на firewall если строгие правила. Точный
адрес запросить у Ordinis team при registration.
## Дальше
+18 -16
View File
@@ -67,7 +67,7 @@ PUBLIC scope, ReferenceValidator возвращает тот же error
(`x_references_target_not_found`) что и на real not-found. Ordinis **не leak'ает**
existence of out-of-scope dictionary через differential errors.
## Orphan reference scanner
## Orphan references
Возможна ситуация когда referenced record **закрывается**, а referencing
record остаётся:
@@ -78,19 +78,20 @@ record остаётся:
3. spacecraft.RESURS-P-3 теперь имеет dangling FK
```
Periodic scanner (`OrphanReferenceScanner`, runs каждые 5 мин) находит
такие orphan'ы и emit'ит metric:
Ordinis периодически детектирует такие orphan'ы серверной стороной — данные
**не корректируются автоматически** (может быть intentional state до
update referencing записей), но Ordinis team получает уведомление и
может coordinate fix с data stewards.
```
nsi_orphan_references_total{source_dict="spacecraft",target_dict="satellite_type"} 1
```
Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин.
Consumer-side defensiveness:
- При получении event `record.closed` для записи на которую ты ссылаешься —
invalidate свой cache + либо fetch свежую версию referencing
записи (если она тоже обновилась), либо handle dangling FK явно.
## Cascade rules — roadmap (v2)
Сегодня закрытие источника НЕ closes referencing automatically. Это
дolжно явно делаться через bulk close либо individual updates.
дoлжно явно делаться через bulk close либо individual updates.
В v2 планируется schema extension `x-references-on-close`:
@@ -106,21 +107,22 @@ Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин
Опции:
- **block** — закрытие источника невозможно пока есть active referencing (default).
- **warn** — закрытие проходит, в response warning + metric `nsi_cascade_warnings_total`.
- **cascade** — все referencing закрываются в той же транзакции.
- **warn** — закрытие проходит, в response warning.
- **cascade** — все referencing закрываются в той же транзакции, отдельные
events emit'ятся per record.
См. design doc `dict-relationships-v2` (внутренний artifact в
`~/.gstack/projects/claude/`) для полного scope.
Конкретный timeline и финальная семантика будут опубликованы перед
deployment v2 — следи за анонсами от Ordinis team.
## Lineage discovery — roadmap (v2)
Сегодня нет endpoint для "что depends on X". Admin должен вручную проходить
schemas чтобы найти ссылки. В v2 планируется:
Сегодня нет endpoint для "что depends on X". В v2 планируется:
- `GET /api/v1/dictionaries/{name}/dependents` — словари которые ссылаются.
- `GET /api/v1/records/{dict}/{key}/dependents` — конкретные записи.
См. design выше для подробностей.
Это позволит consumer'ам и UI узнавать связи без чтения всех schemas
руками. Spec будет опубликован перед deployment v2.
## Best practices