Node.js Inspector

デバッグNode.jsJavaScriptChrome DevToolsサーバーサイドリモートデバッグ

デバッグツール

Node.js Inspector

概要

Node.js Inspectorは、Node.jsのビルトインデバッガーです。Chrome DevToolsと統合され、サーバーサイドJavaScriptのデバッグが可能で、リモートデバッグにも対応しています。

詳細

Node.js Inspector は、Node.js 6.3以降に組み込まれているデバッグ機能で、Chrome DevTools Protocol を使用してChrome/Chromiumブラウザーとシームレスに統合されています。従来のnode-inspectorパッケージ(現在は非推奨)を置き換える形で標準化され、追加のインストールなしでプロフェッショナル級のデバッグ機能を提供します。

Inspector の最大の特徴は、ブラウザー上のChrome DevToolsを使用してサーバーサイドJavaScriptをデバッグできることです。これにより、フロントエンドとバックエンドで一貫したデバッグ体験が実現されます。WebSocket接続によるリアルタイム通信により、ブレークポイント設定、変数監視、ステップ実行、パフォーマンス分析、メモリプロファイリングなど、豊富な機能が利用できます。

VS Code、IntelliJ IDEA、WebStormなど主要なIDEとの統合も進んでおり、それぞれのエディター内でデバッグ機能を利用できます。特にVS CodeのNode.jsデバッグサポートは非常に優秀で、設定ファイル(launch.json)による柔軟なデバッグ設定、自動アタッチ機能、TypeScriptソースマップ対応により、モダンなNode.js開発において標準的なツールとなっています。

Docker環境、Kubernetes、AWS Lambda、Azure Functionsなどのクラウド環境でのデバッグも可能で、リモートデバッグ機能により本番環境に近い環境での問題調査が実現できます。また、CPU プロファイリング、ヒープスナップショット、非同期スタックトレースなど、Node.js特有の問題に対応した高度な機能も提供します。

Node.js 12以降では--inspect-brk--inspect-portオプションによる詳細制御、worker_threadsのデバッグサポート、ES Modules(ESM)の完全対応など、継続的な機能強化が行われています。

メリット・デメリット

メリット

  • 標準搭載: Node.js組み込み、追加インストール不要
  • Chrome DevTools統合: 慣れ親しんだデバッグインターフェース
  • リモートデバッグ: ネットワーク経由でのデバッグ可能
  • IDE統合: VS Code、WebStormなどとの優秀な統合
  • 豊富な機能: プロファイリング、メモリ分析、非同期デバッグ
  • TypeScript対応: ソースマップによる完全なTypeScriptサポート
  • Docker対応: コンテナ環境でのデバッグが容易
  • 無料利用: オープンソースで商用利用も可能

デメリット

  • パフォーマンス影響: デバッグ有効時の実行速度低下
  • セキュリティリスク: 本番環境でのInspector有効化は危険
  • ポート管理: デバッグポートの適切な管理が必要
  • 学習コスト: Chrome DevToolsの習得が必要
  • ネットワーク依存: リモートデバッグ時のネットワーク品質影響
  • メモリ消費: デバッグ情報によるメモリ使用量増加
  • バージョン依存: Node.jsバージョンによる機能差異

主要リンク

書き方の例

基本的なInspector起動

# 基本的なInspector起動(デフォルトポート9229)
node --inspect app.js

# 起動時にブレークポイントで停止
node --inspect-brk app.js

# カスタムポート指定
node --inspect=9230 app.js

# 特定のホストとポートで起動
node --inspect=localhost:9229 app.js

# 全てのネットワークインターフェースでリッスン(危険:本番では避ける)
node --inspect=0.0.0.0:9229 app.js

# Inspectorで起動後の接続URL例:
# chrome://inspect で表示される
# または直接 chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/uuid

サンプルアプリケーション

// app.js - デバッグ対象のNode.jsアプリケーション
const express = require('express');
const app = express();
const port = 3000;

// ミドルウェア
app.use(express.json());

// ルート処理(デバッグポイント候補)
app.get('/', (req, res) => {
    // ここにブレークポイントを設定
    const message = 'Hello World!';
    const timestamp = new Date().toISOString();
    
    res.json({ 
        message, 
        timestamp,
        nodeVersion: process.version 
    });
});

