Logic in Code,
Freedom in Travel.

인생 뭐 있나 사람 사는거 다 똑같지

DevOps/모니터링

[모니터링] - Node.js로 그라파나에서 DB 데이터 로그 보기

귀찮은 개발자 2024. 2. 16. 02:14 계산 중...
목차 (Table of Contents)

Node.js 백엔드 API를 통해 데이터베이스의 데이터나 로그를 그라파나에서 시각화하는 방법을 다뤄보았다. 이 목표를 위해 Prometheus 메트릭 형식으로 데이터를 노출하고 그라파나 대시보드로 모니터링하는 전체 과정을 다뤄보았다.

아키텍처

1. 프로젝트 초기 설정

1.1 프로젝트 생성

mkdir nodejs-grafana-db-logger
cd nodejs-grafana-db-logger
npm init -y

1.2 필수 패키지 설치

# 기본 패키지
npm install express prom-client

# 데이터베이스 드라이버
npm install mysql2 pg  # MySQL, PostgreSQL

# 로깅
npm install winston winston-loki

# 환경 변수
npm install dotenv

# 개발 도구
npm install --save-dev nodemon

1.3 프로젝트 구조

nodejs-grafana-db-logger/
├── src/
│   ├── config/
│   │   ├── database.js
│   │   └── logger.js
│   ├── metrics/
│   │   ├── dbMetrics.js
│   │   └── appMetrics.js
│   ├── routes/
│   │   ├── metrics.js
│   │   └── api.js
│   ├── services/
│   │   └── dbService.js
│   └── app.js
├── docker-compose.yml
├── .env
└── package.json

2. 데이터베이스 연결 설정

2.1 환경 변수 (.env)

# 서버 설정
PORT=3000
NODE_ENV=production

# MySQL 설정
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=password
MYSQL_DATABASE=myapp

# PostgreSQL 설정
PG_HOST=localhost
PG_PORT=5432
PG_USER=postgres
PG_PASSWORD=password
PG_DATABASE=myapp

# Loki 설정
LOKI_HOST=http://localhost:3100

2.2 데이터베이스 연결 (MySQL)

src/config/database.js

const mysql = require('mysql2/promise');
require('dotenv').config();

const pool = mysql.createPool({
  host: process.env.MYSQL_HOST,
  port: process.env.MYSQL_PORT,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
  database: process.env.MYSQL_DATABASE,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
  enableKeepAlive: true,
  keepAliveInitialDelay: 0
});

// 연결 테스트
pool.getConnection()
  .then(connection => {
    console.log('MySQL connected successfully');
    connection.release();
  })
  .catch(err => {
    console.error('MySQL connection failed:', err.message);
  });

module.exports = pool;

2.3 PostgreSQL 연결

const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.PG_HOST,
  port: process.env.PG_PORT,
  user: process.env.PG_USER,
  password: process.env.PG_PASSWORD,
  database: process.env.PG_DATABASE,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

pool.on('connect', () => {
  console.log('PostgreSQL connected');
});

pool.on('error', (err) => {
  console.error('PostgreSQL error:', err);
});

module.exports = pool;

3. Winston 로거 설정

src/config/logger.js

const winston = require('winston');
const LokiTransport = require('winston-loki');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { 
    service: 'nodejs-db-logger',
    environment: process.env.NODE_ENV 
  },
  transports: [
    // 콘솔 출력
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),

    // 파일 저장
    new winston.transports.File({ 
      filename: 'logs/error.log', 
      level: 'error' 
    }),
    new winston.transports.File({ 
      filename: 'logs/combined.log' 
    }),

    // Loki 전송
    new LokiTransport({
      host: process.env.LOKI_HOST,
      labels: { 
        app: 'nodejs-db-logger',
        env: process.env.NODE_ENV 
      },
      json: true,
      format: winston.format.json(),
      replaceTimestamp: true,
      onConnectionError: (err) => console.error('Loki connection error:', err)
    })
  ]
});

module.exports = logger;

4. Prometheus 메트릭 설정

4.1 데이터베이스 메트릭

src/metrics/dbMetrics.js

const client = require('prom-client');
const logger = require('../config/logger');

// 기본 메트릭 수집
const register = new client.Registry();
client.collectDefaultMetrics({ register });

// DB 쿼리 실행 시간
const dbQueryDuration = new client.Histogram({
  name: 'db_query_duration_seconds',
  help: 'Duration of database queries in seconds',
  labelNames: ['query_type', 'table', 'status'],
  buckets: [0.001, 0.01, 0.1, 0.5, 1, 2, 5]
});

