Node.js Uygulamalarında Loglama: Winston ve Morgan Kurulumu ile Yapılandırması

Production ortamında çalışan bir Node.js uygulamasında bir şeyler ters gittiğinde, elinizdeki tek arkadaşınız loglardır. “Uygulama neden çöktü?”, “O hata ne zamandan beri var?”, “Hangi kullanıcı hangi isteği attı?” gibi soruların cevabını ancak düzgün yapılandırılmış bir loglama sistemiyle bulabilirsiniz. console.log ile başladığınız macera, production’a geçince sizi yarı yolda bırakır. Bu yazıda Node.js ekosisteminin en popüler loglama araçları olan Winston ve Morgan‘ı gerçek dünya senaryolarıyla nasıl kullanacağınızı ele alacağız.

Neden console.log Yetmez?

Geliştirme ortamında console.log kullanmak oldukça masum görünür. Ancak production’da şu sorunlarla karşılaşırsınız:

  • Log seviyeleri yoktur: Hata mı, uyarı mı, bilgi mi? Hepsi aynı çıktıda kaybolur.
  • Dosyaya yazma desteği yoktur: Uygulama yeniden başladığında tüm loglar uçar.
  • Log rotasyonu yapılamaz: Tek bir dosya gigabaytlarca büyüyebilir.
  • Yapılandırılmış loglama yoktur: JSON formatında log üretmek için ekstra çaba gerekir.
  • HTTP isteklerini otomatik loglamaz: Her endpoint için manuel log yazmak gerekir.

Winston bu sorunların büyük çoğunluğunu çözerken Morgan, HTTP istek loglamasını otomatize eder. İkisini birlikte kullanmak güçlü bir loglama altyapısı oluşturur.

Winston Nedir, Morgan Nedir?

Winston, Node.js için esnek ve çok katmanlı bir loglama kütüphanesidir. Transport (taşıyıcı) tabanlı mimarisi sayesinde aynı anda hem dosyaya hem konsola hem de uzak bir servise log yazabilirsiniz.

Morgan, Express.js için geliştirilmiş HTTP istek loglama middleware’idir. Her gelen isteği otomatik olarak yakalar, yanıt süresini, status kodunu ve diğer bilgileri loglar.

Kurulum

Önce yeni bir Node.js projesi oluşturalım veya mevcut projenize geçelim:

mkdir nodejs-logging-demo
cd nodejs-logging-demo
npm init -y
npm install express winston morgan winston-daily-rotate-file

Paket açıklamaları:

  • express: Web framework
  • winston: Ana loglama kütüphanesi
  • morgan: HTTP istek logger middleware
  • winston-daily-rotate-file: Günlük log rotasyonu için ek transport

Proje yapımızı şöyle düzenleyelim:

mkdir -p src/config src/middleware src/routes logs
touch src/config/logger.js src/middleware/httpLogger.js src/app.js index.js

Winston Logger Yapılandırması

Winston’ın kalbinde “transport” kavramı yatar. Her transport, logların nereye yazılacağını tanımlar. Şimdi kapsamlı bir logger konfigürasyonu yazalım:

// src/config/logger.js
const { createLogger, format, transports } = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const path = require('path');

const { combine, timestamp, errors, json, colorize, printf, splat } = format;

// Ortam değişkeni yoksa development kabul et
const NODE_ENV = process.env.NODE_ENV || 'development';
const LOG_LEVEL = process.env.LOG_LEVEL || (NODE_ENV === 'production' ? 'warn' : 'debug');
const LOG_DIR = process.env.LOG_DIR || path.join(process.cwd(), 'logs');

// Konsol için okunabilir format
const consoleFormat = printf(({ level, message, timestamp, stack, ...meta }) => {
  let log = `${timestamp} [${level}]: ${message}`;

  // Hata stack trace varsa ekle
  if (stack) {
    log += `n${stack}`;
  }

  // Ekstra metadata varsa ekle
  const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '';
  if (metaStr) {
    log += `n${metaStr}`;
  }

  return log;
});

// Günlük dönen error log dosyası için transport
const errorFileTransport = new DailyRotateFile({
  filename: path.join(LOG_DIR, 'error-%DATE%.log'),
  datePattern: 'YYYY-MM-DD',
  level: 'error',
  maxFiles: '30d',    // 30 gün sakla
  maxSize: '20m',     // Maksimum 20MB
  zippedArchive: true // Eski logları sıkıştır
});

// Tüm loglar için dönen dosya transport
const combinedFileTransport = new DailyRotateFile({
  filename: path.join(LOG_DIR, 'combined-%DATE%.log'),
  datePattern: 'YYYY-MM-DD',
  maxFiles: '14d',
  maxSize: '50m',
  zippedArchive: true
});