// 非同期処理のデバッグ例
app.get('/async', async (req, res) => {
    try {
        // 非同期関数のデバッグ
        const data = await fetchDataFromAPI();
        const processed = processData(data);
        
        res.json({ result: processed });
    } catch (error) {
        // エラーハンドリングのデバッグ
        console.error('Error in async handler:', error);
        res.status(500).json({ error: error.message });
    }
});

// データベース操作のデバッグ例
app.post('/users', async (req, res) => {
    const { name, email } = req.body;
    
    // バリデーション(デバッグポイント)
    if (!name || !email) {
        return res.status(400).json({ 
            error: 'Name and email are required' 
        });
    }
    
    try {
        // データベース操作(デバッグ対象)
        const user = await createUser({ name, email });
        res.status(201).json(user);
    } catch (error) {
        res.status(500).json({ error: 'Failed to create user' });
    }
});

// 重い処理のプロファイリング対象
app.get('/heavy', (req, res) => {
    const start = Date.now();
    
    // CPU集約的な処理
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
        result += Math.sqrt(i);
    }
    
    const duration = Date.now() - start;
    res.json({ result, duration });
});

// ヘルパー関数
async function fetchDataFromAPI() {
    // 外部API呼び出しのシミュレーション
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ data: 'sample data', count: 10 });
        }, 100);
    });
}

function processData(data) {
    // データ処理ロジック
    return {
        ...data,
        processed: true,
        processedAt: new Date().toISOString()
    };
}

async function createUser(userData) {
    // データベース操作のシミュレーション
    const user = {
        id: Math.random().toString(36).substr(2, 9),
        ...userData,
        createdAt: new Date().toISOString()
    };
    
    // 疑似データベース保存処理
    await new Promise(resolve => setTimeout(resolve, 50));
    
    return user;
}

// サーバー起動
app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
    console.log('Inspector ready - Open chrome://inspect to debug');
});

// 実行コマンド:
// node --inspect app.js
// または
// node --inspect-brk app.js  # 起動時に停止

VS Codeでのデバッグ設定

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Program",
            "type": "node",
            "request": "launch",
            "program": "${workspaceFolder}/app.js",
            "env": {
                "NODE_ENV": "development"
            },
            "args": [],
            "console": "integratedTerminal",
            "internalConsoleOptions": "neverOpen"
        },
        {
            "name": "Launch with Inspector",
            "type": "node",
            "request": "launch",
            "program": "${workspaceFolder}/app.js",
            "port": 9229,
            "restart": true,
            "env": {
                "NODE_ENV": "development"
            }
        },
        {
            "name": "Attach to Process",
            "type": "node",
            "request": "attach",
            "port": 9229,
            "restart": true,
            "localRoot": "${workspaceFolder}",
            "remoteRoot": "/app"  // Dockerコンテナ用
        },
        {
            "name": "Jest Tests",
            "type": "node",
            "request": "launch",
            "program": "${workspaceFolder}/node_modules/.bin/jest",
            "args": ["--runInBand"],
            "console": "integratedTerminal",
            "internalConsoleOptions": "neverOpen"
        }
    ]
}

TypeScriptデバッグ設定

// tsconfig.json - TypeScript設定
{
    "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "sourceMap": true,  // ソースマップ生成を有効化
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "dist"]
}
// src/app.ts - TypeScriptサンプル
interface User {
    id: string;
    name: string;
    email: string;
    createdAt: Date;
}

class UserService {
    private users: User[] = [];
    
    async createUser(userData: Omit<User, 'id' | 'createdAt'>): Promise<User> {
        // デバッグポイント: TypeScriptの型情報も確認可能
        const user: User = {
            id: this.generateId(),
            name: userData.name,
            email: userData.email,
            createdAt: new Date()
        };
        
        // バリデーション
        if (!this.isValidEmail(user.email)) {
            throw new Error('Invalid email format');
        }
        
        this.users.push(user);
        return user;
    }
    
    private generateId(): string {
        return Math.random().toString(36).substr(2, 9);
    }
    
    private isValidEmail(email: string): boolean {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return emailRegex.test(email);
    }
    
    getUsers(): User[] {
        return [...this.users];  // defensive copy
    }
}

// Express アプリケーション
import express from 'express';

const app = express();
const userService = new UserService();

app.use(express.json());

