Wzorce logowania
ENWzorce logowania dla agentów AI to cztery receptury, w których Claude Code, Codex lub Cursor zamieniają rozrzucone wywołania console.log na strukturalne logi JSON z jednej utrzymywanej biblioteki, z ID żądania w każdej linii i sekretami redagowanymi przez konfigurację. Każda receptura kończy się sprawdzeniem maszynowym (test kontraktu logów, kanarek sekretów i reguła lintera), więc nikt nie przegląda każdego miejsca wywołania wzrokiem.
Ta strona jest dla programistów, którzy odpowiadają za usługę w Node.js, i dla tech leadów, którzy decydują, co musi udowodnić pull request zmieniający logowanie. Twoje API sporadycznie zwraca błędy 500, logi to sklejane napisy, a pojedynczego żądania nie da się prześledzić przez trzy usługi. Poproszony o „dodanie logowania” agent rozsypuje wywołania logger.info bez żadnego schematu. Poniższe receptury dają mu zamiast tego kontrakt do spełnienia.
Co dają te receptury logowania
Dział zatytułowany „Co dają te receptury logowania”- Kontrakt logów zapisany jako test, zanim agent dotknie pierwszego miejsca wywołania, dzięki czemu „strukturalne” i „zredagowane” to wynik testu, a nie opinia
- Pięć promptów do skopiowania: kontrakt logów, konwersja
console.*, korelacja żądań przezAsyncLocalStorage, audyt sekretów i danych osobowych oraz przegląd gotowego pull requestu - Dokładne polecenia uruchamiające każdy przebieg w Claude Code i Codeksie oraz odpowiednik w Cursorze
- Regułę lintera, która nie pozwala
console.logwrócić tydzień po refaktoryzacji - Sposoby, w jakie logowanie napisane przez agenta przechodzi przegląd i zawodzi na produkcji, z wyjściem z każdej sytuacji
Od którego przebiegu logowania zacząć?
Dział zatytułowany „Od którego przebiegu logowania zacząć?”Podziel pracę na cztery małe przebiegi zamiast jednej prośby „dodaj logowanie”. Każdy przebieg ma własne sprawdzenie, a następny zaczyna się dopiero wtedy, gdy to sprawdzenie jest zielone.
| Przebieg | Co zmienia agent | Sprawdzenie, które to dowodzi |
|---|---|---|
| 1. Kontrakt logów | src/lib/logger.ts i jeden plik testu | Test kontraktu zielony i czerwony po usunięciu jednej ścieżki redakcji |
| 2. Konwersja | Każde miejsce wywołania console.* | Test kontraktu zielony, zero naruszeń reguły no-console |
| 3. Korelacja | Middleware i hak kontekstu w loggerze | Test loguje z funkcji, która nigdy nie widzi req, i znajduje requestId |
| 4. Audyt | Na początku nic: tabela ustaleń | Test kanarków zielony, tabela ustaleń pusta lub rozpatrzona |
Te same przebiegi działają w Pythonie (structlog z contextvars) i Go (log/slog z context.Context). Przykłady używają Node.js i TypeScriptu.
Czy agent ma użyć pino, czy winstona?
Dział zatytułowany „Czy agent ma użyć pino, czy winstona?”Wskaż bibliotekę w prompcie. Pozostawiony sam sobie agent często pisze własną klasę Logger z transportami i poziomami, którą potem utrzymujesz ty. Obie biblioteki z tabeli są utrzymywane (najnowsze wersje w npm na 2026-09-26: pino 10.3.1, winston 3.19.0).
| Potrzeba | pino | winston |
|---|---|---|
| JSON domyślnie | Tak | Przez format.json() |
| Redakcja | Wbudowana: redact: { paths, censor }, oparta na ścieżkach | Własny format, który piszesz sam |
| Kontekst żądania w każdej linii | Opcja mixin(), wywoływana przy każdym logu | Własny format, który czyta kontekst |
| Wiele transportów (pliki, HTTP, syslog) | pino.transport(), które uruchamia transporty w wątku roboczym | Szeroki ekosystem transportów konfigurowanych w jednym miejscu |
| Sygnatura wywołania | logger.info({ userId }, 'User login'): najpierw obiekt | logger.info('User login', { userId }): najpierw komunikat |
Domyślnym wyborem na tej stronie jest pino, bo redakcja i kontekst w każdej linii to konfiguracja, a nie kod. Wybierz winstona, gdy usługa już go używa albo potrzebuje kilku transportów skonfigurowanych razem. Wiersz z sygnaturą wywołania ma większe znaczenie, niż się wydaje: zobacz sposoby awarii poniżej.
Najpierw zapisz kontrakt logów jako test
Dział zatytułowany „Najpierw zapisz kontrakt logów jako test”Kontrakt sprawia, że resztę da się udowodnić. Przypina pola, które musi mieć każda linia, i podkłada kanarki, czyli fałszywe sekrety, które nigdy nie mogą trafić na wyjście. Poproś agenta, żeby napisał go przed czymkolwiek innym. Potem udowodnij, że test potrafi zawieść: usuń jedną ścieżkę redakcji, zobacz, jak test robi się czerwony, i przywróć ścieżkę. Kontrakt, który nie może zawieść, niczego nie dowodzi.
Rdzeń src/lib/logger.ts, do którego prowadzi kontrakt:
import { AsyncLocalStorage } from 'node:async_hooks';import pino, { type DestinationStream } from 'pino';
export const requestContext = new AsyncLocalStorage<{ requestId: string; userId?: string }>();
export function createLogger(destination?: DestinationStream) { return pino( { level: process.env.LOG_LEVEL ?? 'info', base: { service: 'api-gateway', version: process.env.APP_VERSION }, timestamp: pino.stdTimeFunctions.isoTime, redact: { paths: ['*.password', '*.token', '*.apiKey', '*.headers.authorization', '*.headers.cookie'], censor: '[REDACTED]', }, mixin: () => requestContext.getStore() ?? {}, }, destination, );}
export const logger = createLogger();I test kontraktu w runnerze, którego projekt już używa (tu Vitest):
import { Writable } from 'node:stream';import { expect, it } from 'vitest';import { createLogger, requestContext } from '../src/lib/logger';
function capture() { const lines: string[] = []; const sink = new Writable({ write(chunk, _enc, done) { lines.push(chunk.toString()); done(); }, }); return { log: createLogger(sink), lines };}
it('emits JSON with a constant message and no canary secrets', () => { const { log, lines } = capture(); log.info({ user: { id: 'u1', password: 'CANARY-pw' }, req: { headers: { authorization: 'Bearer CANARY-tok' } } }, 'User login'); expect(lines.join('')).not.toContain('CANARY'); expect(JSON.parse(lines[0])).toMatchObject({ msg: 'User login', service: 'api-gateway' }); expect(JSON.parse(lines[0]).user.id).toBe('u1');});
it('carries the requestId across an async boundary', async () => { const { log, lines } = capture(); await requestContext.run({ requestId: 'req-test-1' }, async () => { await new Promise((resolve) => setTimeout(resolve, 5)); log.info('Downstream call'); }); expect(JSON.parse(lines[0]).requestId).toBe('req-test-1');});Cały trik to prefiks CANARY-: jedno not.toContain obejmuje każde pole, niezależnie od tego, jak głęboko agent je zagnieździ. Dodaj kanarek dla każdego rodzaju sekretu, który obsługuje usługa. Powyższe ścieżki redakcji obejmują jeden poziom zagnieżdżenia; kanarek na drugim poziomie z promptu poniżej zmusza agenta do dodania głębszych ścieżek, takich jak *.*.password, albo do rezygnacji z logowania obiektu nadrzędnego. Logger i test zostały uruchomione dokładnie w tej postaci na pino 10.3.1 z AsyncLocalStorage z Node.js.
Najpierw sam zainstaluj bibliotekę (npm install pino), żeby żaden przebieg nie potrzebował dostępu do sieci. Potem uruchom prompt kontraktu.
Czytaj test, nie logger. Jeśli asercje odpowiadają powyższym wymaganiom, logger albo je spełnia, albo nie.
Zamień console.log na strukturalne logi w całym repozytorium
Dział zatytułowany „Zamień console.log na strukturalne logi w całym repozytorium”Gdy kontrakt jest zielony, konwersja to mechaniczna refaktoryzacja wielu plików. Prompt jest ten sam dla każdego narzędzia; różni się tylko sposób uruchomienia.
# Terminal, repository root. Flags checked against Claude Code 2.1.283; the# v2.1.285 change is from the Claude Code changelog (checked 2026-10-12).# claude -p starts in Manual mode (auto mode on third-party providers or with# telemetry off, from v2.1.285); --permission-mode dontAsk denies anything that# would prompt, on every provider. Reads and read-only commands still run.claude -p "$(cat prompts/convert-logging.txt)" \ --permission-mode dontAsk \ --allowedTools "Read" "Edit" "Write" "Bash(npm test:*)" "Bash(npx eslint:*)"Zapisz prompt konwersji poniżej jako prompts/convert-logging.txt. Allowlista pozwala agentowi edytować pliki oraz uruchamiać testy i linter, i nic poza tym: żadnego git push, żadnej instalacji pakietów.
# Terminal, repository root. Checked against codex-cli 0.157.1.# Permission profiles are beta (Codex CLI 0.138.0 or later).codex exec -c default_permissions=":workspace" "$(cat prompts/convert-logging.txt)"Profil :workspace pozwala Codeksowi zapisywać tylko wewnątrz repozytorium. W wersji Codeksa bez profili uprawnień użyj zamiast tego --sandbox workspace-write; nie łącz obu. Przy dużym repozytorium uruchom ten sam prompt jako zadanie w chmurze Codeksa i przejrzyj wynik jako pull request.
Najpierw dodaj regułę projektu (Rule): „Never use console.* in src/. Import logger from src/lib/logger.ts. pino signature: object first, message second.” Reguła nie pozwala kolejnym sesjom agenta przywrócić console.log. Potem wklej prompt konwersji do Agenta, przejrzyj diff wielu plików przed akceptacją i uruchom npm test w terminalu. Przy dużym repozytorium przekaż ten sam prompt Cloud Agentowi i przejrzyj jego pull request.
W podsumowaniu sprawdź trzy rzeczy, a nie każdy plik: testy i linter są zielone, lista niepewnych pól jest na tyle krótka, że rozstrzygniesz ją w kilka minut, a oba poniższe wyszukiwania nic nie zwracają:
# Terminal, repository rootgit grep -n "console\." src/ # leftover console callsgit grep -nE "logger\.[a-z]+\([^)]*\`" src/ # messages built with template literalsDodaj korelację żądań, która przetrwa granice asynchroniczności
Dział zatytułowany „Dodaj korelację żądań, która przetrwa granice asynchroniczności”requestId zapisane w req znika w chwili, gdy funkcja serwisu nie przyjmuje req. AsyncLocalStorage rozwiązuje ten problem: middleware zapisuje kontekst raz, a mixin loggera odczytuje go z dowolnego miejsca stosu wywołań. Agenci domyślnie przekazują req.logger przez argumenty funkcji, więc nazwij mechanizm w prompcie.
Jeśli projekt nie ma supertest, sam dodaj go jako devDependency przed tym przebiegiem. Dowodem jest przypadek z supertestem. Implementacja działająca tylko na ścieżce synchronicznej go nie przejdzie, a to właśnie ta wersja gubi ID na produkcji. Loguj wzorzec trasy (/users/:id), a nie surowy URL: surowe adresy niosą w query stringu identyfikatory, adresy e-mail i tokeny.
Jeśli usługa już eksportuje ślady OpenTelemetry, @opentelemetry/instrumentation-pino (najnowsza wersja w npm na 2026-09-26: 0.68.0) dodaje trace_id i span_id do każdej linii logu, więc logi i ślady łączą się w twoim backendzie. Przenoszenie tych pól przez potok logów opisują wzorce monitoringu i logowania.
Przeprowadź audyt logów pod kątem sekretów i danych osobowych
Dział zatytułowany „Przeprowadź audyt logów pod kątem sekretów i danych osobowych”Redakcja oparta na ścieżkach łapie klucze, które wymieniłeś. Nie złapie adresu e-mail w treści komunikatu, adresu IP ani tokenu pod kluczem, o którym nikt nie pomyślał. Adresy e-mail i adresy IP to dane osobowe w rozumieniu RODO, więc niewinnie wyglądająca linia info przy każdym logowaniu staje się problemem retencji i prawa do usunięcia danych. Uruchom audyt w trybie tylko do odczytu, żeby agent najpierw raportował, a dopiero potem edytował.
# Plan mode keeps the run read-onlyclaude -p --permission-mode plan "$(cat prompts/audit-logging.txt)" > log-audit.mdcodex exec -c default_permissions=":read-only" "$(cat prompts/audit-logging.txt)" > log-audit.mdPrzełącz się na Plan Mode, wklej prompt audytu i zapisz zwróconą tabelę. Plan Mode tworzy plan zamiast edycji.
Rozpatrz tabelę, a potem odeślij poprawki jako zwykły przebieg edycji, z nowymi kanarkami dodanymi do testu kontraktu. Audyt jest zakończony, gdy test kanarków jest zielony, a każdy wiersz tabeli jest poprawiony albo imiennie zaakceptowany przez konkretną osobę. Kwestie zasad (jakie dane w ogóle wolno logować i jak długo) opisuje strona Prywatność i przetwarzanie danych.
Jak udowodnić refaktoryzację logowania bez czytania każdego wywołania?
Dział zatytułowany „Jak udowodnić refaktoryzację logowania bez czytania każdego wywołania?”Pull request zmieniający logowanie dotyka dziesiątek plików. Przeglądaj dowody zamiast diffu:
- Test kontraktu: zielony i pokazany jako czerwony po usunięciu ścieżki redakcji. Obejmuje kształt JSON, stałe komunikaty, kanarki redakcji i przenoszenie
requestIdprzezawaitisetTimeout. - Bramka lintera:
no-consolema poziomerrorw konfiguracji lintera i działa w CI, więc reguła przeżyje ten pull request. - Wyszukiwania z pustym wynikiem: dwa polecenia
git grepz przebiegu konwersji. - Jedna prawdziwa linia: uruchom usługę lokalnie, wyślij jedno żądanie i wklej jedną linię logu do opisu pull requestu. Recenzent sprawdzi pola w kilka sekund.
- Tabela audytu: dołączona, z każdym wierszem poprawionym lub zaakceptowanym.
Autor podpisuje punkty 1–4. Pozostałe wiersze audytu akceptuje lub odrzuca osoba odpowiedzialna za ochronę danych w tej usłudze. Jeśli chcesz mieć prompt przeglądu do wielokrotnego użytku, poproś agenta o sprawdzenie gotowego diffu według tej listy:
Co się psuje, gdy agent przepisuje logowanie?
Dział zatytułowany „Co się psuje, gdy agent przepisuje logowanie?”- Metadane po cichu znikają. Agent przyzwyczajony do winstona pisze
logger.info('User login', { userId })dla pino. pino traktuje drugi argument jako parametr formatowania, więcuserIdnie trafia do logu jako pole strukturalne. Wyjście:git grep -nE "logger\.[a-z]+\('[^']*', *\{" src/wypisuje każde wywołanie w kolejności winstona, a prompt przeglądu sprawdza kolejność argumentów w diffie. Test kontraktu pilnuje tylko konfiguracji loggera: sprawdza, żeuser.idprzetrwa, więc zbyt szeroka ścieżka redakcji zmieni go na czerwony, ale wywołuje logger bezpośrednio i nie widzi miejsc wywołań. - Redakcja pomija zagnieżdżone sekrety. Ścieżki redakcji w pino są dokładne:
*.passwordpasuje douser.password, ale nie dobody.user.password. Wyjście: kanarki na dwóch poziomach zagnieżdżenia w teście kontraktu i głębsze ścieżki (albo rezygnacja z logowania obiektu nadrzędnego), gdy któryś wycieknie. - Cała treść żądania (body) leży pod bezpiecznie wyglądającym kluczem.
logger.debug({ payload: req.body })omija każdą regułę opartą na kluczach, a ktoś podczas incydentu włączadebugna produkcji. Wyjście: prompt konwersji zabrania logowania całych treści żądań, a prompt przeglądu to sprawdza. requestIdznika w workerach kolejek i timerach. Kontekst wAsyncLocalStoragenie przekracza granicy procesu. Zadanie odebrane przez workera nie ma żadnego kontekstu. Wyjście: umieśćrequestIdw danych zadania i wywołajrequestContext.run()na początku handlera workera.- Każda linia stała się
info. Refaktoryzacja spłaszczyła poziomy, więc alert na odsetek błędów albo nigdy nie odpala, albo odpala przy każdym żądaniu. Wyjście: uruchom ponownie reguły poziomów z promptu konwersji tylko na dotkniętych plikach, a potem sprawdź, czy odsetek błędów z logów zgadza się z odsetkiem 5xx z metryk. - Logowanie dodaje opóźnienie na ścieżce żądania. Synchroniczny zapis do wolnego miejsca docelowego blokuje żądanie. Wyjście: zapisuj JSON na stdout i pozwól platformie go przesłać albo użyj
pino.transport(), które uruchamia transport w wątku roboczym. Nigdy nie używajawaitna zapisie logu w handlerze. - Agent zgaduje API biblioteki. Wpisuje opcję z innej wersji głównej albo idiom z winstona do konfiguracji pino, a wynik wygląda wiarygodnie w przeglądzie. Wyjście: podłącz serwer MCP Context7, żeby agent czytał dokumentację zainstalowanej wersji (konfiguracja dla wszystkich trzech narzędzi na stronie Context7), a arbitrem niech zostanie test kontraktu.
Dokąd dalej z logowaniem
Dział zatytułowany „Dokąd dalej z logowaniem”- Wzorce debugowania: użyj tych strukturalnych logów do korelacji żądań udanych i nieudanych
- Wzorce monitoringu i logowania: przenieś
trace_idprzez potok logów i ustaw alerty na to, co logi teraz emitują - Wzorce odzyskiwania: co robi usługa, gdy błędy, które teraz logujesz, naprawdę wystąpią
- Serwery MCP do obserwowalności: pozwól agentowi odpytywać logi i błędy produkcyjne w trybie tylko do odczytu
- Receptury obsługi błędów: pozostałe strony tej sekcji
Czy ten artykuł Ci pomógł?
Jedno kliknięcie, bez logowania.
Co z tym zrobisz?
Jak się czytało?
Co przeszkadzało? Możesz zaznaczyć kilka
Twoja rola?
Dzięki. Czytam każdą odpowiedź. Michał Jaskólski
Pomógł Ci ten artykuł?
Każde to jedno kliknięcie. Zamknij, kiedy chcesz.
bez logowania