// DB 연결 풀 상태
const dbConnectionPool = new client.Gauge({
  name: 'db_connection_pool',
  help: 'Database connection pool status',
  labelNames: ['state']  // active, idle, total
});

// DB 쿼리 카운터
const dbQueryCounter = new client.Counter({
  name: 'db_query_total',
  help: 'Total number of database queries',
  labelNames: ['query_type', 'table', 'status']
});

// DB 에러 카운터
const dbErrorCounter = new client.Counter({
  name: 'db_error_total',
  help: 'Total number of database errors',
  labelNames: ['error_type', 'table']
});

// DB 레코드 수
const dbRecordCount = new client.Gauge({
  name: 'db_record_count',
  help: 'Number of records in database tables',
  labelNames: ['table']
});

// DB 테이블 크기
const dbTableSize = new client.Gauge({
  name: 'db_table_size_bytes',
  help: 'Size of database tables in bytes',
  labelNames: ['table']
});

// 메트릭 등록
register.registerMetric(dbQueryDuration);
register.registerMetric(dbConnectionPool);
register.registerMetric(dbQueryCounter);
register.registerMetric(dbErrorCounter);
register.registerMetric(dbRecordCount);
register.registerMetric(dbTableSize);

module.exports = {
  register,
  dbQueryDuration,
  dbConnectionPool,
  dbQueryCounter,
  dbErrorCounter,
  dbRecordCount,
  dbTableSize
};

4.2 애플리케이션 메트릭

src/metrics/appMetrics.js

const client = require('prom-client');
const { register } = require('./dbMetrics');

// HTTP 요청 카운터
const httpRequestCounter = new client.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status_code']
});

// HTTP 요청 시간
const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5]
});

// 활성 사용자 수
const activeUsers = new client.Gauge({
  name: 'active_users',
  help: 'Number of active users'
});

// 비즈니스 메트릭 예시
const orderTotal = new client.Counter({
  name: 'orders_total',
  help: 'Total number of orders',
  labelNames: ['status']
});

const revenueTotal = new client.Counter({
  name: 'revenue_total',
  help: 'Total revenue amount',
  labelNames: ['currency']
});

register.registerMetric(httpRequestCounter);
register.registerMetric(httpRequestDuration);
register.registerMetric(activeUsers);
register.registerMetric(orderTotal);
register.registerMetric(revenueTotal);

module.exports = {
  httpRequestCounter,
  httpRequestDuration,
  activeUsers,
  orderTotal,
  revenueTotal
};

5. DB 서비스 구현

src/services/dbService.js

const db = require('../config/database');
const logger = require('../config/logger');
const { 
  dbQueryDuration, 
  dbQueryCounter, 
  dbErrorCounter,
  dbRecordCount,
  dbTableSize,
  dbConnectionPool 
} = require('../metrics/dbMetrics');

class DBService {
  // 쿼리 실행 with 메트릭
  async executeQuery(query, params = [], options = {}) {
    const { queryType = 'SELECT', table = 'unknown' } = options;
    const end = dbQueryDuration.startTimer({ query_type: queryType, table });

    try {
      logger.info('Executing query', { query, params, queryType, table });

      const [rows] = await db.execute(query, params);

      // 성공 메트릭
      dbQueryCounter.inc({ query_type: queryType, table, status: 'success' });
      end({ status: 'success' });

      logger.info('Query executed successfully', { 
        rowCount: rows.length,
        queryType,
        table 
      });

      return rows;
    } catch (error) {
      // 에러 메트릭
      dbQueryCounter.inc({ query_type: queryType, table, status: 'error' });
      dbErrorCounter.inc({ error_type: error.code || 'unknown', table });
      end({ status: 'error' });

      logger.error('Query execution failed', { 
        error: error.message, 
        query, 
        queryType,
        table 
      });

      throw error;
    }
  }

  // 테이블 레코드 수 조회
  async getTableRecordCount(tableName) {
    try {
      const query = `SELECT COUNT(*) as count FROM ${tableName}`;
      const rows = await this.executeQuery(query, [], { 
        queryType: 'COUNT', 
        table: tableName 
      });

      const count = rows[0].count;
      dbRecordCount.set({ table: tableName }, count);

      return count;
    } catch (error) {
      logger.error('Failed to get record count', { 
        table: tableName, 
        error: error.message 
      });
      return 0;
    }
  }

