Strukturowane dane
Logi sformatowane w JSON z konsystentnymi polami
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 AIimport 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 danymilogger.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"}
import structlogimport loggingfrom datetime import datetime
# Konfiguruj strukturowane logowaniestructlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer() ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True,)
# Stwórz logger z kontekstemlogger = structlog.get_logger()logger = logger.bind( service="api-gateway", environment="production", region="us-east-1")
# Loguj ze strukturowanymi danymilogger.info( "user_login_successful", user_id=user.id, email=user.email, ip_address=request.remote_addr, login_method="oauth", provider="github", duration_ms=152)
Ś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 middlewareconst 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 trasachapp.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ściclass 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życieconst perfLogger = new PerformanceLogger(logger);
// Ręczny pomiar czasuconst 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 czasuconst 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 OpenTelemetryimport { 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życieconst 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 }); } }}
// Konfiguracjaconst 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 wielotransportconst logger = new MultiTransportLogger();
// Konsola dla rozwojulogger.addTransport(new ConsoleTransport(), { levels: ['error', 'warn', 'info'], transform: (log) => ({ ...log, // Dodaj kolorowanie message: colorize(log.level, log.message) })});
// Plik dla trwałościlogger.addTransport(new FileTransport('app.log'), { levels: ['error', 'warn', 'info', 'debug']});
// CloudWatch dla produkcjilogger.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ówlogger.addTransport(new SentryTransport(), { levels: ['error'], transform: (log) => ({ ...log, fingerprint: generateFingerprint(log) })});
Poziomy logów
Co logować
Czego NIE logować
Wskazówki wydajności
Używaj konsystentnych, przeszukiwalnych komunikatów logów:
// Wzorce komunikatów logów zalecane przez AIconst 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życielogger.info(LogMessages.OPERATION_START('payment_processing'), { paymentId: payment.id, amount: payment.amount});
// Przykład testu Jestdescribe('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: