Pricing — operator runbook

Операционное руководство для P2 PriceRule aggregate + auto-derivation (см. ADR-0036). Описывает feature flags, env vars, метрики и readiness-пробы. Инцидентные runbooks (kafka lag, outbox stall и т.п.) — в 40-operations/runbooks/.

Feature flags

Engine + admin HTTP + readiness probe — always on. P1 stub retired после достижения production parity. Единственный feature flag — auto-derivation pipeline.

FlagDefaultЧто включает
PRICING_DERIVATION_ENABLEDfalseKafka consumer (offers.events.v1), weekly cron, manual /derivation-runs/trigger

Порядок включения в проде:

  1. Прогнать миграции 0018–0021 (price_rules / drafts / derivation_runs / qty_observed).
  2. Запустить api-server. PollingLoader.Bootstrap считает rules; /readyz красный до окончания первой загрузки.
  3. Засеять operator-curated PriceRule через admin HTTP (JWT role: pricing_admin). Snapshot догонит через PRICING_SNAPSHOT_POLL_INTERVAL (default 10s).
  4. Через несколько часов поднять PRICING_DERIVATION_ENABLED=true. Сначала появятся drafts; CF3 ≥ 0.9 → auto-applied derived rules.

Откат derivation: flip PRICING_DERIVATION_ENABLED=false + restart. Engine продолжает работать на existing rules; новые drafts не появляются.

Env vars

Полный набор — в deploy/.env.example секция Pricing engine. Применимые к api-server:

VarDefaultTuning guidance
PRICING_SNAPSHOT_POLL_INTERVAL10sIncremental fetch cadence. Поднять до 30s если PG нагружен write-heavy.
PRICING_SNAPSHOT_FULL_RELOAD_INTERVAL5mПолный rebuild snapshot. Самостраховка от drift; не трогать без причины.
PRICING_CF3_AUTO_APPLY_THRESHOLD0.9Порог авто-применения derived rules. Понижать → больше automation, выше риск.
PRICING_CF3_HALF_LIFE_DAYS14Скорость затухания старых наблюдений. Меньше → сильнее recency-bias.
PRICING_CF3_CV_MULTIPLIER2.0Penalty за вариативность. Поднять, если drafts шумят.
PRICING_CF3_SAMPLE_SATURATION10Bonus saturation. Меньше = быстрее насыщение confidence.
PRICING_CF3_MIN_SAMPLES3Минимум обсёрвейшенов для derivation. Опускать ниже 3 нельзя (см. spec §4.3.2).

Readiness

Pricing регистрирует один health.Checker — pricing.snapshot:

  • Не готов до завершения PollingLoader.Bootstrap. /readyz возвращает 503 с component=pricing.snapshot.
  • Готов после первой загрузки snapshot. Дальнейшие incremental/full reload failures не flip pricing.snapshot обратно — это шум который должен ловиться через метрику, не через probe.

Метрики (counters)

Все counters пишутся в slog (level INFO/WARN). Pull-through OTel/Prometheus exporter планируется отдельной задачей; до тех пор alerting — через event=pricing.metrics.* в log aggregator.

MetricLabelsКогда увеличивается
pricing_derivation_runs_totalstatus ∈ {ok, skipped, failed}Каждое завершение Deriver.Execute.
pricing_snapshot_reload_totalmode × resultПосле tick / fullReload в PollingLoader.
pricing_outbox_emit_totalevent_kind × resultПосле каждой Create/Update/Deprecate/UpsertDerived в CrudSvc.
pricing_overlap_conflicts_totalКогда OverlapValidator отклоняет mutation.

Alerts (пример):

  • rate(pricing_derivation_runs_total{status="failed"}[5m]) > 0 — derivation failures, P2.
  • rate(pricing_snapshot_reload_total{result="failed"}[5m]) > 0 — DB unreachable, P1.
  • rate(pricing_overlap_conflicts_total[1h]) > N (тюнинг по нагрузке) — operators ставят пересекающиеся правила, P3.

Что делать при инциденте

СимптомПервый шаг
/readyz красный после deploy, причина pricing.snapshotПроверить PG доступность (postgres checker). Если PG жив но Bootstrap зависает — глянуть pricing.snapshot.bootstrap log + длинные locks/queries в pg_stat_activity.
Шквал pricing.metrics.derivation_run со status=failedГлянуть последние commit’ы pricing/app/derivation; временно PRICING_DERIVATION_ENABLED=false чтобы остановить шум.
Operator жалуется что rule “не применился”Проверить snapshot lag (pricing.snapshot.reload events). Snapshot — eventual; до 10s+5m задержки — норма.
Drafts ноль за неделюCF3 threshold слишком высокий (или нет наблюдений ≥ 3 supplier credentials). Глянуть PRICING_CF3_MIN_SAMPLES + samples.

Связано

CustomPricingHandler registry (P3 closure)

P3 plugin extension point. См. ADR-0039.

Built-in Go handlers compile-time registered. WASM/DSL runtime — Phase 4+.

Env vars

VarDefaultTuning
PRICING_HANDLER_POLL_INTERVAL10sIncremental fetch cadence
PRICING_HANDLER_FULL_RELOAD_INTERVAL5mFull rebuild safety net

Built-in handlers

HandlerIDЧто делает
volume_discount_v1Tier-based percentage discount: qty≥10→-5%, ≥50→-8%, ≥100→-12%
contract_markup_v1Fixed +5% markup (для customers с offline contract)

GET /api/v1/admin/pricing/handlers — список из api-server runtime.

Trigger predicate (v1)

JSON equality predicate. Allowed keys: supplier, customer_type, customer_id, canonical_id, observation.has_volume_discount, observation.on_request. Multiple keys = AND. Array value = IN-clause.

Example:

{"supplier": "etm", "observation.has_volume_discount": false}

Метрики (counters)

MetricLabelsSource
pricing_handler_invocation_totalhandler_ref × result ∈ {ok, timeout, error, panic, unknown_handler}Registry.InvokeMatching
pricing_handler_snapshot_reload_totalmode × resulthandler PollingLoader

Triage

СимптомШаг
Шквал pricing.metrics.handler_invocation{result=timeout}Handler превышает 50ms. Проверить handler latency profile. Может быть infinite loop в built-in.
result=unknown_handlerOperator зарегистрировал handler_ref которого нет в инвентаре (handler retired или typo).
Operator ожидает custom logic, но не appliedGlance trigger predicate vs HandlerInvocationContext. Use GET /handlers для список валидных IDs.
result=panicBuilt-in handler bug. Найти по pricing.handler.invocation_panic slog event.

Развёртывание новых built-in handlers

P3 — handlers compile-time. Требует rebuild api-server binary с новым handler в core/pricing/handlers/builtin/. Steps:

  1. Реализовать Handler interface (Descriptor() + Apply())
  2. Регистрация через fx group pricing.handlers в pricing/fx.go
  3. Deploy api-server
  4. Operator активирует handler через POST /handler-registrations с handler_ref новой ID

Связанное