Przejdź do głównej zawartości

Wzorce 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.

  • 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ń przez AsyncLocalStorage, 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.log wró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

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.

PrzebiegCo zmienia agentSprawdzenie, które to dowodzi
1. Kontrakt logówsrc/lib/logger.ts i jeden plik testuTest kontraktu zielony i czerwony po usunięciu jednej ścieżki redakcji
2. KonwersjaKażde miejsce wywołania console.*Test kontraktu zielony, zero naruszeń reguły no-console
3. KorelacjaMiddleware i hak kontekstu w loggerzeTest loguje z funkcji, która nigdy nie widzi req, i znajduje requestId
4. AudytNa 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.

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).

Potrzebapinowinston
JSON domyślnieTakPrzez format.json()
RedakcjaWbudowana: redact: { paths, censor }, oparta na ścieżkachWłasny format, który piszesz sam
Kontekst żądania w każdej liniiOpcja mixin(), wywoływana przy każdym loguWłasny format, który czyta kontekst
Wiele transportów (pliki, HTTP, syslog)pino.transport(), które uruchamia transporty w wątku roboczymSzeroki ekosystem transportów konfigurowanych w jednym miejscu
Sygnatura wywołanialogger.info({ userId }, 'User login'): najpierw obiektlogger.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.

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.

Okno terminala
# 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.

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ą:

Okno terminala
# Terminal, repository root
git grep -n "console\." src/ # leftover console calls
git grep -nE "logger\.[a-z]+\([^)]*\`" src/ # messages built with template literals

Dodaj 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ł.

Okno terminala
# Plan mode keeps the run read-only
claude -p --permission-mode plan "$(cat prompts/audit-logging.txt)" > log-audit.md

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:

  1. Test kontraktu: zielony i pokazany jako czerwony po usunięciu ścieżki redakcji. Obejmuje kształt JSON, stałe komunikaty, kanarki redakcji i przenoszenie requestId przez await i setTimeout.
  2. Bramka lintera: no-console ma poziom error w konfiguracji lintera i działa w CI, więc reguła przeżyje ten pull request.
  3. Wyszukiwania z pustym wynikiem: dwa polecenia git grep z przebiegu konwersji.
  4. 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.
  5. 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:

  • Metadane po cichu znikają. Agent przyzwyczajony do winstona pisze logger.info('User login', { userId }) dla pino. pino traktuje drugi argument jako parametr formatowania, więc userId nie 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, że user.id przetrwa, 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: *.password pasuje do user.password, ale nie do body.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łącza debug na produkcji. Wyjście: prompt konwersji zabrania logowania całych treści żądań, a prompt przeglądu to sprawdza.
  • requestId znika w workerach kolejek i timerach. Kontekst w AsyncLocalStorage nie przekracza granicy procesu. Zadanie odebrane przez workera nie ma żadnego kontekstu. Wyjście: umieść requestId w danych zadania i wywołaj requestContext.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żywaj await na 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.

Czy ten artykuł Ci pomógł?

Jedno kliknięcie, bez logowania.

wcale bardzo