  // 테이블 크기 조회 (MySQL)
  async getTableSize(tableName) {
    try {
      const query = `
        SELECT 
          data_length + index_length as size_bytes
        FROM information_schema.TABLES 
        WHERE table_schema = DATABASE() 
        AND table_name = ?
      `;

      const rows = await this.executeQuery(query, [tableName], { 
        queryType: 'SELECT', 
        table: 'information_schema' 
      });

      const sizeBytes = rows[0]?.size_bytes || 0;
      dbTableSize.set({ table: tableName }, sizeBytes);

      return sizeBytes;
    } catch (error) {
      logger.error('Failed to get table size', { 
        table: tableName, 
        error: error.message 
      });
      return 0;
    }
  }

  // 연결 풀 상태 업데이트
  async updateConnectionPoolMetrics() {
    try {
      const pool = db.pool;

      dbConnectionPool.set({ state: 'total' }, pool.config.connectionLimit);
      dbConnectionPool.set({ state: 'active' }, pool._allConnections.length);
      dbConnectionPool.set({ state: 'idle' }, pool._freeConnections.length);

      logger.debug('Connection pool metrics updated', {
        total: pool.config.connectionLimit,
        active: pool._allConnections.length,
        idle: pool._freeConnections.length
      });
    } catch (error) {
      logger.error('Failed to update connection pool metrics', { 
        error: error.message 
      });
    }
  }

  // 모든 테이블 메트릭 수집
  async collectAllTableMetrics() {
    try {
      const [tables] = await db.query('SHOW TABLES');

      for (const tableObj of tables) {
        const tableName = Object.values(tableObj)[0];

        await this.getTableRecordCount(tableName);
        await this.getTableSize(tableName);
      }

      logger.info('All table metrics collected');
    } catch (error) {
      logger.error('Failed to collect table metrics', { 
        error: error.message 
      });
    }
  }

  // 슬로우 쿼리 로깅
  async getSlowQueries() {
    try {
      const query = `
        SELECT 
          query_time,
          lock_time,
          rows_sent,
          rows_examined,
          sql_text
        FROM mysql.slow_log 
        ORDER BY query_time DESC 
        LIMIT 10
      `;

      const rows = await this.executeQuery(query, [], { 
        queryType: 'SELECT', 
        table: 'mysql.slow_log' 
      });

      rows.forEach(row => {
        logger.warn('Slow query detected', {
          query_time: row.query_time,
          rows_examined: row.rows_examined,
          sql: row.sql_text.substring(0, 200)
        });
      });

      return rows;
    } catch (error) {
      logger.error('Failed to get slow queries', { 
        error: error.message 
      });
      return [];
    }
  }
}

module.exports = new DBService();

6. API 라우트 구현

6.1 메트릭 엔드포인트

src/routes/metrics.js

const express = require('express');
const router = express.Router();
const { register } = require('../metrics/dbMetrics');
const dbService = require('../services/dbService');