const logger = createLogger({
  level: LOG_LEVEL,
  format: combine(
    timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
    errors({ stack: true }), // Error nesnelerinde stack trace göster
    splat(),                 // String interpolation desteği
    json()                   // Dosya logları için JSON format
  ),
  transports: [
    errorFileTransport,
    combinedFileTransport
  ],
  exitOnError: false // Uncaught exception'da process'i öldürme
});

// Sadece development ve test ortamlarında konsola da yaz
if (NODE_ENV !== 'production') {
  logger.add(new transports.Console({
    format: combine(
      colorize({ all: true }),
      timestamp({ format: 'HH:mm:ss' }),
      errors({ stack: true }),
      consoleFormat
    )
  }));
}

// Production'da da konsola yaz ama JSON formatında (container logları için)
if (NODE_ENV === 'production') {
  logger.add(new transports.Console({
    format: combine(
      timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
      errors({ stack: true }),
      json()
    )
  }));
}

module.exports = logger;

Bu yapılandırmayla dikkat etmeniz gereken birkaç nokta var:

  • LOG_LEVEL environment variable ile dinamik olarak log seviyesi ayarlanabiliyor
  • DailyRotateFile transport, logları günlük döndürüyor ve sıkıştırıyor
  • Production ortamında JSON çıktı kullanıyoruz, çünkü Elasticsearch veya Loki gibi sistemler JSON’u çok daha kolay parse eder

Log Seviyeleri

Winston’ın varsayılan log seviyeleri şunlardır (önem sırasına göre):

  • error: Kritik hatalar, uygulamanın çalışmasını engelleyen durumlar
  • warn: Uyarılar, potansiyel problemler ama uygulama çalışmaya devam ediyor
  • info: Genel bilgi mesajları, önemli olaylar
  • http: HTTP istekleri (Morgan entegrasyonunda kullanacağız)
  • verbose: Detaylı bilgi, debug’dan az ayrıntılı
  • debug: Geliştirme sürecinde hata ayıklama bilgileri
  • silly: En detaylı seviye, nadiren kullanılır

Seviye hiyerarşisi şöyle çalışır: error seviyesinde çalışan bir logger, yalnızca error loglarını yazar. info seviyesinde çalışan bir logger ise error, warn ve info loglarını yazar.

Morgan ile HTTP İstek Loglama

Morgan’ı Winston ile entegre etmek için bir stream oluşturmamız gerekiyor:

// src/middleware/httpLogger.js
const morgan = require('morgan');
const logger = require('../config/logger');

// Morgan'ın çıktısını Winston'a yönlendirmek için stream tanımla
const stream = {
  write: (message) => {
    // Morgan mesajın sonuna n ekler, trim ile temizle
    logger.http(message.trim());
  }
};

// Production'da 'combined' formatı, development'ta 'dev' formatı
const morganFormat = process.env.NODE_ENV === 'production'
  ? 'combined'
  : 'dev';

// Sağlık kontrolü endpoint'lerini ve statik dosyaları loglamayı atla
const skipRoutes = (req, res) => {
  const skipPaths = ['/health', '/ping', '/favicon.ico'];
  return skipPaths.includes(req.path);
};

const httpLogger = morgan(morganFormat, {
  stream,
  skip: skipRoutes
});

module.exports = httpLogger;

Özel Morgan format kullanmak istiyorsanız şöyle bir şey yazabilirsiniz:

// src/middleware/httpLogger.js - Özel format örneği
const customFormat = ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" - :response-time ms';

// Ya da JSON formatında log için token tanımlama
morgan.token('body', (req) => {
  // Hassas alanları loglamaktan kaçın
  const sensitiveFields = ['password', 'token', 'secret', 'authorization'];
  if (req.body && Object.keys(req.body).length > 0) {
    const sanitized = { ...req.body };
    sensitiveFields.forEach(field => {
      if (sanitized[field]) sanitized[field] = '[REDACTED]';
    });
    return JSON.stringify(sanitized);
  }
  return '';
});

morgan.token('user-id', (req) => {
  return req.user ? req.user.id : 'anonymous';
});

const jsonFormat = JSON.stringify({
  method: ':method',
  url: ':url',
  status: ':status',
  responseTime: ':response-time ms',
  contentLength: ':res[content-length]',
  userId: ':user-id'
});

Express Uygulamasına Entegrasyon

// src/app.js
const express = require('express');
const logger = require('./config/logger');
const httpLogger = require('./middleware/httpLogger');

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// HTTP loglama middleware'ini en başa ekle
app.use(httpLogger);

// Örnek route'lar
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.get('/users/:id', async (req, res) => {
  const { id } = req.params;

  logger.info('Kullanıcı bilgisi istendi', { userId: id, requestId: req.headers['x-request-id'] });

  try {
    // Simüle edilmiş veritabanı işlemi
    if (id === '999') {
      throw new Error(`Kullanıcı bulunamadı: ${id}`);
    }

    const user = { id, name: 'Test Kullanıcı', email: '[email protected]' };
    logger.debug('Kullanıcı veritabanından getirildi', { userId: id });

    res.json(user);
  } catch (error) {
    logger.error('Kullanıcı getirme hatası', {
      error: error.message,
      userId: id,
      stack: error.stack
    });
    res.status(404).json({ error: error.message });
  }
});

// Global hata yakalama middleware'i
app.use((err, req, res, next) => {
  logger.error('İşlenmeyen uygulama hatası', {
    error: err.message,
    stack: err.stack,
    method: req.method,
    url: req.url,
    ip: req.ip
  });

  res.status(500).json({
    error: 'Sunucu hatası oluştu',
    requestId: req.headers['x-request-id'] || 'unknown'
  });
});

module.exports = app;
// index.js
const app = require('./src/app');
const logger = require('./src/config/logger');

const PORT = process.env.PORT || 3000;

const server = app.listen(PORT, () => {
  logger.info(`Uygulama başlatıldı`, {
    port: PORT,
    environment: process.env.NODE_ENV || 'development',
    nodeVersion: process.version,
    pid: process.pid
  });
});

// Beklenmedik hataları yakala ve logla
process.on('uncaughtException', (error) => {
  logger.error('Yakalanmamış istisna', {
    error: error.message,
    stack: error.stack
  });
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error('İşlenmeyen Promise reddi', {
    reason: reason?.message || reason,
    promise: promise.toString()
  });
});

// Graceful shutdown
process.on('SIGTERM', () => {
  logger.info('SIGTERM sinyali alındı, uygulama kapatılıyor...');
  server.close(() => {
    logger.info('HTTP sunucusu kapatıldı');
    process.exit(0);
  });
});

Request ID ile Log Takibi

Production ortamında birden fazla eşzamanlı isteğin loglarını birbirinden ayırt etmek kritik önem taşır. Her isteğe benzersiz bir ID atayarak tüm loglarda bu ID’yi kullanmak, tek bir isteğin hayatını başından sonuna kadar takip etmenizi sağlar:

// src/middleware/requestId.js
const { v4: uuidv4 } = require('uuid');

// npm install uuid komutunu çalıştırmayı unutmayın

const requestIdMiddleware = (req, res, next) => {
  const requestId = req.headers['x-request-id'] || uuidv4();

  req.requestId = requestId;
  res.setHeader('X-Request-Id', requestId);

  // Request context'ini logger'a bağla
  req.logger = req.app.get('logger') || require('../config/logger');

  // Her log çağrısına requestId'yi otomatik ekle
  req.log = {
    info: (msg, meta = {}) => req.logger.info(msg, { ...meta, requestId }),
    warn: (msg, meta = {}) => req.logger.warn(msg, { ...meta, requestId }),
    error: (msg, meta = {}) => req.logger.error(msg, { ...meta, requestId }),
    debug: (msg, meta = {}) => req.logger.debug(msg, { ...meta, requestId })
  };

  next();
};

module.exports = requestIdMiddleware;

Bu middleware’i kullandıktan sonra route handler’larında req.log.info(...) şeklinde kullanabilirsiniz. Her log otomatik olarak requestId bilgisini taşır.

Gerçek Dünya: Veritabanı İşlemlerini Loglama

Veritabanı sorgularını loglamak, performans sorunlarını tespit etmek için vazgeçilmezdir:

// src/utils/dbLogger.js
const logger = require('../config/logger');

const DB_SLOW_QUERY_THRESHOLD_MS = parseInt(process.env.DB_SLOW_QUERY_THRESHOLD_MS) || 1000;

const withQueryLogging = async (queryName, queryFn, meta = {}) => {
  const startTime = Date.now();

  logger.debug(`Sorgu başlatıldı: ${queryName}`, meta);

  try {
    const result = await queryFn();
    const duration = Date.now() - startTime;

    const logMeta = { ...meta, queryName, durationMs: duration };

    if (duration > DB_SLOW_QUERY_THRESHOLD_MS) {
      logger.warn(`Yavaş sorgu tespit edildi: ${queryName}`, logMeta);
    } else {
      logger.debug(`Sorgu tamamlandı: ${queryName}`, logMeta);
    }

    return result;
  } catch (error) {
    const duration = Date.now() - startTime;
    logger.error(`Sorgu başarısız: ${queryName}`, {
      ...meta,
      queryName,
      durationMs: duration,
      error: error.message
    });
    throw error;
  }
};

