This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Производство редакционных партий
|
||||
|
||||
На 31 июля 2026 года строгий аудит проходит 307 из 358 созданных материалов. Остальные 51 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
||||
На 31 июля 2026 года строгий аудит проходит 310 из 358 созданных материалов. Остальные 48 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
|
||||
|
||||
## Одна партия
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# P102 — August 2026: «End-to-end наблюдаемость»
|
||||
|
||||
## Статус и фактическая граница
|
||||
|
||||
Это изолированный draft-пакет. На дату подготовки — **31 июля 2026** — август ещё не наступил. Поэтому все три текста прямо оформлены как план/сценарий на август 2026, а не как field report, внедрение, измерение, публикация или результат production-наблюдаемости. Это обязательное применение риска Р-Ф из очереди.
|
||||
|
||||
Разрешённый положительный результат кода — только `synthetic-observability-plan-hand-off`. Он не выполняет release, deployment, approval, доступ к production, чтение сети, файлов, секретов, браузера, очереди или telemetry backend. Во всех accepted result присутствует `productionEffect: not-attempted`.
|
||||
|
||||
## Исследование источников до cutoff
|
||||
|
||||
1. [OpenTelemetry Specification, immutable commit `c6520a73287040ca16499cba62cea1b3508dc4da`](https://github.com/open-telemetry/opentelemetry-specification/tree/c6520a73287040ca16499cba62cea1b3508dc4da), Release 1.29.0, 11 January 2024. Локально проверены pinned `specification/trace/api.md`: API содержит `SpanContext`, `TraceId` и определение parent context; pinned `specification/metrics/data-model.md` описывает удаление attributes и re-aggregation с меньшим набором attributes. Граница вывода: спецификация даёт терминологию и варианты обработки данных, но не подтверждает наличие SDK, collector, pipeline, telemetry или safety конкретного продукта.
|
||||
2. [W3C Trace Context, dated Recommendation 23 November 2021](https://www.w3.org/TR/2021/REC-trace-context-1-20211123/). Локально проверены дата документа, `traceparent`, `trace-id`, `parent-id`, требование переносить `traceparent`/`tracestate`, а также разделы privacy/security considerations. Граница вывода: документ задаёт переносимый HTTP context; он не доказывает прохождение context через конкретные UI, API, queue или worker и не даёт право передавать персональные данные.
|
||||
|
||||
Ни один source claim не зависит от факта после 2026-07-31. Исторические факты отделены от моей модели fixed literals.
|
||||
|
||||
## Review pass 1 — проблема, плотность, голос, независимость
|
||||
|
||||
- **Practice** начинает с утраты correlation на API → worker и цены в виде невозможности различить очередь, API и worker. Угол статьи — карта проектирования: один путь, разные trace/log/metric роли, ownership schema и запретные поля.
|
||||
- **Mechanism** начинает с ложной причинной истории из совпадающих имён сигналов. Угол — пределы вывода: propagation, error classes, time boundary, cardinality и sampling как независимые решения.
|
||||
- **Field** начинается с небезопасного hand-off без data boundary. Угол — будущий, безопасный сценарий передачи evidence/retention-вопроса; это не production report.
|
||||
- Общие формулировки убраны из открывающих абзацев. Тон M9: короткие утверждения, named boundaries, stop вместо уверенной риторики, без benchmark, incident или claim о внедрении.
|
||||
- Основной текст: 10 029 / 9 775 / 9 553 знака. Каждая статья в диапазоне 5 000–15 000 и в целевом M9 диапазоне 9 000–13 000.
|
||||
|
||||
## Review pass 2 — факты, Р-Ф и literal execution
|
||||
|
||||
- В `web/scripts/upgrade-2026-08.mjs` все данные — named fixed synthetic literals; factory применяет JSON clone и deep freeze.
|
||||
- Literal snippets выполнены через public exports. Получены именно заявленные статусы: `synthetic-observability-plan-hand-off`, `stop-broken-correlation-context`, `stop-forbidden-signal-field`.
|
||||
- Fixture: 6/6. Он проверяет hand-off без production effect, три signal entries, fail-closed отсутствие worker context, запрет `email` и disallowed positive result.
|
||||
- Code не использует часы, сеть, файлы, env, браузер, SDK, queue, collector или secrets. В источниках и тексте нет утверждений о факте августа.
|
||||
- Три статьи содержат отдельную доступную HTML-таблицу, SVG с русским alt/caption, literal executable example, ordered actions, limitations и следующий шаг.
|
||||
|
||||
## Review pass 3 — публикационное качество, mobile SVG, уникальность
|
||||
|
||||
- XML валиден для трёх SVG; safety surface scan не нашёл `script`, `foreignObject`, `javascript:`, `data:image` или event handlers.
|
||||
- Каждый SVG отрендерен Sharp в 375 px и визуально проверен. После первой мобильной проверки в графике cardinality сокращён обрезавшийся заголовок, а в investigation loop сокращена нижняя подпись. Итог: текст читается, стрелки и подписи не обрезаны.
|
||||
- Figures не декоративны: signal map фиксирует ownership по границе; tradeoff разъединяет sampling и cardinality; loop показывает допустимый hand-off и stop.
|
||||
- Строгая pairwise проверка исключает source lists и включает code: во всех трёх парах `exactParagraphs160: 0`, `common12WordFragments: 0`.
|
||||
|
||||
## Команды локальной проверки
|
||||
|
||||
Запуск из `web/`:
|
||||
|
||||
```sh
|
||||
node --check scripts/upgrade-2026-08.mjs
|
||||
node scripts/upgrade-2026-08.mjs --verify-fixture
|
||||
npm run audit:draft -- scripts/upgrade-2026-08.mjs
|
||||
node --input-type=module -e "import { createFixedObservabilityPlan, reviewFixedObservabilityHandOff, reviewFixedSignalBoundary } from './scripts/upgrade-2026-08.mjs'; const a=reviewFixedObservabilityHandOff(createFixedObservabilityPlan('ui-api-worker-plan-v1')); const b=reviewFixedSignalBoundary(createFixedObservabilityPlan('missing-worker-context-v1')); const c=reviewFixedObservabilityHandOff(createFixedObservabilityPlan('personal-field-v1')); console.log(a.status, b.status, c.status);"
|
||||
xmllint --noout public/assets/editorial/2026/end-to-end-observability-2026-signal-boundary-map.svg public/assets/editorial/2026/end-to-end-observability-2026-cardinality-sampling-tradeoff.svg public/assets/editorial/2026/end-to-end-observability-2026-investigation-evidence-loop.svg
|
||||
```
|
||||
|
||||
Результат: syntax pass; fixture `6/6`; audit pass для всех трёх статей; literal statuses совпадают с комментариями; SVG XML pass.
|
||||
|
||||
## Disposition
|
||||
|
||||
Draft готов к отдельной независимой приёмке. Registry, README, application files, articles JSON, production queue, staging, commit, push и любые Git-команды намеренно не выполнялись.
|
||||
|
||||
## Независимая приёмка основного редактора — 31.07.2026
|
||||
|
||||
Принято как **плановая августовская партия**, не как описание совершившейся работы. Во всех трёх текстах сохранены явные `2026-08` и cutoff `2026-07-31`; единственный положительный результат — `synthetic-observability-plan-hand-off` c `productionEffect: not-attempted`. Поэтому публикация не создаёт легенду о production-наблюдаемости в ещё не наступившем месяце.
|
||||
|
||||
Повторно проверены первоисточники в их зафиксированных версиях: W3C Recommendation от 23.11.2021 задаёт `traceparent`, `trace-id`, `parent-id` и разделы privacy/security; pinned OpenTelemetry source задаёт `SpanContext`/`TraceId` и допускает re-aggregation метрик при меньшем наборе attributes. Из них сделаны только словарные и модельные выводы — не вывод о наличии pipeline, SDK, collector, telemetry backend или доступа к данным конкретного продукта.
|
||||
|
||||
Три SVG прошли XML и safety-scan; их PNG-рендеры осмотрены на ширине 375 px: подписи, стрелки и границы читаемы, обрезания нет. Повторные локальные проверки: syntax, fixture `6/6`, draft audit и audit статей; literal checks возвращают `synthetic-observability-plan-hand-off`, `stop-broken-correlation-context`, `stop-forbidden-signal-field` и `stop-disallowed-positive-result` на соответствующих fixed inputs. Строгий cross-article scan, включая code и исключая source lists, дал ноль совпадающих абзацев от 160 символов и ноль общих 12-словных фрагментов во всех трёх парах.
|
||||
|
||||
В registry добавлены только три ревизии августа; пользовательские application-файлы, `articles.json`, production queue и остальная незакоммиченная работа в приёмку не включены.
|
||||
@@ -98,6 +98,7 @@ import { revisions as april2026Revisions } from '../scripts/upgrade-2026-04.mjs'
|
||||
import { revisions as may2026Revisions } from '../scripts/upgrade-2026-05.mjs';
|
||||
import { revisions as june2026Revisions } from '../scripts/upgrade-2026-06.mjs';
|
||||
import { revisions as july2026Revisions } from '../scripts/upgrade-2026-07.mjs';
|
||||
import { revisions as august2026Revisions } from '../scripts/upgrade-2026-08.mjs';
|
||||
|
||||
// This layer replaces archived source entries without losing their stable slug and date.
|
||||
export const editorialRevisions = [
|
||||
@@ -201,4 +202,5 @@ export const editorialRevisions = [
|
||||
...may2026Revisions,
|
||||
...june2026Revisions,
|
||||
...july2026Revisions,
|
||||
...august2026Revisions,
|
||||
];
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 900" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Cardinality и sampling — разные оси решения</title><desc id="desc">Слева низкокардинальные классы, справа индивидуальные данные; внизу sampling относится только к traces.</desc>
|
||||
<rect width="720" height="900" fill="#101827"/><style>text{font-family:Arial,sans-serif;fill:#f8fafc}.h{font-size:29px;font-weight:700}.s{font-size:19px;fill:#cbd5e1}.n{font-size:23px;font-weight:700}.b{font-size:20px}.safe{fill:#103b39;stroke:#59dbc8;stroke-width:3}.risk{fill:#3b202b;stroke:#fb7185;stroke-width:3}.line{stroke:#94a3b8;stroke-width:4}</style>
|
||||
<text x="40" y="55" class="h">Cardinality не лечится sampling</text><text x="40" y="85" class="s">Сначала форма поля, затем объём будущей trace-выборки.</text>
|
||||
<line x1="360" y1="135" x2="360" y2="610" class="line"/><text x="65" y="145" class="n">МАЛЫЙ СЛОВАРЬ</text><text x="425" y="145" class="n">ЛИЧНЫЕ ПОЛЯ</text>
|
||||
<rect x="48" y="180" width="270" height="115" rx="16" class="safe"/><text x="75" y="222" class="b">job-kind</text><text x="75" y="260" class="s">ограниченный класс</text>
|
||||
<rect x="48" y="330" width="270" height="115" rx="16" class="safe"/><text x="75" y="372" class="b">outcome-class</text><text x="75" y="410" class="s">ограниченный исход</text>
|
||||
<rect x="402" y="180" width="270" height="115" rx="16" class="risk"/><text x="429" y="222" class="b">email / raw user id</text><text x="429" y="260" class="s">не metric label</text>
|
||||
<rect x="402" y="330" width="270" height="115" rx="16" class="risk"/><text x="429" y="372" class="b">URL query / text</text><text x="429" y="410" class="s">не свободный attribute</text>
|
||||
<rect x="48" y="525" width="624" height="170" rx="16" class="safe"/><text x="78" y="570" class="n">SAMPLING — ОТДЕЛЬНЫЙ PLAN RULE</text><text x="78" y="610" class="b">fixed error-or-1-of-20</text><text x="78" y="650" class="s">Выбирает synthetic traces. Не разрешает новые поля.</text>
|
||||
<text x="48" y="790" class="n">Сначала fields, потом trace rule</text><text x="48" y="830" class="s">Нет measured cost, coverage или production configuration.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
+16
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 900" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Цикл планового investigation hand-off</title><desc id="desc">Вопрос проходит проверки context, data boundary и sampling; успех передаёт synthetic plan, ошибки возвращают named stop.</desc>
|
||||
<rect width="720" height="900" fill="#101827"/><style>text{font-family:Arial,sans-serif;fill:#f8fafc}.h{font-size:29px;font-weight:700}.s{font-size:19px;fill:#cbd5e1}.n{font-size:22px;font-weight:700}.b{font-size:19px}.box{fill:#172b4d;stroke:#69a7ff;stroke-width:3}.ok{fill:#103b39;stroke:#59dbc8;stroke-width:3}.stop{fill:#3b202b;stroke:#fb7185;stroke-width:3}.a{stroke:#59dbc8;stroke-width:5;fill:none}.r{stroke:#fb7185;stroke-width:5;fill:none}</style>
|
||||
<text x="40" y="55" class="h">Плановый evidence loop</text><text x="40" y="85" class="s">Не field report: только будущая область investigation.</text>
|
||||
<rect x="52" y="140" width="270" height="110" rx="16" class="box"/><text x="78" y="182" class="n">1. ВОПРОС</text><text x="78" y="218" class="b">один named path</text>
|
||||
<rect x="398" y="140" width="270" height="110" rx="16" class="box"/><text x="424" y="182" class="n">2. CONTEXT</text><text x="424" y="218" class="b">UI → API → worker</text>
|
||||
<path d="M322 195h74" class="a"/><path d="M382 181l16 14-16 14" fill="#59dbc8"/>
|
||||
<rect x="398" y="350" width="270" height="110" rx="16" class="box"/><text x="424" y="392" class="n">3. DATA BOUNDARY</text><text x="424" y="428" class="b">allow-list, no payload</text>
|
||||
<path d="M533 250v96" class="a"/><path d="M519 332l14 16 14-16" fill="none" stroke="#59dbc8" stroke-width="5"/>
|
||||
<rect x="52" y="350" width="270" height="110" rx="16" class="box"/><text x="78" y="392" class="n">4. SAMPLING</text><text x="78" y="428" class="b">rule + boundary</text>
|
||||
<path d="M398 405h-74" class="a"/><path d="M338 391l-16 14 16 14" fill="#59dbc8"/>
|
||||
<rect x="155" y="570" width="410" height="130" rx="16" class="ok"/><text x="190" y="618" class="n">5. SYNTHETIC HAND-OFF</text><text x="190" y="657" class="b">review only · production effect: not attempted</text>
|
||||
<path d="M187 460c0 55 100 70 170 105" class="a"/><path d="M345 551l16 15-22 3" fill="#59dbc8"/>
|
||||
<path d="M533 460c0 55-100 70-170 105" class="a"/><path d="M381 551l-16 15 22 3" fill="#59dbc8"/>
|
||||
<path d="M668 195h28v265h-28" class="r"/><text x="205" y="770" class="n" fill="#fda4af">нет boundary → stop</text><text x="62" y="830" class="s">Stop называет недостающий факт; не подбирает похожий evidence.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
+11
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 900" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Карта сигналов UI API worker</title><desc id="desc">Три границы одного планового пути с trace context, разными сигналами и списком запрещённых полей.</desc>
|
||||
<rect width="720" height="900" fill="#101827"/><style>text{font-family:Arial,sans-serif;fill:#f8fafc}.h{font-size:29px;font-weight:700}.s{font-size:19px;fill:#cbd5e1}.n{font-size:22px;font-weight:700}.b{font-size:19px}.box{fill:#172b4d;stroke:#69a7ff;stroke-width:3}.warn{fill:#3b202b;stroke:#fb7185;stroke-width:3}.ok{fill:#103b39;stroke:#59dbc8;stroke-width:3}</style>
|
||||
<text x="40" y="55" class="h">План: один context, три сигнала</text><text x="40" y="85" class="s">Август 2026 · источники до 31.07 · не production trace</text>
|
||||
<rect x="48" y="130" width="624" height="150" rx="16" class="box"/><text x="76" y="170" class="n">1. UI</text><text x="76" y="207" class="b">span: ui.checkout.submit</text><text x="76" y="240" class="s">context: fixed-trace-7f · route-template</text>
|
||||
<path d="M360 280v48" stroke="#59dbc8" stroke-width="5"/><path d="M346 314l14 16 14-16" fill="none" stroke="#59dbc8" stroke-width="5"/>
|
||||
<rect x="48" y="340" width="624" height="150" rx="16" class="ok"/><text x="76" y="380" class="n">2. API</text><text x="76" y="417" class="b">log: api.accepted</text><text x="76" y="450" class="s">context переносится · outcome-class</text>
|
||||
<path d="M360 490v48" stroke="#59dbc8" stroke-width="5"/><path d="M346 524l14 16 14-16" fill="none" stroke="#59dbc8" stroke-width="5"/>
|
||||
<rect x="48" y="550" width="624" height="150" rx="16" class="box"/><text x="76" y="590" class="n">3. Worker</text><text x="76" y="627" class="b">metric: worker.jobs</text><text x="76" y="660" class="s">context проверен · job-kind + outcome-class</text>
|
||||
<rect x="48" y="750" width="624" height="105" rx="16" class="warn"/><text x="76" y="790" class="n">НЕ ДОБАВЛЯТЬ</text><text x="76" y="825" class="b">email · raw user id · free text · URL query</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,263 @@
|
||||
function escapeHtml(value) {
|
||||
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||
}
|
||||
|
||||
const p = (text) => '<p>' + text + '</p>';
|
||||
const h2 = (text) => '<h2>' + text + '</h2>';
|
||||
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
|
||||
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((cell) => '<th scope="col">' + cell + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
||||
|
||||
function deepFreeze(value) {
|
||||
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
||||
Object.values(value).forEach(deepFreeze);
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function cloneFixed(value) { return JSON.parse(JSON.stringify(value)); }
|
||||
function plainText(html) { return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
|
||||
function bodyText(html) { return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
|
||||
|
||||
const REFERENCES = deepFreeze({
|
||||
otel: {
|
||||
title: 'OpenTelemetry Specification, trace API and metrics data model',
|
||||
url: 'https://github.com/open-telemetry/opentelemetry-specification/tree/c6520a73287040ca16499cba62cea1b3508dc4da',
|
||||
version: 'immutable commit c6520a73287040ca16499cba62cea1b3508dc4da, Release 1.29.0, 11 January 2024',
|
||||
},
|
||||
traceContext: {
|
||||
title: 'W3C Trace Context',
|
||||
url: 'https://www.w3.org/TR/2021/REC-trace-context-1-20211123/',
|
||||
version: 'W3C Recommendation, dated 23 November 2021',
|
||||
},
|
||||
});
|
||||
|
||||
function sources(entries) {
|
||||
return '<ul>' + entries.map(({ key, use, boundary }) => {
|
||||
const ref = REFERENCES[key];
|
||||
return '<li><a href="' + ref.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(ref.title) + '</a> — версия: ' + escapeHtml(ref.version) + '. ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>';
|
||||
}).join('') + '</ul>';
|
||||
}
|
||||
|
||||
const FIXED_SIGNAL_CASES = deepFreeze({
|
||||
'ui-api-worker-plan-v1': {
|
||||
id: 'ui-api-worker-plan-v1',
|
||||
planDate: '2026-08',
|
||||
sourceCutoff: '2026-07-31',
|
||||
path: [
|
||||
{ component: 'fixed-ui', signal: 'span: ui.checkout.submit', context: 'fixed-trace-7f', safeFields: ['route-template', 'request-kind'] },
|
||||
{ component: 'fixed-api', signal: 'log: api.accepted', context: 'fixed-trace-7f', safeFields: ['operation-name', 'outcome-class'] },
|
||||
{ component: 'fixed-worker', signal: 'metric: worker.jobs', context: 'fixed-trace-7f', safeFields: ['job-kind', 'outcome-class'] },
|
||||
],
|
||||
forbiddenFields: ['email', 'phone', 'free-text-query', 'raw-user-id', 'request-url-with-query'],
|
||||
sampling: { policy: 'fixed-error-or-1-of-20-plan', reason: 'synthetic rule; no production rate is claimed' },
|
||||
handOff: 'synthetic-observability-plan-hand-off',
|
||||
boundary: 'Fixed in-memory planning literal. No browser, HTTP request, queue, clock, collector, telemetry backend, file, secret, customer data, production system, measurement, rollout or deployment is contacted or represented.',
|
||||
},
|
||||
'missing-worker-context-v1': {
|
||||
id: 'missing-worker-context-v1', planDate: '2026-08', sourceCutoff: '2026-07-31',
|
||||
path: [
|
||||
{ component: 'fixed-ui', signal: 'span: ui.checkout.submit', context: 'fixed-trace-7f', safeFields: ['route-template'] },
|
||||
{ component: 'fixed-api', signal: 'log: api.accepted', context: 'fixed-trace-7f', safeFields: ['operation-name'] },
|
||||
{ component: 'fixed-worker', signal: 'metric: worker.jobs', context: '', safeFields: ['job-kind'] },
|
||||
], forbiddenFields: [], sampling: { policy: 'fixed-error-or-1-of-20-plan', reason: 'synthetic' }, handOff: 'synthetic-observability-plan-hand-off', boundary: 'fixed synthetic literal',
|
||||
},
|
||||
'personal-field-v1': {
|
||||
id: 'personal-field-v1', planDate: '2026-08', sourceCutoff: '2026-07-31',
|
||||
path: [
|
||||
{ component: 'fixed-ui', signal: 'span: ui.checkout.submit', context: 'fixed-trace-7f', safeFields: ['route-template', 'email'] },
|
||||
{ component: 'fixed-api', signal: 'log: api.accepted', context: 'fixed-trace-7f', safeFields: ['operation-name'] },
|
||||
{ component: 'fixed-worker', signal: 'metric: worker.jobs', context: 'fixed-trace-7f', safeFields: ['job-kind'] },
|
||||
], forbiddenFields: ['email'], sampling: { policy: 'fixed-error-or-1-of-20-plan', reason: 'synthetic' }, handOff: 'synthetic-observability-plan-hand-off', boundary: 'fixed synthetic literal',
|
||||
},
|
||||
'unsafe-positive-v1': {
|
||||
id: 'unsafe-positive-v1', planDate: '2026-08', sourceCutoff: '2026-07-31',
|
||||
path: [
|
||||
{ component: 'fixed-ui', signal: 'span: ui.checkout.submit', context: 'fixed-trace-7f', safeFields: ['route-template'] },
|
||||
{ component: 'fixed-api', signal: 'log: api.accepted', context: 'fixed-trace-7f', safeFields: ['operation-name'] },
|
||||
{ component: 'fixed-worker', signal: 'metric: worker.jobs', context: 'fixed-trace-7f', safeFields: ['job-kind'] },
|
||||
], forbiddenFields: [], sampling: { policy: 'fixed-error-or-1-of-20-plan', reason: 'synthetic' }, handOff: 'deploy-observability', boundary: 'fixed synthetic literal',
|
||||
},
|
||||
});
|
||||
|
||||
export function createFixedObservabilityPlan(id) {
|
||||
const value = FIXED_SIGNAL_CASES[id];
|
||||
return value ? deepFreeze(cloneFixed(value)) : undefined;
|
||||
}
|
||||
|
||||
export function reviewFixedSignalBoundary(plan) {
|
||||
if (!plan || plan.planDate !== '2026-08' || plan.sourceCutoff !== '2026-07-31') return deepFreeze({ status: 'stop-undated-plan', reason: 'plan-date-and-cutoff-are-required', nextAction: 'name-august-plan-and-july-cutoff' });
|
||||
if (!Array.isArray(plan.path) || plan.path.length !== 3 || plan.path.some((step) => !step.context) || new Set(plan.path.map((step) => step.context)).size !== 1) return deepFreeze({ status: 'stop-broken-correlation-context', reason: 'ui-api-worker-must-carry-one-named-fixed-context', nextAction: 'name-boundary-and-propagation-owner' });
|
||||
const unsafe = plan.path.flatMap((step) => step.safeFields).filter((field) => plan.forbiddenFields.includes(field));
|
||||
if (unsafe.length) return deepFreeze({ status: 'stop-forbidden-signal-field', reason: 'fixed-forbidden-field-is-present', fields: deepFreeze([...new Set(unsafe)]), nextAction: 'replace-with-named-low-cardinality-class-or-omit' });
|
||||
if (!plan.sampling?.policy || !plan.sampling.reason) return deepFreeze({ status: 'stop-undecided-sampling', reason: 'sampling-policy-and-boundary-are-required', nextAction: 'name-synthetic-policy-without-rate-claim' });
|
||||
return deepFreeze({ status: 'fixed-signal-boundary-map', context: plan.path[0].context, signals: deepFreeze(plan.path.map((step) => ({ component: step.component, signal: step.signal, fields: [...step.safeFields] }))), sampling: deepFreeze(cloneFixed(plan.sampling)), boundary: plan.boundary });
|
||||
}
|
||||
|
||||
export function reviewFixedObservabilityHandOff(plan) {
|
||||
const checked = reviewFixedSignalBoundary(plan);
|
||||
if (checked.status !== 'fixed-signal-boundary-map') return checked;
|
||||
if (plan.handOff !== 'synthetic-observability-plan-hand-off') return deepFreeze({ status: 'stop-disallowed-positive-result', reason: 'positive-result-is-plan-hand-off-only', nextAction: 'replace-operational-verb-with-synthetic-hand-off' });
|
||||
return deepFreeze({ status: 'synthetic-observability-plan-hand-off', signalMap: checked, productionEffect: 'not-attempted', nextAction: plan.handOff });
|
||||
}
|
||||
|
||||
export function runFixedObservabilityFixture() {
|
||||
const ready = reviewFixedObservabilityHandOff(createFixedObservabilityPlan('ui-api-worker-plan-v1'));
|
||||
const missing = reviewFixedObservabilityHandOff(createFixedObservabilityPlan('missing-worker-context-v1'));
|
||||
const personal = reviewFixedObservabilityHandOff(createFixedObservabilityPlan('personal-field-v1'));
|
||||
const unsafe = reviewFixedObservabilityHandOff(createFixedObservabilityPlan('unsafe-positive-v1'));
|
||||
return deepFreeze({ assertions: deepFreeze({
|
||||
readyIsHandOffOnly: ready.status === 'synthetic-observability-plan-hand-off' && ready.productionEffect === 'not-attempted',
|
||||
readyKeepsThreeSignals: ready.signalMap.signals.length === 3,
|
||||
missingContextStops: missing.status === 'stop-broken-correlation-context',
|
||||
personalFieldStops: personal.status === 'stop-forbidden-signal-field' && personal.fields.includes('email'),
|
||||
unsafePositiveStops: unsafe.status === 'stop-disallowed-positive-result',
|
||||
factoryClonesFrozenLiteral: Object.isFrozen(createFixedObservabilityPlan('ui-api-worker-plan-v1')),
|
||||
}) });
|
||||
}
|
||||
|
||||
function revision(meta, parts, sourceEntries) {
|
||||
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + sources(sourceEntries);
|
||||
const proseLength = bodyText(contentHtml).length;
|
||||
if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength);
|
||||
return deepFreeze({ ...meta, contentHtml, proseLength });
|
||||
}
|
||||
|
||||
const practice = revision({
|
||||
slug: 'editorial-2026-08-practice-end-to-end-observability',
|
||||
title: 'Августовский план сквозной наблюдаемости: карта сигналов от UI до worker без лишних данных',
|
||||
categories: ['Архитектура', 'Наблюдаемость'],
|
||||
cover: '/assets/editorial/2026/end-to-end-observability-2026-signal-boundary-map.svg',
|
||||
excerpt: 'Сценарий на август 2026: спроектировать trace, log и metric вокруг одного UI → API → worker пути, не превращая correlation context в канал персональных данных.', readingMinutes: 17,
|
||||
}, [
|
||||
p('В августе 2026 я бы начал не с выбора панели, а с потери одного correlation context на границе API и worker. UI ещё показывает запрос, API уже пишет строку, worker считает задачу, но три сигнала нельзя собрать в один вопрос. Цена не в отсутствии красивого trace: инженер получает три правдоподобных фрагмента и не может отделить задержку очереди от ошибки API или повторной работы worker.'),
|
||||
p('Вторая цена появляется, когда для склейки начинают добавлять email, полный URL, текст формы или сырой user id. Такой label помогает ровно до первого масштабирования: он расширяет поверхность данных и дробит series. Поэтому это не отчёт об уже выполненной августовской работе. Это датированный план: состояние источников на 31.07.2026, один fixed UI → API → worker сценарий и ограничение, что положительный результат бывает только <code>synthetic-observability-plan-hand-off</code>.'),
|
||||
h2('Сначала назвать путь, затем три разных сигнала'),
|
||||
p('Для планирования достаточно одной операции с нейтральным именем <code>ui.checkout.submit</code>. UI создаёт span, API пишет структурированный log о принятом действии, worker увеличивает счётчик jobs. Эти сигналы не взаимозаменяемы. Span отвечает на вопрос о последовательности и длительности. Log хранит объяснение одной именованной ветки. Metric пригодна для агрегированного числа однотипных работ. Если у всех один и тот же набор полей и один и тот же смысл, команда не получила три сигнала, а три хрупкие копии.'),
|
||||
p('W3C Trace Context задаёт переносимый <code>traceparent</code> и отдельно предупреждает о privacy considerations. Спецификация не говорит, что у нас есть реальная трасса, и не разрешает класть туда пользовательские значения. В августовском плане это переводится в узкое правило: переносится технический context, а бизнес-идентификатор остаётся вне него, пока не определены полномочия, срок хранения и отдельная модель доступа.'),
|
||||
figure('/assets/editorial/2026/end-to-end-observability-2026-signal-boundary-map.svg', 'Карта границ сигнала: UI создаёт trace context и span, API переносит context и пишет структурированный log, worker получает context и создаёт агрегированную metric; рядом показаны запрещённые персональные и высококардинальные поля.', 'Карта не изображает работающую систему. Она показывает, где в августовском плане владелец каждого перехода обязан назвать контекст, тип сигнала и запрещённые поля.'),
|
||||
table('Плановая карточка одного пути', ['Граница', 'Сигнал', 'Безопасные классы полей', 'Не делать вывод'], [['UI → API', 'span', 'route-template, request-kind', 'что пользователь завершил действие'], ['API', 'structured log', 'operation-name, outcome-class', 'что ответ был доставлен'], ['API → worker', 'propagation contract', 'trace context, job-kind', 'что задача выполнена'], ['worker', 'metric', 'job-kind, outcome-class', 'что известна причина каждой серии'], ['общая', 'sampling rule', 'fixed error-or-1-of-20 plan', 'что измерена доля или стоимость']]),
|
||||
h2('Correlation context — ключ к вопросу, не контейнер для данных'),
|
||||
p('Нужно различать correlation и identity. Correlation говорит: эти три synthetic записи относятся к одному named path. Identity говорит: кто именно совершил действие. Первое необходимо для исследования границы; второе часто лишнее и требует отдельной правовой и технической оценки. В fixed literal используется <code>fixed-trace-7f</code>. Он не выглядит как настоящий trace-id и не проверяет формат заголовка: его задача — сделать связь видимой, а не имитировать сетевой протокол.'),
|
||||
p('Владелец API не должен «просто пробросить всё». Он принимает контекст, создаёт дочернюю операцию или точный stop, если правила границы не описаны. Владелец worker не должен строить metric из произвольного поля очереди: он получает job-kind и outcome-class, а не payload. Владелец UI не пишет текст формы в атрибуты только потому, что он уже есть в памяти браузера. Такая дисциплина уменьшает число мест, где случайный diagnostic payload может пережить полезный срок.'),
|
||||
h2('Буквально исполнимый плановый validator'),
|
||||
code("import { createFixedObservabilityPlan, reviewFixedObservabilityHandOff } from './upgrade-2026-08.mjs';\n\nconst plan = createFixedObservabilityPlan('ui-api-worker-plan-v1');\nconst result = reviewFixedObservabilityHandOff(plan);\nconsole.log({ status: result.status, context: result.signalMap.context, effect: result.productionEffect });\n// { status: 'synthetic-observability-plan-hand-off', context: 'fixed-trace-7f', effect: 'not-attempted' }"),
|
||||
p('Этот код читает JSON-cloned и deeply frozen literal. Он не создаёт header, не вызывает SDK, не пишет log, не отправляет metric и не читает сеть. Его полезный эффект намного скромнее: validator не разрешит назвать hand-off, если пропал worker context, встречается заранее запрещённое поле или positive branch стала похожа на deployment. Для плана это лучше «успешной» демонстрации, которая молча получает доступ к окружению.'),
|
||||
h2('High cardinality лечат проектированием вопроса'),
|
||||
p('Высокая кардинальность — не свойство «плохой метрики», а следствие вопроса, который пытаются задать без группировки. <code>job-kind</code> может быть классом работ; <code>raw-user-id</code> почти всегда превращает счётчик в журнал идентификаторов. <code>outcome-class</code> годится для небольшого списка исходов; текст исключения и URL с query образуют бесконечный словарь. Если расследованию нужен индивидуальный след, в августовском плане он не подменяется metric label: ему нужен отдельный ограниченный канал и отдельная policy.'),
|
||||
p('OpenTelemetry metrics data model описывает re-aggregation и уменьшение набора attributes как способы работать с нежелательными attributes. Из этого не следует готовый лимит для конкретной команды. Наше правило консервативнее: до появления доказанного потребителя оставить только classes, а каждое новое поле связывать с вопросом, сроком хранения и местом, где оно может быть удалено. Нет вопроса — нет поля.'),
|
||||
h2('Порядок действий на август'),
|
||||
ol(['Выбрать один пользовательский путь и записать UI, API и worker как три именованные границы.', 'Для каждой границы выбрать один главный сигнал и вопрос, на который он отвечает.', 'Записать один технический correlation context и явный список полей, которые в него не попадают.', 'Назвать низкокардинальные classes для logs и metrics; свободный текст, идентификаторы и query сразу вынести в запрет.', 'Сформулировать synthetic sampling rule как план, не как измеренный процент и не как обещание экономии.', 'Прогнать fixed validator и передать только plan hand-off либо точный stop следующему review.']),
|
||||
h2('Sampling не равен отсутствию наблюдаемости'),
|
||||
p('Семплирование отвечает на вопрос, какие synthetic traces будут сохранены в плановой модели; оно не делает log и metric автоматически согласованными. Ошибка может требовать отдельного правила, а обычная ветка — детерминированного отбора. Но даже фраза «error-or-1-of-20» здесь не является production configuration: это именованная гипотеза для review. До августа не было запуска, не было доли отбора, не было стоимости хранения и не было результата расследования.'),
|
||||
p('Полезная граница — хранить в hand-off причину rule: она создана для ограничения объёма fixed model, а не для потери неприятных данных. Если позднее появится отдельная эксплуатационная работа, ей придётся показать реальные требования к ошибкам, privacy, capacity и collector. Нельзя перескочить через эту работу, сославшись на сработавший local snippet.'),
|
||||
h2('Кому принадлежит граница сигнала'),
|
||||
p('У сквозного пути нет одного технического владельца, но у каждого перехода должен быть владелец вопроса. UI-владелец отвечает за то, что route-template не превращается в полный адрес и что отправляемый context не получает данные формы. API-владелец отвечает за разбор, создание и передачу контекста, а также за словарь outcome-class. Владелец worker определяет, какие job-kind вообще существуют и почему его metric не содержит payload. Платформенный reviewer проверяет только совместимость этих решений, а не подменяет их одной общей библиотекой. Такой разбор предотвращает типичную ошибку: SDK установлен, поэтому будто бы кто-то уже отвечает за смысл атрибутов.'),
|
||||
p('Для каждой boundary полезно оставить два явных решения: кто имеет право предложить новое поле и кто обязан отклонить его без evidence. Предложение «добавим request id в metric, чтобы было проще» должно отвечать на четыре вопроса: это correlation или identity, каков допустимый словарь, как будет удалено значение и почему log или trace не решают задачу точнее. Если ответа нет, это не технический долг на будущее, а текущий запрет. Он не замедляет разработку: наоборот, не даёт перенести последствия плохой схемы на storage и людей, которые будут потом разбирать данные.'),
|
||||
h2('Как отличить schema от названия поля'),
|
||||
p('Название <code>outcome</code> ещё не делает поле безопасным. Schema появляется, когда перечислены допустимые values, граница их происхождения и реакция на неизвестное значение. В плановом контуре неизвестный outcome не надо записывать как исходный текст: он должен стать named class <code>unknown</code> вместе с отдельным вопросом к владельцу taxonomy. То же относится к <code>job-kind</code>: новая работа не дописывает произвольное имя из сообщения, а проходит новый review. Это немного менее удобно при первом прототипе, зато не позволяет случайному input расширить число series и семантику отчёта.'),
|
||||
p('Trace attributes требуют той же строгости, хотя не являются metric dimensions. У поля должна быть техническая причина: помогает ли оно различить границу, класс операции или договорённость propagation. «Может пригодиться в расследовании» не является причиной, потому что это фраза без времени, consumer и limits. В августовском плане достаточно schema note возле каждого allowed field. Когда появится отдельная authorisation на реальный pilot, именно эти notes станут входом для review, а не воспоминания автора о том, что он имел в виду под коротким именем.'),
|
||||
h2('Ограничения и следующий шаг'),
|
||||
p('Этот августовский план не доказывает propagation библиотек, формат queue message, доступность backend, качество trace, стоимость кардинальности или отсутствие персональных данных в существующих системах. W3C и OpenTelemetry здесь служат словарём для context, spans и attributes; они не подтверждают наш synthetic маршрут. Таблица и SVG также не являются architecture decision или политикой хранения.'),
|
||||
p('Следующий шаг — провести узкий design review одной карточки: у каждого поля спросить «какой вопрос, где владелец, какой срок и почему это не идентификатор». Если worker не может получить тот же named context, вернуть <code>stop-broken-correlation-context</code>. Если поле нельзя объяснить без «на всякий случай», убрать его. Только после этого имеет смысл планировать отдельный authorised discovery вне данного пакета.'),
|
||||
], [
|
||||
{ key: 'otel', use: 'Pinned OpenTelemetry source задаёт терминологию SpanContext, TraceId и attributes, а также описывает уменьшение нежелательных metric attributes.', boundary: 'Source не подтверждает наличие SDK, collector или telemetry data в этой будущей работе.' },
|
||||
{ key: 'traceContext', use: 'W3C Recommendation описывает перенос traceparent/tracestate и содержит privacy considerations.', boundary: 'Стандарт не делает synthetic fixed context реальным заголовком и не разрешает передавать персональные данные.' },
|
||||
]);
|
||||
|
||||
const mechanism = revision({
|
||||
slug: 'editorial-2026-08-mechanism-end-to-end-observability',
|
||||
title: 'Семантика сквозного сигнала на август 2026: context, ошибка, время и cardinality без ложного вывода',
|
||||
categories: ['Архитектура', 'Наблюдаемость'],
|
||||
cover: '/assets/editorial/2026/end-to-end-observability-2026-cardinality-sampling-tradeoff.svg',
|
||||
excerpt: 'Сценарий M9 на август: определить, какие выводы разрешают span, log и metric, где обрывается correlation context и почему sampling не превращает план в измерение.', readingMinutes: 18,
|
||||
}, [
|
||||
p('Самая дорогая ошибка в сквозной наблюдаемости — принять совпадение имён за общую причинную историю. API log с тем же job name, metric worker и UI span могут относиться к разным попыткам, разным часам и разным пользователям. Цена — ложный incident narrative: команда меняет worker, хотя разрыв был в propagation, или объявляет проблему очередью, хотя metric агрегировала несколько исходов.'),
|
||||
p('В августе 2026 эта статья описывает только будущий сценарий проектирования со срезом источников на 31.07.2026. Никакой trace не собирался, latency не измерялась, sampling не включался. Задача — задать семантику до инструмента: что переносит context, что означает error, где допустимо время, какие labels можно агрегировать и когда ответ обязан быть stop.'),
|
||||
h2('Один context не делает сигнал одинаковым'),
|
||||
p('Trace context нужен, чтобы связать позицию операции в графе, а не чтобы сделать span, log и metric тремя форматами одного документа. Span может указывать начало и конец ограниченной операции. Log может пояснить решение в одной ветке. Metric складывает повторяющиеся события. У этих объектов разные единицы анализа и разные риски. Если metric получает trace id как label, она начинает хранить идентификатор в агрегате и теряет назначение; если log требует длительность каждого span, он становится хранилищем timing-модели.'),
|
||||
p('W3C определяет <code>traceparent</code> как переносимый fixed-length format и отдельно различает trace-id и parent-id. В нашей модели это означает только проверяемый инвариант: UI, API и worker должны назвать один fixed context или прекратить построение end-to-end вывода. Не означает, что любая асинхронная очередь автоматически сохраняет parent relation. В реальном протоколе это потребовало бы отдельного контракта message boundary; его здесь нет.'),
|
||||
figure('/assets/editorial/2026/end-to-end-observability-2026-cardinality-sampling-tradeoff.svg', 'Диаграмма компромисса: слева безопасные классы low-cardinality полей, справа запрещённые индивидуальные значения; sampling относится к traces, а не даёт разрешения добавлять поля в metrics.', 'Схема отделяет два решения. Sampling выбирает объём trace-наблюдений; cardinality определяет форму агрегируемого измерения. Одно не компенсирует другое.'),
|
||||
table('Допустимый вывод и обязательная граница', ['Объект', 'Разрешённый вопрос', 'Нельзя заключать', 'Fail-closed повод'], [['span', 'какая named operation следует после другой в fixed model', 'что пользователь увидел результат', 'нет context'], ['log', 'какой named outcome-class записан у API', 'что ошибка уникальна или исчерпана', 'есть свободный payload'], ['metric', 'сколько synthetic jobs по small class', 'какая конкретная попытка виновата', 'label содержит identity'], ['sampling policy', 'какое правило планируется обсуждать', 'что известны затраты и coverage', 'нет причины и границы'], ['hand-off', 'материал структурно готов к review', 'есть выпуск, rollout или production evidence', 'положительный verb сильнее плана']]),
|
||||
h2('Ошибка — это смысл, а не красная точка'),
|
||||
p('У слова error минимум три слоя. Transport failure говорит, что граница вызова не завершилась ожидаемо. Domain rejection говорит, что операция дошла до правила и получила допустимый отрицательный исход. Retryable processing failure говорит, что worker может повторить работу, но не обещает её результат. Если все три записать как <code>error=true</code>, будущий reader увидит ярлык без вопроса: нужно ли искать отсутствие ответа, спор правила или повторный маршрут.'),
|
||||
p('План хранит <code>outcome-class</code>, а не текст исключения. Это не маскировка: классу можно назначить ограниченный словарь и владельца. Текст исключения может содержать input, URL, имя клиента или случайный id; его нельзя делать metric dimension. Если для расследования нужен подробный event, механизм должен быть отделён от этой статьи: другой доступ, другой retention и другой stop, а не новая строка в label map.'),
|
||||
h2('Время без часов — сначала модель, затем измерение'),
|
||||
p('Внутри synthetic plan нельзя говорить «операция заняла столько-то» даже условно, если нет определённых clock boundaries. UI время, server receive time и worker processing time не складываются без контекста очереди, ретраев и разных источников времени. Поэтому timing в этой статье обозначает только место будущего вопроса: UI ожидание, API обработка, queue delay и worker execution должны быть разными named segments. Пока таких данных нет, общее число было бы декоративным.'),
|
||||
p('Это важнее, чем кажется. Один длинный root span способен скрыть ожидание доставки и повторную работу; три коротких spans способны не показывать путь через worker, если context потерян. Механика не выбирает «правильную длительность». Она запрещает писать performance conclusion до того, как есть complete boundary, источник времени и отдельный смысл для ожидания. Такая строгость удерживает августовский план от заранее придуманного bottleneck.'),
|
||||
h2('Literal проверка семантики'),
|
||||
code("import { createFixedObservabilityPlan, reviewFixedSignalBoundary } from './upgrade-2026-08.mjs';\n\nconst incomplete = createFixedObservabilityPlan('missing-worker-context-v1');\nconst result = reviewFixedSignalBoundary(incomplete);\nconsole.log({ status: result.status, reason: result.reason });\n// { status: 'stop-broken-correlation-context', reason: 'ui-api-worker-must-carry-one-named-fixed-context' }"),
|
||||
p('Пример deliberately выбирает неполный literal. Он показывает, что correlation нельзя восстановить похожим именем job или предположением о порядке. Public export работает в памяти, клон возвращаемого объекта заморожен, сеть и часы не читаются. Stop не утверждает, что в какой-либо системе context потерян; он указывает, какой факт обязан появиться в будущем plan before any cross-boundary inference.'),
|
||||
h2('Cardinality и sampling решают разные задачи'),
|
||||
p('Cardinality растёт от количества отличающихся комбинаций attributes. Sampling отбирает traces по rule. Сэмплирование не делает безопасным label с email: меньшее число записей не меняет характер поля. И наоборот, low-cardinality outcome-class не требует trace на каждую операцию, чтобы быть пригодной metric dimension. Эти оси часто смешивают потому, что обе связаны с объёмом. Но первая определяет форму данных, а вторая — выбор наблюдений.'),
|
||||
p('OpenTelemetry metrics model допускает удалять attributes и переагрегировать data с меньшим набором attributes. Это техническая возможность, а не оправдание сначала собрать всё. В августовском сценарии safer order обратный: сначала минимальный словарь, затем explicit consumer, затем отдельное решение, можно ли расширять. Если исходное поле уже идентифицирует человека или пытается описать произвольный payload, переагрегация не отменяет сам факт лишнего сбора.'),
|
||||
h2('Propagation — контракт между состояниями, а не побочный эффект SDK'),
|
||||
p('UI создаёт client-side intent; API получает network boundary; worker получает message boundary. На каждом шаге context можно продолжить, создать новый relationship или потерять. Эти варианты нельзя спрятать под одно слово «автоматически». В будущей схеме нужно назвать carrier, точку extraction, точку injection и поведение при отсутствующем или некорректном input. Именно поведение при ошибке важнее happy path: если API не может разобрать context, он обязан выдать stop для end-to-end вывода, а не сгенерировать похожий идентификатор и склеить две истории задним числом.'),
|
||||
p('Асинхронность добавляет ещё одну границу: принятие задачи и её выполнение не обязаны быть частью одной операции времени. План не выбирает тип span и не спорит о модели links; он требует не выдавать дальнейшую связь за доказанную, пока её нет в explicit contract. Это сохраняет возможность позже выбрать технический механизм по версии SDK и требованиям команды. Сначала определяется допустимый вывод, затем структура событий. Если перепутать порядок, инструмент начнёт диктовать семантику, а передача через worker окажется «особым случаем», который никто не проверял.'),
|
||||
h2('Почему fail-closed полезнее корреляционной эвристики'),
|
||||
p('В реальных потоках имена операций, payload shape и порядок событий часто повторяются. Эвристика может решить, что log API относится к «ближайшему» trace, а metric worker — к той же попытке, потому что похожи job-kind. Для будущего incident review это опаснее честной пустоты: ответ выглядит детальным, но его невозможно оспорить на уровне контракта. Fixed validator выбран намеренно грубым: context либо один named value на трёх границах, либо связи нет. Он не пытается быть умнее данных.'),
|
||||
p('Такой stop ещё и улучшает будущий контракт. Вместо просьбы «добавьте больше логов» он показывает точное место: определить owner propagation, carrier и boundary absent-context. Вместо «сделайте метрики полезнее» — назвать class и запретить identity label. Неопределённость становится очередью конкретных решений. Это формат зрелой технической речи: не обещать, что контекст всё объяснит, а зафиксировать, какой вывод запрещён до появления названного звена.'),
|
||||
h2('Порядок семантического review'),
|
||||
ol(['Зафиксировать дату сценария и source cutoff; убрать формулировки о свершившемся августе.', 'Проверить, что каждый UI, API и worker шаг несёт один named fixed context.', 'Развести span, log и metric по вопросу, а не по удобству одного SDK.', 'Составить закрытый словарь outcome-class и отдельный список запрещённых payload-полей.', 'Пометить будущие time boundaries без чисел и не складывать интервалы до появления отдельного evidence.', 'Назвать sampling rule и его предел; передавать только synthetic hand-off или точный stop.']),
|
||||
h2('Граница доказательства'),
|
||||
p('Даже complete fixed map не доказывает, что telemetry backend примет данные, что headers дойдут через proxy, что worker согласован с API или что consumer найдёт incident быстрее. Она не подтверждает compliance, retention, access control, privacy impact или стоимость. Спецификации дают общие модели контекста и измерений, но не назначают нашу taxonomy и не делают её достаточной для конкретного продукта.'),
|
||||
p('Следующий шаг — написать контракт одной асинхронной границы как вопросник, а не конфигурацию: что именно переносится, кто создаёт новую операцию, какие error classes остаются на стороне API, какие у worker, где заканчивается payload и какой evidence способен опровергнуть связь. До такого контракта любой граф остаётся визуальной гипотезой. Это допустимо для августа как plan; недопустимо как заявленный operational fact.'),
|
||||
], [
|
||||
{ key: 'otel', use: 'Pinned OpenTelemetry source определяет SpanContext и терминологию trace API, а metrics model описывает attribute reduction/re-aggregation.', boundary: 'Он не устанавливает наш словарь outcome-class, retention или фактический telemetry pipeline.' },
|
||||
{ key: 'traceContext', use: 'W3C Recommendation определяет traceparent, trace-id, parent-id и обсуждает privacy risks.', boundary: 'Документ не доказывает propagation через конкретный browser, API, queue или worker.' },
|
||||
]);
|
||||
|
||||
const field = revision({
|
||||
slug: 'editorial-2026-08-field-end-to-end-observability',
|
||||
title: 'Августовский сценарий расследования: безопасный hand-off по UI → API → worker без production-легенды',
|
||||
categories: ['Наблюдаемость', 'Инженерная практика'],
|
||||
cover: '/assets/editorial/2026/end-to-end-observability-2026-investigation-evidence-loop.svg',
|
||||
excerpt: 'Не field report: сценарий на август 2026, как передать ограниченный synthetic evidence для будущего расследования и сохранить stop при разрыве context, data boundary или sampling rule.', readingMinutes: 17,
|
||||
}, [
|
||||
p('Расследование ломается, когда участник передаёт «есть trace, посмотри worker» без границы данных. Следующий инженер не знает, какой пользовательский путь имелся в виду, что было отобрано sampling rule, какие поля уже отфильтрованы и где оборвался context. Цена — не только потерянное время. В hand-off попадает либо слишком мало смысла для проверки, либо лишний payload, который потом начинает жить в чатах и таблицах дольше самой задачи.'),
|
||||
p('На 31.07.2026 это не отчёт из production и не описание августовского инцидента. Ни один пользовательский сценарий не был наблюдён, никакие логи не читались, ничего не измерялось. Ниже — плановый synthetic hand-off: fixed literals, заранее названные stops и один положительный исход, который разрешает лишь обсудить будущую работу, но не даёт доступа, approval, release или deployment.'),
|
||||
h2('Evidence начинается с вопроса, который можно передать'),
|
||||
p('Плохой вопрос: «почему checkout медленный?». Он смешивает UI, сеть, API, очередь, worker и продуктовый результат. Плановый вопрос уже: «в fixed scenario есть ли один named context на UI, API и worker, и не содержит ли signal map запрещённое поле?». Он не обещает найти причину. Зато другой reviewer может получить тот же status без внешней панели, устного контекста и доступа к customer data.'),
|
||||
p('В этом сценарии evidence состоит не из скриншота. Это карта трёх signals, fixed context, маленький allow-list полей, запретный список и named sampling rule. Скриншот может быть полезен в authorised investigation, но сам по себе он скрывает query, время, фильтр и доступ. Здесь нет такого артефакта, чтобы не создать ложное ощущение уже проведённого исследования.'),
|
||||
figure('/assets/editorial/2026/end-to-end-observability-2026-investigation-evidence-loop.svg', 'Цикл планового расследования: сформулировать вопрос, проверить correlation context и data boundary, проверить sampling boundary, передать synthetic hand-off или сохранить точный stop.', 'Цикл заканчивается передачей области исследования, а не выводом о причине, качестве сервиса или готовности что-либо менять.'),
|
||||
table('Карточка безопасного hand-off', ['Поле', 'Что передаётся в сценарии', 'Чего получатель не получает', 'Стоп'], [['question', 'проверка одной UI → API → worker связи', 'incident narrative', 'вопрос шире данных'], ['context', 'fixed-trace-7f', 'реальный trace header', 'одна граница пуста'], ['signal map', '3 named signal classes', 'payload, URL query, user id', 'встречается forbidden field'], ['sampling', 'fixed planned rule + reason', 'coverage, объём, SLA', 'rule не названо'], ['result', 'synthetic plan hand-off', 'production permission', 'positive verb сильнее hand-off']]),
|
||||
h2('Retaining context не означает retaining payload'),
|
||||
p('Для передачи полезен минимальный identity-free identifier, если он существует только внутри fixed literal. Он даёт reviewer возможность сравнить три записи модели. Но он не должен открывать путь к реальному профилю, заказу или сообщению. Поэтому никакого raw user id нет даже в log example. Если будущему расследованию понадобится связь с персональными данными, это должен быть отдельный authorised процесс с отдельным purpose, storage и access review. Нельзя добавить поле задним числом, потому что «иначе неудобно дебажить».'),
|
||||
p('Retention тоже нельзя выдавать за техническую мелочь. Срок определяется не возможностью collector, а тем, что именно и зачем хранится. В этом пакете retention не задан намеренно: нет реального data class и владельца. Это не пробел, который надо заполнить произвольным количеством дней. Это stop для будущей работы: пока контур не определил purpose и data boundary, rule хранения не является инженерным фактом.'),
|
||||
h2('Исполняемая проверка payload boundary'),
|
||||
code("import { createFixedObservabilityPlan, reviewFixedObservabilityHandOff } from './upgrade-2026-08.mjs';\n\nconst unsafe = createFixedObservabilityPlan('personal-field-v1');\nconst result = reviewFixedObservabilityHandOff(unsafe);\nconsole.log({ status: result.status, fields: result.fields, next: result.nextAction });\n// { status: 'stop-forbidden-signal-field', fields: ['email'], next: 'replace-with-named-low-cardinality-class-or-omit' }"),
|
||||
p('Snippet не ищет email в системе: он получает заранее названный literal, JSON-cloned и deep-frozen. Поэтому вывод строго локален: этот учебный input не проходит boundary. Он не доказывает утечку, не объявляет набор данных персональным по закону и не сообщает результат третьей стороне. Такая точность нужна, чтобы stop не стал неосторожным security claim.'),
|
||||
h2('Как идти по investigation loop'),
|
||||
p('Первый reviewer проверяет связность context. Второй проверяет семантику signals: не пытаются ли metric labels стать журналом, а logs — неструктурированным дампом. Третий читает sampling reason и заголовок hand-off: не обещает ли он того, чего fixed input не может знать. Эти роли не нужны как должности. Это три вопроса, которые нельзя заменить одним «посмотрел дашборд». В сценарии они последовательно либо уменьшают область, либо оставляют именованный stop.'),
|
||||
p('Если worker context пуст, результат <code>stop-broken-correlation-context</code>. Если в безопасных fields появляется <code>email</code>, результат <code>stop-forbidden-signal-field</code>. Если автор меняет hand-off на <code>deploy-observability</code>, validator возвращает <code>stop-disallowed-positive-result</code>. Важно не «починить статус» вручную: у каждого stop есть next action, а положительный результат не пытается выдать себя за решение о системе.'),
|
||||
h2('Порядок планового расследования в августе'),
|
||||
ol(['Записать, что это сценарий на август 2026 и что источники ограничены 31.07.2026.', 'Сузить вопрос до одного named path, без слов «вся задержка» и «все ошибки».', 'Проверить один fixed correlation context на UI, API и worker; при разрыве остановиться.', 'Проверить allow-list и forbidden-list attributes до разговора о retention или поиске данных.', 'Зафиксировать sampling как будущую гипотезу и не приписывать ей coverage, cost или effect.', 'Передать synthetic evidence map следующему reviewer либо сохранить stop и его precise next action.']),
|
||||
h2('Почему sampling не может быть оправданием для доступа'),
|
||||
p('Иногда sampling формулируют как «сохраним только немного, значит можно собирать подробнее». Это неверная логика. Отбор уменьшает количество выбранных traces, но не изменяет purpose конкретного поля и не превращает payload в outcome-class. В плановом сценарии rule нужна лишь для будущего discussion о границе объёма. Она не создаёт право читать данные, не подтверждает budget и не делает trace с персональным атрибутом приемлемой.'),
|
||||
p('Для errors правило может отличаться от normal path, но это также не доказательство, что error trace безопаснее. Ошибка часто несёт больше случайного контекста, а не меньше. Поэтому план требует сначала назвать classes и запрещённые значения, а уже затем обсуждать вариацию sampling. Если такого порядка нет, team получает не observability design, а произвольное исключение для самого чувствительного набора событий.'),
|
||||
h2('Что именно передать следующему reviewer'),
|
||||
p('Хороший hand-off меньше похож на ссылку и больше — на карточку проверки. В нём есть один вопрос, версия schema, named context, список разрешённых полей, список запрещённых полей, sampling reason, дата сценария и source cutoff. Этого достаточно, чтобы другой инженер повторил проверку literal и понял, почему результат не сильнее плана. В нём специально нет customer payload, снимка панели, «успешной» длительности и общего вывода о сервисе. Чем больше таких свободных деталей, тем труднее отличить evidence от интерпретации.'),
|
||||
p('Получатель hand-off имеет три законных действия. Он может подтвердить структуру и оставить её synthetic. Он может вернуть конкретный stop: например, потребовать owner у job-kind или найти forbidden field. Он может инициировать отдельный процесс с полномочиями на реальный discovery, но только как новый scope. Чего он не может — переназвать hand-off в approval и включить существующий pipeline. Это ограничение записано не ради формальности: в технической коммуникации именно на переходе между людьми чаще всего модель незаметно получает несуществующий production effect.'),
|
||||
h2('Как не спутать retention с архивом расследования'),
|
||||
p('Retention появляется после того, как данные уже признаны нужными для определённой цели; архив расследования возникает после того, как кто-то собрал больше, чем может объяснить. Первый вариант требует schema и политики до записи. Второй обычно оправдывает лишний payload задним числом. Августовский сценарий выбирает первый порядок, но не назначает срок: без реального data class, business purpose и access model число дней было бы произвольным. Полезная техническая работа здесь — подготовить вопросник, а не заполнить retention cell правдоподобной цифрой.'),
|
||||
p('Отдельно надо проверять производные поля. Хеш, сокращённый URL или «обезличенный» идентификатор не становятся автоматически low-cardinality class. Если значение уникально на событие, оно всё ещё дробит series; если его можно связать с субъектом, он всё ещё нуждается в отдельной оценке. Поэтому validator не пытается угадывать безопасность преобразования. Он допускает только заранее named safe classes и останавливает любой literal, где запрещённое поле уже присутствует. Это ограничение делает hand-off скромнее, но честнее.'),
|
||||
h2('Граница будущего field work'),
|
||||
p('Сценарий не запускает browser, API client, queue consumer или telemetry SDK. Он не читает headers, files, logs, metrics, secrets, dashes или сеть. Он не фиксирует инцидент, не сравнивает периоды, не подтверждает SLA и не даёт заключение о production privacy. Источники до cutoff описывают vocabulary и standard context, но не авторизуют такую деятельность и не заменяют внутреннюю policy.'),
|
||||
p('Следующий шаг — перед тем как создавать реальный инструментальный план, согласовать один data contract: какие technical identifiers разрешены, где они удаляются, кто владелец schema, какой consumer нуждается в поле и какой stop сработает при неизвестном источнике. Если это нельзя назвать, не переходить к retention table и не рисовать будущий incident graph. Вернуть hand-off как неполный план — честнее, чем задним числом сочинить evidence.'),
|
||||
], [
|
||||
{ key: 'otel', use: 'Pinned OpenTelemetry source используется для различения trace context, signal attributes и metrics vocabulary.', boundary: 'Source не утверждает, что предлагаемый hand-off достаточен для incident response, retention или privacy review.' },
|
||||
{ key: 'traceContext', use: 'W3C Recommendation фиксирует формат и перенос trace context, а также наличие privacy/security considerations.', boundary: 'Recommendation не даёт доступ к trace data и не определяет организационные полномочия расследования.' },
|
||||
]);
|
||||
|
||||
export const revisions = deepFreeze([practice, mechanism, field]);
|
||||
|
||||
if (process.argv.includes('--verify-fixture')) {
|
||||
const result = runFixedObservabilityFixture();
|
||||
const failed = Object.entries(result.assertions).filter(([, ok]) => ok !== true).map(([name]) => name);
|
||||
if (failed.length) { process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n'); process.exitCode = 1; }
|
||||
else { const count = Object.keys(result.assertions).length; process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n'); }
|
||||
}
|
||||
|
||||
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');
|
||||
Reference in New Issue
Block a user