// Prometheus 메트릭 엔드포인트
router.get('/metrics', async (req, res) => {
  try {
    // 최신 메트릭 수집
    await dbService.updateConnectionPoolMetrics();
    await dbService.collectAllTableMetrics();

    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

module.exports = router;

6.2 애플리케이션 API

src/routes/api.js

const express = require('express');
const router = express.Router();
const dbService = require('../services/dbService');
const logger = require('../config/logger');
const { orderTotal, revenueTotal } = require('../metrics/appMetrics');

// 사용자 조회
router.get('/users', async (req, res) => {
  try {
    const users = await dbService.executeQuery(
      'SELECT * FROM users LIMIT 100',
      [],
      { queryType: 'SELECT', table: 'users' }
    );

    logger.info('Users fetched', { count: users.length });
    res.json({ success: true, data: users });
  } catch (error) {
    logger.error('Failed to fetch users', { error: error.message });
    res.status(500).json({ success: false, error: error.message });
  }
});

// 주문 생성 (비즈니스 메트릭)
router.post('/orders', async (req, res) => {
  try {
    const { user_id, amount, currency = 'USD' } = req.body;

    const result = await dbService.executeQuery(
      'INSERT INTO orders (user_id, amount, currency, status) VALUES (?, ?, ?, ?)',
      [user_id, amount, currency, 'pending'],
      { queryType: 'INSERT', table: 'orders' }
    );

    // 비즈니스 메트릭 업데이트
    orderTotal.inc({ status: 'pending' });
    revenueTotal.inc({ currency }, amount);

    logger.info('Order created', { 
      orderId: result.insertId, 
      userId: user_id, 
      amount 
    });

    res.json({ 
      success: true, 
      orderId: result.insertId 
    });
  } catch (error) {
    logger.error('Failed to create order', { error: error.message });
    res.status(500).json({ success: false, error: error.message });
  }
});

// 로그 조회 API
router.get('/logs', async (req, res) => {
  try {
    const { level = 'info', limit = 100 } = req.query;

    const logs = await dbService.executeQuery(
      'SELECT * FROM application_logs WHERE level = ? ORDER BY timestamp DESC LIMIT ?',
      [level, parseInt(limit)],
      { queryType: 'SELECT', table: 'application_logs' }
    );

    res.json({ success: true, data: logs });
  } catch (error) {
    logger.error('Failed to fetch logs', { error: error.message });
    res.status(500).json({ success: false, error: error.message });
  }
});

module.exports = router;

7. Express 애플리케이션 설정

src/app.js

const express = require('express');
const logger = require('./config/logger');
const metricsRouter = require('./routes/metrics');
const apiRouter = require('./routes/api');
const { httpRequestCounter, httpRequestDuration } = require('./metrics/appMetrics');

const app = express();
const PORT = process.env.PORT || 3000;

// 미들웨어
app.use(express.json());

// HTTP 메트릭 미들웨어
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    const route = req.route ? req.route.path : req.path;

    httpRequestCounter.inc({
      method: req.method,
      route: route,
      status_code: res.statusCode
    });

    httpRequestDuration.observe({
      method: req.method,
      route: route,
      status_code: res.statusCode
    }, duration);

    logger.info('HTTP Request', {
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration: `${duration}s`
    });
  });

  next();
});

// 라우트
app.use('/metrics', metricsRouter);
app.use('/api', apiRouter);

// 헬스 체크
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// 에러 핸들러
app.use((err, req, res, next) => {
  logger.error('Unhandled error', { 
    error: err.message, 
    stack: err.stack 
  });

  res.status(500).json({ 
    error: 'Internal server error',
    message: err.message 
  });
});

// 서버 시작
app.listen(PORT, () => {
  logger.info(`Server running on port ${PORT}`);
  console.log(`Server: http://localhost:${PORT}`);
  console.log(`Metrics: http://localhost:${PORT}/metrics`);
});

module.exports = app;

8. Docker Compose 설정

docker-compose.yml

version: '3.8'

services:
  # Node.js 애플리케이션
  nodejs-app:
    build: .
    container_name: nodejs-app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - MYSQL_HOST=mysql
      - MYSQL_DATABASE=myapp
    depends_on:
      - mysql
      - prometheus
      - loki
    restart: unless-stopped

  # MySQL
  mysql:
    image: mysql:8.0
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: myapp
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    restart: unless-stopped

  # Prometheus
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    restart: unless-stopped

  # Grafana
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    depends_on:
      - prometheus
      - loki
    restart: unless-stopped

  # Loki (로그 수집)
  loki:
    image: grafana/loki:latest
    container_name: loki
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yml:/etc/loki/local-config.yaml
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped

volumes:
  mysql-data:
  prometheus-data:
  grafana-data:
  loki-data:

9. Prometheus 설정

prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'nodejs-app'
    static_configs:
      - targets: ['nodejs-app:3000']
    metrics_path: /metrics
    scrape_interval: 10s

10. Grafana 대시보드 쿼리

10.1 데이터베이스 메트릭 쿼리

# 쿼리 실행 속도 (평균)
rate(db_query_duration_seconds_sum[5m]) / rate(db_query_duration_seconds_count[5m])

# 쿼리 실행 속도 (95 percentile)
histogram_quantile(0.95, rate(db_query_duration_seconds_bucket[5m]))

# 테이블별 쿼리 수
sum(rate(db_query_total[5m])) by (table)

# DB 에러율
rate(db_error_total[5m])

# 연결 풀 사용률
db_connection_pool{state="active"} / db_connection_pool{state="total"} * 100

# 테이블 레코드 수
db_record_count

# 테이블 크기 (MB)
db_table_size_bytes / 1024 / 1024

# 쿼리 성공률
sum(rate(db_query_total{status="success"}[5m])) / sum(rate(db_query_total[5m])) * 100

10.2 애플리케이션 메트릭 쿼리

# HTTP 요청 수
sum(rate(http_requests_total[5m])) by (method, route)

# HTTP 응답 시간 (평균)
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])