module.exports = { withQueryLogging };

Kullanımı şöyle olur:

const { withQueryLogging } = require('../utils/dbLogger');

// Route handler içinde
const user = await withQueryLogging(
  'getUserById',
  () => UserModel.findById(userId),
  { userId, requestId: req.requestId }
);

Log Dosyalarını İzlemek

Logları terminalde canlı izlemek için:

# Tüm combined logları izle
tail -f logs/combined-$(date +%Y-%m-%d).log

# Sadece error loglarını izle
tail -f logs/error-$(date +%Y-%m-%d).log

# JSON formatındaki logları okunabilir hale getir
tail -f logs/combined-$(date +%Y-%m-%d).log | jq '.'

# Sadece belirli bir requestId'ye ait logları filtrele
tail -f logs/combined-$(date +%Y-%m-%d).log | grep "abc123-request-id"

# Error seviyesindeki logları say
grep '"level":"error"' logs/combined-*.log | wc -l

Log rotasyonu sonucu sıkıştırılmış dosyaları okumak için:

# .gz dosyasını okuma
zcat logs/combined-2024-01-15.log.gz | grep "error" | tail -50

# Birden fazla arşiv dosyasında arama
zgrep "OutOfMemory" logs/combined-*.log.gz

Environment Variable ile Yapılandırma

Production, staging ve development ortamları için .env dosyası örneği:

# .env.production
NODE_ENV=production
LOG_LEVEL=warn
LOG_DIR=/var/log/myapp
DB_SLOW_QUERY_THRESHOLD_MS=500
PORT=3000

# .env.development
NODE_ENV=development
LOG_LEVEL=debug
LOG_DIR=./logs
DB_SLOW_QUERY_THRESHOLD_MS=2000
PORT=3000

dotenv paketi ile bu değişkenleri yükleyin:

npm install dotenv

index.js dosyasının en başına şunu ekleyin:

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`
});

Logları Merkezi Sisteme Göndermek

Birden fazla sunucu veya container çalıştırıyorsanız logları merkezi bir sisteme toplamak gerekir. Winston transport sistemi bunu kolaylaştırır. Örneğin Elasticsearch’e log göndermek için winston-elasticsearch paketi kullanılabilir. Loki için ise winston-loki paketi mevcuttur.

Basit bir HTTP transport örneği:

const { HttpTransportOptions } = require('winston');

// Özel log aggregator'a gönderme
if (process.env.LOG_ENDPOINT) {
  const { default: Transport } = require('winston-transport');

  class RemoteTransport extends Transport {
    log(info, callback) {
      fetch(process.env.LOG_ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(info)
      }).catch(err => {
        console.error('Log gönderme hatası:', err.message);
      });
      callback();
    }
  }

  logger.add(new RemoteTransport({ level: 'warn' }));
}

Sonuç

Winston ve Morgan kombinasyonu, Node.js uygulamanız için sağlam bir loglama altyapısı kurmak adına güçlü bir başlangıç noktasıdır. Bu yazıda anlattıklarımızı bir özetle toparlayalım:

  • Winston ile çok seviyeli, çok hedefli loglama yapılandırması oluşturduk
  • DailyRotateFile transport ile log rotasyonunu ve arşivlemeyi otomatize ettik
  • Morgan ile HTTP isteklerini otomatik olarak logladık ve Winston stream’ine yönlendirdik
  • Request ID sistemiyle birbirine bağlı logları takip etmeyi kolaylaştırdık
  • Veritabanı sorgu loglama yardımcısıyla yavaş sorguları tespit ettik
  • Environment variable tabanlı yapılandırmayla ortama göre farklı davranış sağladık

Loglama konusunda en sık yapılan hata, “zaten çalışıyor, ne gerek var” diyerek sistemi geç kurmaktır. İlk production hatasını yaşadığınızda loglarınız yoksa, problemi analiz etmek için saatlerce uğraşırsınız. Şimdi biraz zaman harcayın, ileride çok daha fazlasını kazanırsınız. Log sisteminizi kurduktan sonra düzenli olarak gözden geçirin, gereksiz logları temizleyin ve log seviyelerini ortama göre doğru ayarladığınızdan emin olun.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir