[{"data":1,"prerenderedAt":4801},["ShallowReactive",2],{"content:\u002F11-observability\u002F01-go-structured-logging-slog":3},{"title":4,"description":5,"path":6,"body":7},"Structured logging в Go: slog, context и request id","Логи нужны не для того, чтобы \"что-то печаталось\". Во время инцидента они должны отвечать на конкретные вопросы:","\u002F11-observability\u002F01-go-structured-logging-slog",{"type":8,"value":9,"toc":4781},"minimark",[10,14,17,36,59,79,84,87,97,100,103,361,382,386,393,679,691,695,698,764,782,786,1267,1272,1276,1281,1284,1309,1812,1816,1826,2014,2017,2383,2398,2402,2409,3104,3107,3177,3181,3187,3260,3270,3432,3435,3501,3507,3511,3514,3878,3888,3892,3895,3926,3945,3949,3960,3963,3969,3972,3976,3979,3982,3988,3991,3997,4005,4008,4011,4098,4102,4105,4108,4125,4128,4142,4145,4149,4152,4155,4177,4180,4186,4189,4193,4196,4257,4260,4264,4305,4309,4312,4374,4377,4412,4415,4419,4456,4614,4777],[11,12,4],"h1",{"id":13},"structured-logging-в-go-slog-context-и-request-id",[15,16,5],"p",{},[18,19,20,24,27,30,33],"ul",{},[21,22,23],"li",{},"какой запрос или job сломался;",[21,25,26],{},"в каком компоненте это произошло;",[21,28,29],{},"какая зависимость была затронута;",[21,31,32],{},"можно ли повторить операцию;",[21,34,35],{},"есть ли рядом trace, metric spike или deploy.",[15,37,38,39,43,44,47,48,47,51,54,55,58],{},"Хороший лог - это событие в виде данных. Человек читает ",[40,41,42],"code",{},"msg",", машина фильтрует поля. Если поля хаотичные, то через месяц команда начинает искать ошибки регулярками по тексту и вручную вспоминать, как назывался идентификатор: ",[40,45,46],{},"requestID",", ",[40,49,50],{},"request_id",[40,52,53],{},"rid"," или ",[40,56,57],{},"trace",".",[15,60,61,62,65,66,47,69,47,72,75,76,58],{},"В Go для курса берем стандартный ",[40,63,64],{},"log\u002Fslog",": он есть в стандартной библиотеке с Go 1.21, умеет JSON handler, уровни, ",[40,67,68],{},"With",[40,70,71],{},"WithGroup",[40,73,74],{},"ReplaceAttr"," и динамический ",[40,77,78],{},"LevelVar",[80,81,83],"h2",{"id":82},"лог-как-контракт","Лог как контракт",[15,85,86],{},"Плохой лог:",[88,89,95],"pre",{"className":90,"code":92,"language":93,"meta":94},[91],"language-text","error while convert\n","text","",[40,96,92],{"__ignoreMap":94},[15,98,99],{},"Он не отвечает почти ни на что. Какой route? Какая валюта? Ошибка пользователя или инфраструктуры? Можно ли найти соседние события?",[15,101,102],{},"Хороший лог:",[88,104,108],{"className":105,"code":106,"language":107,"meta":94,"style":94},"language-json shiki shiki-themes github-dark","{\n  \"time\": \"2026-05-06T12:00:00Z\",\n  \"level\": \"ERROR\",\n  \"msg\": \"convert request failed\",\n  \"event\": \"convert.failed\",\n  \"service\": \"ratedesk-api\",\n  \"env\": \"production\",\n  \"version\": \"1.12.4\",\n  \"request_id\": \"req_42\",\n  \"trace_id\": \"6b6d9c9c7c8f4c8e9a4c7a0a5a8f2e01\",\n  \"span_id\": \"f3d8a7b1c2d3e4f5\",\n  \"http.method\": \"POST\",\n  \"http.route\": \"\u002Fconvert\",\n  \"http.status_code\": 500,\n  \"http.duration_ms\": 183,\n  \"currency.from\": \"USD\",\n  \"currency.to\": \"EUR\",\n  \"error.kind\": \"dependency_timeout\",\n  \"error.message\": \"postgres query timeout\"\n}\n","json",[40,109,110,119,136,149,162,175,188,201,214,227,240,253,266,279,292,305,318,331,344,355],{"__ignoreMap":94},[111,112,115],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"s95oV","{\n",[111,120,122,126,129,133],{"class":113,"line":121},2,[111,123,125],{"class":124},"sDLfK","  \"time\"",[111,127,128],{"class":117},": ",[111,130,132],{"class":131},"sU2Wk","\"2026-05-06T12:00:00Z\"",[111,134,135],{"class":117},",\n",[111,137,139,142,144,147],{"class":113,"line":138},3,[111,140,141],{"class":124},"  \"level\"",[111,143,128],{"class":117},[111,145,146],{"class":131},"\"ERROR\"",[111,148,135],{"class":117},[111,150,152,155,157,160],{"class":113,"line":151},4,[111,153,154],{"class":124},"  \"msg\"",[111,156,128],{"class":117},[111,158,159],{"class":131},"\"convert request failed\"",[111,161,135],{"class":117},[111,163,165,168,170,173],{"class":113,"line":164},5,[111,166,167],{"class":124},"  \"event\"",[111,169,128],{"class":117},[111,171,172],{"class":131},"\"convert.failed\"",[111,174,135],{"class":117},[111,176,178,181,183,186],{"class":113,"line":177},6,[111,179,180],{"class":124},"  \"service\"",[111,182,128],{"class":117},[111,184,185],{"class":131},"\"ratedesk-api\"",[111,187,135],{"class":117},[111,189,191,194,196,199],{"class":113,"line":190},7,[111,192,193],{"class":124},"  \"env\"",[111,195,128],{"class":117},[111,197,198],{"class":131},"\"production\"",[111,200,135],{"class":117},[111,202,204,207,209,212],{"class":113,"line":203},8,[111,205,206],{"class":124},"  \"version\"",[111,208,128],{"class":117},[111,210,211],{"class":131},"\"1.12.4\"",[111,213,135],{"class":117},[111,215,217,220,222,225],{"class":113,"line":216},9,[111,218,219],{"class":124},"  \"request_id\"",[111,221,128],{"class":117},[111,223,224],{"class":131},"\"req_42\"",[111,226,135],{"class":117},[111,228,230,233,235,238],{"class":113,"line":229},10,[111,231,232],{"class":124},"  \"trace_id\"",[111,234,128],{"class":117},[111,236,237],{"class":131},"\"6b6d9c9c7c8f4c8e9a4c7a0a5a8f2e01\"",[111,239,135],{"class":117},[111,241,243,246,248,251],{"class":113,"line":242},11,[111,244,245],{"class":124},"  \"span_id\"",[111,247,128],{"class":117},[111,249,250],{"class":131},"\"f3d8a7b1c2d3e4f5\"",[111,252,135],{"class":117},[111,254,256,259,261,264],{"class":113,"line":255},12,[111,257,258],{"class":124},"  \"http.method\"",[111,260,128],{"class":117},[111,262,263],{"class":131},"\"POST\"",[111,265,135],{"class":117},[111,267,269,272,274,277],{"class":113,"line":268},13,[111,270,271],{"class":124},"  \"http.route\"",[111,273,128],{"class":117},[111,275,276],{"class":131},"\"\u002Fconvert\"",[111,278,135],{"class":117},[111,280,282,285,287,290],{"class":113,"line":281},14,[111,283,284],{"class":124},"  \"http.status_code\"",[111,286,128],{"class":117},[111,288,289],{"class":124},"500",[111,291,135],{"class":117},[111,293,295,298,300,303],{"class":113,"line":294},15,[111,296,297],{"class":124},"  \"http.duration_ms\"",[111,299,128],{"class":117},[111,301,302],{"class":124},"183",[111,304,135],{"class":117},[111,306,308,311,313,316],{"class":113,"line":307},16,[111,309,310],{"class":124},"  \"currency.from\"",[111,312,128],{"class":117},[111,314,315],{"class":131},"\"USD\"",[111,317,135],{"class":117},[111,319,321,324,326,329],{"class":113,"line":320},17,[111,322,323],{"class":124},"  \"currency.to\"",[111,325,128],{"class":117},[111,327,328],{"class":131},"\"EUR\"",[111,330,135],{"class":117},[111,332,334,337,339,342],{"class":113,"line":333},18,[111,335,336],{"class":124},"  \"error.kind\"",[111,338,128],{"class":117},[111,340,341],{"class":131},"\"dependency_timeout\"",[111,343,135],{"class":117},[111,345,347,350,352],{"class":113,"line":346},19,[111,348,349],{"class":124},"  \"error.message\"",[111,351,128],{"class":117},[111,353,354],{"class":131},"\"postgres query timeout\"\n",[111,356,358],{"class":113,"line":357},20,[111,359,360],{"class":117},"}\n",[15,362,363,364,366,367,47,370,47,372,47,375,47,378,381],{},"Здесь ",[40,365,42],{}," можно читать глазами, а ",[40,368,369],{},"event",[40,371,50],{},[40,373,374],{},"trace_id",[40,376,377],{},"http.route",[40,379,380],{},"error.kind"," можно использовать в Loki, Elastic\u002FOpenSearch, Grafana и runbook.",[80,383,385],{"id":384},"базовая-log-schema","Базовая log schema",[15,387,388,389,392],{},"Схему лучше описать прямо в README или ",[40,390,391],{},"docs\u002Fobservability\u002Flogging.md",". Минимальный набор для backend-сервиса:",[394,395,396,413],"table",{},[397,398,399],"thead",{},[400,401,402,407,410],"tr",{},[403,404,406],"th",{"align":405},"left","Поле",[403,408,409],{"align":405},"Пример",[403,411,412],{"align":405},"Зачем",[414,415,416,432,453,467,481,496,517,530,551,565,577,590,605,619,633,647,664],"tbody",{},[400,417,418,424,429],{},[419,420,421],"td",{"align":405},[40,422,423],{},"time",[419,425,426],{"align":405},[40,427,428],{},"2026-05-06T12:00:00Z",[419,430,431],{"align":405},"Время события.",[400,433,434,439,450],{},[419,435,436],{"align":405},[40,437,438],{},"level",[419,440,441,47,444,47,447],{"align":405},[40,442,443],{},"INFO",[40,445,446],{},"WARN",[40,448,449],{},"ERROR",[419,451,452],{"align":405},"Фильтрация по важности.",[400,454,455,459,464],{},[419,456,457],{"align":405},[40,458,42],{},[419,460,461],{"align":405},[40,462,463],{},"convert request failed",[419,465,466],{"align":405},"Читаемый текст.",[400,468,469,473,478],{},[419,470,471],{"align":405},[40,472,369],{},[419,474,475],{"align":405},[40,476,477],{},"convert.failed",[419,479,480],{"align":405},"Стабильный machine-readable код.",[400,482,483,488,493],{},[419,484,485],{"align":405},[40,486,487],{},"service",[419,489,490],{"align":405},[40,491,492],{},"ratedesk-api",[419,494,495],{"align":405},"Кто породил лог.",[400,497,498,503,514],{},[419,499,500],{"align":405},[40,501,502],{},"env",[419,504,505,47,508,47,511],{"align":405},[40,506,507],{},"prod",[40,509,510],{},"stage",[40,512,513],{},"local",[419,515,516],{"align":405},"Окружение.",[400,518,519,524,527],{},[419,520,521],{"align":405},[40,522,523],{},"version",[419,525,526],{"align":405},"git SHA или semver",[419,528,529],{"align":405},"Связь с релизом.",[400,531,532,537,548],{},[419,533,534],{"align":405},[40,535,536],{},"component",[419,538,539,47,542,47,545],{"align":405},[40,540,541],{},"http",[40,543,544],{},"repository",[40,546,547],{},"consumer",[419,549,550],{"align":405},"Зона кода.",[400,552,553,557,562],{},[419,554,555],{"align":405},[40,556,50],{},[419,558,559],{"align":405},[40,560,561],{},"req_...",[419,563,564],{"align":405},"Корреляция без tracing.",[400,566,567,571,574],{},[419,568,569],{"align":405},[40,570,374],{},[419,572,573],{"align":405},"hex trace id",[419,575,576],{"align":405},"Переход в trace.",[400,578,579,584,587],{},[419,580,581],{"align":405},[40,582,583],{},"span_id",[419,585,586],{"align":405},"hex span id",[419,588,589],{"align":405},"Точный span внутри trace.",[400,591,592,597,602],{},[419,593,594],{"align":405},[40,595,596],{},"http.method",[419,598,599],{"align":405},[40,600,601],{},"POST",[419,603,604],{"align":405},"HTTP-разрез.",[400,606,607,611,616],{},[419,608,609],{"align":405},[40,610,377],{},[419,612,613],{"align":405},[40,614,615],{},"\u002Fconvert",[419,617,618],{"align":405},"Нормализованный route, не raw URL.",[400,620,621,626,630],{},[419,622,623],{"align":405},[40,624,625],{},"http.status_code",[419,627,628],{"align":405},[40,629,289],{},[419,631,632],{"align":405},"Статус ответа.",[400,634,635,640,644],{},[419,636,637],{"align":405},[40,638,639],{},"http.duration_ms",[419,641,642],{"align":405},[40,643,302],{},[419,645,646],{"align":405},"Длительность запроса.",[400,648,649,653,661],{},[419,650,651],{"align":405},[40,652,380],{},[419,654,655,47,658],{"align":405},[40,656,657],{},"validation",[40,659,660],{},"db_timeout",[419,662,663],{"align":405},"Классификация ошибки.",[400,665,666,671,676],{},[419,667,668],{"align":405},[40,669,670],{},"error.message",[419,672,673],{"align":405},[40,674,675],{},"context deadline exceeded",[419,677,678],{"align":405},"Текст ошибки без секретов.",[15,680,681,682,684,685,687,688,690],{},"Важно отделять ",[40,683,369],{}," от ",[40,686,42],{},". Текст можно улучшать, переводить, уточнять. ",[40,689,369],{}," должен оставаться стабильным, иначе сохраненные запросы и alert rules начнут ломаться.",[80,692,694],{"id":693},"уровни-логирования","Уровни логирования",[15,696,697],{},"Политика уровней должна быть скучной и предсказуемой:",[394,699,700,713],{},[397,701,702],{},[400,703,704,707,710],{},[403,705,706],{"align":405},"Level",[403,708,709],{"align":405},"Когда использовать",[403,711,712],{"align":405},"Когда не использовать",[414,714,715,728,740,752],{},[400,716,717,722,725],{},[419,718,719],{"align":405},[40,720,721],{},"DEBUG",[419,723,724],{"align":405},"Временная диагностика, детали retry, payload shape без PII.",[419,726,727],{"align":405},"Постоянный production-шум.",[400,729,730,734,737],{},[419,731,732],{"align":405},[40,733,443],{},[419,735,736],{"align":405},"Жизненный цикл сервиса, успешные важные business events, access logs.",[419,738,739],{"align":405},"Каждая внутренняя функция.",[400,741,742,746,749],{},[419,743,744],{"align":405},[40,745,446],{},[419,747,748],{"align":405},"Деградация, fallback, retry exhausted soon, подозрительное состояние.",[419,750,751],{"align":405},"Обычная 404\u002Fvalidation ошибка.",[400,753,754,758,761],{},[419,755,756],{"align":405},[40,757,449],{},[419,759,760],{"align":405},"Операция не выполнена из-за ошибки системы или зависимости.",[419,762,763],{"align":405},"Любая пользовательская ошибка.",[15,765,766,769,770,772,773,54,776,779,780,58],{},[40,767,768],{},"rate not found"," может быть доменной ошибкой и HTTP 404, а не ",[40,771,449],{},". ",[40,774,775],{},"postgres timeout",[40,777,778],{},"kafka publish failed"," - почти всегда ",[40,781,449],{},[80,783,785],{"id":784},"настройка-slog","Настройка slog",[88,787,792],{"className":788,"code":789,"language":790,"meta":791,"style":94},"language-go shiki shiki-themes github-dark","package observability\n\nimport (\n    \"log\u002Fslog\"\n    \"os\"\n    \"strings\"\n)\n\nvar level slog.LevelVar\n\nfunc redactAttr(_ []string, a slog.Attr) slog.Attr {\n    key := strings.ToLower(a.Key)\n    switch key {\n    case \"password\", \"token\", \"access_token\", \"refresh_token\",\n        \"authorization\", \"cookie\", \"secret\", \"api_key\":\n        return slog.String(a.Key, \"[REDACTED]\")\n    }\n    return a\n}\n\nfunc NewLogger(service, env, version string) *slog.Logger {\n    if env == \"local\" || env == \"dev\" {\n        level.Set(slog.LevelDebug)\n    } else {\n        level.Set(slog.LevelInfo)\n    }\n\n    handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{\n        Level:       &level,\n        AddSource:   env != \"production\",\n        ReplaceAttr: redactAttr,\n    })\n\n    return slog.New(handler).With(\n        \"service\", service,\n        \"env\", env,\n        \"version\", version,\n    )\n}\n","go","no-run",[40,793,794,804,810,818,828,837,846,851,855,871,875,921,938,946,971,994,1013,1018,1026,1030,1034,1071,1098,1110,1121,1131,1136,1141,1169,1180,1194,1200,1206,1211,1229,1238,1247,1256,1262],{"__ignoreMap":94},[111,795,796,800],{"class":113,"line":114},[111,797,799],{"class":798},"snl16","package",[111,801,803],{"class":802},"svObZ"," observability\n",[111,805,806],{"class":113,"line":121},[111,807,809],{"emptyLinePlaceholder":808},true,"\n",[111,811,812,815],{"class":113,"line":138},[111,813,814],{"class":798},"import",[111,816,817],{"class":117}," (\n",[111,819,820,823,825],{"class":113,"line":151},[111,821,822],{"class":131},"    \"",[111,824,64],{"class":802},[111,826,827],{"class":131},"\"\n",[111,829,830,832,835],{"class":113,"line":164},[111,831,822],{"class":131},[111,833,834],{"class":802},"os",[111,836,827],{"class":131},[111,838,839,841,844],{"class":113,"line":177},[111,840,822],{"class":131},[111,842,843],{"class":802},"strings",[111,845,827],{"class":131},[111,847,848],{"class":113,"line":190},[111,849,850],{"class":117},")\n",[111,852,853],{"class":113,"line":203},[111,854,809],{"emptyLinePlaceholder":808},[111,856,857,860,863,866,868],{"class":113,"line":216},[111,858,859],{"class":798},"var",[111,861,862],{"class":117}," level ",[111,864,865],{"class":802},"slog",[111,867,58],{"class":117},[111,869,870],{"class":802},"LevelVar\n",[111,872,873],{"class":113,"line":229},[111,874,809],{"emptyLinePlaceholder":808},[111,876,877,880,883,886,890,893,896,898,901,904,906,909,912,914,916,918],{"class":113,"line":242},[111,878,879],{"class":798},"func",[111,881,882],{"class":802}," redactAttr",[111,884,885],{"class":117},"(",[111,887,889],{"class":888},"s9osk","_",[111,891,892],{"class":117}," []",[111,894,895],{"class":798},"string",[111,897,47],{"class":117},[111,899,900],{"class":888},"a",[111,902,903],{"class":802}," slog",[111,905,58],{"class":117},[111,907,908],{"class":802},"Attr",[111,910,911],{"class":117},") ",[111,913,865],{"class":802},[111,915,58],{"class":117},[111,917,908],{"class":802},[111,919,920],{"class":117}," {\n",[111,922,923,926,929,932,935],{"class":113,"line":255},[111,924,925],{"class":117},"    key ",[111,927,928],{"class":798},":=",[111,930,931],{"class":117}," strings.",[111,933,934],{"class":802},"ToLower",[111,936,937],{"class":117},"(a.Key)\n",[111,939,940,943],{"class":113,"line":268},[111,941,942],{"class":798},"    switch",[111,944,945],{"class":117}," key {\n",[111,947,948,951,954,956,959,961,964,966,969],{"class":113,"line":281},[111,949,950],{"class":798},"    case",[111,952,953],{"class":131}," \"password\"",[111,955,47],{"class":117},[111,957,958],{"class":131},"\"token\"",[111,960,47],{"class":117},[111,962,963],{"class":131},"\"access_token\"",[111,965,47],{"class":117},[111,967,968],{"class":131},"\"refresh_token\"",[111,970,135],{"class":117},[111,972,973,976,978,981,983,986,988,991],{"class":113,"line":294},[111,974,975],{"class":131},"        \"authorization\"",[111,977,47],{"class":117},[111,979,980],{"class":131},"\"cookie\"",[111,982,47],{"class":117},[111,984,985],{"class":131},"\"secret\"",[111,987,47],{"class":117},[111,989,990],{"class":131},"\"api_key\"",[111,992,993],{"class":117},":\n",[111,995,996,999,1002,1005,1008,1011],{"class":113,"line":307},[111,997,998],{"class":798},"        return",[111,1000,1001],{"class":117}," slog.",[111,1003,1004],{"class":802},"String",[111,1006,1007],{"class":117},"(a.Key, ",[111,1009,1010],{"class":131},"\"[REDACTED]\"",[111,1012,850],{"class":117},[111,1014,1015],{"class":113,"line":320},[111,1016,1017],{"class":117},"    }\n",[111,1019,1020,1023],{"class":113,"line":333},[111,1021,1022],{"class":798},"    return",[111,1024,1025],{"class":117}," a\n",[111,1027,1028],{"class":113,"line":346},[111,1029,360],{"class":117},[111,1031,1032],{"class":113,"line":357},[111,1033,809],{"emptyLinePlaceholder":808},[111,1035,1037,1039,1042,1044,1046,1048,1050,1052,1054,1057,1059,1062,1064,1066,1069],{"class":113,"line":1036},21,[111,1038,879],{"class":798},[111,1040,1041],{"class":802}," NewLogger",[111,1043,885],{"class":117},[111,1045,487],{"class":888},[111,1047,47],{"class":117},[111,1049,502],{"class":888},[111,1051,47],{"class":117},[111,1053,523],{"class":888},[111,1055,1056],{"class":798}," string",[111,1058,911],{"class":117},[111,1060,1061],{"class":798},"*",[111,1063,865],{"class":802},[111,1065,58],{"class":117},[111,1067,1068],{"class":802},"Logger",[111,1070,920],{"class":117},[111,1072,1074,1077,1080,1083,1086,1089,1091,1093,1096],{"class":113,"line":1073},22,[111,1075,1076],{"class":798},"    if",[111,1078,1079],{"class":117}," env ",[111,1081,1082],{"class":798},"==",[111,1084,1085],{"class":131}," \"local\"",[111,1087,1088],{"class":798}," ||",[111,1090,1079],{"class":117},[111,1092,1082],{"class":798},[111,1094,1095],{"class":131}," \"dev\"",[111,1097,920],{"class":117},[111,1099,1101,1104,1107],{"class":113,"line":1100},23,[111,1102,1103],{"class":117},"        level.",[111,1105,1106],{"class":802},"Set",[111,1108,1109],{"class":117},"(slog.LevelDebug)\n",[111,1111,1113,1116,1119],{"class":113,"line":1112},24,[111,1114,1115],{"class":117},"    } ",[111,1117,1118],{"class":798},"else",[111,1120,920],{"class":117},[111,1122,1124,1126,1128],{"class":113,"line":1123},25,[111,1125,1103],{"class":117},[111,1127,1106],{"class":802},[111,1129,1130],{"class":117},"(slog.LevelInfo)\n",[111,1132,1134],{"class":113,"line":1133},26,[111,1135,1017],{"class":117},[111,1137,1139],{"class":113,"line":1138},27,[111,1140,809],{"emptyLinePlaceholder":808},[111,1142,1144,1147,1149,1151,1154,1157,1160,1162,1164,1167],{"class":113,"line":1143},28,[111,1145,1146],{"class":117},"    handler ",[111,1148,928],{"class":798},[111,1150,1001],{"class":117},[111,1152,1153],{"class":802},"NewJSONHandler",[111,1155,1156],{"class":117},"(os.Stdout, ",[111,1158,1159],{"class":798},"&",[111,1161,865],{"class":802},[111,1163,58],{"class":117},[111,1165,1166],{"class":802},"HandlerOptions",[111,1168,118],{"class":117},[111,1170,1172,1175,1177],{"class":113,"line":1171},29,[111,1173,1174],{"class":117},"        Level:       ",[111,1176,1159],{"class":798},[111,1178,1179],{"class":117},"level,\n",[111,1181,1183,1186,1189,1192],{"class":113,"line":1182},30,[111,1184,1185],{"class":117},"        AddSource:   env ",[111,1187,1188],{"class":798},"!=",[111,1190,1191],{"class":131}," \"production\"",[111,1193,135],{"class":117},[111,1195,1197],{"class":113,"line":1196},31,[111,1198,1199],{"class":117},"        ReplaceAttr: redactAttr,\n",[111,1201,1203],{"class":113,"line":1202},32,[111,1204,1205],{"class":117},"    })\n",[111,1207,1209],{"class":113,"line":1208},33,[111,1210,809],{"emptyLinePlaceholder":808},[111,1212,1214,1216,1218,1221,1224,1226],{"class":113,"line":1213},34,[111,1215,1022],{"class":798},[111,1217,1001],{"class":117},[111,1219,1220],{"class":802},"New",[111,1222,1223],{"class":117},"(handler).",[111,1225,68],{"class":802},[111,1227,1228],{"class":117},"(\n",[111,1230,1232,1235],{"class":113,"line":1231},35,[111,1233,1234],{"class":131},"        \"service\"",[111,1236,1237],{"class":117},", service,\n",[111,1239,1241,1244],{"class":113,"line":1240},36,[111,1242,1243],{"class":131},"        \"env\"",[111,1245,1246],{"class":117},", env,\n",[111,1248,1250,1253],{"class":113,"line":1249},37,[111,1251,1252],{"class":131},"        \"version\"",[111,1254,1255],{"class":117},", version,\n",[111,1257,1259],{"class":113,"line":1258},38,[111,1260,1261],{"class":117},"    )\n",[111,1263,1265],{"class":113,"line":1264},39,[111,1266,360],{"class":117},[15,1268,1269,1271],{},[40,1270,74],{}," не заменяет нормальную security-политику, но это хороший последний барьер. Лучше редактировать секреты централизованно, чем надеяться, что каждый разработчик вспомнит про это в каждом handler.",[80,1273,1275],{"id":1274},"request-id","Request id",[15,1277,1278,1280],{},[40,1279,50],{}," - минимальный ключ корреляции. Он работает даже без OpenTelemetry и distributed tracing.",[15,1282,1283],{},"Правила:",[18,1285,1286,1293,1296,1299,1306],{},[21,1287,1288,1289,1292],{},"если клиент прислал валидный ",[40,1290,1291],{},"X-Request-Id",", сохраняем его;",[21,1294,1295],{},"если не прислал, генерируем;",[21,1297,1298],{},"возвращаем id в response header;",[21,1300,1301,1302,1305],{},"кладем id в ",[40,1303,1304],{},"context.Context",";",[21,1307,1308],{},"добавляем id во все logs на пути запроса.",[88,1310,1312],{"className":788,"code":1311,"language":790,"meta":791,"style":94},"package observability\n\nimport (\n    \"context\"\n    \"crypto\u002Frand\"\n    \"encoding\u002Fhex\"\n    \"net\u002Fhttp\"\n)\n\ntype requestIDKey struct{}\n\nfunc WithRequestID(ctx context.Context, id string) context.Context {\n    return context.WithValue(ctx, requestIDKey{}, id)\n}\n\nfunc RequestID(ctx context.Context) string {\n    id, _ := ctx.Value(requestIDKey{}).(string)\n    return id\n}\n\nfunc newRequestID() string {\n    var b [16]byte\n    if _, err := rand.Read(b[:]); err != nil {\n        return \"req_unknown\"\n    }\n    return \"req_\" + hex.EncodeToString(b[:])\n}\n\nfunc RequestIDMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        id := r.Header.Get(\"X-Request-Id\")\n        if id == \"\" || len(id) > 128 {\n            id = newRequestID()\n        }\n\n        w.Header().Set(\"X-Request-Id\", id)\n        next.ServeHTTP(w, r.WithContext(WithRequestID(r.Context(), id)))\n    })\n}\n",[40,1313,1314,1320,1324,1330,1339,1348,1357,1366,1370,1374,1388,1392,1429,1448,1452,1456,1479,1503,1510,1514,1518,1532,1549,1574,1581,1585,1604,1608,1612,1642,1686,1706,1735,1748,1753,1757,1777,1804,1808],{"__ignoreMap":94},[111,1315,1316,1318],{"class":113,"line":114},[111,1317,799],{"class":798},[111,1319,803],{"class":802},[111,1321,1322],{"class":113,"line":121},[111,1323,809],{"emptyLinePlaceholder":808},[111,1325,1326,1328],{"class":113,"line":138},[111,1327,814],{"class":798},[111,1329,817],{"class":117},[111,1331,1332,1334,1337],{"class":113,"line":151},[111,1333,822],{"class":131},[111,1335,1336],{"class":802},"context",[111,1338,827],{"class":131},[111,1340,1341,1343,1346],{"class":113,"line":164},[111,1342,822],{"class":131},[111,1344,1345],{"class":802},"crypto\u002Frand",[111,1347,827],{"class":131},[111,1349,1350,1352,1355],{"class":113,"line":177},[111,1351,822],{"class":131},[111,1353,1354],{"class":802},"encoding\u002Fhex",[111,1356,827],{"class":131},[111,1358,1359,1361,1364],{"class":113,"line":190},[111,1360,822],{"class":131},[111,1362,1363],{"class":802},"net\u002Fhttp",[111,1365,827],{"class":131},[111,1367,1368],{"class":113,"line":203},[111,1369,850],{"class":117},[111,1371,1372],{"class":113,"line":216},[111,1373,809],{"emptyLinePlaceholder":808},[111,1375,1376,1379,1382,1385],{"class":113,"line":229},[111,1377,1378],{"class":798},"type",[111,1380,1381],{"class":802}," requestIDKey",[111,1383,1384],{"class":798}," struct",[111,1386,1387],{"class":117},"{}\n",[111,1389,1390],{"class":113,"line":242},[111,1391,809],{"emptyLinePlaceholder":808},[111,1393,1394,1396,1399,1401,1404,1407,1409,1412,1414,1417,1419,1421,1423,1425,1427],{"class":113,"line":255},[111,1395,879],{"class":798},[111,1397,1398],{"class":802}," WithRequestID",[111,1400,885],{"class":117},[111,1402,1403],{"class":888},"ctx",[111,1405,1406],{"class":802}," context",[111,1408,58],{"class":117},[111,1410,1411],{"class":802},"Context",[111,1413,47],{"class":117},[111,1415,1416],{"class":888},"id",[111,1418,1056],{"class":798},[111,1420,911],{"class":117},[111,1422,1336],{"class":802},[111,1424,58],{"class":117},[111,1426,1411],{"class":802},[111,1428,920],{"class":117},[111,1430,1431,1433,1436,1439,1442,1445],{"class":113,"line":268},[111,1432,1022],{"class":798},[111,1434,1435],{"class":117}," context.",[111,1437,1438],{"class":802},"WithValue",[111,1440,1441],{"class":117},"(ctx, ",[111,1443,1444],{"class":802},"requestIDKey",[111,1446,1447],{"class":117},"{}, id)\n",[111,1449,1450],{"class":113,"line":281},[111,1451,360],{"class":117},[111,1453,1454],{"class":113,"line":294},[111,1455,809],{"emptyLinePlaceholder":808},[111,1457,1458,1460,1463,1465,1467,1469,1471,1473,1475,1477],{"class":113,"line":307},[111,1459,879],{"class":798},[111,1461,1462],{"class":802}," RequestID",[111,1464,885],{"class":117},[111,1466,1403],{"class":888},[111,1468,1406],{"class":802},[111,1470,58],{"class":117},[111,1472,1411],{"class":802},[111,1474,911],{"class":117},[111,1476,895],{"class":798},[111,1478,920],{"class":117},[111,1480,1481,1484,1486,1489,1492,1494,1496,1499,1501],{"class":113,"line":320},[111,1482,1483],{"class":117},"    id, _ ",[111,1485,928],{"class":798},[111,1487,1488],{"class":117}," ctx.",[111,1490,1491],{"class":802},"Value",[111,1493,885],{"class":117},[111,1495,1444],{"class":802},[111,1497,1498],{"class":117},"{}).(",[111,1500,895],{"class":798},[111,1502,850],{"class":117},[111,1504,1505,1507],{"class":113,"line":333},[111,1506,1022],{"class":798},[111,1508,1509],{"class":117}," id\n",[111,1511,1512],{"class":113,"line":346},[111,1513,360],{"class":117},[111,1515,1516],{"class":113,"line":357},[111,1517,809],{"emptyLinePlaceholder":808},[111,1519,1520,1522,1525,1528,1530],{"class":113,"line":1036},[111,1521,879],{"class":798},[111,1523,1524],{"class":802}," newRequestID",[111,1526,1527],{"class":117},"() ",[111,1529,895],{"class":798},[111,1531,920],{"class":117},[111,1533,1534,1537,1540,1543,1546],{"class":113,"line":1073},[111,1535,1536],{"class":798},"    var",[111,1538,1539],{"class":117}," b [",[111,1541,1542],{"class":124},"16",[111,1544,1545],{"class":117},"]",[111,1547,1548],{"class":798},"byte\n",[111,1550,1551,1553,1556,1558,1561,1564,1567,1569,1572],{"class":113,"line":1100},[111,1552,1076],{"class":798},[111,1554,1555],{"class":117}," _, err ",[111,1557,928],{"class":798},[111,1559,1560],{"class":117}," rand.",[111,1562,1563],{"class":802},"Read",[111,1565,1566],{"class":117},"(b[:]); err ",[111,1568,1188],{"class":798},[111,1570,1571],{"class":124}," nil",[111,1573,920],{"class":117},[111,1575,1576,1578],{"class":113,"line":1112},[111,1577,998],{"class":798},[111,1579,1580],{"class":131}," \"req_unknown\"\n",[111,1582,1583],{"class":113,"line":1123},[111,1584,1017],{"class":117},[111,1586,1587,1589,1592,1595,1598,1601],{"class":113,"line":1133},[111,1588,1022],{"class":798},[111,1590,1591],{"class":131}," \"req_\"",[111,1593,1594],{"class":798}," +",[111,1596,1597],{"class":117}," hex.",[111,1599,1600],{"class":802},"EncodeToString",[111,1602,1603],{"class":117},"(b[:])\n",[111,1605,1606],{"class":113,"line":1138},[111,1607,360],{"class":117},[111,1609,1610],{"class":113,"line":1143},[111,1611,809],{"emptyLinePlaceholder":808},[111,1613,1614,1616,1619,1621,1624,1627,1629,1632,1634,1636,1638,1640],{"class":113,"line":1171},[111,1615,879],{"class":798},[111,1617,1618],{"class":802}," RequestIDMiddleware",[111,1620,885],{"class":117},[111,1622,1623],{"class":888},"next",[111,1625,1626],{"class":802}," http",[111,1628,58],{"class":117},[111,1630,1631],{"class":802},"Handler",[111,1633,911],{"class":117},[111,1635,541],{"class":802},[111,1637,58],{"class":117},[111,1639,1631],{"class":802},[111,1641,920],{"class":117},[111,1643,1644,1646,1649,1652,1654,1656,1658,1661,1663,1665,1668,1670,1673,1676,1678,1680,1683],{"class":113,"line":1182},[111,1645,1022],{"class":798},[111,1647,1648],{"class":117}," http.",[111,1650,1651],{"class":802},"HandlerFunc",[111,1653,885],{"class":117},[111,1655,879],{"class":798},[111,1657,885],{"class":117},[111,1659,1660],{"class":888},"w",[111,1662,1626],{"class":802},[111,1664,58],{"class":117},[111,1666,1667],{"class":802},"ResponseWriter",[111,1669,47],{"class":117},[111,1671,1672],{"class":888},"r",[111,1674,1675],{"class":798}," *",[111,1677,541],{"class":802},[111,1679,58],{"class":117},[111,1681,1682],{"class":802},"Request",[111,1684,1685],{"class":117},") {\n",[111,1687,1688,1691,1693,1696,1699,1701,1704],{"class":113,"line":1196},[111,1689,1690],{"class":117},"        id ",[111,1692,928],{"class":798},[111,1694,1695],{"class":117}," r.Header.",[111,1697,1698],{"class":802},"Get",[111,1700,885],{"class":117},[111,1702,1703],{"class":131},"\"X-Request-Id\"",[111,1705,850],{"class":117},[111,1707,1708,1711,1714,1716,1719,1721,1724,1727,1730,1733],{"class":113,"line":1202},[111,1709,1710],{"class":798},"        if",[111,1712,1713],{"class":117}," id ",[111,1715,1082],{"class":798},[111,1717,1718],{"class":131}," \"\"",[111,1720,1088],{"class":798},[111,1722,1723],{"class":802}," len",[111,1725,1726],{"class":117},"(id) ",[111,1728,1729],{"class":798},">",[111,1731,1732],{"class":124}," 128",[111,1734,920],{"class":117},[111,1736,1737,1740,1743,1745],{"class":113,"line":1208},[111,1738,1739],{"class":117},"            id ",[111,1741,1742],{"class":798},"=",[111,1744,1524],{"class":802},[111,1746,1747],{"class":117},"()\n",[111,1749,1750],{"class":113,"line":1213},[111,1751,1752],{"class":117},"        }\n",[111,1754,1755],{"class":113,"line":1231},[111,1756,809],{"emptyLinePlaceholder":808},[111,1758,1759,1762,1765,1768,1770,1772,1774],{"class":113,"line":1240},[111,1760,1761],{"class":117},"        w.",[111,1763,1764],{"class":802},"Header",[111,1766,1767],{"class":117},"().",[111,1769,1106],{"class":802},[111,1771,885],{"class":117},[111,1773,1703],{"class":131},[111,1775,1776],{"class":117},", id)\n",[111,1778,1779,1782,1785,1788,1791,1793,1796,1799,1801],{"class":113,"line":1249},[111,1780,1781],{"class":117},"        next.",[111,1783,1784],{"class":802},"ServeHTTP",[111,1786,1787],{"class":117},"(w, r.",[111,1789,1790],{"class":802},"WithContext",[111,1792,885],{"class":117},[111,1794,1795],{"class":802},"WithRequestID",[111,1797,1798],{"class":117},"(r.",[111,1800,1411],{"class":802},[111,1802,1803],{"class":117},"(), id)))\n",[111,1805,1806],{"class":113,"line":1258},[111,1807,1205],{"class":117},[111,1809,1810],{"class":113,"line":1264},[111,1811,360],{"class":117},[80,1813,1815],{"id":1814},"logger-из-context","Logger из context",[15,1817,1818,1819,1822,1823,1825],{},"Не надо прокидывать ",[40,1820,1821],{},"requestID string"," через все слои. Передавайте ",[40,1824,1304],{},", а logger собирайте на границе transport\u002Fmiddleware.",[88,1827,1829],{"className":788,"code":1828,"language":790,"meta":791,"style":94},"type loggerKey struct{}\n\nfunc WithLogger(ctx context.Context, logger *slog.Logger) context.Context {\n    return context.WithValue(ctx, loggerKey{}, logger)\n}\n\nfunc Logger(ctx context.Context, fallback *slog.Logger) *slog.Logger {\n    logger, _ := ctx.Value(loggerKey{}).(*slog.Logger)\n    if logger == nil {\n        return fallback\n    }\n    return logger\n}\n",[40,1830,1831,1842,1846,1886,1902,1906,1910,1952,1979,1992,1999,2003,2010],{"__ignoreMap":94},[111,1832,1833,1835,1838,1840],{"class":113,"line":114},[111,1834,1378],{"class":798},[111,1836,1837],{"class":802}," loggerKey",[111,1839,1384],{"class":798},[111,1841,1387],{"class":117},[111,1843,1844],{"class":113,"line":121},[111,1845,809],{"emptyLinePlaceholder":808},[111,1847,1848,1850,1853,1855,1857,1859,1861,1863,1865,1868,1870,1872,1874,1876,1878,1880,1882,1884],{"class":113,"line":138},[111,1849,879],{"class":798},[111,1851,1852],{"class":802}," WithLogger",[111,1854,885],{"class":117},[111,1856,1403],{"class":888},[111,1858,1406],{"class":802},[111,1860,58],{"class":117},[111,1862,1411],{"class":802},[111,1864,47],{"class":117},[111,1866,1867],{"class":888},"logger",[111,1869,1675],{"class":798},[111,1871,865],{"class":802},[111,1873,58],{"class":117},[111,1875,1068],{"class":802},[111,1877,911],{"class":117},[111,1879,1336],{"class":802},[111,1881,58],{"class":117},[111,1883,1411],{"class":802},[111,1885,920],{"class":117},[111,1887,1888,1890,1892,1894,1896,1899],{"class":113,"line":151},[111,1889,1022],{"class":798},[111,1891,1435],{"class":117},[111,1893,1438],{"class":802},[111,1895,1441],{"class":117},[111,1897,1898],{"class":802},"loggerKey",[111,1900,1901],{"class":117},"{}, logger)\n",[111,1903,1904],{"class":113,"line":164},[111,1905,360],{"class":117},[111,1907,1908],{"class":113,"line":177},[111,1909,809],{"emptyLinePlaceholder":808},[111,1911,1912,1914,1917,1919,1921,1923,1925,1927,1929,1932,1934,1936,1938,1940,1942,1944,1946,1948,1950],{"class":113,"line":190},[111,1913,879],{"class":798},[111,1915,1916],{"class":802}," Logger",[111,1918,885],{"class":117},[111,1920,1403],{"class":888},[111,1922,1406],{"class":802},[111,1924,58],{"class":117},[111,1926,1411],{"class":802},[111,1928,47],{"class":117},[111,1930,1931],{"class":888},"fallback",[111,1933,1675],{"class":798},[111,1935,865],{"class":802},[111,1937,58],{"class":117},[111,1939,1068],{"class":802},[111,1941,911],{"class":117},[111,1943,1061],{"class":798},[111,1945,865],{"class":802},[111,1947,58],{"class":117},[111,1949,1068],{"class":802},[111,1951,920],{"class":117},[111,1953,1954,1957,1959,1961,1963,1965,1967,1969,1971,1973,1975,1977],{"class":113,"line":203},[111,1955,1956],{"class":117},"    logger, _ ",[111,1958,928],{"class":798},[111,1960,1488],{"class":117},[111,1962,1491],{"class":802},[111,1964,885],{"class":117},[111,1966,1898],{"class":802},[111,1968,1498],{"class":117},[111,1970,1061],{"class":798},[111,1972,865],{"class":802},[111,1974,58],{"class":117},[111,1976,1068],{"class":802},[111,1978,850],{"class":117},[111,1980,1981,1983,1986,1988,1990],{"class":113,"line":216},[111,1982,1076],{"class":798},[111,1984,1985],{"class":117}," logger ",[111,1987,1082],{"class":798},[111,1989,1571],{"class":124},[111,1991,920],{"class":117},[111,1993,1994,1996],{"class":113,"line":229},[111,1995,998],{"class":798},[111,1997,1998],{"class":117}," fallback\n",[111,2000,2001],{"class":113,"line":242},[111,2002,1017],{"class":117},[111,2004,2005,2007],{"class":113,"line":255},[111,2006,1022],{"class":798},[111,2008,2009],{"class":117}," logger\n",[111,2011,2012],{"class":113,"line":268},[111,2013,360],{"class":117},[15,2015,2016],{},"Middleware:",[88,2018,2020],{"className":788,"code":2019,"language":790,"meta":791,"style":94},"func LoggingMiddleware(base *slog.Logger, next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        start := time.Now()\n        rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}\n\n        logger := base.With(\n            \"request_id\", RequestID(r.Context()),\n            \"http.method\", r.Method,\n            \"http.route\", routePattern(r),\n        )\n\n        if sc := trace.SpanContextFromContext(r.Context()); sc.IsValid() {\n            logger = logger.With(\n                \"trace_id\", sc.TraceID().String(),\n                \"span_id\", sc.SpanID().String(),\n            )\n        }\n\n        ctx := WithLogger(r.Context(), logger)\n        next.ServeHTTP(rec, r.WithContext(ctx))\n\n        logger.Info(\"http request completed\",\n            \"event\", \"http.request.completed\",\n            \"http.status_code\", rec.status,\n            \"http.duration_ms\", time.Since(start).Milliseconds(),\n        )\n    })\n}\n",[40,2021,2022,2062,2098,2113,2129,2133,2147,2164,2172,2185,2190,2194,2222,2236,2254,2270,2275,2279,2283,2299,2313,2317,2332,2344,2352,2371,2375,2379],{"__ignoreMap":94},[111,2023,2024,2026,2029,2031,2034,2036,2038,2040,2042,2044,2046,2048,2050,2052,2054,2056,2058,2060],{"class":113,"line":114},[111,2025,879],{"class":798},[111,2027,2028],{"class":802}," LoggingMiddleware",[111,2030,885],{"class":117},[111,2032,2033],{"class":888},"base",[111,2035,1675],{"class":798},[111,2037,865],{"class":802},[111,2039,58],{"class":117},[111,2041,1068],{"class":802},[111,2043,47],{"class":117},[111,2045,1623],{"class":888},[111,2047,1626],{"class":802},[111,2049,58],{"class":117},[111,2051,1631],{"class":802},[111,2053,911],{"class":117},[111,2055,541],{"class":802},[111,2057,58],{"class":117},[111,2059,1631],{"class":802},[111,2061,920],{"class":117},[111,2063,2064,2066,2068,2070,2072,2074,2076,2078,2080,2082,2084,2086,2088,2090,2092,2094,2096],{"class":113,"line":121},[111,2065,1022],{"class":798},[111,2067,1648],{"class":117},[111,2069,1651],{"class":802},[111,2071,885],{"class":117},[111,2073,879],{"class":798},[111,2075,885],{"class":117},[111,2077,1660],{"class":888},[111,2079,1626],{"class":802},[111,2081,58],{"class":117},[111,2083,1667],{"class":802},[111,2085,47],{"class":117},[111,2087,1672],{"class":888},[111,2089,1675],{"class":798},[111,2091,541],{"class":802},[111,2093,58],{"class":117},[111,2095,1682],{"class":802},[111,2097,1685],{"class":117},[111,2099,2100,2103,2105,2108,2111],{"class":113,"line":138},[111,2101,2102],{"class":117},"        start ",[111,2104,928],{"class":798},[111,2106,2107],{"class":117}," time.",[111,2109,2110],{"class":802},"Now",[111,2112,1747],{"class":117},[111,2114,2115,2118,2120,2123,2126],{"class":113,"line":151},[111,2116,2117],{"class":117},"        rec ",[111,2119,928],{"class":798},[111,2121,2122],{"class":798}," &",[111,2124,2125],{"class":802},"statusRecorder",[111,2127,2128],{"class":117},"{ResponseWriter: w, status: http.StatusOK}\n",[111,2130,2131],{"class":113,"line":164},[111,2132,809],{"emptyLinePlaceholder":808},[111,2134,2135,2138,2140,2143,2145],{"class":113,"line":177},[111,2136,2137],{"class":117},"        logger ",[111,2139,928],{"class":798},[111,2141,2142],{"class":117}," base.",[111,2144,68],{"class":802},[111,2146,1228],{"class":117},[111,2148,2149,2152,2154,2157,2159,2161],{"class":113,"line":190},[111,2150,2151],{"class":131},"            \"request_id\"",[111,2153,47],{"class":117},[111,2155,2156],{"class":802},"RequestID",[111,2158,1798],{"class":117},[111,2160,1411],{"class":802},[111,2162,2163],{"class":117},"()),\n",[111,2165,2166,2169],{"class":113,"line":203},[111,2167,2168],{"class":131},"            \"http.method\"",[111,2170,2171],{"class":117},", r.Method,\n",[111,2173,2174,2177,2179,2182],{"class":113,"line":216},[111,2175,2176],{"class":131},"            \"http.route\"",[111,2178,47],{"class":117},[111,2180,2181],{"class":802},"routePattern",[111,2183,2184],{"class":117},"(r),\n",[111,2186,2187],{"class":113,"line":229},[111,2188,2189],{"class":117},"        )\n",[111,2191,2192],{"class":113,"line":242},[111,2193,809],{"emptyLinePlaceholder":808},[111,2195,2196,2198,2201,2203,2206,2209,2211,2213,2216,2219],{"class":113,"line":255},[111,2197,1710],{"class":798},[111,2199,2200],{"class":117}," sc ",[111,2202,928],{"class":798},[111,2204,2205],{"class":117}," trace.",[111,2207,2208],{"class":802},"SpanContextFromContext",[111,2210,1798],{"class":117},[111,2212,1411],{"class":802},[111,2214,2215],{"class":117},"()); sc.",[111,2217,2218],{"class":802},"IsValid",[111,2220,2221],{"class":117},"() {\n",[111,2223,2224,2227,2229,2232,2234],{"class":113,"line":268},[111,2225,2226],{"class":117},"            logger ",[111,2228,1742],{"class":798},[111,2230,2231],{"class":117}," logger.",[111,2233,68],{"class":802},[111,2235,1228],{"class":117},[111,2237,2238,2241,2244,2247,2249,2251],{"class":113,"line":281},[111,2239,2240],{"class":131},"                \"trace_id\"",[111,2242,2243],{"class":117},", sc.",[111,2245,2246],{"class":802},"TraceID",[111,2248,1767],{"class":117},[111,2250,1004],{"class":802},[111,2252,2253],{"class":117},"(),\n",[111,2255,2256,2259,2261,2264,2266,2268],{"class":113,"line":294},[111,2257,2258],{"class":131},"                \"span_id\"",[111,2260,2243],{"class":117},[111,2262,2263],{"class":802},"SpanID",[111,2265,1767],{"class":117},[111,2267,1004],{"class":802},[111,2269,2253],{"class":117},[111,2271,2272],{"class":113,"line":307},[111,2273,2274],{"class":117},"            )\n",[111,2276,2277],{"class":113,"line":320},[111,2278,1752],{"class":117},[111,2280,2281],{"class":113,"line":333},[111,2282,809],{"emptyLinePlaceholder":808},[111,2284,2285,2288,2290,2292,2294,2296],{"class":113,"line":346},[111,2286,2287],{"class":117},"        ctx ",[111,2289,928],{"class":798},[111,2291,1852],{"class":802},[111,2293,1798],{"class":117},[111,2295,1411],{"class":802},[111,2297,2298],{"class":117},"(), logger)\n",[111,2300,2301,2303,2305,2308,2310],{"class":113,"line":357},[111,2302,1781],{"class":117},[111,2304,1784],{"class":802},[111,2306,2307],{"class":117},"(rec, r.",[111,2309,1790],{"class":802},[111,2311,2312],{"class":117},"(ctx))\n",[111,2314,2315],{"class":113,"line":1036},[111,2316,809],{"emptyLinePlaceholder":808},[111,2318,2319,2322,2325,2327,2330],{"class":113,"line":1073},[111,2320,2321],{"class":117},"        logger.",[111,2323,2324],{"class":802},"Info",[111,2326,885],{"class":117},[111,2328,2329],{"class":131},"\"http request completed\"",[111,2331,135],{"class":117},[111,2333,2334,2337,2339,2342],{"class":113,"line":1100},[111,2335,2336],{"class":131},"            \"event\"",[111,2338,47],{"class":117},[111,2340,2341],{"class":131},"\"http.request.completed\"",[111,2343,135],{"class":117},[111,2345,2346,2349],{"class":113,"line":1112},[111,2347,2348],{"class":131},"            \"http.status_code\"",[111,2350,2351],{"class":117},", rec.status,\n",[111,2353,2354,2357,2360,2363,2366,2369],{"class":113,"line":1123},[111,2355,2356],{"class":131},"            \"http.duration_ms\"",[111,2358,2359],{"class":117},", time.",[111,2361,2362],{"class":802},"Since",[111,2364,2365],{"class":117},"(start).",[111,2367,2368],{"class":802},"Milliseconds",[111,2370,2253],{"class":117},[111,2372,2373],{"class":113,"line":1133},[111,2374,2189],{"class":117},[111,2376,2377],{"class":113,"line":1138},[111,2378,1205],{"class":117},[111,2380,2381],{"class":113,"line":1143},[111,2382,360],{"class":117},[15,2384,2385,2386,2389,2390,2393,2394,2397],{},"В реальном router используйте route pattern (",[40,2387,2388],{},"\u002Fusers\u002F{id}","), а не ",[40,2391,2392],{},"r.URL.Path"," (",[40,2395,2396],{},"\u002Fusers\u002F123","). Полный path с id создает лишнюю кардинальность и в логах, и в метриках.",[80,2399,2401],{"id":2400},"echo-middleware","Echo middleware",[15,2403,2404,2405,2408],{},"В учебном backend используется Echo v4, поэтому тот же контракт удобно завернуть в middleware. Важно брать route pattern через ",[40,2406,2407],{},"c.Path()",", а не raw path.",[88,2410,2412],{"className":788,"code":2411,"language":790,"meta":791,"style":94},"func EchoRequestID() echo.MiddlewareFunc {\n    return func(next echo.HandlerFunc) echo.HandlerFunc {\n        return func(c echo.Context) error {\n            id := c.Request().Header.Get(\"X-Request-Id\")\n            if id == \"\" || len(id) > 128 {\n                id = newRequestID()\n            }\n\n            c.Response().Header().Set(\"X-Request-Id\", id)\n            req := c.Request()\n            ctx := WithRequestID(req.Context(), id)\n            c.SetRequest(req.WithContext(ctx))\n\n            return next(c)\n        }\n    }\n}\n\nfunc EchoAccessLog(base *slog.Logger) echo.MiddlewareFunc {\n    return func(next echo.HandlerFunc) echo.HandlerFunc {\n        return func(c echo.Context) error {\n            start := time.Now()\n            req := c.Request()\n\n            logger := base.With(\n                \"request_id\", RequestID(req.Context()),\n                \"http.method\", req.Method,\n                \"http.route\", c.Path(),\n            )\n            if sc := trace.SpanContextFromContext(req.Context()); sc.IsValid() {\n                logger = logger.With(\n                    \"trace_id\", sc.TraceID().String(),\n                    \"span_id\", sc.SpanID().String(),\n                )\n            }\n\n            ctx := WithLogger(req.Context(), logger)\n            c.SetRequest(req.WithContext(ctx))\n\n            err := next(c)\n            if err != nil {\n                c.Error(err)\n            }\n\n            status := c.Response().Status\n            level := slog.LevelInfo\n            if status >= 500 {\n                level = slog.LevelError\n            }\n\n            logger.Log(ctx, level, \"http request completed\",\n                \"event\", \"http.request.completed\",\n                \"http.status_code\", status,\n                \"http.duration_ms\", time.Since(start).Milliseconds(),\n            )\n            return nil\n        }\n    }\n}\n",[40,2413,2414,2433,2461,2485,2507,2530,2541,2546,2550,2572,2585,2602,2615,2619,2630,2634,2638,2642,2646,2675,2701,2723,2736,2748,2752,2764,2779,2787,2800,2804,2826,2839,2854,2869,2874,2878,2882,2896,2908,2912,2924,2938,2950,2955,2960,2975,2986,3002,3013,3018,3023,3039,3051,3060,3076,3081,3089,3094,3099],{"__ignoreMap":94},[111,2415,2416,2418,2421,2423,2426,2428,2431],{"class":113,"line":114},[111,2417,879],{"class":798},[111,2419,2420],{"class":802}," EchoRequestID",[111,2422,1527],{"class":117},[111,2424,2425],{"class":802},"echo",[111,2427,58],{"class":117},[111,2429,2430],{"class":802},"MiddlewareFunc",[111,2432,920],{"class":117},[111,2434,2435,2437,2440,2442,2444,2447,2449,2451,2453,2455,2457,2459],{"class":113,"line":121},[111,2436,1022],{"class":798},[111,2438,2439],{"class":798}," func",[111,2441,885],{"class":117},[111,2443,1623],{"class":888},[111,2445,2446],{"class":802}," echo",[111,2448,58],{"class":117},[111,2450,1651],{"class":802},[111,2452,911],{"class":117},[111,2454,2425],{"class":802},[111,2456,58],{"class":117},[111,2458,1651],{"class":802},[111,2460,920],{"class":117},[111,2462,2463,2465,2467,2469,2472,2474,2476,2478,2480,2483],{"class":113,"line":138},[111,2464,998],{"class":798},[111,2466,2439],{"class":798},[111,2468,885],{"class":117},[111,2470,2471],{"class":888},"c",[111,2473,2446],{"class":802},[111,2475,58],{"class":117},[111,2477,1411],{"class":802},[111,2479,911],{"class":117},[111,2481,2482],{"class":798},"error",[111,2484,920],{"class":117},[111,2486,2487,2489,2491,2494,2496,2499,2501,2503,2505],{"class":113,"line":151},[111,2488,1739],{"class":117},[111,2490,928],{"class":798},[111,2492,2493],{"class":117}," c.",[111,2495,1682],{"class":802},[111,2497,2498],{"class":117},"().Header.",[111,2500,1698],{"class":802},[111,2502,885],{"class":117},[111,2504,1703],{"class":131},[111,2506,850],{"class":117},[111,2508,2509,2512,2514,2516,2518,2520,2522,2524,2526,2528],{"class":113,"line":164},[111,2510,2511],{"class":798},"            if",[111,2513,1713],{"class":117},[111,2515,1082],{"class":798},[111,2517,1718],{"class":131},[111,2519,1088],{"class":798},[111,2521,1723],{"class":802},[111,2523,1726],{"class":117},[111,2525,1729],{"class":798},[111,2527,1732],{"class":124},[111,2529,920],{"class":117},[111,2531,2532,2535,2537,2539],{"class":113,"line":177},[111,2533,2534],{"class":117},"                id ",[111,2536,1742],{"class":798},[111,2538,1524],{"class":802},[111,2540,1747],{"class":117},[111,2542,2543],{"class":113,"line":190},[111,2544,2545],{"class":117},"            }\n",[111,2547,2548],{"class":113,"line":203},[111,2549,809],{"emptyLinePlaceholder":808},[111,2551,2552,2555,2558,2560,2562,2564,2566,2568,2570],{"class":113,"line":216},[111,2553,2554],{"class":117},"            c.",[111,2556,2557],{"class":802},"Response",[111,2559,1767],{"class":117},[111,2561,1764],{"class":802},[111,2563,1767],{"class":117},[111,2565,1106],{"class":802},[111,2567,885],{"class":117},[111,2569,1703],{"class":131},[111,2571,1776],{"class":117},[111,2573,2574,2577,2579,2581,2583],{"class":113,"line":229},[111,2575,2576],{"class":117},"            req ",[111,2578,928],{"class":798},[111,2580,2493],{"class":117},[111,2582,1682],{"class":802},[111,2584,1747],{"class":117},[111,2586,2587,2590,2592,2594,2597,2599],{"class":113,"line":242},[111,2588,2589],{"class":117},"            ctx ",[111,2591,928],{"class":798},[111,2593,1398],{"class":802},[111,2595,2596],{"class":117},"(req.",[111,2598,1411],{"class":802},[111,2600,2601],{"class":117},"(), id)\n",[111,2603,2604,2606,2609,2611,2613],{"class":113,"line":255},[111,2605,2554],{"class":117},[111,2607,2608],{"class":802},"SetRequest",[111,2610,2596],{"class":117},[111,2612,1790],{"class":802},[111,2614,2312],{"class":117},[111,2616,2617],{"class":113,"line":268},[111,2618,809],{"emptyLinePlaceholder":808},[111,2620,2621,2624,2627],{"class":113,"line":281},[111,2622,2623],{"class":798},"            return",[111,2625,2626],{"class":802}," next",[111,2628,2629],{"class":117},"(c)\n",[111,2631,2632],{"class":113,"line":294},[111,2633,1752],{"class":117},[111,2635,2636],{"class":113,"line":307},[111,2637,1017],{"class":117},[111,2639,2640],{"class":113,"line":320},[111,2641,360],{"class":117},[111,2643,2644],{"class":113,"line":333},[111,2645,809],{"emptyLinePlaceholder":808},[111,2647,2648,2650,2653,2655,2657,2659,2661,2663,2665,2667,2669,2671,2673],{"class":113,"line":346},[111,2649,879],{"class":798},[111,2651,2652],{"class":802}," EchoAccessLog",[111,2654,885],{"class":117},[111,2656,2033],{"class":888},[111,2658,1675],{"class":798},[111,2660,865],{"class":802},[111,2662,58],{"class":117},[111,2664,1068],{"class":802},[111,2666,911],{"class":117},[111,2668,2425],{"class":802},[111,2670,58],{"class":117},[111,2672,2430],{"class":802},[111,2674,920],{"class":117},[111,2676,2677,2679,2681,2683,2685,2687,2689,2691,2693,2695,2697,2699],{"class":113,"line":357},[111,2678,1022],{"class":798},[111,2680,2439],{"class":798},[111,2682,885],{"class":117},[111,2684,1623],{"class":888},[111,2686,2446],{"class":802},[111,2688,58],{"class":117},[111,2690,1651],{"class":802},[111,2692,911],{"class":117},[111,2694,2425],{"class":802},[111,2696,58],{"class":117},[111,2698,1651],{"class":802},[111,2700,920],{"class":117},[111,2702,2703,2705,2707,2709,2711,2713,2715,2717,2719,2721],{"class":113,"line":1036},[111,2704,998],{"class":798},[111,2706,2439],{"class":798},[111,2708,885],{"class":117},[111,2710,2471],{"class":888},[111,2712,2446],{"class":802},[111,2714,58],{"class":117},[111,2716,1411],{"class":802},[111,2718,911],{"class":117},[111,2720,2482],{"class":798},[111,2722,920],{"class":117},[111,2724,2725,2728,2730,2732,2734],{"class":113,"line":1073},[111,2726,2727],{"class":117},"            start ",[111,2729,928],{"class":798},[111,2731,2107],{"class":117},[111,2733,2110],{"class":802},[111,2735,1747],{"class":117},[111,2737,2738,2740,2742,2744,2746],{"class":113,"line":1100},[111,2739,2576],{"class":117},[111,2741,928],{"class":798},[111,2743,2493],{"class":117},[111,2745,1682],{"class":802},[111,2747,1747],{"class":117},[111,2749,2750],{"class":113,"line":1112},[111,2751,809],{"emptyLinePlaceholder":808},[111,2753,2754,2756,2758,2760,2762],{"class":113,"line":1123},[111,2755,2226],{"class":117},[111,2757,928],{"class":798},[111,2759,2142],{"class":117},[111,2761,68],{"class":802},[111,2763,1228],{"class":117},[111,2765,2766,2769,2771,2773,2775,2777],{"class":113,"line":1133},[111,2767,2768],{"class":131},"                \"request_id\"",[111,2770,47],{"class":117},[111,2772,2156],{"class":802},[111,2774,2596],{"class":117},[111,2776,1411],{"class":802},[111,2778,2163],{"class":117},[111,2780,2781,2784],{"class":113,"line":1138},[111,2782,2783],{"class":131},"                \"http.method\"",[111,2785,2786],{"class":117},", req.Method,\n",[111,2788,2789,2792,2795,2798],{"class":113,"line":1143},[111,2790,2791],{"class":131},"                \"http.route\"",[111,2793,2794],{"class":117},", c.",[111,2796,2797],{"class":802},"Path",[111,2799,2253],{"class":117},[111,2801,2802],{"class":113,"line":1171},[111,2803,2274],{"class":117},[111,2805,2806,2808,2810,2812,2814,2816,2818,2820,2822,2824],{"class":113,"line":1182},[111,2807,2511],{"class":798},[111,2809,2200],{"class":117},[111,2811,928],{"class":798},[111,2813,2205],{"class":117},[111,2815,2208],{"class":802},[111,2817,2596],{"class":117},[111,2819,1411],{"class":802},[111,2821,2215],{"class":117},[111,2823,2218],{"class":802},[111,2825,2221],{"class":117},[111,2827,2828,2831,2833,2835,2837],{"class":113,"line":1196},[111,2829,2830],{"class":117},"                logger ",[111,2832,1742],{"class":798},[111,2834,2231],{"class":117},[111,2836,68],{"class":802},[111,2838,1228],{"class":117},[111,2840,2841,2844,2846,2848,2850,2852],{"class":113,"line":1202},[111,2842,2843],{"class":131},"                    \"trace_id\"",[111,2845,2243],{"class":117},[111,2847,2246],{"class":802},[111,2849,1767],{"class":117},[111,2851,1004],{"class":802},[111,2853,2253],{"class":117},[111,2855,2856,2859,2861,2863,2865,2867],{"class":113,"line":1208},[111,2857,2858],{"class":131},"                    \"span_id\"",[111,2860,2243],{"class":117},[111,2862,2263],{"class":802},[111,2864,1767],{"class":117},[111,2866,1004],{"class":802},[111,2868,2253],{"class":117},[111,2870,2871],{"class":113,"line":1213},[111,2872,2873],{"class":117},"                )\n",[111,2875,2876],{"class":113,"line":1231},[111,2877,2545],{"class":117},[111,2879,2880],{"class":113,"line":1240},[111,2881,809],{"emptyLinePlaceholder":808},[111,2883,2884,2886,2888,2890,2892,2894],{"class":113,"line":1249},[111,2885,2589],{"class":117},[111,2887,928],{"class":798},[111,2889,1852],{"class":802},[111,2891,2596],{"class":117},[111,2893,1411],{"class":802},[111,2895,2298],{"class":117},[111,2897,2898,2900,2902,2904,2906],{"class":113,"line":1258},[111,2899,2554],{"class":117},[111,2901,2608],{"class":802},[111,2903,2596],{"class":117},[111,2905,1790],{"class":802},[111,2907,2312],{"class":117},[111,2909,2910],{"class":113,"line":1264},[111,2911,809],{"emptyLinePlaceholder":808},[111,2913,2915,2918,2920,2922],{"class":113,"line":2914},40,[111,2916,2917],{"class":117},"            err ",[111,2919,928],{"class":798},[111,2921,2626],{"class":802},[111,2923,2629],{"class":117},[111,2925,2927,2929,2932,2934,2936],{"class":113,"line":2926},41,[111,2928,2511],{"class":798},[111,2930,2931],{"class":117}," err ",[111,2933,1188],{"class":798},[111,2935,1571],{"class":124},[111,2937,920],{"class":117},[111,2939,2941,2944,2947],{"class":113,"line":2940},42,[111,2942,2943],{"class":117},"                c.",[111,2945,2946],{"class":802},"Error",[111,2948,2949],{"class":117},"(err)\n",[111,2951,2953],{"class":113,"line":2952},43,[111,2954,2545],{"class":117},[111,2956,2958],{"class":113,"line":2957},44,[111,2959,809],{"emptyLinePlaceholder":808},[111,2961,2963,2966,2968,2970,2972],{"class":113,"line":2962},45,[111,2964,2965],{"class":117},"            status ",[111,2967,928],{"class":798},[111,2969,2493],{"class":117},[111,2971,2557],{"class":802},[111,2973,2974],{"class":117},"().Status\n",[111,2976,2978,2981,2983],{"class":113,"line":2977},46,[111,2979,2980],{"class":117},"            level ",[111,2982,928],{"class":798},[111,2984,2985],{"class":117}," slog.LevelInfo\n",[111,2987,2989,2991,2994,2997,3000],{"class":113,"line":2988},47,[111,2990,2511],{"class":798},[111,2992,2993],{"class":117}," status ",[111,2995,2996],{"class":798},">=",[111,2998,2999],{"class":124}," 500",[111,3001,920],{"class":117},[111,3003,3005,3008,3010],{"class":113,"line":3004},48,[111,3006,3007],{"class":117},"                level ",[111,3009,1742],{"class":798},[111,3011,3012],{"class":117}," slog.LevelError\n",[111,3014,3016],{"class":113,"line":3015},49,[111,3017,2545],{"class":117},[111,3019,3021],{"class":113,"line":3020},50,[111,3022,809],{"emptyLinePlaceholder":808},[111,3024,3026,3029,3032,3035,3037],{"class":113,"line":3025},51,[111,3027,3028],{"class":117},"            logger.",[111,3030,3031],{"class":802},"Log",[111,3033,3034],{"class":117},"(ctx, level, ",[111,3036,2329],{"class":131},[111,3038,135],{"class":117},[111,3040,3042,3045,3047,3049],{"class":113,"line":3041},52,[111,3043,3044],{"class":131},"                \"event\"",[111,3046,47],{"class":117},[111,3048,2341],{"class":131},[111,3050,135],{"class":117},[111,3052,3054,3057],{"class":113,"line":3053},53,[111,3055,3056],{"class":131},"                \"http.status_code\"",[111,3058,3059],{"class":117},", status,\n",[111,3061,3063,3066,3068,3070,3072,3074],{"class":113,"line":3062},54,[111,3064,3065],{"class":131},"                \"http.duration_ms\"",[111,3067,2359],{"class":117},[111,3069,2362],{"class":802},[111,3071,2365],{"class":117},[111,3073,2368],{"class":802},[111,3075,2253],{"class":117},[111,3077,3079],{"class":113,"line":3078},55,[111,3080,2274],{"class":117},[111,3082,3084,3086],{"class":113,"line":3083},56,[111,3085,2623],{"class":798},[111,3087,3088],{"class":124}," nil\n",[111,3090,3092],{"class":113,"line":3091},57,[111,3093,1752],{"class":117},[111,3095,3097],{"class":113,"line":3096},58,[111,3098,1017],{"class":117},[111,3100,3102],{"class":113,"line":3101},59,[111,3103,360],{"class":117},[15,3105,3106],{},"Подключение:",[88,3108,3110],{"className":788,"code":3109,"language":790,"meta":791,"style":94},"e := echo.New()\nlogger := observability.NewLogger(\"ratedesk-api\", cfg.Env, cfg.Version)\ne.Use(observability.EchoRequestID())\ne.Use(observability.EchoAccessLog(logger))\n",[40,3111,3112,3126,3146,3163],{"__ignoreMap":94},[111,3113,3114,3117,3119,3122,3124],{"class":113,"line":114},[111,3115,3116],{"class":117},"e ",[111,3118,928],{"class":798},[111,3120,3121],{"class":117}," echo.",[111,3123,1220],{"class":802},[111,3125,1747],{"class":117},[111,3127,3128,3131,3133,3136,3139,3141,3143],{"class":113,"line":121},[111,3129,3130],{"class":117},"logger ",[111,3132,928],{"class":798},[111,3134,3135],{"class":117}," observability.",[111,3137,3138],{"class":802},"NewLogger",[111,3140,885],{"class":117},[111,3142,185],{"class":131},[111,3144,3145],{"class":117},", cfg.Env, cfg.Version)\n",[111,3147,3148,3151,3154,3157,3160],{"class":113,"line":138},[111,3149,3150],{"class":117},"e.",[111,3152,3153],{"class":802},"Use",[111,3155,3156],{"class":117},"(observability.",[111,3158,3159],{"class":802},"EchoRequestID",[111,3161,3162],{"class":117},"())\n",[111,3164,3165,3167,3169,3171,3174],{"class":113,"line":151},[111,3166,3150],{"class":117},[111,3168,3153],{"class":802},[111,3170,3156],{"class":117},[111,3172,3173],{"class":802},"EchoAccessLog",[111,3175,3176],{"class":117},"(logger))\n",[80,3178,3180],{"id":3179},"error-taxonomy","Error taxonomy",[15,3182,3183,3184,3186],{},"Если все ошибки логируются как ",[40,3185,2482],{},", во время инцидента придется читать текст каждой. Заведите небольшую классификацию:",[394,3188,3189,3199],{},[397,3190,3191],{},[400,3192,3193,3197],{},[403,3194,3195],{"align":405},[40,3196,380],{},[403,3198,409],{"align":405},[414,3200,3201,3210,3220,3230,3240,3250],{},[400,3202,3203,3207],{},[419,3204,3205],{"align":405},[40,3206,657],{},[419,3208,3209],{"align":405},"Некорректный input.",[400,3211,3212,3217],{},[419,3213,3214],{"align":405},[40,3215,3216],{},"not_found",[419,3218,3219],{"align":405},"Доменный объект не найден.",[400,3221,3222,3227],{},[419,3223,3224],{"align":405},[40,3225,3226],{},"dependency_timeout",[419,3228,3229],{"align":405},"Timeout PostgreSQL, Redis, provider API.",[400,3231,3232,3237],{},[419,3233,3234],{"align":405},[40,3235,3236],{},"dependency_unavailable",[419,3238,3239],{"align":405},"Connection refused, broker down.",[400,3241,3242,3247],{},[419,3243,3244],{"align":405},[40,3245,3246],{},"conflict",[419,3248,3249],{"align":405},"Optimistic lock, duplicate command.",[400,3251,3252,3257],{},[419,3253,3254],{"align":405},[40,3255,3256],{},"internal",[419,3258,3259],{"align":405},"Неожиданная ошибка приложения.",[15,3261,3262,3263,54,3265,3267,3268,58],{},"Для expected domain errors можно логировать ",[40,3264,443],{},[40,3266,446],{},", а для dependency\u002Fsystem failures - ",[40,3269,449],{},[88,3271,3273],{"className":788,"code":3272,"language":790,"meta":791,"style":94},"func LogError(ctx context.Context, base *slog.Logger, msg string, err error, attrs ...any) {\n    logger := Logger(ctx, base)\n    fields := []any{\n        \"event\", \"operation.failed\",\n        \"error.kind\", classify(err),\n        \"error.message\", safeErrorMessage(err),\n    }\n    fields = append(fields, attrs...)\n    logger.Error(msg, fields...)\n}\n",[40,3274,3275,3331,3343,3356,3368,3381,3393,3397,3414,3428],{"__ignoreMap":94},[111,3276,3277,3279,3282,3284,3286,3288,3290,3292,3294,3296,3298,3300,3302,3304,3306,3308,3310,3312,3315,3318,3320,3323,3326,3329],{"class":113,"line":114},[111,3278,879],{"class":798},[111,3280,3281],{"class":802}," LogError",[111,3283,885],{"class":117},[111,3285,1403],{"class":888},[111,3287,1406],{"class":802},[111,3289,58],{"class":117},[111,3291,1411],{"class":802},[111,3293,47],{"class":117},[111,3295,2033],{"class":888},[111,3297,1675],{"class":798},[111,3299,865],{"class":802},[111,3301,58],{"class":117},[111,3303,1068],{"class":802},[111,3305,47],{"class":117},[111,3307,42],{"class":888},[111,3309,1056],{"class":798},[111,3311,47],{"class":117},[111,3313,3314],{"class":888},"err",[111,3316,3317],{"class":798}," error",[111,3319,47],{"class":117},[111,3321,3322],{"class":888},"attrs",[111,3324,3325],{"class":798}," ...",[111,3327,3328],{"class":802},"any",[111,3330,1685],{"class":117},[111,3332,3333,3336,3338,3340],{"class":113,"line":121},[111,3334,3335],{"class":117},"    logger ",[111,3337,928],{"class":798},[111,3339,1916],{"class":802},[111,3341,3342],{"class":117},"(ctx, base)\n",[111,3344,3345,3348,3350,3352,3354],{"class":113,"line":138},[111,3346,3347],{"class":117},"    fields ",[111,3349,928],{"class":798},[111,3351,892],{"class":117},[111,3353,3328],{"class":802},[111,3355,118],{"class":117},[111,3357,3358,3361,3363,3366],{"class":113,"line":151},[111,3359,3360],{"class":131},"        \"event\"",[111,3362,47],{"class":117},[111,3364,3365],{"class":131},"\"operation.failed\"",[111,3367,135],{"class":117},[111,3369,3370,3373,3375,3378],{"class":113,"line":164},[111,3371,3372],{"class":131},"        \"error.kind\"",[111,3374,47],{"class":117},[111,3376,3377],{"class":802},"classify",[111,3379,3380],{"class":117},"(err),\n",[111,3382,3383,3386,3388,3391],{"class":113,"line":177},[111,3384,3385],{"class":131},"        \"error.message\"",[111,3387,47],{"class":117},[111,3389,3390],{"class":802},"safeErrorMessage",[111,3392,3380],{"class":117},[111,3394,3395],{"class":113,"line":190},[111,3396,1017],{"class":117},[111,3398,3399,3401,3403,3406,3409,3412],{"class":113,"line":203},[111,3400,3347],{"class":117},[111,3402,1742],{"class":798},[111,3404,3405],{"class":802}," append",[111,3407,3408],{"class":117},"(fields, attrs",[111,3410,3411],{"class":798},"...",[111,3413,850],{"class":117},[111,3415,3416,3419,3421,3424,3426],{"class":113,"line":216},[111,3417,3418],{"class":117},"    logger.",[111,3420,2946],{"class":802},[111,3422,3423],{"class":117},"(msg, fields",[111,3425,3411],{"class":798},[111,3427,850],{"class":117},[111,3429,3430],{"class":113,"line":229},[111,3431,360],{"class":117},[15,3433,3434],{},"Таксономия должна рождаться рядом с границей, где ошибка получает смысл:",[394,3436,3437,3450],{},[397,3438,3439],{},[400,3440,3441,3444,3447],{},[403,3442,3443],{"align":405},"Слой",[403,3445,3446],{"align":405},"Что знает",[403,3448,3449],{"align":405},"Что ставит",[414,3451,3452,3466,3483],{},[400,3453,3454,3457,3460],{},[419,3455,3456],{"align":405},"Repository\u002Fadapter",[419,3458,3459],{"align":405},"PostgreSQL timeout, Redis unavailable, provider 502.",[419,3461,3462,47,3464,58],{"align":405},[40,3463,3226],{},[40,3465,3236],{},[400,3467,3468,3471,3474],{},[419,3469,3470],{"align":405},"Usecase",[419,3472,3473],{"align":405},"Доменный конфликт, rate not found, stale data policy.",[419,3475,3476,47,3478,47,3480,58],{"align":405},[40,3477,3246],{},[40,3479,3216],{},[40,3481,3482],{},"stale_data",[400,3484,3485,3488,3491],{},[419,3486,3487],{"align":405},"Transport",[419,3489,3490],{"align":405},"HTTP status, route, validation body.",[419,3492,3493,47,3495,47,3498,58],{"align":405},[40,3494,657],{},[40,3496,3497],{},"unauthorized",[40,3499,3500],{},"bad_request",[15,3502,3503,3504,3506],{},"Не заставляйте domain layer импортировать logging package. Domain возвращает типизированную ошибку, а transport\u002Fusecase boundary превращает ее в ",[40,3505,380],{},", HTTP\u002FgRPC status и span status.",[80,3508,3510],{"id":3509},"тестирование-логов","Тестирование логов",[15,3512,3513],{},"Логи можно и нужно тестировать. Проверяйте JSON-поля как данные, а не через fragile substring по всей строке.",[88,3515,3517],{"className":788,"code":3516,"language":790,"meta":791,"style":94},"func TestLoggerRedactsSecrets(t *testing.T) {\n    var buf bytes.Buffer\n    logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{\n        ReplaceAttr: redactAttr,\n    }))\n\n    logger.Info(\"login failed\",\n        \"event\", \"auth.login.failed\",\n        \"request_id\", \"req_test\",\n        \"Authorization\", \"Bearer secret\",\n        \"password\", \"qwerty\",\n    )\n\n    var got map[string]any\n    if err := json.Unmarshal(buf.Bytes(), &got); err != nil {\n        t.Fatal(err)\n    }\n    if got[\"request_id\"] != \"req_test\" {\n        t.Fatalf(\"request_id = %v\", got[\"request_id\"])\n    }\n    if got[\"Authorization\"] != \"[REDACTED]\" {\n        t.Fatalf(\"authorization was not redacted: %v\", got[\"Authorization\"])\n    }\n    if got[\"password\"] != \"[REDACTED]\" {\n        t.Fatalf(\"password was not redacted: %v\", got[\"password\"])\n    }\n}\n",[40,3518,3519,3543,3558,3590,3594,3599,3603,3616,3627,3639,3651,3663,3667,3671,3691,3725,3735,3739,3759,3785,3789,3807,3828,3832,3849,3870,3874],{"__ignoreMap":94},[111,3520,3521,3523,3526,3528,3531,3533,3536,3538,3541],{"class":113,"line":114},[111,3522,879],{"class":798},[111,3524,3525],{"class":802}," TestLoggerRedactsSecrets",[111,3527,885],{"class":117},[111,3529,3530],{"class":888},"t",[111,3532,1675],{"class":798},[111,3534,3535],{"class":802},"testing",[111,3537,58],{"class":117},[111,3539,3540],{"class":802},"T",[111,3542,1685],{"class":117},[111,3544,3545,3547,3550,3553,3555],{"class":113,"line":121},[111,3546,1536],{"class":798},[111,3548,3549],{"class":117}," buf ",[111,3551,3552],{"class":802},"bytes",[111,3554,58],{"class":117},[111,3556,3557],{"class":802},"Buffer\n",[111,3559,3560,3562,3564,3566,3568,3571,3573,3575,3577,3580,3582,3584,3586,3588],{"class":113,"line":138},[111,3561,3335],{"class":117},[111,3563,928],{"class":798},[111,3565,1001],{"class":117},[111,3567,1220],{"class":802},[111,3569,3570],{"class":117},"(slog.",[111,3572,1153],{"class":802},[111,3574,885],{"class":117},[111,3576,1159],{"class":798},[111,3578,3579],{"class":117},"buf, ",[111,3581,1159],{"class":798},[111,3583,865],{"class":802},[111,3585,58],{"class":117},[111,3587,1166],{"class":802},[111,3589,118],{"class":117},[111,3591,3592],{"class":113,"line":151},[111,3593,1199],{"class":117},[111,3595,3596],{"class":113,"line":164},[111,3597,3598],{"class":117},"    }))\n",[111,3600,3601],{"class":113,"line":177},[111,3602,809],{"emptyLinePlaceholder":808},[111,3604,3605,3607,3609,3611,3614],{"class":113,"line":190},[111,3606,3418],{"class":117},[111,3608,2324],{"class":802},[111,3610,885],{"class":117},[111,3612,3613],{"class":131},"\"login failed\"",[111,3615,135],{"class":117},[111,3617,3618,3620,3622,3625],{"class":113,"line":203},[111,3619,3360],{"class":131},[111,3621,47],{"class":117},[111,3623,3624],{"class":131},"\"auth.login.failed\"",[111,3626,135],{"class":117},[111,3628,3629,3632,3634,3637],{"class":113,"line":216},[111,3630,3631],{"class":131},"        \"request_id\"",[111,3633,47],{"class":117},[111,3635,3636],{"class":131},"\"req_test\"",[111,3638,135],{"class":117},[111,3640,3641,3644,3646,3649],{"class":113,"line":229},[111,3642,3643],{"class":131},"        \"Authorization\"",[111,3645,47],{"class":117},[111,3647,3648],{"class":131},"\"Bearer secret\"",[111,3650,135],{"class":117},[111,3652,3653,3656,3658,3661],{"class":113,"line":242},[111,3654,3655],{"class":131},"        \"password\"",[111,3657,47],{"class":117},[111,3659,3660],{"class":131},"\"qwerty\"",[111,3662,135],{"class":117},[111,3664,3665],{"class":113,"line":255},[111,3666,1261],{"class":117},[111,3668,3669],{"class":113,"line":268},[111,3670,809],{"emptyLinePlaceholder":808},[111,3672,3673,3675,3678,3681,3684,3686,3688],{"class":113,"line":281},[111,3674,1536],{"class":798},[111,3676,3677],{"class":117}," got ",[111,3679,3680],{"class":798},"map",[111,3682,3683],{"class":117},"[",[111,3685,895],{"class":798},[111,3687,1545],{"class":117},[111,3689,3690],{"class":802},"any\n",[111,3692,3693,3695,3697,3699,3702,3705,3708,3711,3714,3716,3719,3721,3723],{"class":113,"line":294},[111,3694,1076],{"class":798},[111,3696,2931],{"class":117},[111,3698,928],{"class":798},[111,3700,3701],{"class":117}," json.",[111,3703,3704],{"class":802},"Unmarshal",[111,3706,3707],{"class":117},"(buf.",[111,3709,3710],{"class":802},"Bytes",[111,3712,3713],{"class":117},"(), ",[111,3715,1159],{"class":798},[111,3717,3718],{"class":117},"got); err ",[111,3720,1188],{"class":798},[111,3722,1571],{"class":124},[111,3724,920],{"class":117},[111,3726,3727,3730,3733],{"class":113,"line":307},[111,3728,3729],{"class":117},"        t.",[111,3731,3732],{"class":802},"Fatal",[111,3734,2949],{"class":117},[111,3736,3737],{"class":113,"line":320},[111,3738,1017],{"class":117},[111,3740,3741,3743,3746,3749,3752,3754,3757],{"class":113,"line":333},[111,3742,1076],{"class":798},[111,3744,3745],{"class":117}," got[",[111,3747,3748],{"class":131},"\"request_id\"",[111,3750,3751],{"class":117},"] ",[111,3753,1188],{"class":798},[111,3755,3756],{"class":131}," \"req_test\"",[111,3758,920],{"class":117},[111,3760,3761,3763,3766,3768,3771,3774,3777,3780,3782],{"class":113,"line":346},[111,3762,3729],{"class":117},[111,3764,3765],{"class":802},"Fatalf",[111,3767,885],{"class":117},[111,3769,3770],{"class":131},"\"request_id = ",[111,3772,3773],{"class":124},"%v",[111,3775,3776],{"class":131},"\"",[111,3778,3779],{"class":117},", got[",[111,3781,3748],{"class":131},[111,3783,3784],{"class":117},"])\n",[111,3786,3787],{"class":113,"line":357},[111,3788,1017],{"class":117},[111,3790,3791,3793,3795,3798,3800,3802,3805],{"class":113,"line":1036},[111,3792,1076],{"class":798},[111,3794,3745],{"class":117},[111,3796,3797],{"class":131},"\"Authorization\"",[111,3799,3751],{"class":117},[111,3801,1188],{"class":798},[111,3803,3804],{"class":131}," \"[REDACTED]\"",[111,3806,920],{"class":117},[111,3808,3809,3811,3813,3815,3818,3820,3822,3824,3826],{"class":113,"line":1073},[111,3810,3729],{"class":117},[111,3812,3765],{"class":802},[111,3814,885],{"class":117},[111,3816,3817],{"class":131},"\"authorization was not redacted: ",[111,3819,3773],{"class":124},[111,3821,3776],{"class":131},[111,3823,3779],{"class":117},[111,3825,3797],{"class":131},[111,3827,3784],{"class":117},[111,3829,3830],{"class":113,"line":1100},[111,3831,1017],{"class":117},[111,3833,3834,3836,3838,3841,3843,3845,3847],{"class":113,"line":1112},[111,3835,1076],{"class":798},[111,3837,3745],{"class":117},[111,3839,3840],{"class":131},"\"password\"",[111,3842,3751],{"class":117},[111,3844,1188],{"class":798},[111,3846,3804],{"class":131},[111,3848,920],{"class":117},[111,3850,3851,3853,3855,3857,3860,3862,3864,3866,3868],{"class":113,"line":1123},[111,3852,3729],{"class":117},[111,3854,3765],{"class":802},[111,3856,885],{"class":117},[111,3858,3859],{"class":131},"\"password was not redacted: ",[111,3861,3773],{"class":124},[111,3863,3776],{"class":131},[111,3865,3779],{"class":117},[111,3867,3840],{"class":131},[111,3869,3784],{"class":117},[111,3871,3872],{"class":113,"line":1133},[111,3873,1017],{"class":117},[111,3875,3876],{"class":113,"line":1138},[111,3877,360],{"class":117},[15,3879,3880,3881,3884,3885,3887],{},"Для access log добавьте тест через ",[40,3882,3883],{},"httptest"," или Echo test context: входящий ",[40,3886,1291],{}," должен вернуться в response и попасть в JSON log.",[80,3889,3891],{"id":3890},"что-нельзя-логировать","Что нельзя логировать",[15,3893,3894],{},"Не логируйте:",[18,3896,3897,3900,3911,3914,3917,3920,3923],{},[21,3898,3899],{},"access token, refresh token, API key, password;",[21,3901,3902,47,3905,47,3908,1305],{},[40,3903,3904],{},"Authorization",[40,3906,3907],{},"Cookie",[40,3909,3910],{},"Set-Cookie",[21,3912,3913],{},"полный номер карты, CVV, приватные ключи;",[21,3915,3916],{},"паспортные данные и PII без явного основания;",[21,3918,3919],{},"большие request\u002Fresponse body целиком;",[21,3921,3922],{},"SQL с чувствительными параметрами;",[21,3924,3925],{},"ошибки, в которые случайно попали секреты.",[15,3927,3928,3929,47,3932,47,3935,47,3938,47,3941,3944],{},"Если payload нужен для диагностики, логируйте форму и безопасные признаки: ",[40,3930,3931],{},"payload_size",[40,3933,3934],{},"schema_version",[40,3936,3937],{},"field_count",[40,3939,3940],{},"currency.from",[40,3942,3943],{},"currency.to",", но не весь JSON.",[80,3946,3948],{"id":3947},"stdout-в-контейнерах","stdout в контейнерах",[15,3950,3951,3952,3955,3956,3959],{},"В контейнере приложение обычно пишет JSON logs в ",[40,3953,3954],{},"stdout","\u002F",[40,3957,3958],{},"stderr",". Оно не должно само писать application logs в файл внутри контейнера.",[15,3961,3962],{},"Типовая схема:",[88,3964,3967],{"className":3965,"code":3966,"language":93,"meta":94},[91],"Go app stdout\n  -> container runtime\n  -> node agent \u002F sidecar \u002F collector\n  -> Loki, Elasticsearch, OpenSearch or vendor\n  -> Grafana\u002FKibana\u002FOpenSearch Dashboards\n",[40,3968,3966],{"__ignoreMap":94},[15,3970,3971],{},"Ротация, доставка и retention - ответственность runtime\u002Fcollector\u002Fbackend. Приложение отвечает за смысл и структуру события.",[80,3973,3975],{"id":3974},"loki-alloy-и-labels","Loki, Alloy и labels",[15,3977,3978],{},"Loki дешевле и проще, когда вам нужен поиск рядом с Grafana и связь logs -> traces -> metrics. Важное ограничение: Loki индексирует labels, а не весь JSON как Elasticsearch. Поэтому labels должны быть низкокардинальными:",[15,3980,3981],{},"Хорошие labels:",[88,3983,3986],{"className":3984,"code":3985,"language":93,"meta":94},[91],"service=\"ratedesk-api\"\nenv=\"production\"\nlevel=\"error\"\nnamespace=\"mentor\"\n",[40,3987,3985],{"__ignoreMap":94},[15,3989,3990],{},"Плохие labels:",[88,3992,3995],{"className":3993,"code":3994,"language":93,"meta":94},[91],"request_id=\"req_...\"\ntrace_id=\"...\"\nuser_id=\"42\"\norder_id=\"...\"\nerror_message=\"...\"\n",[40,3996,3994],{"__ignoreMap":94},[15,3998,3999,4001,4002,4004],{},[40,4000,50],{}," и ",[40,4003,374],{}," оставляйте JSON-полями, по которым можно искать, но не превращайте их в labels.",[15,4006,4007],{},"На 6 мая 2026 Promtail уже EOL с 2 марта 2026. Для новых учебных материалов основной путь лучше показывать через Grafana Alloy, а Promtail упоминать как legacy\u002Fmigration topic.",[15,4009,4010],{},"Пример Alloy-фрагмента для файловых логов:",[88,4012,4016],{"className":4013,"code":4014,"language":4015,"meta":94,"style":94},"language-alloy shiki shiki-themes github-dark","local.file_match \"containers\" {\n  path_targets = [{\n    __path__ = \"\u002Fvar\u002Flog\u002Fcontainers\u002F*.log\",\n    job      = \"kubernetes\",\n  }]\n}\n\nloki.source.file \"containers\" {\n  targets    = local.file_match.containers.targets\n  forward_to = [loki.write.default.receiver]\n}\n\nloki.write \"default\" {\n  endpoint {\n    url = \"http:\u002F\u002Floki:3100\u002Floki\u002Fapi\u002Fv1\u002Fpush\"\n  }\n}\n","alloy",[40,4017,4018,4023,4028,4033,4038,4043,4047,4051,4056,4061,4066,4070,4074,4079,4084,4089,4094],{"__ignoreMap":94},[111,4019,4020],{"class":113,"line":114},[111,4021,4022],{},"local.file_match \"containers\" {\n",[111,4024,4025],{"class":113,"line":121},[111,4026,4027],{},"  path_targets = [{\n",[111,4029,4030],{"class":113,"line":138},[111,4031,4032],{},"    __path__ = \"\u002Fvar\u002Flog\u002Fcontainers\u002F*.log\",\n",[111,4034,4035],{"class":113,"line":151},[111,4036,4037],{},"    job      = \"kubernetes\",\n",[111,4039,4040],{"class":113,"line":164},[111,4041,4042],{},"  }]\n",[111,4044,4045],{"class":113,"line":177},[111,4046,360],{},[111,4048,4049],{"class":113,"line":190},[111,4050,809],{"emptyLinePlaceholder":808},[111,4052,4053],{"class":113,"line":203},[111,4054,4055],{},"loki.source.file \"containers\" {\n",[111,4057,4058],{"class":113,"line":216},[111,4059,4060],{},"  targets    = local.file_match.containers.targets\n",[111,4062,4063],{"class":113,"line":229},[111,4064,4065],{},"  forward_to = [loki.write.default.receiver]\n",[111,4067,4068],{"class":113,"line":242},[111,4069,360],{},[111,4071,4072],{"class":113,"line":255},[111,4073,809],{"emptyLinePlaceholder":808},[111,4075,4076],{"class":113,"line":268},[111,4077,4078],{},"loki.write \"default\" {\n",[111,4080,4081],{"class":113,"line":281},[111,4082,4083],{},"  endpoint {\n",[111,4085,4086],{"class":113,"line":294},[111,4087,4088],{},"    url = \"http:\u002F\u002Floki:3100\u002Floki\u002Fapi\u002Fv1\u002Fpush\"\n",[111,4090,4091],{"class":113,"line":307},[111,4092,4093],{},"  }\n",[111,4095,4096],{"class":113,"line":320},[111,4097,360],{},[80,4099,4101],{"id":4100},"elkefk","ELK\u002FEFK",[15,4103,4104],{},"ELK - Elasticsearch + Logstash + Kibana. EFK обычно заменяет Logstash на Fluent Bit или Fluentd. В новых проектах часто встречается OpenSearch + OpenSearch Dashboards.",[15,4106,4107],{},"Сильные стороны Elastic\u002FOpenSearch:",[18,4109,4110,4113,4116,4119,4122],{},[21,4111,4112],{},"полнотекстовый поиск;",[21,4114,4115],{},"сложные фильтры и агрегации по JSON;",[21,4117,4118],{},"ingest pipelines и enrichment;",[21,4120,4121],{},"index lifecycle management;",[21,4123,4124],{},"удобство для security\u002Faudit\u002Fsearch-heavy сценариев.",[15,4126,4127],{},"Цена:",[18,4129,4130,4133,4136,4139],{},[21,4131,4132],{},"больше operational complexity;",[21,4134,4135],{},"нужно думать о shards, mappings, rollover, retention;",[21,4137,4138],{},"дорого хранить все подряд без политики;",[21,4140,4141],{},"можно сломать cluster высокой кардинальностью и огромными документами.",[15,4143,4144],{},"Для курса разумный путь: обязательный профиль Loki + Grafana, optional профиль ELK\u002FEFK с README-сравнением.",[80,4146,4148],{"id":4147},"поиск-в-логах","Поиск в логах",[15,4150,4151],{},"Примеры запросов должны лежать рядом с runbook, иначе во время инцидента их будут вспоминать руками.",[15,4153,4154],{},"LogQL:",[88,4156,4160],{"className":4157,"code":4158,"language":4159,"meta":94,"style":94},"language-logql shiki shiki-themes github-dark","{service=\"ratedesk-api\", env=\"production\"} |= \"req_01H\"\n{service=\"ratedesk-api\"} | json | trace_id=\"6b6d9c9c7c8f4c8e9a4c7a0a5a8f2e01\"\n{service=\"ratedesk-api\"} | json | event=\"convert.failed\" | error_kind=\"dependency_timeout\"\n","logql",[40,4161,4162,4167,4172],{"__ignoreMap":94},[111,4163,4164],{"class":113,"line":114},[111,4165,4166],{},"{service=\"ratedesk-api\", env=\"production\"} |= \"req_01H\"\n",[111,4168,4169],{"class":113,"line":121},[111,4170,4171],{},"{service=\"ratedesk-api\"} | json | trace_id=\"6b6d9c9c7c8f4c8e9a4c7a0a5a8f2e01\"\n",[111,4173,4174],{"class":113,"line":138},[111,4175,4176],{},"{service=\"ratedesk-api\"} | json | event=\"convert.failed\" | error_kind=\"dependency_timeout\"\n",[15,4178,4179],{},"Elastic\u002FOpenSearch:",[88,4181,4184],{"className":4182,"code":4183,"language":93,"meta":94},[91],"service:\"ratedesk-api\" AND request_id:\"req_01H\"\nservice:\"ratedesk-api\" AND trace_id:\"6b6d9c9c7c8f4c8e9a4c7a0a5a8f2e01\"\nevent:\"convert.failed\" AND error.kind:\"dependency_timeout\"\n",[40,4185,4183],{"__ignoreMap":94},[15,4187,4188],{},"Если таких запросов нет в README\u002Frunbook, pipeline логов формально есть, но incident workflow все еще слабый.",[80,4190,4192],{"id":4191},"retention","Retention",[15,4194,4195],{},"Retention не должен быть один для всех логов:",[394,4197,4198,4211],{},[397,4199,4200],{},[400,4201,4202,4205,4208],{},[403,4203,4204],{"align":405},"Тип логов",[403,4206,4207],{"align":405},"Примерный срок",[403,4209,4210],{"align":405},"Комментарий",[414,4212,4213,4224,4235,4246],{},[400,4214,4215,4218,4221],{},[419,4216,4217],{"align":405},"Debug\u002Fdev",[419,4219,4220],{"align":405},"часы или дни",[419,4222,4223],{"align":405},"Дешево удалять.",[400,4225,4226,4229,4232],{},[419,4227,4228],{"align":405},"Application prod",[419,4230,4231],{"align":405},"7-30 дней",[419,4233,4234],{"align":405},"Основной debug инцидентов.",[400,4236,4237,4240,4243],{},[419,4238,4239],{"align":405},"Audit\u002Fsecurity",[419,4241,4242],{"align":405},"90+ дней",[419,4244,4245],{"align":405},"Отдельные правила доступа и хранения.",[400,4247,4248,4251,4254],{},[419,4249,4250],{"align":405},"High-volume access",[419,4252,4253],{"align":405},"3-14 дней",[419,4255,4256],{"align":405},"Часто агрегируется в метрики.",[15,4258,4259],{},"В Elastic\u002FOpenSearch используйте data streams или time-based indexes, rollover и delete phase. В Loki контролируйте cardinality labels и объем ingestion.",[80,4261,4263],{"id":4262},"антипаттерны","Антипаттерны",[18,4265,4266,4272,4277,4280,4285,4288,4291,4296,4299,4302],{},[21,4267,4268,4271],{},[40,4269,4270],{},"fmt.Printf"," в production-коде вместо logger.",[21,4273,4274,4275,58],{},"Логи без ",[40,4276,50],{},[21,4278,4279],{},"Разные имена одного поля в разных слоях.",[21,4281,4282,4284],{},[40,4283,449],{}," для нормальных 404\u002Fvalidation.",[21,4286,4287],{},"Логирование request\u002Fresponse body целиком.",[21,4289,4290],{},"Секреты в логах.",[21,4292,4293,4295],{},[40,4294,50],{}," как Loki label.",[21,4297,4298],{},"Многострочные stack traces, которые collector не умеет склеивать.",[21,4300,4301],{},"Логи в файл внутри контейнера.",[21,4303,4304],{},"Debug включен в prod навсегда.",[80,4306,4308],{"id":4307},"практика","Практика",[15,4310,4311],{},"В RateDesk добавьте:",[4313,4314,4315,4324,4329,4335,4354,4357,4360,4368],"ol",{},[21,4316,4317,4320,4321,58],{},[40,4318,4319],{},"internal\u002Fobservability\u002Flogger.go"," с ",[40,4322,4323],{},"slog.JSONHandler",[21,4325,4326,58],{},[40,4327,4328],{},"RequestIDMiddleware",[21,4330,4331,4334],{},[40,4332,4333],{},"LoggingMiddleware"," с access log.",[21,4336,4337,4338,47,4340,47,4342,47,4344,47,4346,47,4348,47,4350,47,4352,58],{},"Единые поля ",[40,4339,50],{},[40,4341,374],{},[40,4343,583],{},[40,4345,596],{},[40,4347,377],{},[40,4349,625],{},[40,4351,639],{},[40,4353,380],{},[21,4355,4356],{},"Redaction для секретных ключей.",[21,4358,4359],{},"README-раздел с log schema.",[21,4361,4362,4363,4365,4366,58],{},"Loki\u002FAlloy pipeline, где logs ищутся по ",[40,4364,50],{},", а из log entry можно перейти в trace по ",[40,4367,374],{},[21,4369,4370,4371,4373],{},"Тесты на redaction, ",[40,4372,50],{}," и обязательные access-log поля.",[15,4375,4376],{},"Acceptance:",[18,4378,4379,4384,4387,4394,4401,4406,4409],{},[21,4380,4381,4383],{},[40,4382,1291],{}," возвращается клиенту;",[21,4385,4386],{},"id есть в logs;",[21,4388,4389,4001,4391,4393],{},[40,4390,3904],{},[40,4392,3907],{}," не попадают в logs;",[21,4395,4396,4001,4398,4400],{},[40,4397,50],{},[40,4399,374],{}," не являются Loki labels;",[21,4402,4403,4405],{},[40,4404,3216],{}," не логируется как production incident;",[21,4407,4408],{},"Echo middleware использует route pattern, а не raw path;",[21,4410,4411],{},"в MR есть примеры LogQL\u002FElastic-запросов для поиска одного запроса.",[4413,4414],"hr",{},[80,4416,4418],{"id":4417},"интерактивная-практика","Интерактивная практика",[4420,4421,4425,4428,4449],"quiz",{"answer":4422,"id":4423,"xp":4424},"3","obs-logging-q1","10",[15,4426,4427],{},"Какое поле опаснее всего класть в Loki label?",[4429,4430,4431],"template",{"v-slot:options":94},[18,4432,4433,4437,4441,4445],{},[21,4434,4435],{},[40,4436,487],{},[21,4438,4439],{},[40,4440,502],{},[21,4442,4443],{},[40,4444,50],{},[21,4446,4447],{},[40,4448,438],{},[4429,4450,4451],{"v-slot:explanation":94},[15,4452,4453,4455],{},[40,4454,50],{}," имеет огромную кардинальность: он полезен внутри JSON log body, но как label быстро раздувает индекс и стоимость хранения.",[4457,4458,4462,4465,4609],"predict",{"answer":4459,"id":4460,"xp":4461},"info\\nerror","obs-logging-p1","15",[15,4463,4464],{},"Что выведет программа?",[4429,4466,4467],{"v-slot:code":94},[88,4468,4470],{"className":788,"code":4469,"language":790,"meta":94,"style":94},"package main\n\nimport \"fmt\"\n\nfunc LogLevel(status int) string {\n    if status >= 500 {\n        return \"error\"\n    }\n    return \"info\"\n}\n\nfunc main() {\n    fmt.Println(LogLevel(404))\n    fmt.Println(LogLevel(503))\n}\n",[40,4471,4472,4479,4483,4495,4499,4520,4532,4539,4543,4550,4554,4558,4567,4588,4605],{"__ignoreMap":94},[111,4473,4474,4476],{"class":113,"line":114},[111,4475,799],{"class":798},[111,4477,4478],{"class":802}," main\n",[111,4480,4481],{"class":113,"line":121},[111,4482,809],{"emptyLinePlaceholder":808},[111,4484,4485,4487,4490,4493],{"class":113,"line":138},[111,4486,814],{"class":798},[111,4488,4489],{"class":131}," \"",[111,4491,4492],{"class":802},"fmt",[111,4494,827],{"class":131},[111,4496,4497],{"class":113,"line":151},[111,4498,809],{"emptyLinePlaceholder":808},[111,4500,4501,4503,4506,4508,4511,4514,4516,4518],{"class":113,"line":164},[111,4502,879],{"class":798},[111,4504,4505],{"class":802}," LogLevel",[111,4507,885],{"class":117},[111,4509,4510],{"class":888},"status",[111,4512,4513],{"class":798}," int",[111,4515,911],{"class":117},[111,4517,895],{"class":798},[111,4519,920],{"class":117},[111,4521,4522,4524,4526,4528,4530],{"class":113,"line":177},[111,4523,1076],{"class":798},[111,4525,2993],{"class":117},[111,4527,2996],{"class":798},[111,4529,2999],{"class":124},[111,4531,920],{"class":117},[111,4533,4534,4536],{"class":113,"line":190},[111,4535,998],{"class":798},[111,4537,4538],{"class":131}," \"error\"\n",[111,4540,4541],{"class":113,"line":203},[111,4542,1017],{"class":117},[111,4544,4545,4547],{"class":113,"line":216},[111,4546,1022],{"class":798},[111,4548,4549],{"class":131}," \"info\"\n",[111,4551,4552],{"class":113,"line":229},[111,4553,360],{"class":117},[111,4555,4556],{"class":113,"line":242},[111,4557,809],{"emptyLinePlaceholder":808},[111,4559,4560,4562,4565],{"class":113,"line":255},[111,4561,879],{"class":798},[111,4563,4564],{"class":802}," main",[111,4566,2221],{"class":117},[111,4568,4569,4572,4575,4577,4580,4582,4585],{"class":113,"line":268},[111,4570,4571],{"class":117},"    fmt.",[111,4573,4574],{"class":802},"Println",[111,4576,885],{"class":117},[111,4578,4579],{"class":802},"LogLevel",[111,4581,885],{"class":117},[111,4583,4584],{"class":124},"404",[111,4586,4587],{"class":117},"))\n",[111,4589,4590,4592,4594,4596,4598,4600,4603],{"class":113,"line":281},[111,4591,4571],{"class":117},[111,4593,4574],{"class":802},[111,4595,885],{"class":117},[111,4597,4579],{"class":802},[111,4599,885],{"class":117},[111,4601,4602],{"class":124},"503",[111,4604,4587],{"class":117},[111,4606,4607],{"class":113,"line":294},[111,4608,360],{"class":117},[4429,4610,4611],{"v-slot:hint":94},[15,4612,4613],{},"Expected 4xx обычно не production incident, а 5xx уже системная ошибка.",[4615,4616,4620,4637,4764],"code-task",{"expected":4617,"id":4618,"xp":4619},"field\\nredact\\navoid-label","obs-logging-ct1","20",[15,4621,4622,4623,128,4626,4628,4629,4632,4633,4636],{},"Реализуй ",[40,4624,4625],{},"LogPolicy",[40,4627,50],{}," хранится как обычное поле, ",[40,4630,4631],{},"password"," нужно редактировать, ",[40,4634,4635],{},"user_id_label"," нельзя делать label.",[4429,4638,4639],{"v-slot:template":94},[88,4640,4642],{"className":788,"code":4641,"language":790,"meta":94,"style":94},"package main\n\nimport \"fmt\"\n\nfunc LogPolicy(name string) string {\n    return \"todo\"\n}\n\nfunc main() {\n    fmt.Println(LogPolicy(\"request_id\"))\n    fmt.Println(LogPolicy(\"password\"))\n    fmt.Println(LogPolicy(\"user_id_label\"))\n}\n",[40,4643,4644,4650,4654,4664,4668,4688,4695,4699,4703,4711,4727,4743,4760],{"__ignoreMap":94},[111,4645,4646,4648],{"class":113,"line":114},[111,4647,799],{"class":798},[111,4649,4478],{"class":802},[111,4651,4652],{"class":113,"line":121},[111,4653,809],{"emptyLinePlaceholder":808},[111,4655,4656,4658,4660,4662],{"class":113,"line":138},[111,4657,814],{"class":798},[111,4659,4489],{"class":131},[111,4661,4492],{"class":802},[111,4663,827],{"class":131},[111,4665,4666],{"class":113,"line":151},[111,4667,809],{"emptyLinePlaceholder":808},[111,4669,4670,4672,4675,4677,4680,4682,4684,4686],{"class":113,"line":164},[111,4671,879],{"class":798},[111,4673,4674],{"class":802}," LogPolicy",[111,4676,885],{"class":117},[111,4678,4679],{"class":888},"name",[111,4681,1056],{"class":798},[111,4683,911],{"class":117},[111,4685,895],{"class":798},[111,4687,920],{"class":117},[111,4689,4690,4692],{"class":113,"line":177},[111,4691,1022],{"class":798},[111,4693,4694],{"class":131}," \"todo\"\n",[111,4696,4697],{"class":113,"line":190},[111,4698,360],{"class":117},[111,4700,4701],{"class":113,"line":203},[111,4702,809],{"emptyLinePlaceholder":808},[111,4704,4705,4707,4709],{"class":113,"line":216},[111,4706,879],{"class":798},[111,4708,4564],{"class":802},[111,4710,2221],{"class":117},[111,4712,4713,4715,4717,4719,4721,4723,4725],{"class":113,"line":229},[111,4714,4571],{"class":117},[111,4716,4574],{"class":802},[111,4718,885],{"class":117},[111,4720,4625],{"class":802},[111,4722,885],{"class":117},[111,4724,3748],{"class":131},[111,4726,4587],{"class":117},[111,4728,4729,4731,4733,4735,4737,4739,4741],{"class":113,"line":242},[111,4730,4571],{"class":117},[111,4732,4574],{"class":802},[111,4734,885],{"class":117},[111,4736,4625],{"class":802},[111,4738,885],{"class":117},[111,4740,3840],{"class":131},[111,4742,4587],{"class":117},[111,4744,4745,4747,4749,4751,4753,4755,4758],{"class":113,"line":255},[111,4746,4571],{"class":117},[111,4748,4574],{"class":802},[111,4750,885],{"class":117},[111,4752,4625],{"class":802},[111,4754,885],{"class":117},[111,4756,4757],{"class":131},"\"user_id_label\"",[111,4759,4587],{"class":117},[111,4761,4762],{"class":113,"line":268},[111,4763,360],{"class":117},[4429,4765,4766],{"v-slot:hints":94},[18,4767,4768,4771,4774],{},[21,4769,4770],{},"Correlation id нужен для поиска, но не как индексный label высокой кардинальности.",[21,4772,4773],{},"Secrets и PII должны редактироваться до попадания в storage.",[21,4775,4776],{},"Labels должны быть bounded.",[4778,4779,4780],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":94,"searchDepth":121,"depth":121,"links":4782},[4783,4784,4785,4786,4787,4788,4789,4790,4791,4792,4793,4794,4795,4796,4797,4798,4799,4800],{"id":82,"depth":121,"text":83},{"id":384,"depth":121,"text":385},{"id":693,"depth":121,"text":694},{"id":784,"depth":121,"text":785},{"id":1274,"depth":121,"text":1275},{"id":1814,"depth":121,"text":1815},{"id":2400,"depth":121,"text":2401},{"id":3179,"depth":121,"text":3180},{"id":3509,"depth":121,"text":3510},{"id":3890,"depth":121,"text":3891},{"id":3947,"depth":121,"text":3948},{"id":3974,"depth":121,"text":3975},{"id":4100,"depth":121,"text":4101},{"id":4147,"depth":121,"text":4148},{"id":4191,"depth":121,"text":4192},{"id":4262,"depth":121,"text":4263},{"id":4307,"depth":121,"text":4308},{"id":4417,"depth":121,"text":4418},1781022063219]