# HTTP 에러율
sum(rate(http_requests_total{status_code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) * 100

# 주문 생성 속도
rate(orders_total[5m])

# 매출 추이
rate(revenue_total[1h])

10.3 Loki 로그 쿼리

# 에러 로그 조회
{app="nodejs-db-logger"} |= "error"

# 특정 테이블 쿼리 로그
{app="nodejs-db-logger"} | json | table="users"

# 슬로우 쿼리 감지
{app="nodejs-db-logger"} | json | duration > 1s

# HTTP 5xx 에러
{app="nodejs-db-logger"} | json | status_code =~ "5.."

# 로그 레벨별 카운트
sum(count_over_time({app="nodejs-db-logger"} [5m])) by (level)

11. 샘플 데이터베이스 스키마

init.sql

CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  amount DECIMAL(10, 2) NOT NULL,
  currency VARCHAR(3) DEFAULT 'USD',
  status ENUM('pending', 'completed', 'failed') DEFAULT 'pending',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE IF NOT EXISTS application_logs (
  id INT AUTO_INCREMENT PRIMARY KEY,
  level ENUM('error', 'warn', 'info', 'debug') NOT NULL,
  message TEXT NOT NULL,
  metadata JSON,
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_level (level),
  INDEX idx_timestamp (timestamp)
);

-- 샘플 데이터 추가
INSERT INTO users (username, email) VALUES
  ('john_doe', 'john@example.com'),
  ('jane_smith', 'jane@example.com'),
  ('bob_wilson', 'bob@example.com');

INSERT INTO orders (user_id, amount, status) VALUES
  (1, 99.99, 'completed'),
  (2, 149.50, 'pending'),
  (3, 79.99, 'completed');

12. Grafana 대시보드 JSON

{
  "dashboard": {
    "title": "Node.js DB Monitoring",
    "panels": [
      {
        "title": "Database Query Duration (p95)",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(db_query_duration_seconds_bucket[5m]))",
            "legendFormat": "{{table}} - {{query_type}}"
          }
        ],
        "type": "graph"
      },
      {
        "title": "Query Rate by Table",
        "targets": [
          {
            "expr": "sum(rate(db_query_total[5m])) by (table)",
            "legendFormat": "{{table}}"
          }
        ],
        "type": "graph"
      },
      {
        "title": "Connection Pool Usage",
        "targets": [
          {
            "expr": "db_connection_pool{state=\"active\"} / db_connection_pool{state=\"total\"} * 100",
            "legendFormat": "Pool Usage %"
          }
        ],
        "type": "gauge"
      },
      {
        "title": "Table Record Counts",
        "targets": [
          {
            "expr": "db_record_count",
            "legendFormat": "{{table}}"
          }
        ],
        "type": "stat"
      },
      {
        "title": "Error Logs (Last 1h)",
        "targets": [
          {
            "expr": "{app=\"nodejs-db-logger\"} |= \"error\"",
            "refId": "A"
          }
        ],
        "type": "logs",
        "datasource": "Loki"
      }
    ]
  }
}

13. 실행 및 테스트

13.1 애플리케이션 실행

npm run dev

npm start

docker-compose up -d

13.2 메트릭 확인

# 메트릭 엔드포인트 호출
curl http://localhost:3000/metrics

# API 테스트
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/orders \
  -H "Content-Type: application/json" \
  -d '{"user_id": 1, "amount": 99.99}'

13.3 접속 URL

14. 모범 사례

14.1 메트릭 네이밍

// good
db_query_duration_seconds
http_requests_total
db_connection_pool_active

// bad
queryTime
requests
connections

14.2 로깅 구조화

logger.info('User action', {
  userId: user.id,
  action: 'purchase',
  amount: 99.99,
  timestamp: new Date().toISOString()
});

14.3 에러 처리

try {
  await dbService.executeQuery(query, params);
} catch (error) {
  logger.error('Query failed', {
    error: error.message,
    stack: error.stack,
    query: query.substring(0, 100)
  });
  throw error;
}

마치며

Node.js 로 데이터베이스 로그를 그라파나에서 시각화하는 전체 파이프라인을 구축해보았으며 이로 인해 이러한 장점이 있다.

  • 실시간 DB 성능 모니터링
  • 슬로우 쿼리 감지 및 최적화
  • 애플리케이션 로그 중앙 관리
  • 비즈니스 메트릭 추적

운영 환경에서는 보안(API 키, DB 암호화), 성능(쿼리 최적화), 확장성(샤딩, 읽기 복제본)을 추가로 고려해야 할 것 같다.

참고 자료