{"id":481425,"date":"2026-05-28T14:09:07","date_gmt":"2026-05-28T14:09:07","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=481425"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=481425","title":{"rendered":"\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0442\u0440\u0435\u0439\u0441\u0438\u043d\u0433 \u0432 Node.js: @cleverbrush\/log \u0438 @cleverbrush\/otel"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0421\u0442\u0430\u0442\u044c\u044f \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0441\u0442\u044c (observability) \u0432  \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u0441 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u043c \u043a\u043e\u0434\u043e\u043c, \u0430 \u0431\u043e\u043d\u0443\u0441\u043e\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043b\u043e\u0433\u0438 \u0441 \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0448\u0430\u0431\u043b\u043e\u043d\u0430\u043c\u0438, \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u043a\u043e\u0440\u0440\u0435\u043b\u044f\u0446\u0438\u044e \u0441\u043e \u0441\u043f\u0430\u043d\u0430\u043c\u0438 OpenTelemetry, \u0432\u0441\u0451 \u044d\u0442\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u0430\u0431\u043e\u0440\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u044f \u043d\u0430\u0437\u044b\u0432\u0430\u044e CleverBrush Framework.<\/p>\n<p>\u0412\u0441\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u043d\u0438\u0436\u0435 \u0432\u0437\u044f\u0442\u044b \u0438\u0437 <a href=\"https:\/\/xpenser.cleverbrush.com\" rel=\"noopener noreferrer nofollow\">xpenser<\/a> \u2014 open-source \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u0430 \u043b\u0438\u0447\u043d\u044b\u0445 \u0434\u043e\u0445\u043e\u0434\u043e\u0432 \u0438 \u0440\u0430\u0441\u0445\u043e\u0434\u043e\u0432. \u0421 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u044d\u0442\u043e \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0435\u0439 Cleverbrush Framework, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u0432\u0441\u0435\u0439 \u043c\u0430\u0448\u0438\u043d\u0435\u0440\u0438\u0438, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0435\u0441\u0442\u044c \u0432 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0435, \u0442\u0430\u043a\u043e\u0439 \u043a\u0430\u043a: \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b, \u0441\u0435\u0440\u0432\u0435\u0440, \u043a\u043b\u0438\u0435\u043d\u0442, auth, \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 OpenTelemetry. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u044d\u0442\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u044f \u0441\u0430\u043c \u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0441\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0444\u0438\u043d\u0430\u043d\u0441\u043e\u0432. \u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043e\u0442\u043a\u0440\u044b\u0442 \u043d\u0430 GitHub: <a href=\"https:\/\/github.com\/cleverbrush\/xpenser\" rel=\"noopener noreferrer nofollow\">github.com\/cleverbrush\/xpenser<\/a>.<\/p>\n<p><strong>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440:<\/strong> \u0432\u0441\u0435 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043d\u043e\u0441\u044f\u0442 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440. \u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u044d\u0442\u043e, \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0443 \u043d\u0438\u0445 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0445\u043e\u0440\u043e\u0448\u0435\u0435.<\/p>\n<h3>\u041f\u0440\u0435\u0434\u044b\u0441\u0442\u043e\u0440\u0438\u044f<\/h3>\n<p>\u0412 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445 (<a href=\"https:\/\/habr.com\/ru\/articles\/1027922\/\" rel=\"noopener noreferrer nofollow\">\u043f\u0435\u0440\u0432\u0430\u044f<\/a> \u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1030342\/\" rel=\"noopener noreferrer nofollow\">\u0432\u0442\u043e\u0440\u0430\u044f<\/a>) \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u043b \u043e <code>@cleverbrush\/schema<\/code> \u0438 \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043d\u0430 \u0435\u0451 \u043e\u0441\u043d\u043e\u0432\u0435 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u044b <code>@cleverbrush\/server<\/code> \u0438 <code>@cleverbrush\/client<\/code>. \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0440\u0435\u0447\u044c \u043f\u043e\u0439\u0434\u0451\u0442 \u043e\u0431 \u0435\u0449\u0451 \u0434\u0432\u0443\u0445 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0445 \u0442\u043e\u0433\u043e \u0436\u0435 \u043c\u043e\u043d\u043e\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f: <code>@cleverbrush\/log<\/code> \u2014 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0438 <code>@cleverbrush\/otel<\/code> \u2014 \u0442\u043e\u043d\u043a\u0430\u044f \u043e\u0431\u0451\u0440\u0442\u043a\u0430 \u043d\u0430\u0434 OpenTelemetry SDK.<\/p>\n<p>\u041e\u0431\u0430 \u043f\u0430\u043a\u0435\u0442\u0430 \u0438\u0437\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u043a\u0430\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u043d\u043e \u043c\u043e\u0433\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u2014 \u0432 \u043b\u044e\u0431\u043e\u043c Node.js-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438. \u0412 xpenser \u043e\u043d\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0441\u0440\u0430\u0437\u0443 \u0432 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430\u0445: API, Next.js web app \u0438 Telegram bot.<\/p>\n<h3>@cleverbrush\/log: \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/h3>\n<p>\u0418\u0434\u0435\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043f\u0440\u0438\u0448\u043b\u0430 \u0438\u0437 \u044d\u043a\u043e\u0441\u0438\u0441\u0442\u0435\u043c\u044b .NET: \u0442\u0430\u043c \u0435\u0441\u0442\u044c Serilog \u0441 \u0435\u0433\u043e message templates. \u0418\u0434\u0435\u044f \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0442\u043e\u043c \u0447\u0442\u043e\u0431\u044b \u0432\u043c\u0435\u0441\u0442\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u044b \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d \u0441 \u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440\u0430\u043c\u0438, \u0430 \u0432\u0441\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043a\u0430\u043a \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c \u2014 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043d\u0430\u0439\u0442\u0438 \u0432\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043f\u043e <code>TransactionId<\/code> \u0438\u043b\u0438 <code>UserId<\/code>.<\/p>\n<p>\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0432 xpenser \u0443\u0434\u043e\u0431\u043d\u043e \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u043c \u0448\u0430\u0431\u043b\u043e\u043d\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">import { number, object, parseString } from '@cleverbrush\/schema';const TransactionCreated = parseString(    object({ TransactionId: number(), UserId: number() }),    $t =&gt;        $t`Transaction ${t =&gt; t.TransactionId} created by ${t =&gt; t.UserId}`);logger.info(TransactionCreated, {    TransactionId: transaction.id,    UserId: principal.userId});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430 <code>{TransactionId}<\/code> \u0438 <code>{UserId}<\/code> \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0442 \u0442\u0435\u043a\u0441\u0442\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u0430 \u0441\u0430\u043c \u0448\u0430\u0431\u043b\u043e\u043d \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u043c: <code>Transaction {TransactionId} created by {UserId}<\/code>. \u0412 SigNoz, Clickstack \u0438\u043b\u0438 \u043b\u044e\u0431\u043e\u0439 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0449\u0435\u0439 CLEF-\u0444\u043e\u0440\u043c\u0430\u0442, \u044d\u0442\u0438 \u043f\u043e\u043b\u044f \u043c\u043e\u0436\u043d\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0430\u0433\u0440\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u0430\u043a \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0435 \u043a\u043e\u043b\u043e\u043d\u043a\u0438.<\/p>\n<p>\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u0443\u0440\u043e\u0432\u043d\u0438: trace, debug, info, warn, error, fatal. \u041c\u0435\u0442\u043e\u0434\u044b <code>logger.error()<\/code> \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0442 \u043e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 <code>Error<\/code> \u043f\u0435\u0440\u0432\u044b\u043c \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u043e\u043c. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a MCP transport \u0432 xpenser \u043f\u0438\u0448\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043e\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f:<\/p>\n<pre><code class=\"typescript\">transport. =&gt; {    logger.error(error, McpTransportError, {        UserId: apiKeyPrincipal.userId    });};<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c9e\/105\/75d\/c9e10575d01d6b54e6e6bc7d33b9ef24.png\" alt=\"\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043b\u043e\u0433\u0438 \u0432 SigNoz: \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 TransactionId, UserId \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u043a\u043e\u043b\u043e\u043d\u043a\u0438\" width=\"3786\" height=\"2091\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/c9e\/105\/75d\/c9e10575d01d6b54e6e6bc7d33b9ef24.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c9e\/105\/75d\/c9e10575d01d6b54e6e6bc7d33b9ef24.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043b\u043e\u0433\u0438 \u0432 SigNoz: \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 TransactionId, UserId \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u043a\u043e\u043b\u043e\u043d\u043a\u0438<\/figcaption><\/div>\n<\/figure>\n<h4>Sinks: \u043a\u0443\u0434\u0430 \u043f\u0438\u0441\u0430\u0442\u044c \u043b\u043e\u0433\u0438<\/h4>\n<p>Sink \u2014 \u044d\u0442\u043e \u043f\u0440\u0438\u0451\u043c\u043d\u0438\u043a \u043b\u043e\u0433-\u0441\u043e\u0431\u044b\u0442\u0438\u0439. \u041c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e. \u0412 xpenser API \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 logger \u043f\u0438\u0448\u0435\u0442 \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u044c \u0438 \u0432 OpenTelemetry Logs:<\/p>\n<pre><code class=\"typescript\">import {    consoleSink,    createLogger,    hostnameEnricher,    processIdEnricher} from '@cleverbrush\/log';import { otelLogSink, traceEnricher } from '@cleverbrush\/otel';const logger = createLogger({    minimumLevel: config.logLevel,    sinks: [consoleSink({ theme: 'dark' }), otelLogSink()],    enrichers: [hostnameEnricher(), processIdEnricher(), traceEnricher()],    handleProcessExit: true});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>consoleSink<\/code> \u0432\u044b\u0432\u043e\u0434\u0438\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432 stdout\/stderr \u0441 \u0446\u0432\u0435\u0442\u043e\u0432\u044b\u043c \u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u0443\u0440\u043e\u0432\u043d\u0435\u0439. <code>otelLogSink()<\/code> \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0442\u0435 \u0436\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043a\u0430\u043a OTel Log Records \u0432 OTLP-\u043a\u043e\u043b\u043b\u0435\u043a\u0442\u043e\u0440. \u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u043b\u043e\u0433\u0438, \u0442\u0440\u0435\u0439\u0441\u044b \u0438 \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u044e\u0442 \u0432 \u043e\u0434\u043d\u0443 observability-\u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u043c\u0435\u0436\u0434\u0443 \u0441\u043e\u0431\u043e\u0439.<\/p>\n<p>\u0414\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430 \u0438\u043b\u0438 \u0431\u0430\u0442\u0447\u0438\u043d\u0433\u0430 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <code>fileSink<\/code> \u0438 <code>BatchingSink<\/code>, \u043d\u043e \u0432 xpenser \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d-\u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u2014 console + OTLP, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430\u0445.<\/p>\n<h4>Enrichers: \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430<\/h4>\n<p>Enricher \u2014 \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u043a\u043e \u0432\u0441\u0435\u043c \u043b\u043e\u0433-\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u043c. \u0412\u043c\u0435\u0441\u0442\u043e \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0432 \u043a\u0430\u0436\u0434\u043e\u043c \u0432\u044b\u0437\u043e\u0432\u0435 <a href=\"http:\/\/logger.info\" rel=\"noopener noreferrer nofollow\"><code>logger.info<\/code><\/a><code>(...)<\/code> \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c PID, \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 trace id, \u0438\u0445 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 enricher.<\/p>\n<p>\u0412 API xpenser \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b hostname, process id \u0438 trace\/span correlation:<\/p>\n<pre><code class=\"typescript\">const logger = createLogger({    minimumLevel: config.logLevel,    sinks: [consoleSink({ theme: 'dark' }), otelLogSink()],    enrichers: [hostnameEnricher(), processIdEnricher(), traceEnricher()]});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>traceEnricher()<\/code> \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u043d\u043e \u0441 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c OpenTelemetry-\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043e\u043c: \u0435\u0441\u043b\u0438 \u043b\u043e\u0433 \u0437\u0430\u043f\u0438\u0441\u0430\u043d \u0432\u043d\u0443\u0442\u0440\u0438 HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438\u043b\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0441\u043f\u0430\u043d\u0430, \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 <code>TraceId<\/code> \u0438 <code>SpanId<\/code>.<\/p>\n<p>\u0414\u043b\u044f HTTP correlation id \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <code>useLogging()<\/code> \u0438\u0437 <code>@cleverbrush\/log<\/code>. \u0412 xpenser API middleware \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043a <code>@cleverbrush\/server<\/code> \u0442\u0430\u043a:<\/p>\n<pre><code class=\"typescript\">const [correlationMiddleware, requestLogMiddleware] = useLogging(logger, {    excludePaths: ['\/health'],    correlationResponseHeader: false});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>CorrelationId \u043f\u043e\u043b\u0435\u0437\u0435\u043d \u0432 \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0445, \u0433\u0434\u0435 \u043e\u0434\u0438\u043d \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432. \u041e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u0447\u0435\u0440\u0435\u0437 HTTP-\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a, \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0435 \u043b\u043e\u0433\u0438 \u044d\u0442\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u2014 \u043e\u0442 web app \u0434\u043e API \u0438 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u043f\u043e\u043b\u044e.<\/p>\n<h4>\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u044b\u0435 \u043b\u043e\u0433\u0433\u0435\u0440\u044b<\/h4>\n<p>\u041c\u0435\u0442\u043e\u0434 <code>forContext()<\/code> \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0439 \u043b\u043e\u0433\u0433\u0435\u0440 \u0441 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438, \u043d\u0435 \u043c\u0435\u043d\u044f\u044f \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b. \u0412 Next.js \u0447\u0430\u0441\u0442\u0438 xpenser \u0435\u0441\u0442\u044c helper <code>loggerFor()<\/code>:<\/p>\n<pre><code class=\"typescript\">export function loggerFor(sourceContext: string): Logger {    return logger.forContext('SourceContext', sourceContext);}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Auth.js \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0435\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u0434\u0435\u043b\u0438\u0442\u044c auth-\u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043e\u0442 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u043b\u043e\u0433\u043e\u0432:<\/p>\n<pre><code class=\"typescript\">const authLogger = loggerFor('Auth.js');authLogger.error(error, AuthErrorLogged, {    AuthErrorType: authErrorType(error),    AuthErrorMessage: error.message});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0442\u0430\u043a\u043e\u0433\u043e \u043b\u043e\u0433\u0433\u0435\u0440\u0430 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c <code>SourceContext: 'Auth.js'<\/code>. \u042d\u0442\u043e \u0443\u0434\u043e\u0431\u043d\u043e \u0434\u043b\u044f \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 \u043b\u043e\u0433\u043e\u0432 \u043f\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f: API handlers, Auth.js, MCP server, Telegram bot.<\/p>\n<h4>\u0422\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0447\u0435\u0440\u0435\u0437 @cleverbrush\/schema<\/h4>\n<p>\u041e\u0434\u043d\u0430 \u0438\u0437 \u0441\u0430\u043c\u044b\u0445 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0445 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0435\u0439: message templates \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <code>parseString()<\/code> \u0438\u0437 <code>@cleverbrush\/schema<\/code>. \u042d\u0442\u043e \u0434\u0430\u0451\u0442 \u043f\u043e\u043b\u043d\u0443\u044e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0442\u0438\u043f\u043e\u0432 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432\u044b\u0437\u043e\u0432\u0430 <a href=\"http:\/\/logger.info\" rel=\"noopener noreferrer nofollow\"><code>logger.info<\/code><\/a><code>()<\/code>.<\/p>\n<p>\u0412 xpenser \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432\u044b\u043d\u0435\u0441\u0435\u043d\u044b \u0432 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u0438 \u0441 log templates \u0438 \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u0442\u043e\u0439 \u0436\u0435 \u0441\u0445\u0435\u043c\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043e\u0439, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u044b API-\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b:<\/p>\n<pre><code class=\"typescript\">import { number, object, parseString } from '@cleverbrush\/schema';const TransactionCreated = parseString(    object({ TransactionId: number(), UserId: number() }),    $t =&gt;        $t`Transaction ${t =&gt; t.TransactionId} created by ${t =&gt; t.UserId}`);logger.info(TransactionCreated, {    TransactionId: transaction.id,    UserId: principal.userId});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043f\u043e\u043b\u0435 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0442\u0438\u043f, TypeScript \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0435\u0449\u0451 \u0434\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u0422\u0430\u043a\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e \u0443\u0434\u043e\u0431\u0435\u043d \u0434\u043b\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0442\u043e\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0432 \u0430\u043b\u0435\u0440\u0442\u0430\u0445, \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u0438\u0441\u043a\u0435 \u0438\u043d\u0446\u0438\u0434\u0435\u043d\u0442\u043e\u0432.<\/p>\n<h4>\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 @cleverbrush\/server<\/h4>\n<p>\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043e\u0434\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0412 xpenser API \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"typescript\">import type { Logger } from '@cleverbrush\/log';import { useLogging } from '@cleverbrush\/log';import { tracingMiddleware } from '@cleverbrush\/otel';import { createServer } from '@cleverbrush\/server';export function buildServer(    config: Config,    logger: Logger,    resources: DbResources) {    const [correlationMiddleware, requestLogMiddleware] = useLogging(logger, {        excludePaths: ['\/health'],        correlationResponseHeader: false    });    return createServer({ maxBodySize: 1024 * 1024 })        .use(tracingMiddleware({ excludePaths: ['\/health'] }))        .use(corsMiddleware(config))        .use(correlationMiddleware)        .use(requestLogMiddleware)        .services(services =&gt; configureDI(services, config, logger, resources));}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>useLogging()<\/code> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u0432\u0430 middleware: \u043f\u0435\u0440\u0432\u044b\u0439 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 correlation context \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u0432\u0442\u043e\u0440\u043e\u0439 \u043b\u043e\u0433\u0438\u0440\u0443\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0441 \u043c\u0435\u0442\u043e\u0434\u043e\u043c, \u043f\u0443\u0442\u0451\u043c, \u0441\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434\u043e\u043c \u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f.<\/p>\n<p>\u0417\u0430\u043f\u0438\u0441\u044c \u0432 \u043b\u043e\u0433 \u0432\u043d\u0443\u0442\u0440\u0438 handler \u2014 \u0447\u0435\u0440\u0435\u0437 \u0438\u043d\u044a\u0435\u043a\u0446\u0438\u044e \u043b\u043e\u0433\u0433\u0435\u0440\u0430:<\/p>\n<pre><code class=\"typescript\">export const createTransactionHandler: Handler&lt;    typeof CreateTransactionEndpoint&gt; = async ({ body, principal }, { db, config, logger }) =&gt; {    const transaction = await createTransaction(        db,        config,        principal.userId,        body    );    logger.info(TransactionCreated, {        TransactionId: transaction.id,        UserId: principal.userId    });    return ActionResult.created(        transaction,        `\/api\/transactions\/${transaction.id}`    );};<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h3>@cleverbrush\/otel: OpenTelemetry \u0431\u0435\u0437 \u0431\u043e\u0439\u043b\u0435\u0440\u043f\u043b\u0435\u0439\u0442\u0430<\/h3>\n<p>\u041f\u0430\u043a\u0435\u0442 <code>@cleverbrush\/otel<\/code> \u0440\u0435\u0448\u0430\u0435\u0442 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438: \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c OpenTelemetry SDK \u043e\u0434\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0435\u0439 \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0443\u0434\u043e\u0431\u043d\u044b\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e \u043a\u043e\u0434\u0430 Cleverbrush.<\/p>\n<h4>\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f SDK<\/h4>\n<p><code>setupOtel()<\/code> \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 NodeSDK \u0441 OTLP\/HTTP-\u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0451\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u0442\u0440\u0435\u0439\u0441\u043e\u0432, \u043c\u0435\u0442\u0440\u0438\u043a \u0438 \u043b\u043e\u0433\u043e\u0432. \u0412 xpenser API \u044d\u0442\u043e \u0432\u044b\u043d\u0435\u0441\u0435\u043d\u043e \u0432 <code>telemetry.ts<\/code>:<\/p>\n<pre><code class=\"typescript\">import { setupOtel } from '@cleverbrush\/otel';import { HttpInstrumentation } from '@opentelemetry\/instrumentation-http';import { RuntimeNodeInstrumentation } from '@opentelemetry\/instrumentation-runtime-node';import { UndiciInstrumentation } from '@opentelemetry\/instrumentation-undici';const endpoint =    process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http:\/\/otel-collector:4318';export const otel = setupOtel({    serviceName: process.env.OTEL_SERVICE_NAME ?? 'xpenser-api',    serviceVersion: process.env.npm_package_version,    environment: process.env.NODE_ENV,    otlpEndpoint: endpoint,    instrumentations: [        new HttpInstrumentation({            ignoreIncomingRequestHook: request =&gt; isHealthPath(request.url)        }),        new UndiciInstrumentation(),        new RuntimeNodeInstrumentation()    ]});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e\u0442 \u043c\u043e\u0434\u0443\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043e \u043a\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0430\u0432\u0442\u043e\u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c. \u0412 xpenser API entrypoint \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 <code>otel<\/code> \u0438\u0437 <code>.\/telemetry.js<\/code>, \u0430 \u043f\u0440\u0438 shutdown \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 <code>otel.shutdown()<\/code>.<\/p>\n<p>\u0412 Next.js \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 xpenser \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0445\u043e\u0436\u0430\u044f \u0441\u0445\u0435\u043c\u0430, \u043d\u043e \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0438\u043c\u0435\u043d\u0435\u043c \u0441\u0435\u0440\u0432\u0438\u0441\u0430:<\/p>\n<pre><code class=\"typescript\">export const otel =    existingOtel ??    setupOtel({        serviceName: process.env.WEB_OTEL_SERVICE_NAME ?? 'xpenser-web',        serviceVersion: process.env.npm_package_version,        environment: process.env.NODE_ENV,        otlpEndpoint: endpoint    });<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412 \u0438\u0442\u043e\u0433\u0435 \u0432 SigNoz \u0432\u0438\u0434\u043d\u043e \u0434\u0432\u0430 \u0441\u0435\u0440\u0432\u0438\u0441\u0430: <code>xpenser-web<\/code> \u0438 <code>xpenser-api<\/code>.<\/p>\n<h4>\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u0441\u043f\u0430\u043d\u044b<\/h4>\n<p>\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u043a\u0440\u044b\u0432\u0430\u0435\u0442 HTTP \u0438 SQL \u0432\u044b\u0437\u043e\u0432\u044b, \u043d\u043e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0430 \u0438\u043d\u043e\u0433\u0434\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0441\u0432\u043e\u0438\u0445 \u0441\u043f\u0430\u043d\u043e\u0432. \u0412 xpenser \u0442\u0430\u043a\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 \u0435\u0441\u0442\u044c \u0432 Telegram bot: \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 Telegram updates \u043d\u0435 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043e\u0431\u044b\u0447\u043d\u044b\u043c\u0438 HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430\u043c\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u043b\u044f \u043d\u0438\u0445 \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f consumer span \u0432\u0440\u0443\u0447\u043d\u0443\u044e.<\/p>\n<pre><code class=\"typescript\">const tracer = trace.getTracer('xpenser.telegram-bot');export async function traceTelegramUpdate&lt;T&gt;(    info: TelegramUpdateSpanInfo,    handler: () =&gt; Promise&lt;T&gt;): Promise&lt;T&gt; {    return tracer.startActiveSpan(        telegramSpanName(info),        {            kind: SpanKind.CONSUMER,            attributes: telegramSpanAttributes(info)        },        async span =&gt; {            try {                const result = await handler();                span.setAttribute('telegram.update.success', true);                span.setStatus({ code: SpanStatusCode.OK });                return result;            } catch (err) {                span.setAttribute('telegram.update.success', false);                span.recordException(                    err instanceof Error ? err : errorMessage(err)                );                span.setStatus({                    code: SpanStatusCode.ERROR,                    message: errorMessage(err)                });                throw err;            } finally {                span.end();            }        }    );}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0430\u043a\u043e\u0439 span \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044b \u0432\u0440\u043e\u0434\u0435 <code>messaging.system<\/code>, <code>telegram.update.type<\/code>, <code>telegram.command<\/code> \u0438\u043b\u0438 <code>telegram.callback.action<\/code>. \u0415\u0441\u043b\u0438 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u043f\u0430\u0434\u0430\u0435\u0442, \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432 trace, \u0430 \u0441\u0442\u0430\u0442\u0443\u0441 \u0441\u043f\u0430\u043d\u0430 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f <code>ERROR<\/code>.<\/p>\n<h4>\u0422\u0440\u0435\u0439\u0441\u0438\u043d\u0433 HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432<\/h4>\n<p><code>tracingMiddleware()<\/code> \u0438\u0437 <code>@cleverbrush\/otel<\/code> \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0441\u043f\u0430\u043d \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043a <code>@cleverbrush\/server<\/code>. \u0421\u043f\u0430\u043d \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 OTel HTTP-\u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044b: \u043c\u0435\u0442\u043e\u0434, \u043f\u0443\u0442\u044c, \u0441\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434, user-agent.<\/p>\n<p>\u0412 xpenser middleware \u0441\u0442\u043e\u0438\u0442 \u043f\u0435\u0440\u0432\u044b\u043c \u0432 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0439 \u0446\u0435\u043f\u043e\u0447\u043a\u0435:<\/p>\n<pre><code class=\"typescript\">const server = createServer({ maxBodySize: 1024 * 1024 })    .use(tracingMiddleware({ excludePaths: ['\/health'] }))    .use(corsMiddleware(config))    .use(correlationMiddleware)    .use(requestLogMiddleware);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412\u0430\u0436\u043d\u043e, \u0447\u0442\u043e CORS \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 propagation-\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438:<\/p>\n<pre><code class=\"typescript\">ctx.response.setHeader(    'Access-Control-Allow-Headers',    'Content-Type, Authorization, X-API-Key, Mcp-Protocol-Version, Mcp-Session-Id, traceparent, tracestate, baggage');<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0411\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044f <code>traceparent<\/code>, <code>tracestate<\/code> \u0438 <code>baggage<\/code> trace context \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0439\u0442\u0438 \u043e\u0442 web app \u043a API.<\/p>\n<h4>\u0422\u0440\u0435\u0439\u0441\u0438\u043d\u0433 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432<\/h4>\n<p>xpenser \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 <code>@cleverbrush\/client<\/code>, \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0438\u0437 \u043e\u0431\u0449\u0435\u0433\u043e API-\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430. \u0427\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043f\u043e\u043f\u0430\u0434\u0430\u043b\u0438 \u0432 \u0442\u043e\u0442 \u0436\u0435 distributed trace, \u0432 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0443\u044e \u0446\u0435\u043f\u043e\u0447\u043a\u0443 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d <code>clientTracingMiddleware()<\/code>:<\/p>\n<pre><code class=\"typescript\">import { createClient } from '@cleverbrush\/client';import { clientTracingMiddleware } from '@cleverbrush\/otel\/client';import { api } from '@xpenser\/contracts';export function createXpenserClient(options: XpenserClientOptions) {    return createClient(api, {        baseUrl: options.baseUrl,        getToken: options.getToken,        headers: options.headers,        onUnauthorized: options.onUnauthorized,        fetch: options.fetch,        middlewares: [            clientTracingMiddleware(),            retry({ limit: 2, retryOnTimeout: true }),            timeout({ timeout: 10_000 }),            dedupe(),            cacheTags({                defaultTtl: 5_000,                ttlByTag: {                    currencies: 24 * 60 * 60 * 1_000,                    dashboard: 60_000,                    transactions: 30_000,                    categories: 30_000,                    'user-profile': 30_000                }            })        ]    });}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442 dashboard, Next.js server code \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 API \u0447\u0435\u0440\u0435\u0437 typed client, \u0430 SigNoz \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0441\u0432\u044f\u0437\u043a\u0443 <code>xpenser-web -&gt; xpenser-api -&gt; PostgreSQL<\/code> \u0432 \u043e\u0434\u043d\u043e\u043c trace.<\/p>\n<h4>\u0422\u0440\u0435\u0439\u0441\u0438\u043d\u0433 SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432<\/h4>\n<p><code>instrumentKnex()<\/code> \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0435\u0442 Knex-\u0438\u043d\u0441\u0442\u0430\u043d\u0441 \u0442\u0430\u043a, \u0447\u0442\u043e \u043a\u0430\u0436\u0434\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u043c \u0441\u043f\u0430\u043d\u043e\u043c (CLIENT span) \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u0442\u0440\u0435\u0439\u0441\u0435.<\/p>\n<p>\u0412 xpenser \u0431\u0430\u0437\u0430 \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 <code>@cleverbrush\/orm<\/code> (\u0440\u0435\u0447\u044c \u043e \u043d\u0435\u0439 \u043f\u043e\u0439\u0434\u0451\u0442 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435), \u0430 Knex \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f:<\/p>\n<pre><code class=\"typescript\">import { createDb } from '@cleverbrush\/orm';import { instrumentKnex } from '@cleverbrush\/otel';import knex from 'knex';export function createDbResources(config: Config, logger: Logger): DbResources {    const connection = instrumentKnex(        knex({            client: 'pg',            connection: config.db.connectionString,            pool: { min: 2, max: 10 },            acquireConnectionTimeout: 10_000        })    );    logger.debug('Configured application database connection pool', {});    return {        knex: connection,        db: createDb(connection, entityMap)    };}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0432 trace-\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0449\u0438\u043a\u0435 \u043a\u0430\u0436\u0434\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441 \u0432\u0438\u0434\u0435\u043d \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 span \u0441 \u0442\u0435\u043a\u0441\u0442\u043e\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c\u044e.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d15\/783\/c72\/d15783c72288f5a6e4f7d36f42a2e0c9.png\" alt=\"\u0422\u0440\u0435\u0439\u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 dashboard \u0432 SigNoz: web span, API span \u0438 \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0435 SQL spans\" width=\"3783\" height=\"2093\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/d15\/783\/c72\/d15783c72288f5a6e4f7d36f42a2e0c9.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d15\/783\/c72\/d15783c72288f5a6e4f7d36f42a2e0c9.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0422\u0440\u0435\u0439\u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 dashboard \u0432 SigNoz: web span, API span \u0438 \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0435 SQL spans<\/figcaption><\/div>\n<\/figure>\n<h4>\u0421\u0432\u044f\u0437\u044c \u043b\u043e\u0433\u043e\u0432 \u0438 \u0442\u0440\u0435\u0439\u0441\u043e\u0432<\/h4>\n<p>\u0421\u0430\u043c\u0430\u044f \u043f\u043e\u043b\u0435\u0437\u043d\u0430\u044f \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f: <code>traceEnricher()<\/code> \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 <code>TraceId<\/code> \u0438 <code>SpanId<\/code> \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u0441\u043f\u0430\u043d\u0430 \u043a \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u043b\u043e\u0433-\u0441\u043e\u0431\u044b\u0442\u0438\u044e, \u0430 <code>otelLogSink()<\/code> \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u044d\u0442\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432 \u0442\u043e\u0442 \u0436\u0435 OTLP-\u043a\u043e\u043b\u043b\u0435\u043a\u0442\u043e\u0440.<\/p>\n<p>\u0412 API \u0438 web app xpenser \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u043e:<\/p>\n<pre><code class=\"typescript\">const logger = createLogger({    minimumLevel: config.logLevel,    sinks: [consoleSink({ theme: 'dark' }), otelLogSink()],    enrichers: [hostnameEnricher(), processIdEnricher(), traceEnricher()]});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043b\u043e\u0433 \u0432\u043d\u0443\u0442\u0440\u0438 HTTP handler:<\/p>\n<pre><code class=\"typescript\">logger.info(McpToolCalled, {    ToolName: toolName,    UserId: context.principal.userId,    ApiKeyId: context.principal.apiKeyId});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0441\u0432\u044f\u0437\u0430\u043d \u0441 trace, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u044d\u0442\u043e\u0442 MCP-\u0437\u0430\u043f\u0440\u043e\u0441 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043b\u0441\u044f. \u0412 observability-\u0431\u044d\u043a\u0435\u043d\u0434\u0435 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0447\u0430\u0442\u044c \u0441 \u043e\u0448\u0438\u0431\u043a\u0438 \u0432 \u043b\u043e\u0433\u0430\u0445, \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043a trace \u0438 \u0443\u0432\u0438\u0434\u0435\u0442\u044c HTTP middleware, handler, \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043a \u0431\u0430\u0437\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b.<\/p>\n<h3>\u041f\u043e\u043b\u043d\u0430\u044f \u043a\u0430\u0440\u0442\u0438\u043d\u0430: \u043a\u0430\u043a \u0432\u0441\u0451 \u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u043c\u0435\u0441\u0442\u0435<\/h3>\n<p>\u0412 xpenser \u043d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0441\u0442\u044c \u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u0437 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u0447\u0430\u0441\u0442\u0435\u0439:<\/p>\n<ul>\n<li>\n<p><code>setupOtel()<\/code> \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0432 <code>xpenser-web<\/code>, <code>xpenser-api<\/code> \u0438 <code>xpenser-telegram-bot<\/code><\/p>\n<\/li>\n<li>\n<p><code>otelLogSink()<\/code> \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043b\u043e\u0433\u0438 \u043a\u0430\u043a OTel Log Records<\/p>\n<\/li>\n<li>\n<p><code>traceEnricher()<\/code> \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 <code>TraceId<\/code> \u0438 <code>SpanId<\/code> \u0432 \u043a\u0430\u0436\u0434\u043e\u0435 \u043b\u043e\u0433-\u0441\u043e\u0431\u044b\u0442\u0438\u0435<\/p>\n<\/li>\n<li>\n<p><code>tracingMiddleware()<\/code> \u0441\u043e\u0437\u0434\u0430\u0451\u0442 server span \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e API-\u0437\u0430\u043f\u0440\u043e\u0441\u0430<\/p>\n<\/li>\n<li>\n<p><code>clientTracingMiddleware()<\/code> \u0441\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u0442 web requests \u0441 API calls<\/p>\n<\/li>\n<li>\n<p><code>instrumentKnex()<\/code> \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442 SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0432 \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0435 spans<\/p>\n<\/li>\n<li>\n<p>Telegram bot \u0441\u043e\u0437\u0434\u0430\u0451\u0442 custom spans \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434, \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0438 callback queries<\/p>\n<\/li>\n<\/ul>\n<h4>\u041a\u0443\u0434\u0430 \u0436\u0435 \u0431\u0435\u0437 LLM?<\/h4>\n<p>\u042f \u0432\u044b\u0431\u0440\u0430\u043b SigNoz \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 observability-\u0431\u044d\u043a\u0435\u043d\u0434\u0430, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u0438 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u043d \u0438\u043c\u0435\u0435\u0442 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 MCP \u0441\u0435\u0440\u0432\u0435\u0440. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 AI \u0430\u0433\u0435\u043d\u0442\u0443, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430\u0434 \u0437\u0430\u0434\u0430\u0447\u0435\u0439 \u0434\u0435\u043b\u0430\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435:<\/p>\n<ul>\n<li>\n<p>\u043f\u043e\u0441\u043b\u0435 push \u0432 GitHub, \u0440\u0430\u0437\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u0441\u044f ephemeral \u0441\u0440\u0435\u0434\u0430, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u0435\u0440\u0432\u0438\u0441\u044b \u0431\u0443\u0434\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u0438\u043c\u0435\u043d\u0430 \u0442\u0438\u043f\u0430 <code>xpenser-api-pr-123<\/code>, <code>xpenser-web-pr-123<\/code><\/p>\n<\/li>\n<li>\n<p>LLM \u0430\u0433\u0435\u043d\u0442 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Playwright \u0438\u043b\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b \u0434\u0435\u0440\u0433\u0430\u044e\u0442 API \u0438 web UI<\/p>\n<\/li>\n<li>\n<p>MCP endpoint \u0432 SigNoz \u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0442\u0440\u0435\u0439\u0441\u044b \u0438 \u043b\u043e\u0433\u0438 \u043d\u0430 \u043f\u0440\u0435\u0434\u043c\u0435\u0442 \u043e\u0448\u0438\u0431\u043e\u043a, \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0438\u0445 \u0430\u043d\u043e\u043c\u0430\u043b\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0433\u043b\u0438 \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0438\u0437-\u0437\u0430 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0432 \u043a\u043e\u0434\u0435. \u0415\u0441\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u043f\u043e\u0448\u043b\u043e \u043d\u0435 \u0442\u0430\u043a.<\/p>\n<\/li>\n<\/ul>\n<p>\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u0432\u0441\u0451 \u044d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043d\u044f\u0442\u044c \u0447\u0435\u0440\u0435\u0437 Docker Compose \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 xpenser:<\/p>\n<pre><code class=\"bash\">docker compose up --build<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e\u0442 \u0441\u0442\u0435\u043a \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 web app, API, PostgreSQL, Swagger UI \u0438 observability-\u0441\u0435\u0440\u0432\u0438\u0441\u044b \u0438\u0437 <code>docker-compose.yml<\/code>. \u0417\u0430\u043f\u0440\u043e\u0441\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0442\u0441\u044f \u0432 web app \u0438 \u0438\u0434\u0443\u0442 \u0432 API, \u043f\u043e\u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432 SigNoz \u043a\u0430\u043a \u043e\u0434\u0438\u043d distributed trace \u0441\u043e spans \u043e\u0442 \u043e\u0431\u043e\u0438\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432. \u0412 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0435 otel-collector \u0438 SigNoz \u0443 \u0432\u0430\u0441 \u0441\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e \u0431\u0443\u0434\u0443\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438, \u043d\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u043e\u043d\u0438 \u0436\u0438\u0432\u0443\u0442 \u0432 \u043e\u0434\u043d\u043e\u043c docker-compose \u0444\u0430\u0439\u043b\u0435, \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0440\u0430\u0437\u0432\u0435\u0440\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0433\u043e \u0441\u0442\u0435\u043a\u0430 \u043e\u0434\u043d\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439.<\/p>\n<p>\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 URL:<\/p>\n<ul>\n<li>\n<p>Web app: <a href=\"http:\/\/localhost:3000\" rel=\"noopener noreferrer nofollow\"><code>http:\/\/localhost:3000<\/code><\/a><\/p>\n<\/li>\n<li>\n<p>External API proxy: <a href=\"http:\/\/localhost:3000\/external-api\" rel=\"noopener noreferrer nofollow\"><code>http:\/\/localhost:3000\/external-api<\/code><\/a><\/p>\n<\/li>\n<li>\n<p>Swagger UI: <a href=\"http:\/\/localhost:8090\" rel=\"noopener noreferrer nofollow\"><code>http:\/\/localhost:8090<\/code><\/a><\/p>\n<\/li>\n<li>\n<p>SigNoz: <a href=\"http:\/\/localhost:8080\" rel=\"noopener noreferrer nofollow\"><code>http:\/\/localhost:8080<\/code><\/a><\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d-\u0432\u0435\u0440\u0441\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0437\u0434\u0435\u0441\u044c: <a href=\"http:\/\/xpenser.cleverbrush.com\" rel=\"noopener noreferrer nofollow\">xpenser.cleverbrush.com<\/a>. \u041a\u043e\u0434 \u2014 \u0437\u0434\u0435\u0441\u044c: <a href=\"https:\/\/github.com\/cleverbrush\/xpenser\" rel=\"noopener noreferrer nofollow\">github.com\/cleverbrush\/xpenser<\/a>.<\/p>\n<p>\u041d\u0443 \u0438 \u043d\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u043a \u0441\u043a\u0440\u0438\u043d\u0448\u043e\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0445 \u043c\u0435\u0442\u0440\u0438\u043a \u0434\u043b\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f:<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/7d6\/20b\/065\/7d620b065073996bacd33d6092b48faa.png\" alt=\"\u041e\u0431\u0449\u0438\u0439 \u0432\u0438\u0434 xpenser \u0432 SigNoz: \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u0442\u0440\u0435\u0439\u0441\u043e\u0432, \u043b\u043e\u0433\u043e\u0432 \u0438 \u043c\u0435\u0442\u0440\u0438\u043a\" width=\"3783\" height=\"2057\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/7d6\/20b\/065\/7d620b065073996bacd33d6092b48faa.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/7d6\/20b\/065\/7d620b065073996bacd33d6092b48faa.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041e\u0431\u0449\u0438\u0439 \u0432\u0438\u0434 xpenser \u0432 SigNoz: \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u0442\u0440\u0435\u0439\u0441\u043e\u0432, \u043b\u043e\u0433\u043e\u0432 \u0438 \u043c\u0435\u0442\u0440\u0438\u043a<\/figcaption><\/div>\n<\/figure>\n<h3>\u0418\u0442\u043e\u0433\u0438<\/h3>\n<p><code>@cleverbrush\/log<\/code> \u0438 <code>@cleverbrush\/otel<\/code> \u2014 \u043d\u0435 \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0440\u0435\u0432\u043e\u043b\u044e\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u044b, \u0430 \u043f\u0440\u043e\u0434\u0443\u043c\u0430\u043d\u043d\u044b\u0439 \u043a\u043b\u0435\u0439 \u043c\u0435\u0436\u0434\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u043c \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u043c \u0438 observability-\u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043e\u0439. \u0418\u0445 \u0433\u043b\u0430\u0432\u043d\u0430\u044f \u0446\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u2014 \u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:<\/p>\n<ul>\n<li>\n<p>\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 message templates \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442 \u0432\u0430\u0436\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 \u043f\u043e\u043b\u044f \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 \u0438 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438<\/p>\n<\/li>\n<li>\n<p><code>traceEnricher()<\/code> \u0441\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u0442 \u043a\u0430\u0436\u0434\u043e\u0435 \u043b\u043e\u0433-\u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0441 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c OTel-\u0442\u0440\u0435\u0439\u0441\u043e\u043c<\/p>\n<\/li>\n<li>\n<p><code>otelLogSink()<\/code> \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043b\u043e\u0433\u0438 \u0432 \u0442\u043e\u0442 \u0436\u0435 OTLP pipeline, \u0447\u0442\u043e \u0442\u0440\u0435\u0439\u0441\u044b \u0438 \u043c\u0435\u0442\u0440\u0438\u043a\u0438<\/p>\n<\/li>\n<li>\n<p><code>clientTracingMiddleware()<\/code> \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 distributed trace \u043c\u0435\u0436\u0434\u0443 web app \u0438 API<\/p>\n<\/li>\n<li>\n<p><code>instrumentKnex()<\/code> \u0434\u0430\u0451\u0442 SQL-spans \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0432 query-\u043a\u043e\u0434\u0435<\/p>\n<\/li>\n<li>\n<p><code>useLogging()<\/code> \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442 request logging \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0434\u0432\u0443\u043c\u044f middleware<\/p>\n<\/li>\n<\/ul>\n<p>xpenser \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u044d\u0442\u0443 \u0441\u0432\u044f\u0437\u043a\u0443 \u043d\u0430 \u0440\u0430\u0431\u043e\u0447\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438, \u0430 \u043d\u0435 \u043d\u0430 \u0438\u0441\u043a\u0443\u0441\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435: \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 web UI, typed API client, schema-first server, Postgres, MCP endpoint, Telegram bot \u0438 self-hosted observability \u0436\u0438\u0432\u0443\u0442 \u0432 \u043e\u0434\u043d\u043e\u043c \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438.<\/p>\n<p>\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u2014 \u043a\u0430\u043a \u043f\u0440\u0435\u0432\u0440\u0430\u0442\u0438\u0442\u044c TypeScript-\u0441\u0445\u0435\u043c\u0443 \u0432 \u0435\u0434\u0438\u043d\u044b\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0438\u0441\u0442\u0438\u043d\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445: <code>@cleverbrush\/knex-schema<\/code> \u0438 <code>@cleverbrush\/orm<\/code>. Schema-driven ORM \u0441 \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c query builder, \u0430\u0432\u0442\u043e-DDL \u0438 unit-of-work.<\/p>\n<h3>\u0421\u0441\u044b\u043b\u043a\u0438<\/h3>\n<p>Cleverbrush Framework: <a href=\"https:\/\/github.com\/cleverbrush\/framework\" rel=\"noopener noreferrer nofollow\">github.com\/cleverbrush\/framework<\/a><\/p>\n<p>xpenser app: <a href=\"http:\/\/xpenser.cleverbrush.com\" rel=\"noopener noreferrer nofollow\">xpenser.cleverbrush.com<\/a><\/p>\n<p>xpenser GitHub: <a href=\"https:\/\/github.com\/cleverbrush\/xpenser\" rel=\"noopener noreferrer nofollow\">github.com\/cleverbrush\/xpenser<\/a><\/p>\n<p>\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0438 playground: <a href=\"http:\/\/docs.cleverbrush.com\" rel=\"noopener noreferrer nofollow\">docs.cleverbrush.com<\/a><\/p>\n<h4>npm<\/h4>\n<pre><code class=\"bash\">npm install @cleverbrush\/lognpm install @cleverbrush\/otel<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0411\u0443\u0434\u0443 \u0440\u0430\u0434 \u043b\u044e\u0431\u043e\u0439 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u0432\u044f\u0437\u0438 \u2014 \u043f\u043e API, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u043c \u0444\u0438\u0447\u0430\u043c. Issues \u0438 PR \u043f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442\u0441\u044f.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1040714\/\">https:\/\/habr.com\/ru\/articles\/1040714\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0421\u0442\u0430\u0442\u044c\u044f \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0441\u0442\u044c (observability) \u0432  \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 \u0441 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u043c \u043a\u043e\u0434\u043e\u043c, \u0430 \u0431\u043e\u043d\u0443\u0441\u043e\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043b\u043e\u0433\u0438 \u0441 \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0448\u0430\u0431\u043b\u043e\u043d\u0430\u043c\u0438, \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u043a\u043e\u0440\u0440\u0435\u043b\u044f\u0446\u0438\u044e \u0441\u043e \u0441\u043f\u0430\u043d\u0430\u043c\u0438 OpenTelemetry, \u0432\u0441\u0451 \u044d\u0442\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u0430\u0431\u043e\u0440\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u044f \u043d\u0430\u0437\u044b\u0432\u0430\u044e CleverBrush Framework.\u0412\u0441\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u043d\u0438\u0436\u0435 \u0432\u0437\u044f\u0442\u044b \u0438\u0437 xpenser \u2014 open-source \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u0430 \u043b\u0438\u0447\u043d\u044b\u0445 \u0434\u043e\u0445\u043e\u0434\u043e\u0432 \u0438 \u0440\u0430\u0441\u0445\u043e\u0434\u043e\u0432. \u0421 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u044d\u0442\u043e \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0435\u0439 Cleverbrush Framework, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u0432\u0441\u0435\u0439 \u043c\u0430\u0448\u0438\u043d\u0435\u0440\u0438\u0438, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0435\u0441\u0442\u044c \u0432 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0435, \u0442\u0430\u043a\u043e\u0439 \u043a\u0430\u043a: \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b, \u0441\u0435\u0440\u0432\u0435\u0440, \u043a\u043b\u0438\u0435\u043d\u0442, auth, \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 OpenTelemetry. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u044d\u0442\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u044f \u0441\u0430\u043c \u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0441\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0444\u0438\u043d\u0430\u043d\u0441\u043e\u0432. \u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043e\u0442\u043a\u0440\u044b\u0442 \u043d\u0430 GitHub: github.com\/cleverbrush\/xpenser.\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440: \u0432\u0441\u0435 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043d\u043e\u0441\u044f\u0442 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440. \u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u044d\u0442\u043e, \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0443 \u043d\u0438\u0445 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0445\u043e\u0440\u043e\u0448\u0435\u0435.\u041f\u0440\u0435\u0434\u044b\u0441\u0442\u043e\u0440\u0438\u044f\u0412 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445 (\u043f\u0435\u0440\u0432\u0430\u044f \u0438 \u0432\u0442\u043e\u0440\u0430\u044f) \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u043b \u043e @cleverbrush\/schema \u0438 \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043d\u0430 \u0435\u0451 \u043e\u0441\u043d\u043e\u0432\u0435 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u044b @cleverbrush\/server \u0438 @cleverbrush\/client. \u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0440\u0435\u0447\u044c \u043f\u043e\u0439\u0434\u0451\u0442 \u043e\u0431 \u0435\u0449\u0451 \u0434\u0432\u0443\u0445 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u0445 \u0442\u043e\u0433\u043e \u0436\u0435 \u043c\u043e\u043d\u043e\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f: @cleverbrush\/log \u2014 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0438 @cleverbrush\/otel \u2014 \u0442\u043e\u043d\u043a\u0430\u044f \u043e\u0431\u0451\u0440\u0442\u043a\u0430 \u043d\u0430\u0434 OpenTelemetry SDK.\u041e\u0431\u0430 \u043f\u0430\u043a\u0435\u0442\u0430 \u0438\u0437\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u043a\u0430\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443, \u043d\u043e \u043c\u043e\u0433\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u2014 \u0432 \u043b\u044e\u0431\u043e\u043c Node.js-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438. \u0412 xpenser \u043e\u043d\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0441\u0440\u0430\u0437\u0443 \u0432 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430\u0445: API, Next.js web app \u0438 Telegram bot.@cleverbrush\/log: \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u0418\u0434\u0435\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043f\u0440\u0438\u0448\u043b\u0430 \u0438\u0437 \u044d\u043a\u043e\u0441\u0438\u0441\u0442\u0435\u043c\u044b .NET: \u0442\u0430\u043c \u0435\u0441\u0442\u044c Serilog \u0441 \u0435\u0433\u043e message templates. \u0418\u0434\u0435\u044f \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0442\u043e\u043c \u0447\u0442\u043e\u0431\u044b \u0432\u043c\u0435\u0441\u0442\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u044b \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d \u0441 \u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440\u0430\u043c\u0438, \u0430 \u0432\u0441\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043a\u0430\u043a \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c \u2014 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043d\u0430\u0439\u0442\u0438 \u0432\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043f\u043e TransactionId \u0438\u043b\u0438 UserId.\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0432 xpenser \u0443\u0434\u043e\u0431\u043d\u043e \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u043c \u0448\u0430\u0431\u043b\u043e\u043d\u043e\u043c:import { number, object, parseString } from &#8216;@cleverbrush\/schema&#8217;;const TransactionCreated = parseString(    object({ TransactionId: number(), UserId: number() }),    $t =&gt;        $t`Transaction ${t =&gt; t.TransactionId} created by ${t =&gt; t.UserId}`);logger.info(TransactionCreated, {    TransactionId: transaction.id,    UserId: principal.userId});\u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430 {TransactionId} \u0438 {UserId} \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0442 \u0442\u0435\u043a\u0441\u0442\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u0430 \u0441\u0430\u043c \u0448\u0430\u0431\u043b\u043e\u043d \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u043c: Transaction {TransactionId} created by {UserId}. \u0412 SigNoz, Clickstack \u0438\u043b\u0438 \u043b\u044e\u0431\u043e\u0439 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0449\u0435\u0439 CLEF-\u0444\u043e\u0440\u043c\u0430\u0442, \u044d\u0442\u0438 \u043f\u043e\u043b\u044f \u043c\u043e\u0436\u043d\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0430\u0433\u0440\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u0430\u043a \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0435 \u043a\u043e\u043b\u043e\u043d\u043a\u0438.\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u0443\u0440\u043e\u0432\u043d\u0438: trace, debug, info, warn, error, fatal. \u041c\u0435\u0442\u043e\u0434\u044b logger.error() \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0442 \u043e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 Error \u043f\u0435\u0440\u0432\u044b\u043c \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u043e\u043c. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a MCP transport \u0432 xpenser \u043f\u0438\u0448\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043e\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f:transport. =&gt; {    logger.error(error, McpTransportError, {        UserId: apiKeyPrincipal.userId    });};\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043b\u043e\u0433\u0438 \u0432 SigNoz: \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 TransactionId, UserId \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u043a\u043e\u043b\u043e\u043d\u043a\u0438Sinks: \u043a\u0443\u0434\u0430 \u043f\u0438\u0441\u0430\u0442\u044c \u043b\u043e\u0433\u0438Sink \u2014 \u044d\u0442\u043e \u043f\u0440\u0438\u0451\u043c\u043d\u0438\u043a \u043b\u043e\u0433-\u0441\u043e\u0431\u044b\u0442\u0438\u0439. \u041c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e. \u0412 xpenser API \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 logger \u043f\u0438\u0448\u0435\u0442 \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u044c \u0438 \u0432 OpenTelemetry Logs:import {    consoleSink,    createLogger,    hostnameEnricher,    processIdEnricher} from &#8216;@cleverbrush\/log&#8217;;import { otelLogSink, traceEnricher } from &#8216;@cleverbrush\/otel&#8217;;const logger = createLogger({    minimumLevel: config.logLevel,    sinks: [consoleSink({ theme: &#8216;dark&#8217; }), otelLogSink()],    enrichers: [hostnameEnricher(), processIdEnricher(), traceEnricher()],    handleProcessExit: true});consoleSink \u0432\u044b\u0432\u043e\u0434\u0438\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432 stdout\/stderr \u0441 \u0446\u0432\u0435\u0442\u043e\u0432\u044b\u043c \u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u0443\u0440\u043e\u0432\u043d\u0435\u0439. otelLogSink() \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0442\u0435 \u0436\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043a\u0430\u043a OTel Log Records \u0432 OTLP-\u043a\u043e\u043b\u043b\u0435\u043a\u0442\u043e\u0440. \u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u043b\u043e\u0433\u0438, \u0442\u0440\u0435\u0439\u0441\u044b \u0438 \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u044e\u0442 \u0432 \u043e\u0434\u043d\u0443 observability-\u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u043c\u0435\u0436\u0434\u0443 \u0441\u043e\u0431\u043e\u0439.\u0414\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430 \u0438\u043b\u0438 \u0431\u0430\u0442\u0447\u0438\u043d\u0433\u0430 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c fileSink \u0438 BatchingSink, \u043d\u043e \u0432 xpenser \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d-\u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 \u2014 console + OTLP, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430\u0445.Enrichers: \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430Enricher \u2014 \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u043a\u043e \u0432\u0441\u0435\u043c \u043b\u043e\u0433-\u0441\u043e\u0431\u044b\u0442\u0438\u044f\u043c. \u0412\u043c\u0435\u0441\u0442\u043e \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0432 \u043a\u0430\u0436\u0434\u043e\u043c \u0432\u044b\u0437\u043e\u0432\u0435 logger.info(&#8230;) \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c PID, \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 trace id, \u0438\u0445 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 enricher.\u0412 API xpenser \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b hostname, process id \u0438 trace\/span correlation:const logger = createLogger({    minimumLevel: config.logLevel,    sinks: [consoleSink({ theme: &#8216;dark&#8217; }), otelLogSink()],    enrichers: [hostnameEnricher(), processIdEnricher(), traceEnricher()]});traceEnricher() \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u043d\u043e \u0441 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c OpenTelemetry-\u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043e\u043c: \u0435\u0441\u043b\u0438 \u043b\u043e\u0433 \u0437\u0430\u043f\u0438\u0441\u0430\u043d \u0432\u043d\u0443\u0442\u0440\u0438 HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0438\u043b\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0441\u043f\u0430\u043d\u0430, \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 TraceId \u0438 SpanId.\u0414\u043b\u044f HTTP correlation id \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f useLogging() \u0438\u0437 @cleverbrush\/log. \u0412 xpenser API middleware \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043a @cleverbrush\/server \u0442\u0430\u043a:const [correlationMiddleware, requestLogMiddleware] = useLogging(logger, {    excludePaths: [&#8216;\/health&#8217;],    correlationResponseHeader: false});CorrelationId \u043f\u043e\u043b\u0435\u0437\u0435\u043d \u0432 \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0445, \u0433\u0434\u0435 \u043e\u0434\u0438\u043d \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432. \u041e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0439 \u0447\u0435\u0440\u0435\u0437 HTTP-\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a, \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0435 \u043b\u043e\u0433\u0438 \u044d\u0442\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u2014 \u043e\u0442 web app \u0434\u043e API \u0438 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u043f\u043e\u043b\u044e.\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u044b\u0435 \u043b\u043e\u0433\u0433\u0435\u0440\u044b\u041c\u0435\u0442\u043e\u0434 forContext() \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0439 \u043b\u043e\u0433\u0433\u0435\u0440 \u0441 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438, \u043d\u0435 \u043c\u0435\u043d\u044f\u044f \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b. \u0412 Next.js \u0447\u0430\u0441\u0442\u0438 xpenser \u0435\u0441\u0442\u044c helper loggerFor():export function loggerFor(sourceContext: string): Logger {    return logger.forContext(&#8216;SourceContext&#8217;, sourceContext);}Auth.js \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0435\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u0434\u0435\u043b\u0438\u0442\u044c auth-\u0441\u043e\u0431\u044b\u0442\u0438\u044f \u043e\u0442 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u043b\u043e\u0433\u043e\u0432:const authLogger = loggerFor(&#8216;Auth.js&#8217;);authLogger.error(error, AuthErrorLogged, {    AuthErrorType: authErrorType(error),    AuthErrorMessage: error.message});\u0412\u0441\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0442\u0430\u043a\u043e\u0433\u043e \u043b\u043e\u0433\u0433\u0435\u0440\u0430 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c SourceContext: &#8216;Auth.js&#8217;. \u042d\u0442\u043e \u0443\u0434\u043e\u0431\u043d\u043e \u0434\u043b\u044f \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438 \u043b\u043e\u0433\u043e\u0432 \u043f\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f: API handlers, Auth.js, MCP server, Telegram bot.\u0422\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0447\u0435\u0440\u0435\u0437 @cleverbrush\/schema\u041e\u0434\u043d\u0430 \u0438\u0437 \u0441\u0430\u043c\u044b\u0445 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0445 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0435\u0439: message templates \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f parseString() \u0438\u0437 @cleverbrush\/schema. \u042d\u0442\u043e \u0434\u0430\u0451\u0442 \u043f\u043e\u043b\u043d\u0443\u044e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0442\u0438\u043f\u043e\u0432 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432\u044b\u0437\u043e\u0432\u0430 logger.info().\u0412 xpenser \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432\u044b\u043d\u0435\u0441\u0435\u043d\u044b \u0432 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u0438 \u0441 log templates \u0438 \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u0442\u043e\u0439 \u0436\u0435 \u0441\u0445\u0435\u043c\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043e\u0439, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u044b API-\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b:import { number, object, parseString } from &#8216;@cleverbrush\/schema&#8217;;const TransactionCreated = parseString(    object({ TransactionId: number(), UserId: number() }),    $t =&gt;        $t`Transaction ${t =&gt; t.TransactionId} created by ${t =&gt; t.UserId}`);logger.info(TransactionCreated, {    TransactionId: transaction.id,    UserId: principal.userId});\u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043f\u043e\u043b\u0435 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0442\u0438\u043f, TypeScript \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0435\u0449\u0451 \u0434\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u0422\u0430\u043a\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e \u0443\u0434\u043e\u0431\u0435\u043d \u0434\u043b\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0442\u043e\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0432 \u0430\u043b\u0435\u0440\u0442\u0430\u0445, \u0434\u0430\u0448\u0431\u043e\u0440\u0434\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u0438\u0441\u043a\u0435 \u0438\u043d\u0446\u0438\u0434\u0435\u043d\u0442\u043e\u0432.\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 @cleverbrush\/server\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043e\u0434\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0412 xpenser API \u044d\u0442\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:import type { Logger } from &#8216;@cleverbrush\/log&#8217;;import { useLogging } from &#8216;@cleverbrush\/log&#8217;;import { tracingMiddleware } from &#8216;@cleverbrush\/otel&#8217;;import { createServer } from &#8216;@cleverbrush\/server&#8217;;export function buildServer(    config: Config,    logger: Logger,    resources: DbResources) {    const [correlationMiddleware, requestLogMiddleware] = useLogging(logger, {        excludePaths: [&#8216;\/health&#8217;],        correlationResponseHeader: false    });    return createServer({ maxBodySize: 1024 * 1024 })        .use(tracingMiddleware({ excludePaths: [&#8216;\/health&#8217;] }))        .use(corsMiddleware(config))        .use(correlationMiddleware)        .use(requestLogMiddleware)        .services(services =&gt; configureDI(services, config, logger, resources));}useLogging() \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u0432\u0430 middleware: \u043f\u0435\u0440\u0432\u044b\u0439 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 correlation context \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u0432\u0442\u043e\u0440\u043e\u0439 \u043b\u043e\u0433\u0438\u0440\u0443\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0441 \u043c\u0435\u0442\u043e\u0434\u043e\u043c, \u043f\u0443\u0442\u0451\u043c, \u0441\u0442\u0430\u0442\u0443\u0441-\u043a\u043e\u0434\u043e\u043c \u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f.\u0417\u0430\u043f\u0438\u0441\u044c \u0432 \u043b\u043e\u0433 \u0432\u043d\u0443\u0442\u0440\u0438 handler \u2014 \u0447\u0435\u0440\u0435\u0437 \u0438\u043d\u044a\u0435\u043a\u0446\u0438\u044e \u043b\u043e\u0433\u0433\u0435\u0440\u0430:export const createTransactionHandler: Handler&lt;    typeof CreateTransactionEndpoint&gt; = async ({ body, principal }, { db, config, logger }) =&gt; {    const transaction = await createTransaction(        db,        config,        principal.userId,        body    );    logger.info(TransactionCreated, {        TransactionId: transaction.id,        UserId: principal.userId    });    return ActionResult.created(        transaction,        `\/api\/transactions\/${transaction.id}`    );};@cleverbrush\/otel: OpenTelemetry \u0431\u0435\u0437 \u0431\u043e\u0439\u043b\u0435\u0440\u043f\u043b\u0435\u0439\u0442\u0430\u041f\u0430\u043a\u0435\u0442 @cleverbrush\/otel \u0440\u0435\u0448\u0430\u0435\u0442 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438: \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c OpenTelemetry SDK \u043e\u0434\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0435\u0439 \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0443\u0434\u043e\u0431\u043d\u044b\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0433\u043e \u043a\u043e\u0434\u0430 Cleverbrush.\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f SDKsetupOtel() \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 NodeSDK \u0441 OTLP\/HTTP-\u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0451\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u0442\u0440\u0435\u0439\u0441\u043e\u0432, \u043c\u0435\u0442\u0440\u0438\u043a \u0438 \u043b\u043e\u0433\u043e\u0432. \u0412 xpenser API \u044d\u0442\u043e \u0432\u044b\u043d\u0435\u0441\u0435\u043d\u043e \u0432 telemetry.ts:import { setupOtel } from &#8216;@cleverbrush\/otel&#8217;;import { HttpInstrumentation } from &#8216;@opentelemetry\/instrumentation-http&#8217;;import { RuntimeNodeInstrumentation } from &#8216;@opentelemetry\/instrumentation-runtime-node&#8217;;import { UndiciInstrumentation } from &#8216;@opentelemetry\/instrumentation-undici&#8217;;const endpoint =    process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? &#8216;http:\/\/otel-collector:4318&#8217;;export const otel = setupOtel({    serviceName: process.env.OTEL_SERVICE_NAME ?? &#8216;xpenser-api&#8217;,    serviceVersion: process.env.npm_package_version,    environment: process.env.NODE_ENV,    otlpEndpoint: endpoint,    instrumentations: [        new HttpInstrumentation({            ignoreIncomingRequestHook: request =&gt; isHealthPath(request.url)        }),        new UndiciInstrumentation(),        new RuntimeNodeInstrumentation()    ]});\u042d\u0442\u043e\u0442 \u043c\u043e\u0434\u0443\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043e \u043a\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0430\u0432\u0442\u043e\u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c. \u0412 xpenser API entrypoint \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 otel \u0438\u0437 .\/telemetry.js, \u0430 \u043f\u0440\u0438 &#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-481425","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/481425","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=481425"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/481425\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=481425"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=481425"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=481425"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}