app.post('/users', async (req, res) => {
    try {
        // TypeScript型チェックの恩恵を受けながらデバッグ
        const user = await userService.createUser(req.body);
        res.status(201).json(user);
    } catch (error) {
        if (error instanceof Error) {
            res.status(400).json({ error: error.message });
        } else {
            res.status(500).json({ error: 'Unknown error' });
        }
    }
});

app.listen(3000, () => {
    console.log('TypeScript server running on port 3000');
});

// VS Code launch.json for TypeScript
/*
{
    "name": "TypeScript Debug",
    "type": "node",
    "request": "launch",
    "program": "${workspaceFolder}/src/app.ts",
    "preLaunchTask": "tsc: build - tsconfig.json",
    "outFiles": ["${workspaceFolder}/dist/**/*.js"],
    "env": {
        "NODE_ENV": "development"
    }
}
*/

Docker環境でのデバッグ

# Dockerfile
FROM node:18-alpine

WORKDIR /app

# package files
COPY package*.json ./
RUN npm ci --only=production

# app source
COPY . .

# Inspector用ポート公開
EXPOSE 3000 9229

# デバッグモードで起動
CMD ["node", "--inspect=0.0.0.0:9229", "app.js"]
# docker-compose.yml
version: '3.8'
services:
  node-app:
    build: .
    ports:
      - "3000:3000"
      - "9229:9229"  # Inspector port
    environment:
      - NODE_ENV=development
    volumes:
      - .:/app
      - /app/node_modules
    command: node --inspect=0.0.0.0:9229 app.js
# Docker デバッグコマンド
# コンテナ起動
docker-compose up

# 実行中コンテナへのアタッチ
docker exec -it container_name node --inspect=0.0.0.0:9229 app.js

# VS Codeで接続する場合のlaunch.json設定
# "remoteRoot": "/app",
# "localRoot": "${workspaceFolder}"

高度なデバッグテクニック

// advanced-debugging.js
const { performance } = require('perf_hooks');

class AdvancedDebuggingExample {
    constructor() {
        this.data = new Map();
        this.metrics = [];
    }
    
    // パフォーマンス測定付きメソッド
    async processWithMetrics(items) {
        const start = performance.now();
        
        try {
            // 非同期処理のデバッグ
            const results = await Promise.all(
                items.map(async (item, index) => {
                    // 条件付きブレークポイント設定のポイント
                    if (index === 5) {
                        debugger; // プログラム内ブレークポイント
                    }
                    
                    return await this.processItem(item);
                })
            );
            
            const end = performance.now();
            this.recordMetric('processWithMetrics', end - start);
            
            return results;
        } catch (error) {
            // エラー処理時のデバッグ情報
            console.error('Processing failed:', {
                error: error.message,
                stack: error.stack,
                items: items.length
            });
            throw error;
        }
    }
    
    async processItem(item) {
        // CPU集約的処理のシミュレーション
        const result = await new Promise(resolve => {
            setImmediate(() => {
                const processed = {
                    original: item,
                    hash: this.calculateHash(item),
                    timestamp: Date.now()
                };
                resolve(processed);
            });
        });
        
        this.data.set(item.id, result);
        return result;
    }
    
    calculateHash(item) {
        // 重い計算処理(プロファイリング対象)
        let hash = 0;
        const str = JSON.stringify(item);
        
        for (let i = 0; i < str.length; i++) {
            const char = str.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash = hash & hash; // 32bit integer
        }
        
        return hash;
    }
    
    recordMetric(operation, duration) {
        this.metrics.push({
            operation,
            duration,
            timestamp: Date.now(),
            memoryUsage: process.memoryUsage()
        });
    }
    
    getMetrics() {
        return this.metrics;
    }
    
    // メモリリーク検出のためのヘルパー
    simulateMemoryLeak() {
        const leakyArray = [];
        
        setInterval(() => {
            // 意図的なメモリリーク
            leakyArray.push(new Array(1000).fill('leak'));
            
            if (leakyArray.length > 100) {
                // Inspector のHeap Snapshotでリーク検出可能
                console.log('Memory leak simulation running...', 
                    process.memoryUsage());
            }
        }, 100);
    }
}

// 使用例
async function main() {
    const debugExample = new AdvancedDebuggingExample();
    
    // テストデータ
    const items = Array.from({ length: 10 }, (_, i) => ({
        id: `item-${i}`,
        value: Math.random() * 100,
        category: ['A', 'B', 'C'][i % 3]
    }));
    
    try {
        // メインデバッグ処理
        const results = await debugExample.processWithMetrics(items);
        
        // メトリクス確認(Inspector Consoleで表示)
        console.table(debugExample.getMetrics());
        
        return results;
    } catch (error) {
        console.error('Main process failed:', error);
    }
}

// Inspector起動: node --inspect-brk advanced-debugging.js
main().catch(console.error);

プロファイリングとメモリ分析

// profiling-example.js
const fs = require('fs').promises;
const { performance, PerformanceObserver } = require('perf_hooks');

// Performance Observer設定
const perfObserver = new PerformanceObserver((items) => {
    items.getEntries().forEach((entry) => {
        console.log(`${entry.name}: ${entry.duration}ms`);
    });
});
perfObserver.observe({ entryTypes: ['measure', 'function'] });

class ProfilingExample {
    async fileProcessingWorkload() {
        // CPU Profilerでの分析対象
        performance.mark('start-file-processing');
        
        try {
            // 大量のファイル操作
            const files = await this.generateTestFiles();
            const results = await this.processFiles(files);
            
            performance.mark('end-file-processing');
            performance.measure('file-processing', 
                'start-file-processing', 'end-file-processing');
            
            return results;
        } catch (error) {
            console.error('File processing failed:', error);
            throw error;
        }
    }
    
    async generateTestFiles() {
        const files = [];
        
        for (let i = 0; i < 100; i++) {
            const content = this.generateRandomContent(1000);
            const filename = `/tmp/test-${i}.txt`;
            
            await fs.writeFile(filename, content);
            files.push(filename);
        }
        
        return files;
    }
    
    generateRandomContent(size) {
        // CPU集約的な処理
        let content = '';
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        
        for (let i = 0; i < size; i++) {
            content += chars.charAt(Math.floor(Math.random() * chars.length));
        }
        
        return content;
    }
    
    async processFiles(files) {
        const results = [];
        
        // 並列処理のプロファイリング
        const chunks = this.chunkArray(files, 10);
        
        for (const chunk of chunks) {
            const chunkResults = await Promise.all(
                chunk.map(file => this.processFile(file))
            );
            results.push(...chunkResults);
        }
        
        return results;
    }
    
    async processFile(filename) {
        performance.mark(`start-${filename}`);
        
        try {
            const content = await fs.readFile(filename, 'utf8');
            const processed = this.analyzeContent(content);
            
            // ファイル削除
            await fs.unlink(filename);
            
            performance.mark(`end-${filename}`);
            performance.measure(`process-${filename}`, 
                `start-${filename}`, `end-${filename}`);
            
            return processed;
        } catch (error) {
            console.error(`Failed to process ${filename}:`, error);
            return null;
        }
    }
    
    analyzeContent(content) {
        // 文字列解析処理(プロファイリング対象)
        const words = content.split(/\s+/);
        const wordCount = words.length;
        const charCount = content.length;
        const uniqueWords = new Set(words.map(w => w.toLowerCase()));
        
        return {
            wordCount,
            charCount,
            uniqueWordCount: uniqueWords.size,
            averageWordLength: charCount / wordCount
        };
    }
    
    chunkArray(array, chunkSize) {
        const chunks = [];
        for (let i = 0; i < array.length; i += chunkSize) {
            chunks.push(array.slice(i, i + chunkSize));
        }
        return chunks;
    }
}

// CPU Profilerでの実行
// 1. node --inspect app.js でアプリ起動
// 2. Chrome DevTools → Profiler → Start CPU profiling
// 3. 以下のコード実行
// 4. Stop profiling で結果確認

async function runProfiling() {
    const profiler = new ProfilingExample();
    
    console.log('Starting profiling workload...');
    console.time('total-execution');
    
    const results = await profiler.fileProcessingWorkload();
    
    console.timeEnd('total-execution');
    console.log(`Processed ${results.length} files`);
    
    // Heap Snapshotでメモリ使用量確認
    if (global.gc) {
        global.gc(); // ガベージコレクション実行
    }
    
    console.log('Memory usage:', process.memoryUsage());
}

// 実行コマンド:
// node --inspect --expose-gc profiling-example.js
runProfiling().catch(console.error);