OxL Справочник OxL
РусскийEnglish

Наблюдаемость: спаны, метрики, логи

Руководство пользователя к ТЗ Р7 и [С §15.5]. Решения: D-5240..D-5259 в docs/decisions.md; устройство — docs/ARCHITECTURE.md, раздел «Наблюдаемость».

Без настройки ничего не экспортируется и не открывается: спаны не записываются (проверка одного атомарного флага), порта метрик нет. Внутрипроцессный вызов rpc остаётся в пределах 0.2–0.3 мкс.

Быстрый старт#

oxl
# спаны, метрики и логи в OpenTelemetry Collector по gRPC
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317 OTEL_SERVICE_NAME=shop ./shop

# Prometheus: страница http://127.0.0.1:9464/metrics
./shop --metrics-listen 127.0.0.1:9464
ЧтоФлагПеременная окруженияПо умолчанию
Адрес OTLP/gRPC--otlp-endpoint urlOTEL_EXPORTER_OTLP_ENDPOINT (или …_TRACES_ENDPOINT)выкл.
СпаныOTEL_TRACES_EXPORTER=none выключаетвкл., если задан адрес
Метрики по OTLPOTEL_METRICS_EXPORTER=otlpвыкл.
Логи по OTLPOTEL_LOGS_EXPORTER=otlpвыкл.
Страница Prometheus--metrics-listen addrOXL_METRICS_LISTENвыкл.
Доля выборки--trace-sample pOXL_TRACE_SAMPLE, OTEL_TRACES_SAMPLER_ARG1.0
Имя сервисаOTEL_SERVICE_NAMEoxl
Атрибуты ресурсаOTEL_RESOURCE_ATTRIBUTES=k=v,k=v
Пауза между пакетамиOTEL_BSP_SCHEDULE_DELAY (мс)2000
Очередь / размер пакетаOTEL_BSP_MAX_QUEUE_SIZE / OTEL_BSP_MAX_EXPORT_BATCH_SIZE8192 / 512
Период метрикOTEL_METRIC_EXPORT_INTERVAL (мс)10000

Адрес: http://host:4317 (без TLS, h2c), https://host:4317 (TLS, системные корневые сертификаты) или host:port; порт по умолчанию 4317. Бездельничающий --metrics-listen 9464 слушает только 127.0.0.1; чтобы открыть наружу, укажите 0.0.0.0:9464 (страница не защищена — ставьте за фаервол).

Спаны#

Автоматически создаются спаны:

ГдеИмяВидАтрибуты
обработчик rpcpkg.Service/methodserver (если вызов пришёл по сети; иначе internal)rpc.system, rpc.service, rpc.method, rpc.grpc.status_code
вызов rpc по сетито жеclientrpc.system, rpc.method, server.address, rpc.grpc.status_code
операция std.ormdb.select, db.insert, db.update, db.upsert, db.delete, db.count, db.exec, db.begin, db.commit, db.rollbackclientdb.system, db.operation, db.collection.name (текст SQL и значения параметров в спан не попадают)
HTTP-сервер (std.http)HTTP GETserverhttp.request.method, url.path, http.response.status_code
HTTP-клиент (Client)HTTP GETclienthttp.request.method, url.full (без query), http.response.status_code

Статус ERROR ставится при любом исходе rpc, кроме ok (в том числе при доменной ошибке; сообщение — имя исхода), у HTTP-сервера — при коде 5xx, у HTTP-клиента — при 4xx/5xx и при отсутствии ответа.

Распространение контекста#

  • gRPC: заголовок метаданных traceparent (W3C Trace Context, версия 00); совместимо со всеми OpenTelemetry-SDK. Прежний x-oxl-trace продолжает отправляться и читаться.
  • HTTP (std.http): сервер читает traceparent запроса, клиент добавляет его в исходящие запросы.
  • ctx.traceId (32 hex-цифры, одинаков во всех процессах) и ctx.spanId (16 hex-цифр, спан, в котором выполняется код) доступны в обработчиках rpc.
  • Дочерние задачи (spawn, scope) наследуют текущий спан.

Выборка#

Решение принимается один раз, у корня трассы, по хэшу traceId: все процессы, увидевшие один идентификатор, решают одинаково; дальше решение родителя (флаг 01/00 в traceparent) уважается всеми. Вызов с отброшенной трассой всё равно несёт traceId и ctx.spanId, но спаны не записываются.

  • @trace(sample: 0.1) на rpc — доля выборки для трасс, корнем которых является этот rpc; 0.0 — не трассировать, 1.0 — всегда.
  • Иначе — доля процесса: --trace-sample / OXL_TRACE_SAMPLE. Значение «кластера» из §15.5 сегодня это значение процесса (см. D-5241, вопрос заказчику).

Свои спаны#

oxl
use std.obs { span, metric, log }

fn charge(order: Order) io {
  span.with("charge") {                 // закрывается при выходе, в том числе при панике
    let s = span.start("antifraud")     // или вручную
    s.set("order", "{order.id}")
    s.setInt("amount", order.cents)
    if s.recording() { /* дорогое вычисление атрибута */ }
    s.setError("score too high")          // статус ERROR
    s.end()
  }
}

Закрывайте спаны в обратном порядке открытия и в той же задаче. Если трассировка выключена или трасса не выбрана, span.start ничего не стоит.

Метрики#

oxl
let paid = metric.counter("orders_paid_total", help: "Paid orders.", labels: {"shop": "eu"})
paid.inc()
let queue = metric.gauge("queue_length")
queue.set(12.0); queue.dec()
let size = metric.histogram("payload_bytes", buckets: [64.0, 1024.0, 65536.0])
size.observe(300.0)
  • Имя — имя Prometheus ([a-zA-Z_:][a-zA-Z0-9_:]*), счётчикам принято окончание _total. Неверное имя или метка, а также имя, уже занятое метрикой другого вида, сообщается один раз на stderr, а обработчик ничего не делает.
  • Одинаковые имя и метки — одна и та же серия. Создавайте обработчик один раз и храните его: поиск идёт под мьютексом, увеличение — атомарная операция.
  • Без buckets гистограмма имеет корзины задержек в секундах: 0.0001 … 10.

Встроенные метрики (/metrics и OTLP):

МетрикаВидМетки
oxl_rpc_requests_totalcountermethod, status (ok timeout unavailable cancelled denied internal invalid app_error)
oxl_rpc_duration_secondshistogrammethod, status
oxl_rpc_inflightgauge
oxl_http_requests_total, oxl_http_request_duration_secondscounter, histogrammethod, code (HTTP-сервер)
oxl_http_client_requests_total, oxl_http_client_request_duration_secondscounter, histogrammethod, code (error — ответа нет)
oxl_orm_queries_total, oxl_orm_query_duration_secondscounter, histogramop
oxl_tls_handshakes_totalcounterresult (ok, failed)
oxl_db_tx_retries_totalcounterreason (leader_change, conflict) — повторы тела atomic(db) (D-7282) и одиночных операторов ORM вне atomic(db) (db.insert/update/upsert/delete, find, запросы; D-7300)
oxl_db_tx_conflict_totalcounter— блоки atomic(db) и одиночные операторы вне него (D-7300), завершившиеся TxConflict (и строка oxl: warning: TxConflict … в stderr, D-7282)
oxl_scheduler_workers, …_sleeping_workers, …_run_queue_length, …_background_tasks, …_context_switches_totalgauge, counter
oxl_telemetry_spans_exported_total, …_dropped_total, …_export_failures_totalcounterработа экспортёра

Задержка rpc измеряется для первых 256 вызовов метода и далее для каждого 16-го (с весом 16), счётчики вызовов точны (D-5254).

Формат страницы — Prometheus text 0.0.4. GET /metrics — 200, остальные пути — 404, не-GET — 405.

Логи#

log.info("order {id} paid") внутри обработчика rpc или спана несёт идентификаторы:

oxl
2026-11-03T15:04:05.250Z INFO  order 42 paid id=42 trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7
{"ts":"…","level":"info","msg":"order 42 paid","trace_id":"4bf9…","span_id":"00f0…","fields":{"id":"42"}}

С OTEL_LOGS_EXPORTER=otlp записи уходят и по OTLP (с trace_id/span_id в самой записи, поля — атрибутами).

Экспорт: гарантии и потери#

  • Очередь ограничена (OTEL_BSP_MAX_QUEUE_SIZE); при переполнении новые спаны отбрасываются и учитываются в oxl_telemetry_spans_dropped_total. Горячий путь никогда не ждёт сети.
  • Пакеты уходят отдельной задачей раз в OTEL_BSP_SCHEDULE_DELAY или сразу, когда набрался пакет. Недоступный сборщик не мешает программе: пакет отбрасывается и считается, первые три сбоя пишутся в stderr.
  • При нормальном завершении (и после SIGTERM, после дренажа) остаток очередей отправляется синхронно, не дольше секунды на запрос.

Проверка#

  • scripts/bench.py --rpc — задержка внутрипроцессного вызова (с выключенной трассировкой и с включённой, но отброшенной выборкой).
  • ctest telemetry_otlp: два процесса, свой mock-сборщик OTLP, проверка общего traceId, связи «родитель–потомок» клиент → сервер, /metrics, логов с идентификаторами и выборки.

Инспектор#

--inspect-ui[=addr] (по умолчанию 127.0.0.1:8090) открывает в узле веб-интерфейс состояния кластера [С III.8]: одна встроенная страница и JSON-API GET /api/<раздел> (nodes roles config dynconf services actors sagas streams traces logs rights), POST /api/action (заголовок X-Oxl-Inspect: 1). Любой узел показывает весь кластер: локальные данные других узлов запрашиваются по межузловому каналу. Тот же API — gRPC-сервис oxl.Inspect (runtime/proto/oxl/inspect.proto) на порту узла; oxl inspect --cluster host:port поднимает страницу локально. Буферы трасс и логов — 64 МБ / 15 мин на узел с UI (или на всех узлах с OXL_INSPECT_BUFFERS=1). Доступ — роль Admin или inspect (JWT), действия — отдельные роли, всё пишется в oxl.audit; в профиле dev с --insecure-dev локальный пользователь — оператор. Решения: D-6400..D-6410.

Трассы из командной строки, журнал запросов, A/B#

  • oxl trace <traceId> --cluster host:port печатает трассу из буферов узлов (раздел traces инспектора): дерево спанов по узлам и граф вызовов между ними.
  • oxl trace --record f.oxlrec --cluster host:port --for 60s [--service S] записывает входящие unary-rpc всех узлов (метод, запрос, метаданные без учётных данных, пользователь, трасса, время, ответ или ошибка) в журнал .oxlrec (формат — docs/oxlrec.md); oxl replay f.oxlrec|<traceId> --cluster host:port отправляет их снова и сравнивает ответы.
  • A/B-эксперименты ([С §18.5]): код узнаёт вариант через ab.variant("имя") (std.ab); oxl test --kind ab прогоняет записанный трафик, oxl ab start|status|stop работает в кластере. Встроенные метрики вариантов — latency.p50/p95/p99/mean (время обработчика, мс) и error_rate; пользовательские — любые метрики std.obs по имени. Значимость — t-тест Уэлча для средних и z-тест для долей, p < 0,05 при ≥ 100 запросах на вариант. Решения: D-6850..D-6866.

Исходник: docs/observability.md

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства