Przejdź do głównej zawartości

Wzorce logowania

Strategiczne logowanie przekształca aplikacje z czarnych skrzynek w przejrzyste, obserwowalne systemy. Z pomocą AI możesz wdrożyć zaawansowane wzorce logowania, które dostarczają właściwe informacje w odpowiednim czasie, czyniąc debugowanie szybszym i operacje płynniejszymi. Ten przewodnik obejmuje sprawdzone wzorce logowania dla rzeczywistych aplikacji.

Nowoczesne logowanie polega na tworzeniu narracji o zachowaniu twojej aplikacji:

Strukturowane dane

Logi sformatowane w JSON z konsystentnymi polami

Informacje kontekstowe

ID żądań, kontekst użytkownika i dane śledzenia

Świadomość wydajności

Minimalny narzut z maksymalnym wglądem

Bezpieczeństwo na pierwszym miejscu

Nigdy nie loguj wrażliwych danych

Przekształć niestrukturalne logi w dane, które można przeszukiwać i analizować.

// Tradycyjne logowanie (unikaj tego)
console.log('User ' + userId + ' logged in from ' + ipAddress);
// Strukturowane logowanie zalecane przez AI
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: 'api-gateway',
version: process.env.APP_VERSION
},
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Użycie ze strukturowanymi danymi
logger.info('Logowanie użytkownika udane', {
userId: user.id,
email: user.email,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
loginMethod: 'oauth',
provider: 'google'
});
// Wynik:
{
"timestamp": "2024-01-15T10:30:45.123Z",
"level": "info",
"message": "Logowanie użytkownika udane",
"service": "api-gateway",
"version": "1.2.3",
"userId": "usr_123",
"email": "user@example.com",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"loginMethod": "oauth",
"provider": "google"
}

Śledź żądania przez cały system z ID korelacji.

// Middleware Express dla kontekstu żądań
import { v4 as uuidv4 } from 'uuid';
class ContextualLogger {
constructor(baseLogger) {
this.baseLogger = baseLogger;
}
createRequestLogger(req) {
const requestId = req.headers['x-request-id'] || uuidv4();
const userId = req.user?.id || 'anonymous';
// Przechowaj w żądaniu do użycia downstream
req.requestId = requestId;
req.logger = this.baseLogger.child({
requestId,
userId,
method: req.method,
path: req.path,
ip: req.ip
});
return req.logger;
}
}
// Implementacja middleware
const loggingMiddleware = (logger) => (req, res, next) => {
const contextLogger = new ContextualLogger(logger);
req.logger = contextLogger.createRequestLogger(req);
// Zaloguj początek żądania
req.logger.info('Żądanie rozpoczęte');
// Przechwyć szczegóły odpowiedzi
const startTime = Date.now();
const originalSend = res.send;
res.send = function(data) {
res.send = originalSend;
req.logger.info('Żądanie zakończone', {
statusCode: res.statusCode,
duration: Date.now() - startTime,
responseSize: Buffer.byteLength(data)
});
return res.send(data);
};
next();
};
// Użycie w trasach
app.get('/api/users/:id', (req, res) => {
req.logger.info('Pobieranie użytkownika', {
targetUserId: req.params.id
});
// Twoja logika tutaj...
});

Monitoruj wydajność aplikacji poprzez strategiczne logowanie.

// Narzędzie logowania wydajności
class PerformanceLogger {
constructor(logger) {
this.logger = logger;
this.timers = new Map();
}
startTimer(operation, metadata = {}) {
const id = `${operation}-${Date.now()}-${Math.random()}`;
this.timers.set(id, {
operation,
startTime: process.hrtime.bigint(),
metadata
});
this.logger.debug('Operacja rozpoczęta', {
operation,
timerId: id,
...metadata
});
return id;
}
endTimer(timerId, additionalData = {}) {
const timer = this.timers.get(timerId);
if (!timer) {
this.logger.warn('Timer nie znaleziony', { timerId });
return;
}
const endTime = process.hrtime.bigint();
const durationMs = Number(endTime - timer.startTime) / 1e6;
this.timers.delete(timerId);
const logData = {
operation: timer.operation,
durationMs,
...timer.metadata,
...additionalData
};
// Loguj na podstawie czasu trwania
if (durationMs > 1000) {
this.logger.warn('Wykryto wolną operację', logData);
} else {
this.logger.info('Operacja zakończona', logData);
}
// Emituj metryki jeśli skonfigurowane
if (this.metricsClient) {
this.metricsClient.histogram(
`operation.duration.${timer.operation}`,
durationMs
);
}
return durationMs;
}
async measureAsync(operation, fn, metadata = {}) {
const timerId = this.startTimer(operation, metadata);
try {
const result = await fn();
this.endTimer(timerId, { status: 'success' });
return result;
} catch (error) {
this.endTimer(timerId, {
status: 'error',
error: error.message
});
throw error;
}
}
}
// Użycie
const perfLogger = new PerformanceLogger(logger);
// Ręczny pomiar czasu
const timerId = perfLogger.startTimer('database_query', {
query: 'SELECT * FROM users',
table: 'users'
});
const results = await db.query('SELECT * FROM users');
perfLogger.endTimer(timerId, { rowCount: results.length });
// Automatyczny pomiar czasu
const user = await perfLogger.measureAsync(
'fetch_user',
() => userService.getUser(userId),
{ userId }
);

Przechwyć komprehensywne informacje o błędach dla szybszego debugowania.

Ulepszone logowanie błędów AI

class ErrorLogger {
constructor(logger) {
this.logger = logger;
}
logError(error, context = {}) {
// Wydobądź użyteczne informacje o błędzie
const errorInfo = {
message: error.message,
name: error.name,
stack: error.stack,
code: error.code,
statusCode: error.statusCode || 500
};
// Dodaj kontekst żądania jeśli dostępny
if (context.req) {
errorInfo.request = {
method: context.req.method,
url: context.req.url,
headers: this.sanitizeHeaders(context.req.headers),
body: this.sanitizeBody(context.req.body),
user: context.req.user?.id
};
}
// Dodaj niestandardowe właściwości błędu
if (error.details) {
errorInfo.details = error.details;
}
// Loguj z odpowiednim poziomem
if (error.statusCode >= 500 || !error.statusCode) {
this.logger.error('Błąd aplikacji', {
...errorInfo,
...context
});
} else if (error.statusCode >= 400) {
this.logger.warn('Błąd klienta', {
...errorInfo,
...context
});
}
// Loguj do serwisu śledzenia błędów
if (this.errorTracker) {
this.errorTracker.captureException(error, {
extra: context,
user: context.req?.user
});
}
}
sanitizeHeaders(headers) {
const sensitive = ['authorization', 'cookie', 'x-api-key'];
const sanitized = { ...headers };
sensitive.forEach(key => {
if (sanitized[key]) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
sanitizeBody(body) {
if (!body) return body;
const sensitive = ['password', 'token', 'secret', 'creditCard'];
const sanitized = { ...body };
const recursiveSanitize = (obj) => {
Object.keys(obj).forEach(key => {
if (sensitive.some(s => key.toLowerCase().includes(s))) {
obj[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
recursiveSanitize(obj[key]);
}
});
};
recursiveSanitize(sanitized);
return sanitized;
}
}

Połącz logowanie z rozproszonym śledzeniem dla pełnej obserwowalności.

// Integracja OpenTelemetry
import { trace, context } from '@opentelemetry/api';
class TracingLogger {
constructor(logger) {
this.logger = logger;
this.tracer = trace.getTracer('application');
}
log(level, message, attributes = {}) {
const span = trace.getActiveSpan();
if (span) {
// Dodaj kontekst śledzenia do logów
const spanContext = span.spanContext();
this.logger[level](message, {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
traceFlags: spanContext.traceFlags,
...attributes
});
// Dodaj wydarzenie loga do span
span.addEvent(message, attributes);
} else {
// Brak aktywnego span, loguj normalnie
this.logger[level](message, attributes);
}
}
async traceOperation(name, operation, attributes = {}) {
return this.tracer.startActiveSpan(name, async (span) => {
try {
this.log('info', `Rozpoczynanie ${name}`, attributes);
const result = await operation();
span.setStatus({ code: 1 }); // OK
this.log('info', `Zakończono ${name}`, {
...attributes,
duration: span.duration
});
return result;
} catch (error) {
span.setStatus({
code: 2, // ERROR
message: error.message
});
span.recordException(error);
this.log('error', `Niepowodzenie ${name}`, {
...attributes,
error: error.message,
stack: error.stack
});
throw error;
} finally {
span.end();
}
});
}
}
// Użycie
const tracingLogger = new TracingLogger(logger);
await tracingLogger.traceOperation(
'process_payment',
async () => {
// Twoja logika płatności
return await paymentService.process(payment);
},
{
paymentId: payment.id,
amount: payment.amount,
currency: payment.currency
}
);

Śledź wydarzenia istotne dla bezpieczeństwa dla zgodności i śledztw.

class AuditLogger {
constructor(logger, options = {}) {
this.logger = logger;
this.options = {
includeIp: true,
includeUserAgent: true,
includeTimestamp: true,
...options
};
}
logAuditEvent(event, context = {}) {
const auditEntry = {
eventType: event.type,
eventCategory: event.category,
timestamp: new Date().toISOString(),
actor: {
userId: context.user?.id,
username: context.user?.username,
role: context.user?.role,
ip: context.req?.ip,
userAgent: context.req?.get('User-Agent')
},
target: event.target,
action: event.action,
result: event.result,
reason: event.reason,
metadata: event.metadata
};
// Podpisz wpis audytu dla wykrywania manipulacji
if (this.options.sign) {
auditEntry.signature = this.signEntry(auditEntry);
}
// Loguj do oddzielnego loga audytu
this.logger.info('AUDIT_EVENT', auditEntry);
// Przechowuj w bazie audytu jeśli skonfigurowane
if (this.auditStore) {
this.auditStore.save(auditEntry);
}
}
// Popularne wydarzenia audytu
logLogin(user, req, success = true) {
this.logAuditEvent({
type: 'AUTH_LOGIN',
category: 'AUTHENTICATION',
target: { type: 'USER', id: user.id },
action: 'LOGIN',
result: success ? 'SUCCESS' : 'FAILURE',
metadata: {
loginMethod: req.body.method,
mfaUsed: user.mfaEnabled
}
}, { user, req });
}
logDataAccess(user, resource, action, req) {
this.logAuditEvent({
type: 'DATA_ACCESS',
category: 'AUTHORIZATION',
target: {
type: resource.type,
id: resource.id,
name: resource.name
},
action: action.toUpperCase(),
result: 'SUCCESS',
metadata: {
fields: resource.fields,
filters: resource.filters
}
}, { user, req });
}
logConfigChange(user, config, req) {
this.logAuditEvent({
type: 'CONFIG_CHANGE',
category: 'SYSTEM',
target: {
type: 'CONFIGURATION',
id: config.key
},
action: 'UPDATE',
result: 'SUCCESS',
metadata: {
oldValue: config.oldValue,
newValue: config.newValue,
changeReason: config.reason
}
}, { user, req });
}
}

Zapobiegaj zalewaniu logów zachowując widoczność.

class SamplingLogger {
constructor(logger, config = {}) {
this.logger = logger;
this.config = {
defaultSampleRate: 0.1, // 10%
rules: [],
windowSize: 60000, // 1 minuta
...config
};
this.counters = new Map();
}
shouldLog(level, message, metadata = {}) {
// Zawsze loguj błędy
if (level === 'error' || level === 'fatal') {
return true;
}
// Sprawdź niestandardowe reguły
for (const rule of this.config.rules) {
if (rule.matches(level, message, metadata)) {
return this.applySampling(rule.sampleRate, rule.key);
}
}
// Zastosuj domyślne próbkowanie
return Math.random() < this.config.defaultSampleRate;
}
applySampling(rate, key) {
if (rate === 1) return true;
if (rate === 0) return false;
// Zapewnij minimalną reprezentację
const counter = this.getCounter(key);
if (counter.total === 0) {
counter.sampled++;
counter.total++;
return true;
}
const currentRate = counter.sampled / counter.total;
if (currentRate < rate) {
counter.sampled++;
counter.total++;
return true;
}
counter.total++;
return false;
}
getCounter(key) {
if (!this.counters.has(key)) {
this.counters.set(key, {
total: 0,
sampled: 0,
windowStart: Date.now()
});
}
const counter = this.counters.get(key);
// Zresetuj licznik jeśli okno wygasło
if (Date.now() - counter.windowStart > this.config.windowSize) {
counter.total = 0;
counter.sampled = 0;
counter.windowStart = Date.now();
}
return counter;
}
log(level, message, metadata = {}) {
if (this.shouldLog(level, message, metadata)) {
this.logger[level](message, metadata);
} else {
// Śledź porzucone logi
this.logger.debug('Log porzucony z powodu próbkowania', {
originalLevel: level,
originalMessage: message
});
}
}
}
// Konfiguracja
const samplingLogger = new SamplingLogger(logger, {
defaultSampleRate: 0.1,
rules: [
{
key: 'health-check',
matches: (level, msg) => msg.includes('health check'),
sampleRate: 0.01 // 1% dla sprawdzeń zdrowia
},
{
key: 'debug-verbose',
matches: (level) => level === 'debug',
sampleRate: 0.05 // 5% dla logów debug
}
]
});

Wysyłaj logi do wielu miejsc docelowych na podstawie kryteriów.

class MultiTransportLogger {
constructor() {
this.transports = [];
}
addTransport(transport, options = {}) {
this.transports.push({
transport,
levels: options.levels || ['error', 'warn', 'info', 'debug'],
filter: options.filter || (() => true),
transform: options.transform || (log => log)
});
}
log(level, message, metadata = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
...metadata
};
this.transports.forEach(({ transport, levels, filter, transform }) => {
if (levels.includes(level) && filter(logEntry)) {
const transformed = transform(logEntry);
transport.log(level, transformed);
}
});
}
// Metody wygody
error(message, metadata) { this.log('error', message, metadata); }
warn(message, metadata) { this.log('warn', message, metadata); }
info(message, metadata) { this.log('info', message, metadata); }
debug(message, metadata) { this.log('debug', message, metadata); }
}
// Konfiguruj wielotransport
const logger = new MultiTransportLogger();
// Konsola dla rozwoju
logger.addTransport(new ConsoleTransport(), {
levels: ['error', 'warn', 'info'],
transform: (log) => ({
...log,
// Dodaj kolorowanie
message: colorize(log.level, log.message)
})
});
// Plik dla trwałości
logger.addTransport(new FileTransport('app.log'), {
levels: ['error', 'warn', 'info', 'debug']
});
// CloudWatch dla produkcji
logger.addTransport(new CloudWatchTransport(), {
levels: ['error', 'warn', 'info'],
filter: (log) => process.env.NODE_ENV === 'production',
transform: (log) => ({
...log,
environment: process.env.NODE_ENV,
service: process.env.SERVICE_NAME
})
});
// Sentry dla błędów
logger.addTransport(new SentryTransport(), {
levels: ['error'],
transform: (log) => ({
...log,
fingerprint: generateFingerprint(log)
})
});

Poziomy logów

  • ERROR: Awarie systemu wymagające natychmiastowej uwagi
  • WARN: Potencjalne problemy lub obniżona funkcjonalność
  • INFO: Znaczące wydarzenia biznesowe
  • DEBUG: Szczegółowe informacje diagnostyczne

Co logować

  • Podsumowania żądań/odpowiedzi
  • Zmiany stanu
  • Wywołania zewnętrznych API
  • Metryki wydajności
  • Wydarzenia bezpieczeństwa

Czego NIE logować

  • Hasła lub sekrety
  • Numery kart kredytowych
  • Informacje o zdrowiu osobistym
  • Duże dane binarne
  • Operacje wysokiej częstotliwości

Wskazówki wydajności

  • Używaj asynchronicznego logowania
  • Wdrażaj próbkowanie logów
  • Grupuj zapisy logów
  • Ustaw odpowiednie poziomy logów
  • Monitoruj narzut logowania

Używaj konsystentnych, przeszukiwalnych komunikatów logów:

// Wzorce komunikatów logów zalecane przez AI
const LogMessages = {
// Cykl życia operacji
OPERATION_START: (op) => `Rozpoczynanie ${op}`,
OPERATION_SUCCESS: (op) => `Zakończono ${op}`,
OPERATION_FAILURE: (op) => `Niepowodzenie ${op}`,
OPERATION_RETRY: (op, attempt) => `Ponowna próba ${op} (próba ${attempt})`,
// Wzorce API
API_REQUEST: (method, path) => `${method} ${path}`,
API_RESPONSE: (method, path, status) => `${method} ${path} ${status}`,
API_ERROR: (method, path, error) => `${method} ${path} niepowodzenie: ${error}`,
// Wzorce bazy danych
DB_QUERY: (operation, table) => `Baza danych ${operation} na ${table}`,
DB_CONNECTION: (action) => `Połączenie bazy danych ${action}`,
DB_TRANSACTION: (action) => `Transakcja ${action}`,
// Uwierzytelnianie
AUTH_LOGIN: (method) => `Logowanie użytkownika przez ${method}`,
AUTH_LOGOUT: () => 'Wylogowanie użytkownika',
AUTH_FAILURE: (reason) => `Niepowodzenie uwierzytelniania: ${reason}`,
// Wydajność
SLOW_OPERATION: (op, duration) => `Wykryto wolną operację: ${op} zajęło ${duration}ms`,
MEMORY_WARNING: (usage) => `Wysokie użycie pamięci: ${usage}MB`,
// Wydarzenia biznesowe
ORDER_PLACED: () => 'Zamówienie złożone',
PAYMENT_PROCESSED: () => 'Płatność przetworzona',
USER_REGISTERED: () => 'Nowy użytkownik zarejestrowany'
};
// Użycie
logger.info(LogMessages.OPERATION_START('payment_processing'), {
paymentId: payment.id,
amount: payment.amount
});
// Przykład testu Jest
describe('Testy logowania', () => {
let logger;
let mockTransport;
beforeEach(() => {
mockTransport = {
logs: [],
log: jest.fn((level, entry) => {
mockTransport.logs.push({ level, ...entry });
})
};
logger = new Logger();
logger.addTransport(mockTransport);
});
test('powinien logować strukturowane dane', () => {
logger.info('Akcja użytkownika', {
userId: '123',
action: 'login'
});
expect(mockTransport.logs).toHaveLength(1);
expect(mockTransport.logs[0]).toMatchObject({
level: 'info',
message: 'Akcja użytkownika',
userId: '123',
action: 'login'
});
});
test('powinien sanityzować wrażliwe dane', () => {
logger.info('Aktualizacja użytkownika', {
userId: '123',
password: 'secret123',
creditCard: '4111111111111111'
});
expect(mockTransport.logs[0].password).toBe('[REDACTED]');
expect(mockTransport.logs[0].creditCard).toBe('[REDACTED]');
});
test('powinien respektować reguły próbkowania', () => {
const samplingLogger = new SamplingLogger(logger, {
defaultSampleRate: 0
});
samplingLogger.info('Powinien zostać porzucony');
expect(mockTransport.logs).toHaveLength(0);
samplingLogger.error('Powinien zostać zalogowany');
expect(mockTransport.logs).toHaveLength(1);
});
});

Opanuj logowanie z: